2626 import pytz
2727 import dateutil .parser
2828
29- from .utils import save_to_file , generate_checksum , move_file
30- from .multipart import MultipartReader , parse_boundary
29+ from .utils import generate_checksum , move_file
3130
3231
3332class InvalidProject (Exception ):
@@ -175,11 +174,14 @@ def _do_request(self, request):
175174 raise ClientError (info .get ("detail" ))
176175 raise ClientError (e .read ().decode ("utf-8" ))
177176
178- def get (self , path , data = None ):
177+ def get (self , path , data = None , headers = {} ):
179178 url = urllib .parse .urljoin (self .url , urllib .parse .quote (path ))
180179 if data :
181180 url += "?" + urllib .parse .urlencode (data )
182- request = urllib .request .Request (url )
181+ if headers :
182+ request = urllib .request .Request (url , headers = headers )
183+ else :
184+ request = urllib .request .Request (url )
183185 return self ._do_request (request )
184186
185187 def post (self , path , data , headers = {}):
@@ -341,7 +343,7 @@ def project_versions(self, project_path):
341343
342344 def download_project (self , project_path , directory ):
343345 """
344- Download last version of project into given directory.
346+ Download latest version of project into given directory.
345347
346348 :param project_path: Project's full name (<namespace>/<name>)
347349 :type project_path: String
@@ -353,16 +355,10 @@ def download_project(self, project_path, directory):
353355 raise Exception ("Project directory already exists" )
354356 os .makedirs (directory )
355357
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.
358358 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 ()
359+
360+ for file in project_info ['files' ]:
361+ self ._download_file (project_path , project_info ['version' ], file , directory )
366362
367363 data = {
368364 "name" : project_path ,
@@ -453,20 +449,11 @@ def backup_if_conflict(path, checksum):
453449
454450 fetch_files = pull_changes ["added" ] + pull_changes ["updated" ]
455451 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 ()
463452 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-
453+ if not os .path .exists (temp_dir ):
454+ os .makedirs (temp_dir )
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" ])
@@ -484,3 +471,44 @@ def backup_if_conflict(path, checksum):
484471 local_info ["files" ] = server_info ["files" ]
485472 local_info ["version" ] = server_info ["version" ]
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+ chunk_size = 2 * 1024 * 1024
489+ query_params = {
490+ "file" : file ['path' ],
491+ "version" : project_version
492+ }
493+ length = 0
494+ count = 0
495+ while length < file ['size' ]:
496+ range_header = {"Range" : "bytes={}-{}" .format (length , length + chunk_size )}
497+ resp = self .get ("/v1/project/raw/{}" .format (project_path ), data = query_params , headers = range_header )
498+ # TODO some kind of recovery? do_request already raises exception
499+ if resp .status in [200 , 206 ]:
500+ file_dir = os .path .dirname (os .path .normpath (file ['path' ]))
501+ if file_dir :
502+ if not os .path .exists (os .path .join (directory , file_dir )):
503+ os .makedirs (os .path .join (directory , file_dir ))
504+ with open (os .path .join (directory , file ['path' ] + ".{}" .format (count )), 'wb' ) as output :
505+ output .write (resp .read ())
506+ length += (chunk_size + 1 )
507+ count += 1
508+
509+ # merge chunks together (maybe do checksum check? (might be costly))
510+ with open (os .path .join (directory , file ['path' ]), 'wb' ) as final :
511+ for i in range (count ):
512+ with open (os .path .join (directory , file ['path' ] + ".{}" .format (i )), 'rb' ) as chunk :
513+ shutil .copyfileobj (chunk , final )
514+ os .remove (os .path .join (directory , file ['path' ] + ".{}" .format (i )))
0 commit comments