88import aiohttp
99
1010from dvuploader .file import File
11- from dvuploader .nativeupload import file_sender
1211from dvuploader .utils import build_url
1312
1413TESTING = bool (os .environ .get ("DVUPLOADER_TESTING" , False ))
14+ MAX_FILE_DISPLAY = int (os .environ .get ("DVUPLOADER_MAX_FILE_DISPLAY" , 50 ))
15+ MAX_RETRIES = int (os .environ .get ("DVUPLOADER_MAX_RETRIES" , 10 ))
16+
17+ assert isinstance (
18+ MAX_FILE_DISPLAY , int
19+ ), "DVUPLOADER_MAX_FILE_DISPLAY must be an integer"
20+
21+ assert isinstance (MAX_RETRIES , int ), "DVUPLOADER_MAX_RETRIES must be an integer"
1522
1623TICKET_ENDPOINT = "/api/datasets/:persistentId/uploadurls"
1724ADD_FILE_ENDPOINT = "/api/datasets/:persistentId/addFiles"
18- UPLOAD_ENDPOINT = "/api/datasets/:persistentId/add ?persistentId="
19- REPLACE_ENDPOINT = "/api/files/{FILE_ID}/replace "
25+ UPLOAD_ENDPOINT = "/api/datasets/:persistentId/addFiles ?persistentId="
26+ REPLACE_ENDPOINT = "/api/datasets/:persistentId/replaceFiles?persistentId= "
2027
2128
2229async def direct_upload (
@@ -44,6 +51,7 @@ async def direct_upload(
4451 None
4552 """
4653
54+ leave_bar = len (files ) < MAX_FILE_DISPLAY
4755 connector = aiohttp .TCPConnector (limit = n_parallel_uploads )
4856 async with aiohttp .ClientSession (connector = connector ) as session :
4957 tasks = [
@@ -56,6 +64,7 @@ async def direct_upload(
5664 pbar = pbar ,
5765 progress = progress ,
5866 delay = 0.0 ,
67+ leave_bar = leave_bar ,
5968 )
6069 for pbar , file in zip (pbars , files )
6170 ]
@@ -73,28 +82,20 @@ async def direct_upload(
7382 "x-amz-tagging" : "dv-state=temp" ,
7483 }
7584
76- connector = aiohttp .TCPConnector (limit = 4 )
77- pbar = progress .add_task ("╰── [bold white]Registering files" , total = len (files ))
78- results = []
85+ pbar = progress .add_task ("╰── [bold white]Registering files" , total = 1 )
86+ connector = aiohttp .TCPConnector (limit = 2 )
7987 async with aiohttp .ClientSession (
8088 headers = headers ,
8189 connector = connector ,
8290 ) as session :
83- for file in files :
84- results .append (
85- await _add_file_to_ds (
86- session = session ,
87- file = file ,
88- dataverse_url = dataverse_url ,
89- pid = persistent_id ,
90- )
91- )
92-
93- progress .update (pbar , advance = 1 )
94-
95- for file , status in zip (files , results ):
96- if status is False :
97- print (f"❌ Failed to register file '{ file .fileName } ' at Dataverse" )
91+ await _add_files_to_ds (
92+ session = session ,
93+ files = files ,
94+ dataverse_url = dataverse_url ,
95+ pid = persistent_id ,
96+ progress = progress ,
97+ pbar = pbar ,
98+ )
9899
99100
100101async def _upload_to_store (
@@ -106,6 +107,7 @@ async def _upload_to_store(
106107 pbar ,
107108 progress ,
108109 delay : float ,
110+ leave_bar : bool ,
109111):
110112 """
111113 Uploads a file to a Dataverse collection using direct upload.
@@ -119,6 +121,7 @@ async def _upload_to_store(
119121 pbar: The progress bar object.
120122 progress: The progress object.
121123 delay (float): The delay in seconds before starting the upload.
124+ leave_bar (bool): A flag indicating whether to keep the progress bar visible after the upload is complete.
122125
123126 Returns:
124127 tuple: A tuple containing the upload status (bool) and the file object.
@@ -146,6 +149,7 @@ async def _upload_to_store(
146149 pbar = pbar ,
147150 progress = progress ,
148151 api_token = api_token ,
152+ leave_bar = leave_bar ,
149153 )
150154
151155 else :
@@ -207,6 +211,7 @@ async def _upload_singlepart(
207211 pbar ,
208212 progress ,
209213 api_token : str ,
214+ leave_bar : bool ,
210215) -> Tuple [bool , str ]:
211216 """
212217 Uploads a single part of a file to a remote server using HTTP PUT method.
@@ -217,6 +222,7 @@ async def _upload_singlepart(
217222 filepath (str): The path to the file to be uploaded.
218223 pbar (tqdm): A progress bar object to track the upload progress.
219224 progress: The progress object used to update the progress bar.
225+ leave_bar (bool): A flag indicating whether to keep the progress bar visible after the upload is complete.
220226
221227 Returns:
222228 Tuple[bool, str]: A tuple containing the status of the upload (True for success, False for failure)
@@ -235,19 +241,25 @@ async def _upload_singlepart(
235241 params = {
236242 "headers" : headers ,
237243 "url" : ticket ["url" ],
238- "data" : file_sender (
239- file_name = filepath ,
240- progress = progress ,
241- pbar = pbar ,
242- ),
244+ "data" : open (filepath , "rb" ),
243245 }
244246
245247 async with session .put (** params ) as response :
246248 status = response .status == 200
247249 response .raise_for_status ()
248250
249251 if status :
250- progress .update (pbar , advance = os .path .getsize (filepath ))
252+ progress .update (
253+ pbar ,
254+ advance = os .path .getsize (filepath ),
255+ )
256+
257+ await asyncio .sleep (0.1 )
258+
259+ progress .update (
260+ pbar ,
261+ visible = leave_bar ,
262+ )
251263
252264 return status , storage_identifier
253265
@@ -463,12 +475,14 @@ async def _abort_upload(
463475 response .raise_for_status ()
464476
465477
466- async def _add_file_to_ds (
478+ async def _add_files_to_ds (
467479 session : aiohttp .ClientSession ,
468480 dataverse_url : str ,
469481 pid : str ,
470- file : File ,
471- ) -> bool :
482+ files : List [File ],
483+ progress ,
484+ pbar ,
485+ ) -> None :
472486 """
473487 Adds a file to a Dataverse dataset.
474488
@@ -481,26 +495,77 @@ async def _add_file_to_ds(
481495 Returns:
482496 bool: True if the file was added successfully, False otherwise.
483497 """
484- if not file .to_replace :
485- url = urljoin (dataverse_url , UPLOAD_ENDPOINT + pid )
486- else :
487- url = build_url (
488- dataverse_url = dataverse_url ,
489- endpoint = urljoin (
490- dataverse_url ,
491- REPLACE_ENDPOINT .format (FILE_ID = file .file_id ),
492- ),
493- )
494498
495- json_data = file .model_dump_json (
496- by_alias = True ,
497- exclude = {"to_replace" , "file_id" },
499+ novel_url = urljoin (dataverse_url , UPLOAD_ENDPOINT + pid )
500+ replace_url = urljoin (dataverse_url , REPLACE_ENDPOINT + pid )
501+
502+ novel_json_data = _prepare_registration (files , use_replace = False )
503+ replace_json_data = _prepare_registration (files , use_replace = True )
504+
505+ await _multipart_json_data_request (
506+ session = session ,
507+ json_data = novel_json_data ,
508+ url = novel_url ,
509+ )
510+
511+ await _multipart_json_data_request (
512+ session = session ,
513+ json_data = replace_json_data ,
514+ url = replace_url ,
515+ )
516+
517+ progress .update (pbar , advance = 1 )
518+
519+
520+ def _prepare_registration (files : List [File ], use_replace : bool ) -> str :
521+ """
522+ Prepares the files for registration at the Dataverse instance.
523+
524+ Args:
525+ files (List[File]): The list of files to prepare.
526+
527+ Returns:
528+ str: A JSON string containing the file data.
529+ """
530+
531+ exclude = {"to_replace" } if use_replace else {"to_replace" , "file_id" }
532+
533+ return json .dumps (
534+ [
535+ file .model_dump (
536+ by_alias = True ,
537+ exclude = exclude ,
538+ exclude_none = True ,
539+ )
540+ for file in files
541+ if file .to_replace is use_replace
542+ ],
498543 indent = 2 ,
499544 )
500545
546+
547+ async def _multipart_json_data_request (
548+ json_data : str ,
549+ url : str ,
550+ session : aiohttp .ClientSession ,
551+ ):
552+ """
553+ Sends a multipart/form-data POST request with JSON data to the specified URL using the provided session.
554+
555+ Args:
556+ json_data (str): The JSON data to be sent in the request body.
557+ url (str): The URL to send the request to.
558+ session (aiohttp.ClientSession): The aiohttp client session to use for the request.
559+
560+ Raises:
561+ aiohttp.ClientResponseError: If the response status code is not successful.
562+
563+ Returns:
564+ None
565+ """
501566 with aiohttp .MultipartWriter ("form-data" ) as writer :
502567 json_part = writer .append (json_data )
503568 json_part .set_content_disposition ("form-data" , name = "jsonData" )
504569
505570 async with session .post (url , data = writer ) as response :
506- return response .status == 200
571+ response .raise_for_status ()
0 commit comments