2
2
import asyncio
3
3
import copy
4
4
import datetime
5
+ import hashlib
5
6
import logging
6
7
import mimetypes
7
8
import time
16
17
import requests
17
18
18
19
from annofabapi import AnnofabApi
19
- from annofabapi .exceptions import AnnofabApiException
20
+ from annofabapi .exceptions import AnnofabApiException , CheckSumError
20
21
from annofabapi .models import (
21
22
AdditionalData ,
22
23
AdditionalDataDefinitionType ,
@@ -346,23 +347,38 @@ def __to_dest_annotation_detail(
346
347
) -> Dict [str , Any ]:
347
348
"""
348
349
コピー元の1個のアノテーションを、コピー先用に変換する。
349
- 塗りつぶし画像の場合 、S3にアップロードする。
350
+ 塗りつぶし画像などの外部アノテーションファイルがある場合 、S3にアップロードする。
350
351
351
352
Notes:
352
353
annotation_id をUUIDv4で生成すると、アノテーションリンク属性をコピーしたときに対応できないので、暫定的にannotation_idは維持するようにする。
354
+
355
+ Raises:
356
+ CheckSumError: アップロードした外部アノテーションファイルのMD5ハッシュ値が、S3にアップロードしたときのレスポンスのETagに一致しない
357
+
353
358
"""
354
359
dest_detail = detail
355
360
dest_detail ["account_id" ] = account_id
356
361
if detail ["data_holding_type" ] == AnnotationDataHoldingType .OUTER .value :
357
- outer_file_url = detail ["url" ]
358
- src_response = self ._request_get_wrapper (outer_file_url )
359
- s3_path = self .upload_data_to_s3 (
360
- dest_project_id , data = src_response .content , content_type = src_response .headers ["Content-Type" ]
361
- )
362
- logger .debug ("%s に塗りつぶし画像をアップロードしました。" , s3_path )
363
- dest_detail ["path" ] = s3_path
364
- dest_detail ["url" ] = None
365
- dest_detail ["etag" ] = None
362
+
363
+ try :
364
+ outer_file_url = detail ["url" ]
365
+ src_response = self ._request_get_wrapper (outer_file_url )
366
+ s3_path = self .upload_data_to_s3 (
367
+ dest_project_id , data = src_response .content , content_type = src_response .headers ["Content-Type" ]
368
+ )
369
+ logger .debug ("%s に外部アノテーションファイルをアップロードしました。" , s3_path )
370
+ dest_detail ["path" ] = s3_path
371
+ dest_detail ["url" ] = None
372
+ dest_detail ["etag" ] = None
373
+
374
+ except CheckSumError as e :
375
+ message = (
376
+ f"外部アノテーションファイル { outer_file_url } のレスポンスのMD5ハッシュ値('{ e .uploaded_data_hash } ')が、"
377
+ f"AWS S3にアップロードしたときのレスポンスのETag('{ e .response_etag } ')に一致しませんでした。アップロード時にデータが破損した可能性があります。"
378
+ )
379
+ raise CheckSumError (
380
+ message = message , uploaded_data_hash = e .uploaded_data_hash , response_etag = e .response_etag
381
+ ) from e
366
382
367
383
return dest_detail
368
384
@@ -533,6 +549,9 @@ def __to_annotation_detail_for_request(
533
549
534
550
Returns:
535
551
552
+ Raises:
553
+ CheckSumError: アップロードした外部アノテーションファイルのMD5ハッシュ値が、S3にアップロードしたときのレスポンスのETagに一致しない
554
+
536
555
"""
537
556
label_info = self .__get_label_info_from_label_name (detail ["label" ], annotation_specs_labels )
538
557
if label_info is None :
@@ -559,10 +578,21 @@ def __to_annotation_detail_for_request(
559
578
560
579
if data_holding_type == AnnotationDataHoldingType .OUTER .value :
561
580
data_uri = detail ["data" ]["data_uri" ]
581
+ outer_file_path = f"{ parser .task_id } /{ parser .input_data_id } /{ data_uri } "
562
582
with parser .open_outer_file (data_uri ) as f :
563
- s3_path = self .upload_data_to_s3 (project_id , f , content_type = "image/png" )
564
- dest_obj ["path" ] = s3_path
565
- logger .debug (f"{ parser .task_id } /{ parser .input_data_id } /{ data_uri } をS3にアップロードしました。" )
583
+ try :
584
+ s3_path = self .upload_data_to_s3 (project_id , f , content_type = "image/png" )
585
+ dest_obj ["path" ] = s3_path
586
+ logger .debug (f"{ outer_file_path } をS3にアップロードしました。" )
587
+
588
+ except CheckSumError as e :
589
+ message = (
590
+ f"アップロードした外部アノテーションファイル'{ outer_file_path } 'のMD5ハッシュ値('{ e .uploaded_data_hash } ')が、"
591
+ f"AWS S3にアップロードしたときのレスポンスのETag('{ e .response_etag } ')に一致しませんでした。アップロード時にデータが破損した可能性があります。"
592
+ )
593
+ raise CheckSumError (
594
+ message = message , uploaded_data_hash = e .uploaded_data_hash , response_etag = e .response_etag
595
+ ) from e
566
596
567
597
return dest_obj
568
598
@@ -830,25 +860,51 @@ def upload_file_to_s3(self, project_id: str, file_path: str, content_type: Optio
830
860
831
861
Returns:
832
862
一時データ保存先であるS3パス
863
+
864
+ Raises:
865
+ CheckSumError: アップロードしたファイルのMD5ハッシュ値が、S3にアップロードしたときのレスポンスのETagと一致しない
866
+
833
867
"""
834
868
835
869
# content_type を推測
836
870
new_content_type = self ._get_content_type (file_path , content_type )
837
871
with open (file_path , "rb" ) as f :
838
- return self .upload_data_to_s3 (project_id , data = f , content_type = new_content_type )
872
+ try :
873
+ return self .upload_data_to_s3 (project_id , data = f , content_type = new_content_type )
874
+ except CheckSumError as e :
875
+ message = (
876
+ f"アップロードしたファイル'{ file_path } 'のMD5ハッシュ値('{ e .uploaded_data_hash } ')が、"
877
+ f"AWS S3にアップロードしたときのレスポンスのETag('{ e .response_etag } ')に一致しませんでした。アップロード時にデータが破損した可能性があります。"
878
+ )
879
+ raise CheckSumError (
880
+ message = message , uploaded_data_hash = e .uploaded_data_hash , response_etag = e .response_etag
881
+ ) from e
839
882
840
883
def upload_data_to_s3 (self , project_id : str , data : Any , content_type : str ) -> str :
841
884
"""
842
885
createTempPath APIを使ってアップロード用のURLとS3パスを取得して、"data" をアップロードする。
843
886
844
887
Args:
845
888
project_id: プロジェクトID
846
- data: アップロード対象のdata. ``requests.put`` メソッドの ``data`` 引数にそのまま渡す 。
889
+ data: アップロード対象のdata. ``open(mode="b")`` 関数の戻り値、またはバイナリ型の値です。 `` requests.put`` メソッドの ``data`` 引数にそのまま渡します 。
847
890
content_type: アップロードするfile objectのMIME Type.
848
891
849
892
Returns:
850
893
一時データ保存先であるS3パス
894
+
895
+ Raises:
896
+ CheckSumError: アップロードしたデータのMD5ハッシュ値が、S3にアップロードしたときのレスポンスのETagと一致しない
851
897
"""
898
+
899
+ def get_md5_value_from_file (fp ):
900
+ md5_obj = hashlib .md5 ()
901
+ while True :
902
+ chunk = fp .read (2048 * md5_obj .block_size )
903
+ if len (chunk ) == 0 :
904
+ break
905
+ md5_obj .update (chunk )
906
+ return md5_obj .hexdigest ()
907
+
852
908
# 一時データ保存先を取得
853
909
content = self .api .create_temp_path (project_id , header_params = {"content-type" : content_type })[0 ]
854
910
@@ -865,6 +921,25 @@ def upload_data_to_s3(self, project_id: str, data: Any, content_type: str) -> st
865
921
866
922
_log_error_response (logger , res_put )
867
923
_raise_for_status (res_put )
924
+
925
+ # アップロードしたファイルが破損していなかをチェックする
926
+ if hasattr (data , "read" ):
927
+ # 読み込み位置を先頭に戻す
928
+ data .seek (0 )
929
+ uploaded_data_hash = get_md5_value_from_file (data )
930
+ else :
931
+ uploaded_data_hash = hashlib .md5 (data ).hexdigest ()
932
+
933
+ # ETagにはダブルクォートが含まれているため、`str_md5`もそれに合わせる
934
+ response_etag = res_put .headers ["ETag" ]
935
+
936
+ if f'"{ uploaded_data_hash } "' != response_etag :
937
+ message = (
938
+ f"アップロードしたデータのMD5ハッシュ値('{ uploaded_data_hash } ')が、"
939
+ f"AWS S3にアップロードしたときのレスポンスのETag('{ response_etag } ')に一致しませんでした。アップロード時にデータが破損した可能性があります。"
940
+ )
941
+ raise CheckSumError (message = message , uploaded_data_hash = uploaded_data_hash , response_etag = response_etag )
942
+
868
943
return content ["path" ]
869
944
870
945
def put_input_data_from_file (
0 commit comments