Skip to content

Commit 4d00498

Browse files
authored
Merge pull request #5 from lutraconsulting/download_revisited
download files with multiple sequential requests
2 parents 6ad6445 + 9c0afaf commit 4d00498

File tree

2 files changed

+51
-29
lines changed

2 files changed

+51
-29
lines changed

cli.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ def status():
127127
return
128128

129129
click.echo("Current version: {}".format(project_info["version"]))
130-
last_version = versions[-1]
131130
new_versions = [v for v in versions if num_version(v["name"]) > local_version]
132131
if new_versions:
133132
click.secho("### Available updates: {}".format(len(new_versions)), fg="magenta")

mergin/client.py

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from datetime import datetime
1010

1111
this_dir = os.path.dirname(os.path.realpath(__file__))
12+
CHUNK_SIZE = 10 * 1024 * 1024
1213

1314
try:
1415
from requests_toolbelt import MultipartEncoder
@@ -26,8 +27,7 @@
2627
import pytz
2728
import dateutil.parser
2829

29-
from .utils import save_to_file, generate_checksum, move_file
30-
from .multipart import MultipartReader, parse_boundary
30+
from .utils import generate_checksum, move_file, save_to_file
3131

3232

3333
class InvalidProject(Exception):
@@ -175,11 +175,14 @@ def _do_request(self, request):
175175
raise ClientError(info.get("detail"))
176176
raise ClientError(e.read().decode("utf-8"))
177177

178-
def get(self, path, data=None):
178+
def get(self, path, data=None, headers={}):
179179
url = urllib.parse.urljoin(self.url, urllib.parse.quote(path))
180180
if data:
181181
url += "?" + urllib.parse.urlencode(data)
182-
request = urllib.request.Request(url)
182+
if headers:
183+
request = urllib.request.Request(url, headers=headers)
184+
else:
185+
request = urllib.request.Request(url)
183186
return self._do_request(request)
184187

185188
def post(self, path, data, headers={}):
@@ -341,7 +344,7 @@ def project_versions(self, project_path):
341344

342345
def download_project(self, project_path, directory):
343346
"""
344-
Download last version of project into given directory.
347+
Download latest version of project into given directory.
345348
346349
:param project_path: Project's full name (<namespace>/<name>)
347350
:type project_path: String
@@ -353,20 +356,15 @@ def download_project(self, project_path, directory):
353356
raise Exception("Project directory already exists")
354357
os.makedirs(directory)
355358

356-
# TODO: this shouldn't be two independent operations, or we should use version tag in download requests.
357-
# But current server API doesn't allow something better.
358359
project_info = self.project_info(project_path)
359-
resp = self.get("/v1/project/download/{}".format(project_path))
360-
reader = MultipartReader(resp, parse_boundary(resp.headers["Content-Type"]))
361-
part = reader.next_part()
362-
while part:
363-
dest = os.path.join(directory, part.filename)
364-
save_to_file(part, dest)
365-
part = reader.next_part()
360+
version = project_info['version'] if project_info['version'] else 'v0'
361+
362+
for file in project_info['files']:
363+
self._download_file(project_path, version, file, directory)
366364

367365
data = {
368366
"name": project_path,
369-
"version": project_info["version"],
367+
"version": version,
370368
"files": project_info["files"]
371369
}
372370
save_project_file(directory, data)
@@ -453,20 +451,9 @@ def backup_if_conflict(path, checksum):
453451

454452
fetch_files = pull_changes["added"] + pull_changes["updated"]
455453
if fetch_files:
456-
resp = self.post(
457-
"/v1/project/fetch/{}".format(project_path),
458-
fetch_files,
459-
{"Content-Type": "application/json"}
460-
)
461-
reader = MultipartReader(resp, parse_boundary(resp.headers["Content-Type"]))
462-
part = reader.next_part()
463454
temp_dir = os.path.join(directory, '.mergin', 'fetch_{}-{}'.format(local_info["version"], server_info["version"]))
464-
while part:
465-
dest = os.path.join(temp_dir, part.filename)
466-
save_to_file(part, dest)
467-
part = reader.next_part()
468-
469455
for file in fetch_files:
456+
self._download_file(project_path, server_info['version'], file, temp_dir)
470457
src = os.path.join(temp_dir, file["path"])
471458
dest = local_path(file["path"])
472459
backup_if_conflict(file["path"], file["checksum"])
@@ -482,5 +469,41 @@ def backup_if_conflict(path, checksum):
482469
move_file(local_path(file["path"]), local_path(file["new_path"]))
483470

484471
local_info["files"] = server_info["files"]
485-
local_info["version"] = server_info["version"]
472+
local_info["version"] = server_info["version"] if server_info["version"] else 'v0'
486473
save_project_file(directory, local_info)
474+
475+
def _download_file(self, project_path, project_version, file, directory):
476+
"""
477+
Helper to download single project file from server in chunks.
478+
479+
:param project_path: Project's full name (<namespace>/<name>)
480+
:type project_path: String
481+
:param project_version: Version of the project (v<n>)
482+
:type project_version: String
483+
:param file: File metadata item from Project['files']
484+
:type file: dict
485+
:param directory: Project's directory
486+
:type directory: String
487+
"""
488+
query_params = {
489+
"file": file['path'],
490+
"version": project_version
491+
}
492+
file_dir = os.path.dirname(os.path.normpath(os.path.join(directory, file['path'])))
493+
basename = os.path.basename(file['path'])
494+
length = 0
495+
count = 0
496+
while length < file['size']:
497+
range_header = {"Range": "bytes={}-{}".format(length, length + CHUNK_SIZE)}
498+
resp = self.get("/v1/project/raw/{}".format(project_path), data=query_params, headers=range_header)
499+
if resp.status in [200, 206]:
500+
save_to_file(resp, os.path.join(file_dir, basename+".{}".format(count)))
501+
length += (CHUNK_SIZE + 1)
502+
count += 1
503+
504+
# merge chunks together
505+
with open(os.path.join(file_dir, basename), 'wb') as final:
506+
for i in range(count):
507+
with open(os.path.join(directory, file['path'] + ".{}".format(i)), 'rb') as chunk:
508+
shutil.copyfileobj(chunk, final)
509+
os.remove(os.path.join(directory, file['path'] + ".{}".format(i)))

0 commit comments

Comments
 (0)