1919import concurrent .futures
2020
2121from .common import CHUNK_SIZE , ClientError
22- from .merginproject import MerginProject , InvalidProject
23- from .utils import save_to_file
22+ from .merginproject import MerginProject
23+ from .utils import save_to_file , get_versions_with_file_changes
2424
2525
2626# status = download_project_async(...)
@@ -41,21 +41,17 @@ class DownloadJob:
4141 Used for downloading whole projects but also single files.
4242 """
4343
44- def __init__ (self , project_path , total_size , version , update_tasks , download_queue_items ,
45- directory , mp , mc , project_info ):
44+ def __init__ (self , project_path , total_size , version , update_tasks , download_queue_items , directory , mp , project_info ):
4645 self .project_path = project_path
4746 self .total_size = total_size # size of data to download (in bytes)
4847 self .transferred_size = 0
4948 self .version = version
5049 self .update_tasks = update_tasks
5150 self .download_queue_items = download_queue_items
5251 self .directory = directory # project's directory
53- self .mc = mc # MerginClient instance
5452 self .mp = mp # MerginProject instance
5553 self .is_cancelled = False
5654 self .project_info = project_info # parsed JSON with project info returned from the server
57- self .executor = None
58- self .futures = None
5955
6056 def dump (self ):
6157 print ("--- JOB ---" , self .total_size , "bytes" )
@@ -151,7 +147,7 @@ def download_project_async(mc, project_path, directory, project_version=None):
151147
152148 mp .log .info (f"will download { len (update_tasks )} files in { len (download_list )} chunks, total size { total_size } " )
153149
154- job = DownloadJob (project_path , total_size , version , update_tasks , download_list , directory , mp , mc , project_info )
150+ job = DownloadJob (project_path , total_size , version , update_tasks , download_list , directory , mp , project_info )
155151
156152 # start download
157153 job .executor = concurrent .futures .ThreadPoolExecutor (max_workers = 4 )
@@ -178,8 +174,7 @@ def download_project_is_running(job):
178174 """
179175 for future in job .futures :
180176 if future .done () and future .exception () is not None :
181- if job .mp is not None :
182- _cleanup_failed_download (job .directory , job .mp )
177+ _cleanup_failed_download (job .directory , job .mp )
183178 raise future .exception ()
184179 if future .running ():
185180 return True
@@ -239,10 +234,11 @@ class UpdateTask:
239234 """
240235
241236 # TODO: methods other than COPY
242- def __init__ (self , file_path , download_queue_items , destination_file = None ):
237+ def __init__ (self , file_path , download_queue_items , destination_file = None , remove_download_dir = False ):
243238 self .file_path = file_path
244239 self .destination_file = destination_file
245240 self .download_queue_items = download_queue_items
241+ self .remove_download_dir = remove_download_dir
246242
247243 def apply (self , directory , mp ):
248244 """ assemble downloaded chunks into a single file """
@@ -260,14 +256,18 @@ def apply(self, directory, mp):
260256 file_to_merge = FileToMerge (dest_file_path , self .download_queue_items )
261257 file_to_merge .merge ()
262258
263- if mp is None or self .destination_file is not None :
264- # Some tasks (downloading single file) do not require the project to be downloaded.
265- # In that case mp = None and there is no need to copy the file.
266- # Also skip copying if user specified the destination path for the downloaded file
267- return
268- if mp .is_versioned_file (self .file_path ):
259+ # Make a copy of the file to meta dir only if there is no user-specified path for the file.
260+ # destination_file is None for full project download and takes a meaningful value for a single file download.
261+ if mp .is_versioned_file (self .file_path ) and self .destination_file is None :
269262 mp .geodiff .make_copy_sqlite (mp .fpath (self .file_path ), mp .fpath_meta (self .file_path ))
270263
264+ # For single file download, chunks are saved in a temporary dir that needs to be removed manually
265+ if self .remove_download_dir :
266+ # extract download dir from the first download item
267+ download_item = self .download_queue_items [0 ]
268+ download_dir = os .path .dirname (download_item .download_file_path )
269+ shutil .rmtree (download_dir )
270+
271271
272272class DownloadQueueItem :
273273 """ a piece of data from a project that should be downloaded - it can be either a chunk or it can be a diff """
@@ -287,8 +287,7 @@ def __repr__(self):
287287 def download_blocking (self , mc , mp , project_path ):
288288 """ Starts download and only returns once the file has been fully downloaded and saved """
289289
290- log = mc .log if mp is None else mp .log
291- log .debug (f"Downloading { self .file_path } version={ self .version } diff={ self .diff_only } part={ self .part_index } " )
290+ mp .log .debug (f"Downloading { self .file_path } version={ self .version } diff={ self .diff_only } part={ self .part_index } " )
292291 start = self .part_index * (1 + CHUNK_SIZE )
293292 resp = mc .get ("/v1/project/raw/{}" .format (project_path ), data = {
294293 "file" : self .file_path ,
@@ -299,10 +298,10 @@ def download_blocking(self, mc, mp, project_path):
299298 }
300299 )
301300 if resp .status in [200 , 206 ]:
302- log .debug (f"Download finished: { self .file_path } " )
301+ mp . log .debug (f"Download finished: { self .file_path } " )
303302 save_to_file (resp , self .download_file_path )
304303 else :
305- log .error (f"Download failed: { self .file_path } " )
304+ mp . log .error (f"Download failed: { self .file_path } " )
306305 raise ClientError ('Failed to download part {} of file {}' .format (self .part_index , self .file_path ))
307306
308307
@@ -574,19 +573,20 @@ def pull_project_finalize(job):
574573 return conflicts
575574
576575
577- def download_file_async (mc , project_path , file_path , output_file , version ):
576+ def download_file_async (mc , project_dir , file_path , output_file , version ):
578577 """
579- Starts background download project file at specified versions .
578+ Starts background download project file at specified version .
580579 Returns handle to the pending download.
581580 """
582- mc .log .info (f"--- start download { file_path } for { project_path } " )
581+ mp = MerginProject (project_dir )
582+ project_path = mp .metadata ["name" ]
583+ ver_info = f"at version { version } " if version is not None else "at latest version"
584+ mp .log .info (f"Getting { file_path } { ver_info } " )
583585 project_info = mc .project_info (project_path , version = version )
586+ mp .log .info (f"Got project info. version { project_info ['version' ]} " )
584587
585- mc .log .info (f"Got project info. version { project_info ['version' ]} " )
586-
587- # set temporary directory and make sure the destination directory exists
588- temp_dir = os .path .join (tempfile .gettempdir (), "mergin_temp" )
589- os .makedirs (os .path .dirname (output_file ), exist_ok = True )
588+ # set temporary directory for download
589+ temp_dir = tempfile .mkdtemp (prefix = "mergin-py-client-" )
590590
591591 download_list = []
592592 update_tasks = []
@@ -595,25 +595,26 @@ def download_file_async(mc, project_path, file_path, output_file, version):
595595 if file ["path" ] == file_path :
596596 file ['version' ] = version
597597 items = _download_items (file , temp_dir )
598- task = UpdateTask (file ['path' ], items , output_file )
598+ task = UpdateTask (file ['path' ], items , output_file , remove_download_dir = True )
599599 download_list .extend (task .download_queue_items )
600600 for item in task .download_queue_items :
601601 total_size += item .size
602602 update_tasks .append (task )
603603 break
604604 if not download_list :
605605 warn = f"No { file_path } exists at version { version } "
606- mc .log .warning (warn )
606+ mp .log .warning (warn )
607+ shutil .rmtree (temp_dir )
607608 raise ClientError (warn )
608609
609- mc .log .info (f"will download file { file_path } in { len (download_list )} chunks, total size { total_size } " )
610+ mp .log .info (f"will download file { file_path } in { len (download_list )} chunks, total size { total_size } " )
610611 job = DownloadJob (
611- project_path , total_size , version , update_tasks , download_list , temp_dir , None , mc , project_info
612+ project_path , total_size , version , update_tasks , download_list , temp_dir , mp , project_info
612613 )
613614 job .executor = concurrent .futures .ThreadPoolExecutor (max_workers = 4 )
614615 job .futures = []
615616 for item in download_list :
616- future = job .executor .submit (_do_download , item , mc , None , project_path , job )
617+ future = job .executor .submit (_do_download , item , mc , mp , project_path , job )
617618 job .futures .append (future )
618619
619620 return job
@@ -630,13 +631,13 @@ def download_file_finalize(job):
630631 if future .exception () is not None :
631632 raise future .exception ()
632633
633- job .mc .log .info ("--- download finished" )
634+ job .mp .log .info ("--- download finished" )
634635
635636 for task in job .update_tasks :
636637 task .apply (job .directory , job .mp )
637638
638639
639- def download_diffs_async (mc , project_directory , file_path , versions , file_history = None ):
640+ def download_diffs_async (mc , project_directory , file_path , version_from , version_to ):
640641 """
641642 Starts background download project file diffs for specified versions.
642643 Returns handle to the pending download.
@@ -645,35 +646,34 @@ def download_diffs_async(mc, project_directory, file_path, versions, file_histor
645646 mc (MerginClient): MerginClient instance.
646647 project_directory (str): local project directory.
647648 file_path (str): file path relative to Mergin project root.
648- versions ([ str] ): list of tags of versions to fetch , for example: ["v2", "v5"] .
649- file_history (dict ): optional file history info, result of MerginClient.project_file_history_info()
649+ version_from ( str): starting project version tag for getting diff , for example 'v3' .
650+ version_to (str ): ending project version tag for getting diff.
650651
651652 Returns:
652653 PullJob/None: a handle for the pending download.
653654 """
654- try :
655- mp = MerginProject (project_directory )
656- except InvalidProject as e :
657- mc .log .error (f"Couldn't create Mergin project for directory: { project_directory } : { repr (e )} " )
658- return None
655+ mp = MerginProject (project_directory )
659656 project_path = mp .metadata ["name" ]
657+ file_history = mc .project_file_history_info (project_path , file_path )
658+ versions_to_fetch = get_versions_with_file_changes (
659+ mc , project_path , file_path , version_from = version_from , version_to = version_to , file_history = file_history
660+ )
660661 mp .log .info (f"--- version: { mc .user_agent_info ()} " )
661- mp .log .info (f"--- start download diffs for { file_path } , versions: { [v for v in versions ]} " )
662+ mp .log .info (f"--- start download diffs for { file_path } of { project_path } , versions: { [v for v in versions_to_fetch ]} " )
662663
663664 try :
664665 server_info = mc .project_info (project_path )
665666 if file_history is None :
666667 file_history = mc .project_file_history_info (project_path , file_path )
667668 except ClientError as err :
668669 mp .log .error ("Error getting project info: " + str (err ))
669- mp .log .info ("--- pull aborted" )
670+ mp .log .info ("--- downloading diffs aborted" )
670671 raise
671672
672- temp_dir = os .path .join (tempfile .gettempdir (), "mergin_temp" )
673- os .makedirs (temp_dir , exist_ok = True )
673+ temp_dir = tempfile .mkdtemp (prefix = "mergin-py-client-" )
674674 fetch_files = []
675675
676- for version in versions [1 :]:
676+ for version in versions_to_fetch [1 :]:
677677 version_data = file_history ["history" ][version ]
678678 diff_data = copy .deepcopy (version_data )
679679 diff_data ['version' ] = version
@@ -715,7 +715,7 @@ def download_diffs_finalize(job, output_diff):
715715 for future in job .futures :
716716 if future .exception () is not None :
717717 job .mp .log .error ("Error while pulling data: " + str (future .exception ()))
718- job .mp .log .info ("--- pull aborted" )
718+ job .mp .log .info ("--- diffs download aborted" )
719719 raise future .exception ()
720720
721721 job .mp .log .info ("finalizing diffs pull" )
@@ -731,17 +731,23 @@ def download_diffs_finalize(job, output_diff):
731731
732732 job .mp .log .info ("--- diffs pull finished" )
733733
734- # Concatenate diffs, if needed
734+ # Collect and finally concatenate diffs, if needed
735735 diffs = []
736736 for file_to_merge in job .files_to_merge :
737737 diffs .append (file_to_merge .dest_file )
738738
739739 output_dir = os .path .dirname (output_diff )
740+ temp_dir = None
740741 if len (diffs ) >= 1 :
741742 os .makedirs (output_dir , exist_ok = True )
743+ temp_dir = os .path .dirname (diffs [0 ])
742744 if len (diffs ) > 1 :
743745 job .mp .geodiff .concat_changes (diffs , output_diff )
744746 elif len (diffs ) == 1 :
745747 shutil .copy (diffs [0 ], output_diff )
746748 for diff in diffs :
747749 os .remove (diff )
750+
751+ # remove the diffs download temporary directory
752+ if temp_dir is not None :
753+ shutil .rmtree (temp_dir )
0 commit comments