99from datetime import datetime
1010
1111this_dir = os .path .dirname (os .path .realpath (__file__ ))
12+ CHUNK_SIZE = 10 * 1024 * 1024
1213
1314try :
1415 from requests_toolbelt import MultipartEncoder
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
3333class 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