Skip to content

Commit 1e4cb48

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 1e4cb48

2 files changed

Lines changed: 29 additions & 7 deletions

File tree

qfieldcloud_sdk/sdk.py

Lines changed: 28 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,21 +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+
local_file_size = local_filename.stat().st_size
668670
with open(local_filename, "rb") as local_file:
669-
upload_file = local_file
671+
from_fields_params = {}
672+
670673
if show_progress:
671674
from tqdm import tqdm
672-
from tqdm.utils import CallbackIOWrapper
673675

674676
progress_bar = tqdm(
675-
total=local_filename.stat().st_size,
677+
total=local_file_size,
676678
unit_scale=True,
677-
desc=local_filename.stem,
679+
unit="B",
680+
desc=f'Uploading "{remote_filename}"...',
678681
)
679-
upload_file = CallbackIOWrapper(progress_bar.update, local_file, "read")
682+
683+
def cb(monitor: MultipartEncoderMonitor) -> None:
684+
progress_bar.n = monitor.bytes_read
685+
progress_bar.refresh()
686+
687+
from_fields_params["callback"] = cb
680688
else:
681689
logger.info(f'Uploading file "{remote_filename}"…')
682690

691+
multipart_data = MultipartEncoderMonitor.from_fields(
692+
fields={
693+
"file": (
694+
str(remote_filename),
695+
local_file,
696+
None,
697+
),
698+
},
699+
**from_fields_params,
700+
)
701+
683702
if upload_type == FileTransferType.PROJECT:
684703
url = f"files/{project_id}/{remote_filename}"
685704
elif upload_type == FileTransferType.PACKAGE:
@@ -695,8 +714,10 @@ def upload_file(
695714
return self._request(
696715
"POST",
697716
url,
698-
files={
699-
"file": upload_file,
717+
data=multipart_data,
718+
headers={
719+
"Content-Type": multipart_data.content_type,
720+
"Accept": "application/json",
700721
},
701722
)
702723

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)