Skip to content

Commit 3a4df04

Browse files
authored
Merge pull request #8 from gdcc/fix-singlepart-direct-upload
Fix singlepart direct upload and direct upload file registration
2 parents d760934 + 4bf1123 commit 3a4df04

8 files changed

Lines changed: 248 additions & 63 deletions

File tree

dvuploader/directupload.py

Lines changed: 109 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,22 @@
88
import aiohttp
99

1010
from dvuploader.file import File
11-
from dvuploader.nativeupload import file_sender
1211
from dvuploader.utils import build_url
1312

1413
TESTING = 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

1623
TICKET_ENDPOINT = "/api/datasets/:persistentId/uploadurls"
1724
ADD_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

2229
async 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

100101
async 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

Comments
 (0)