Skip to content

Commit d5a44ee

Browse files
committed
feat: add multipart encoder when uploading (large) files
With the previous implementation of `upload_files`, the SDK was loading the whole file to be uploaded in the memory and only then sending it over the socket. This was painfully slow, memory constrained and lacked real feedback. Therefore the `requests_toolbelt`'s `MultipartEncoderMonitor` is introduced in this PR, which keeps the memory consumption under control. This is quite useful not only for CLI consumers of the SDK, but also for QFieldCloud `qgis` workers, as the memory does not grow too much when uploading large files after `package` or `apply_deltas` job.
1 parent 1ad452d commit d5a44ee

2 files changed

Lines changed: 30 additions & 7 deletions

File tree

qfieldcloud_sdk/sdk.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import requests
1212
import urllib3
1313
from requests.adapters import HTTPAdapter, Retry
14+
from requests_toolbelt.multipart.encoder import MultipartEncoderMonitor
1415

1516
from .interfaces import QfcException, QfcRequest, QfcRequestException
1617
from .utils import calc_etag, log, add_trailing_slash_to_url
@@ -665,20 +666,39 @@ def upload_file(
665666
# if the filepath is invalid, it will throw a new error `pathvalidate.ValidationError`
666667
is_valid_filepath(str(local_filename))
667668

669+
670+
local_file_size = local_filename.stat().st_size
668671
with open(local_filename, "rb") as local_file:
669-
upload_file = local_file
670672
if show_progress:
671673
from tqdm import tqdm
672-
from tqdm.utils import CallbackIOWrapper
673674

674675
progress_bar = tqdm(
675-
total=local_filename.stat().st_size,
676+
total=local_file_size,
676677
unit_scale=True,
677-
desc=local_filename.stem,
678+
unit="B",
679+
desc=f'Uploading "{remote_filename}"...',
678680
)
679-
upload_file = CallbackIOWrapper(progress_bar.update, local_file, "read")
681+
# upload_file = CallbackIOWrapper(progress_bar.update, local_file, "read")
682+
def callback(monitor: MultipartEncoderMonitor) -> None:
683+
progress_bar.n = monitor.bytes_read
684+
progress_bar.refresh()
680685
else:
681686
logger.info(f'Uploading file "{remote_filename}"…')
687+
callback = lambda monitor: None
688+
689+
multipart_data = MultipartEncoderMonitor.from_fields(
690+
fields={
691+
"file": (
692+
str(remote_filename),
693+
local_file,
694+
None,
695+
# {
696+
# "Content-Length": str(local_file_size),
697+
# }
698+
),
699+
},
700+
callback=callback,
701+
)
682702

683703
if upload_type == FileTransferType.PROJECT:
684704
url = f"files/{project_id}/{remote_filename}"
@@ -695,8 +715,10 @@ def upload_file(
695715
return self._request(
696716
"POST",
697717
url,
698-
files={
699-
"file": upload_file,
718+
data=multipart_data,
719+
headers={
720+
"Content-Type": multipart_data.content_type,
721+
"Accept": "application/json",
700722
},
701723
)
702724

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ charset-normalizer>=3.2.0
33
click>=8.1.5
44
idna>=3.4
55
requests>=2.31.0
6+
requests-toolbelt>=1.0.0
67
tqdm>=4.65.0
78
urllib3>=2.0.7
89
pathvalidate>=3.2.1

0 commit comments

Comments
 (0)