Skip to content

Commit fb15025

Browse files
committed
Changes suggested in the review.
1 parent af555c7 commit fb15025

4 files changed

Lines changed: 99 additions & 121 deletions

File tree

mergin/client.py

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -640,21 +640,9 @@ def download_file(self, project_dir, file_path, output_filename, version=None):
640640
:param version: optional version tag for downloaded file
641641
:type version: String
642642
"""
643-
try:
644-
mp = MerginProject(project_dir)
645-
except InvalidProject as e:
646-
err = f"Couldn't create Mergin project for {project_dir}: {repr(e)}"
647-
self.log.error(err)
648-
raise ClientError(err)
649-
650-
project_path = mp.metadata["name"]
651-
ver_info = f"at version {version}" if version is not None else "at latest version"
652-
self.log.info(f"Getting {file_path} from {project_path} {ver_info}")
653-
job = download_file_async(self, project_path, file_path, output_filename, version=version)
654-
if job is None:
655-
return
643+
job = download_file_async(self, project_dir, file_path, output_filename, version=version)
656644
pull_project_wait(job)
657-
return download_file_finalize(job)
645+
download_file_finalize(job)
658646

659647
def get_file_diff(self, project_dir, file_path, output_diff, version_from, version_to):
660648
""" Create concatenated diff for project file diffs between versions version_from and version_to.
@@ -670,23 +658,6 @@ def get_file_diff(self, project_dir, file_path, output_diff, version_from, versi
670658
:param version_to: ending project version tag for getting diff
671659
:type version_to: String
672660
"""
673-
try:
674-
mp = MerginProject(project_dir)
675-
except InvalidProject as e:
676-
err = f"Couldn't create Mergin project for {project_dir}: {repr(e)}"
677-
self.log.error(err)
678-
raise ClientError(err)
679-
680-
project_path = mp.metadata["name"]
681-
self.log.info(f"Getting diffs for file {file_path} of {project_path}")
682-
file_history = self.project_file_history_info(project_path, file_path)
683-
versions_to_fetch = get_versions_with_file_changes(
684-
self, project_path, file_path, version_from=version_from, version_to=version_to, file_history=file_history
685-
)
686-
job = download_diffs_async(self, project_dir, file_path, versions_to_fetch, file_history=file_history)
687-
if job is None:
688-
return
661+
job = download_diffs_async(self, project_dir, file_path, version_from, version_to)
689662
pull_project_wait(job)
690-
691-
# finalize getting diffs
692663
download_diffs_finalize(job, output_diff)

mergin/client_pull.py

Lines changed: 56 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
import concurrent.futures
2020

2121
from .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

272272
class 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

Comments
 (0)