From 9ecbb3e9bfae5147c8eefec53c08baef4a16af3b Mon Sep 17 00:00:00 2001 From: Alexander Bruy Date: Wed, 23 Feb 2022 19:52:44 +0200 Subject: [PATCH 1/4] functions to generate names for conflict files function to generate unique file names --- mergin/utils.py | 81 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/mergin/utils.py b/mergin/utils.py index b6010258..d8b86060 100644 --- a/mergin/utils.py +++ b/mergin/utils.py @@ -5,6 +5,7 @@ import re import sqlite3 from datetime import datetime +from pathlib import Path from .common import ClientError @@ -145,3 +146,83 @@ def get_versions_with_file_changes( idx_to = idx break return [f"v{ver_nr}" for ver_nr in all_version_numbers[idx_from:idx_to + 1]] + + +def unique_file_name(path): + """ + Generates an unique name for the given path. If the given path does + not exist yet it will be returned unchanged, otherwise a sequntial + number will be added to the path in format: + - if path is a directory: "folder" -> "folder (1)" + - if path is a file: "filename.txt" -> "filename (1).txt" + + :param path: path to file or directory + :type path: str + :returns: unique path + :rtype: str + """ + unique_path = str(path) + + is_dir = os.path.isdir(path) + head, tail = os.path.split(os.path.normpath(path)) + ext = ''.join(Path(tail).suffixes) + file_name = tail.replace(ext, '') + + i = 0 + while os.path.exists(unique_path): + i += 1 + + if is_dir: + unique_path = f"{path} ({i})" + else: + unique_path = os.path.join(head, file_name) + f" ({i}){ext}" + + return unique_path + + +def conflict_copy_file_name(path, user, version): + """ + Generates a file name for the conflict copy file in the following form + (conflicted copy, v).gpkg. Example: + * data (conflicted copy, martin v5).gpkg + + :param path: name of the file + :type path: str + :param user: name of the user + :type user: str + :param version: version of the mergin project + :type version: str + :returns: new file name + :rtype: str + """ + if not path: + return '' + + head, tail = os.path.split(os.path.normpath(path)) + ext = ''.join(Path(tail).suffixes) + file_name = tail.replace(ext, '') + return os.path.join(head, file_name) + f" (conflicted copy, {user} v{version}){ext}" + + +def edit_conflict_file_name(path, user, version): + """ + Generates a file name for the edit conflict file in the following form + (edit conflict, v).gpkg. Example: + * data (edit conflict, martin v5).gpkg + + :param path: name of the file + :type path: str + :param user: name of the user + :type user: str + :param version: version of the mergin project + :type version: str + :returns: new file name + :rtype: str + """ + if not path: + return '' + + head, tail = os.path.split(os.path.normpath(path)) + ext = ''.join(Path(tail).suffixes) + file_name = tail.replace(ext, '') + return os.path.join(head, file_name) + f" (edit conflict, {user} v{version}).json" From c6301771592a38ec1d271aae64e6b34447b3ef92 Mon Sep 17 00:00:00 2001 From: Alexander Bruy Date: Wed, 23 Feb 2022 19:53:37 +0200 Subject: [PATCH 2/4] tests for file names generation --- mergin/test/test_client.py | 111 ++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 3 deletions(-) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 80c38252..7fdb3005 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -11,7 +11,13 @@ from ..client import MerginClient, ClientError, MerginProject, LoginError, decode_token_data, TokenError from ..client_push import push_project_async, push_project_cancel -from ..utils import generate_checksum, get_versions_with_file_changes +from ..utils import ( + generate_checksum, + get_versions_with_file_changes, + unique_file_name, + conflict_copy_file_name, + edit_conflict_file_name +) from ..merginproject import pygeodiff @@ -133,7 +139,7 @@ def test_create_remote_project_from_local(mc): assert f in downloads['dir'] if mp.is_versioned_file(f): assert f in downloads['meta'] - + # unable to download to the same directory with pytest.raises(Exception, match='Project directory already exists'): mc.download_project(project, download_dir) @@ -588,7 +594,7 @@ def test_available_storage_validation(mc): except ClientError as e: # Expecting "Storage limit has been reached" error msg. assert str(e).startswith("Storage limit has been reached") - got_right_err = True + got_right_err = True assert got_right_err # Expecting empty project @@ -1304,3 +1310,102 @@ def test_rebase_success(mc, extra_connection): assert _get_table_row_count(test_gpkg, 'simple') == 5 assert _get_table_row_count(test_gpkg_basefile, 'simple') == 4 + + +def test_conflict_file_names(): + """ + Test generation of file names for conflicts files. + """ + + data = [ + ('/home/test/geo.gpkg', 'jack', 10, '/home/test/geo (conflicted copy, jack v10).gpkg'), + ('/home/test/g.pkg', 'j', 0, '/home/test/g (conflicted copy, j v0).pkg'), + ('home/test/geo.gpkg', 'jack', 10, 'home/test/geo (conflicted copy, jack v10).gpkg'), + ('geo.gpkg', 'jack', 10, 'geo (conflicted copy, jack v10).gpkg'), + ('/home/../geo.gpkg', 'jack', 10, '/geo (conflicted copy, jack v10).gpkg'), + ('/home/./geo.gpkg', 'jack', 10, '/home/geo (conflicted copy, jack v10).gpkg'), + ('/home/test/geo.gpkg', '', 10, '/home/test/geo (conflicted copy, v10).gpkg'), + ('/home/test/geo.gpkg', 'jack', -1, '/home/test/geo (conflicted copy, jack v-1).gpkg'), + ('/home/test/geo.tar.gz', 'jack', 100, '/home/test/geo (conflicted copy, jack v100).tar.gz'), + ('', 'jack', 1, '' ) + ] + + for i in data: + file_name = conflict_copy_file_name(i[0], i[1], i[2]) + assert file_name == i[3] + + + data = [ + ('/home/test/geo.json', 'jack', 10, '/home/test/geo (edit conflict, jack v10).json'), + ('/home/test/g.jsn', 'j', 0, '/home/test/g (edit conflict, j v0).json'), + ('home/test/geo.json', 'jack', 10, 'home/test/geo (edit conflict, jack v10).json'), + ('geo.json', 'jack', 10, 'geo (edit conflict, jack v10).json'), + ('/home/../geo.json', 'jack', 10, '/geo (edit conflict, jack v10).json'), + ('/home/./geo.json', 'jack', 10, '/home/geo (edit conflict, jack v10).json'), + ('/home/test/geo.json', '', 10, '/home/test/geo (edit conflict, v10).json'), + ('/home/test/geo.json', 'jack', -1, '/home/test/geo (edit conflict, jack v-1).json'), + ('/home/test/geo.gpkg', 'jack', 10, '/home/test/geo (edit conflict, jack v10).json'), + ('/home/test/geo.tar.gz', 'jack', 100, '/home/test/geo (edit conflict, jack v100).json'), + ('', 'jack', 1, '') + ] + + for i in data: + file_name = edit_conflict_file_name(i[0], i[1], i[2]) + assert file_name == i[3] + + +def test_unique_file_names(): + """ + Test generation of unique file names. + """ + project_dir = os.path.join(TMP_DIR, 'unique_file_names') + + remove_folders([project_dir]) + + os.makedirs(project_dir) + assert os.path.exists(project_dir) + assert not any(os.scandir(project_dir)) + + # Create test directory structure: + # - folderA + # |- fileA.txt + # |- fileA (1).txt + # |- fileB.txt + # |- folderAB + # |- folderAB (1) + # - file.txt + # - another.txt + # - another (1).txt + # - another (2).txt + # - arch.tar.gz + data = {'folderA': {'files': ['fileA.txt', 'fileA (1).txt', 'fileB.txt'], 'folderAB': {}, 'folderAB (1)': {}}, 'files': ['file.txt', 'another.txt', 'another (1).txt', 'another (2).txt', 'arch.tar.gz']} + create_directory(project_dir, data) + + data = [ + ('file.txt', 'file (1).txt'), + ('another.txt', 'another (3).txt'), + ('folderA', 'folderA (1)'), + ('non.txt', 'non.txt'), + ('data.gpkg', 'data.gpkg'), + ('arch.tar.gz', 'arch (1).tar.gz'), + ('folderA/folder', 'folderA/folder'), + ('folderA/fileA.txt', 'folderA/fileA (2).txt'), + ('folderA/fileB.txt', 'folderA/fileB (1).txt'), + ('folderA/fileC.txt', 'folderA/fileC.txt'), + ('folderA/folderAB', 'folderA/folderAB (2)'), + ] + for i in data: + file_name = unique_file_name(os.path.join(project_dir, i[0])) + assert file_name == os.path.join(project_dir, i[1]) + + +def create_directory(root, data): + for k, v in data.items(): + if isinstance(v, dict): + dir_name = os.path.join(root, k) + os.makedirs(dir_name, exist_ok=True) + for kk in v.keys(): + create_directory(dir_name, v) + elif isinstance(v, list): + for file_name in v: + open(os.path.join(root, file_name), 'w').close() From 8636feae855d159645e62995f35bba451e546f68 Mon Sep 17 00:00:00 2001 From: Alexander Bruy Date: Thu, 24 Feb 2022 15:25:19 +0200 Subject: [PATCH 3/4] use new names for conflict files in the client and update tests --- mergin/client.py | 2 +- mergin/client_pull.py | 40 +++++++++++++++++++------------------- mergin/merginproject.py | 38 +++++++++++++++++++++++------------- mergin/test/test_client.py | 10 +++++----- 4 files changed, 51 insertions(+), 39 deletions(-) diff --git a/mergin/client.py b/mergin/client.py index 8b31ea1d..c2f1ceae 100644 --- a/mergin/client.py +++ b/mergin/client.py @@ -614,7 +614,7 @@ def pull_project(self, directory): if job is None: return # project is up to date pull_project_wait(job) - return pull_project_finalize(job) + return pull_project_finalize(job, self._auth_params['login']) def clone_project(self, source_project_path, cloned_project_name, cloned_project_namespace=None): """ diff --git a/mergin/client_pull.py b/mergin/client_pull.py index 6812764a..b41430e8 100644 --- a/mergin/client_pull.py +++ b/mergin/client_pull.py @@ -40,7 +40,7 @@ class DownloadJob: Keeps all the important data about a pending download job. Used for downloading whole projects but also single files. """ - + def __init__(self, project_path, total_size, version, update_tasks, download_queue_items, directory, mp, project_info): self.project_path = project_path self.total_size = total_size # size of data to download (in bytes) @@ -70,13 +70,13 @@ def _download_items(file, directory, diff_only=False): basename = os.path.basename(file['diff']['path']) if diff_only else os.path.basename(file['path']) file_size = file['diff']['size'] if diff_only else file['size'] chunks = math.ceil(file_size / CHUNK_SIZE) - + items = [] for part_index in range(chunks): download_file_path = os.path.join(file_dir, basename + ".{}".format(part_index)) size = min(CHUNK_SIZE, file_size - part_index * CHUNK_SIZE) items.append(DownloadQueueItem(file['path'], size, file['version'], diff_only, part_index, download_file_path)) - + return items @@ -84,7 +84,7 @@ def _do_download(item, mc, mp, project_path, job): """ runs in worker thread """ if job.is_cancelled: return - + # TODO: make download_blocking / save_to_file cancellable so that we can cancel as soon as possible item.download_blocking(mc, mp, project_path) @@ -153,9 +153,9 @@ def download_project_async(mc, project_path, directory, project_version=None): total_size += item.size mp.log.info(f"will download {len(update_tasks)} files in {len(download_list)} chunks, total size {total_size}") - + job = DownloadJob(project_path, total_size, version, update_tasks, download_list, directory, mp, project_info) - + # start download job.executor = concurrent.futures.ThreadPoolExecutor(max_workers=4) job.futures = [] @@ -168,7 +168,7 @@ def download_project_async(mc, project_path, directory, project_version=None): def download_project_wait(job): """ blocks until all download tasks are finished """ - + concurrent.futures.wait(job.futures) @@ -209,10 +209,10 @@ def download_project_finalize(job): job.mp.log.info("--- download finished") for task in job.update_tasks: - + # right now only copy tasks... task.apply(job.directory, job.mp) - + # final update of project metadata # TODO: why not exact copy of project info JSON ? job.mp.metadata = { @@ -239,7 +239,7 @@ class UpdateTask: Entry for each file that will be updated. At the end of a successful download of new data, all the tasks are executed. """ - + # TODO: methods other than COPY def __init__(self, file_path, download_queue_items, destination_file=None, latest_version=True): self.file_path = file_path @@ -249,7 +249,7 @@ def __init__(self, file_path, download_queue_items, destination_file=None, lates def apply(self, directory, mp): """ assemble downloaded chunks into a single file """ - + if self.destination_file is None: basename = os.path.basename(self.file_path) file_dir = os.path.dirname(os.path.normpath(os.path.join(directory, self.file_path))) @@ -273,7 +273,7 @@ def apply(self, directory, mp): class DownloadQueueItem: """ a piece of data from a project that should be downloaded - it can be either a chunk or it can be a diff """ - + def __init__(self, file_path, size, version, diff_only, part_index, download_file_path): self.file_path = file_path # relative path to the file within project self.size = size # size of the item in bytes @@ -281,7 +281,7 @@ def __init__(self, file_path, size, version, diff_only, part_index, download_fil self.diff_only = diff_only # whether downloading diff or full version self.part_index = part_index # index of the chunk self.download_file_path = download_file_path # full path to a temporary file which will receive the content - + def __repr__(self): return "".format( self.file_path, self.version, self.diff_only, self.part_index, self.size, self.download_file_path) @@ -397,7 +397,7 @@ def pull_project_async(mc, directory): for file in fetch_files: diff_only = _pulling_file_with_diffs(file) items = _download_items(file, temp_dir, diff_only) - + # figure out destination path for the file file_dir = os.path.dirname(os.path.normpath(os.path.join(temp_dir, file['path']))) basename = os.path.basename(file['diff']['path']) if diff_only else os.path.basename(file['path']) @@ -447,13 +447,13 @@ def pull_project_async(mc, directory): for item in download_list: future = job.executor.submit(_do_download, item, mc, mp, project_path, job) job.futures.append(future) - + return job def pull_project_wait(job): """ blocks until all download tasks are finished """ - + concurrent.futures.wait(job.futures) @@ -513,16 +513,16 @@ def merge(self): raise ClientError('Download of file {} failed. Please try it again.'.format(self.dest_file)) -def pull_project_finalize(job): +def pull_project_finalize(job, user_name): """ To be called when pull in the background is finished and we need to do the finalization (merge chunks etc.) - + This should not be called from a worker thread (e.g. directly from a handler when download is complete) If any of the workers has thrown any exception, it will be re-raised (e.g. some network errors). That also means that the whole job has been aborted. """ - + job.executor.shutdown(wait=True) # make sure any exceptions from threads are not lost @@ -565,7 +565,7 @@ def pull_project_finalize(job): os.remove(basefile) raise ClientError("Cannot patch basefile {}! Please try syncing again.".format(basefile)) - conflicts = job.mp.apply_pull_changes(job.pull_changes, job.temp_dir) + conflicts = job.mp.apply_pull_changes(job.pull_changes, job.temp_dir, user_name) job.mp.metadata = { 'name': job.project_path, 'version': job.version if job.version else "v0", # for new projects server version is "" diff --git a/mergin/merginproject.py b/mergin/merginproject.py index 862eeeb3..253356a7 100644 --- a/mergin/merginproject.py +++ b/mergin/merginproject.py @@ -10,7 +10,16 @@ from dateutil.tz import tzlocal from .common import UPLOAD_CHUNK_SIZE, InvalidProject, ClientError -from .utils import generate_checksum, move_file, int_version, find, do_sqlite_checkpoint +from .utils import ( + generate_checksum, + move_file, + int_version, + find, + do_sqlite_checkpoint, + unique_file_name, + conflict_copy_file_name, + edit_conflict_file_name +) this_dir = os.path.dirname(os.path.realpath(__file__)) @@ -373,7 +382,7 @@ def get_list_of_push_changes(self, push_changes): pass return changes - def apply_pull_changes(self, changes, temp_dir): + def apply_pull_changes(self, changes, temp_dir, user_name): """ Apply changes pulled from server. @@ -410,7 +419,7 @@ def apply_pull_changes(self, changes, temp_dir): # 'src' here is server version of file and 'dest' is locally modified if self.is_versioned_file(path) and k == 'updated': if path in modified: - conflict = self.update_with_rebase(path, src, dest, basefile, temp_dir) + conflict = self.update_with_rebase(path, src, dest, basefile, temp_dir, user_name) if conflict: conflicts.append(conflict) else: @@ -420,7 +429,7 @@ def apply_pull_changes(self, changes, temp_dir): else: # backup if needed if path in modified and item['checksum'] != local_files_map[path]['checksum']: - conflict = self.backup_file(path) + conflict = self.backup_file(path, user_name) conflicts.append(conflict) if k == 'removed': @@ -440,7 +449,7 @@ def apply_pull_changes(self, changes, temp_dir): return conflicts - def update_with_rebase(self, path, src, dest, basefile, temp_dir): + def update_with_rebase(self, path, src, dest, basefile, temp_dir, user_name): """ Update a versioned file with rebase. @@ -472,6 +481,7 @@ def update_with_rebase(self, path, src, dest, basefile, temp_dir): # create temp backup (ideally with geodiff) of locally modified file if needed later f_conflict_file = self.fpath(f'{path}-local_backup', temp_dir) + try: self.geodiff.create_changeset(basefile, dest, local_diff) self.geodiff.make_copy_sqlite(basefile, f_conflict_file) @@ -483,7 +493,11 @@ def update_with_rebase(self, path, src, dest, basefile, temp_dir): # in case there will be any conflicting operations found during rebase, # they will be stored in a JSON file - if there are no conflicts, the file # won't even be created - rebase_conflicts = self.fpath(f'{path}_rebase_conflicts') + #rebase_conflicts = self.fpath(f'{path}_rebase_conflicts') + + # FiXME: how to get user name? + rebase_conflicts = unique_file_name( + edit_conflict_file_name(self.fpath(path), user_name, int_version(self.metadata['version']))) # try to do rebase magic try: @@ -496,7 +510,7 @@ def update_with_rebase(self, path, src, dest, basefile, temp_dir): self.log.warning("rebase failed! going to create conflict file") # it would not be possible to commit local changes, they need to end up in new conflict file self.geodiff.make_copy_sqlite(f_conflict_file, dest) - conflict = self.backup_file(path) + conflict = self.backup_file(path, user_name) # original file synced with server self.geodiff.make_copy_sqlite(f_server_backup, basefile) self.geodiff.make_copy_sqlite(f_server_backup, dest) @@ -577,7 +591,7 @@ def apply_push_changes(self, changes): else: pass - def backup_file(self, file): + def backup_file(self, file, user_name): """ Create backup file next to its origin. @@ -589,11 +603,9 @@ def backup_file(self, file): src = self.fpath(file) if not os.path.exists(src): return - backup_path = self.fpath(f'{file}_conflict_copy') - index = 2 - while os.path.exists(backup_path): - backup_path = self.fpath(f'{file}_conflict_copy{index}') - index += 1 + + backup_path = unique_file_name(conflict_copy_file_name(self.fpath(file), user_name, int_version(self.metadata['version']))) + if self.is_versioned_file(file): self.geodiff.make_copy_sqlite(src, backup_path) else: diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 7fdb3005..7974ad1a 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -220,8 +220,8 @@ def test_push_pull_changes(mc): assert not os.path.exists(os.path.join(project_dir_2, f_removed)) assert not os.path.exists(os.path.join(project_dir_2, f_renamed)) assert os.path.exists(os.path.join(project_dir_2, 'renamed.txt')) - assert os.path.exists(os.path.join(project_dir_2, f_updated+'_conflict_copy')) - assert generate_checksum(os.path.join(project_dir_2, f_updated+'_conflict_copy')) == f_conflict_checksum + assert os.path.exists(os.path.join(project_dir_2, conflict_copy_file_name(f_updated, API_USER, 1))) + assert generate_checksum(os.path.join(project_dir_2, conflict_copy_file_name(f_updated, API_USER, 1))) == f_conflict_checksum assert generate_checksum(os.path.join(project_dir_2, f_updated)) == f_remote_checksum @@ -1141,7 +1141,7 @@ def test_rebase_local_schema_change(mc, extra_connection): project_dir_2 = os.path.join(TMP_DIR, test_project+'_2') # concurrent project dir test_gpkg = os.path.join(project_dir, 'test.gpkg') test_gpkg_basefile = os.path.join(project_dir, '.mergin', 'test.gpkg') - test_gpkg_conflict = test_gpkg + '_conflict_copy' + test_gpkg_conflict = conflict_copy_file_name(test_gpkg, API_USER, 1) cleanup(mc, project, [project_dir, project_dir_2]) os.makedirs(project_dir) @@ -1207,7 +1207,7 @@ def test_rebase_remote_schema_change(mc, extra_connection): test_gpkg = os.path.join(project_dir, 'test.gpkg') test_gpkg_2 = os.path.join(project_dir_2, 'test.gpkg') test_gpkg_basefile = os.path.join(project_dir, '.mergin', 'test.gpkg') - test_gpkg_conflict = test_gpkg + '_conflict_copy' + test_gpkg_conflict = conflict_copy_file_name(test_gpkg, API_USER, 1) cleanup(mc, project, [project_dir, project_dir_2]) os.makedirs(project_dir) @@ -1272,7 +1272,7 @@ def test_rebase_success(mc, extra_connection): test_gpkg = os.path.join(project_dir, 'test.gpkg') test_gpkg_2 = os.path.join(project_dir_2, 'test.gpkg') test_gpkg_basefile = os.path.join(project_dir, '.mergin', 'test.gpkg') - test_gpkg_conflict = test_gpkg + '_conflict_copy' + test_gpkg_conflict = conflict_copy_file_name(test_gpkg, API_USER, 1) cleanup(mc, project, [project_dir, project_dir_2]) os.makedirs(project_dir) From ed53085f972b31aea14ec8e6d1c95543c8710e33 Mon Sep 17 00:00:00 2001 From: Alexander Bruy Date: Mon, 28 Feb 2022 19:34:46 +0200 Subject: [PATCH 4/4] address review --- mergin/client.py | 2 +- mergin/merginproject.py | 21 +++++++++------------ mergin/test/test_client.py | 20 ++++++++++---------- mergin/utils.py | 4 ++-- 4 files changed, 22 insertions(+), 25 deletions(-) diff --git a/mergin/client.py b/mergin/client.py index c2f1ceae..dc991194 100644 --- a/mergin/client.py +++ b/mergin/client.py @@ -614,7 +614,7 @@ def pull_project(self, directory): if job is None: return # project is up to date pull_project_wait(job) - return pull_project_finalize(job, self._auth_params['login']) + return pull_project_finalize(job, self.username()) def clone_project(self, source_project_path, cloned_project_name, cloned_project_namespace=None): """ diff --git a/mergin/merginproject.py b/mergin/merginproject.py index 253356a7..4ca7d51e 100644 --- a/mergin/merginproject.py +++ b/mergin/merginproject.py @@ -16,8 +16,8 @@ int_version, find, do_sqlite_checkpoint, - unique_file_name, - conflict_copy_file_name, + unique_path_name, + conflicted_copy_file_name, edit_conflict_file_name ) @@ -429,7 +429,7 @@ def apply_pull_changes(self, changes, temp_dir, user_name): else: # backup if needed if path in modified and item['checksum'] != local_files_map[path]['checksum']: - conflict = self.backup_file(path, user_name) + conflict = self.create_conflicted_copy(path, user_name) conflicts.append(conflict) if k == 'removed': @@ -493,10 +493,7 @@ def update_with_rebase(self, path, src, dest, basefile, temp_dir, user_name): # in case there will be any conflicting operations found during rebase, # they will be stored in a JSON file - if there are no conflicts, the file # won't even be created - #rebase_conflicts = self.fpath(f'{path}_rebase_conflicts') - - # FiXME: how to get user name? - rebase_conflicts = unique_file_name( + rebase_conflicts = unique_path_name( edit_conflict_file_name(self.fpath(path), user_name, int_version(self.metadata['version']))) # try to do rebase magic @@ -510,7 +507,7 @@ def update_with_rebase(self, path, src, dest, basefile, temp_dir, user_name): self.log.warning("rebase failed! going to create conflict file") # it would not be possible to commit local changes, they need to end up in new conflict file self.geodiff.make_copy_sqlite(f_conflict_file, dest) - conflict = self.backup_file(path, user_name) + conflict = self.create_conflicted_copy(path, user_name) # original file synced with server self.geodiff.make_copy_sqlite(f_server_backup, basefile) self.geodiff.make_copy_sqlite(f_server_backup, dest) @@ -591,20 +588,20 @@ def apply_push_changes(self, changes): else: pass - def backup_file(self, file, user_name): + def create_conflicted_copy(self, file, user_name): """ - Create backup file next to its origin. + Create conflicted copy file next to its origin. :param file: path of file in project :type file: str - :returns: path to backupfile + :returns: path to conflicted copy :rtype: str """ src = self.fpath(file) if not os.path.exists(src): return - backup_path = unique_file_name(conflict_copy_file_name(self.fpath(file), user_name, int_version(self.metadata['version']))) + backup_path = unique_path_name(conflicted_copy_file_name(self.fpath(file), user_name, int_version(self.metadata['version']))) if self.is_versioned_file(file): self.geodiff.make_copy_sqlite(src, backup_path) diff --git a/mergin/test/test_client.py b/mergin/test/test_client.py index 7974ad1a..13889390 100644 --- a/mergin/test/test_client.py +++ b/mergin/test/test_client.py @@ -14,8 +14,8 @@ from ..utils import ( generate_checksum, get_versions_with_file_changes, - unique_file_name, - conflict_copy_file_name, + unique_path_name, + conflicted_copy_file_name, edit_conflict_file_name ) from ..merginproject import pygeodiff @@ -220,8 +220,8 @@ def test_push_pull_changes(mc): assert not os.path.exists(os.path.join(project_dir_2, f_removed)) assert not os.path.exists(os.path.join(project_dir_2, f_renamed)) assert os.path.exists(os.path.join(project_dir_2, 'renamed.txt')) - assert os.path.exists(os.path.join(project_dir_2, conflict_copy_file_name(f_updated, API_USER, 1))) - assert generate_checksum(os.path.join(project_dir_2, conflict_copy_file_name(f_updated, API_USER, 1))) == f_conflict_checksum + assert os.path.exists(os.path.join(project_dir_2, conflicted_copy_file_name(f_updated, API_USER, 1))) + assert generate_checksum(os.path.join(project_dir_2, conflicted_copy_file_name(f_updated, API_USER, 1))) == f_conflict_checksum assert generate_checksum(os.path.join(project_dir_2, f_updated)) == f_remote_checksum @@ -1141,7 +1141,7 @@ def test_rebase_local_schema_change(mc, extra_connection): project_dir_2 = os.path.join(TMP_DIR, test_project+'_2') # concurrent project dir test_gpkg = os.path.join(project_dir, 'test.gpkg') test_gpkg_basefile = os.path.join(project_dir, '.mergin', 'test.gpkg') - test_gpkg_conflict = conflict_copy_file_name(test_gpkg, API_USER, 1) + test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, API_USER, 1) cleanup(mc, project, [project_dir, project_dir_2]) os.makedirs(project_dir) @@ -1207,7 +1207,7 @@ def test_rebase_remote_schema_change(mc, extra_connection): test_gpkg = os.path.join(project_dir, 'test.gpkg') test_gpkg_2 = os.path.join(project_dir_2, 'test.gpkg') test_gpkg_basefile = os.path.join(project_dir, '.mergin', 'test.gpkg') - test_gpkg_conflict = conflict_copy_file_name(test_gpkg, API_USER, 1) + test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, API_USER, 1) cleanup(mc, project, [project_dir, project_dir_2]) os.makedirs(project_dir) @@ -1272,7 +1272,7 @@ def test_rebase_success(mc, extra_connection): test_gpkg = os.path.join(project_dir, 'test.gpkg') test_gpkg_2 = os.path.join(project_dir_2, 'test.gpkg') test_gpkg_basefile = os.path.join(project_dir, '.mergin', 'test.gpkg') - test_gpkg_conflict = conflict_copy_file_name(test_gpkg, API_USER, 1) + test_gpkg_conflict = conflicted_copy_file_name(test_gpkg, API_USER, 1) cleanup(mc, project, [project_dir, project_dir_2]) os.makedirs(project_dir) @@ -1331,7 +1331,7 @@ def test_conflict_file_names(): ] for i in data: - file_name = conflict_copy_file_name(i[0], i[1], i[2]) + file_name = conflicted_copy_file_name(i[0], i[1], i[2]) assert file_name == i[3] @@ -1354,7 +1354,7 @@ def test_conflict_file_names(): assert file_name == i[3] -def test_unique_file_names(): +def test_unique_path_names(): """ Test generation of unique file names. """ @@ -1395,7 +1395,7 @@ def test_unique_file_names(): ('folderA/folderAB', 'folderA/folderAB (2)'), ] for i in data: - file_name = unique_file_name(os.path.join(project_dir, i[0])) + file_name = unique_path_name(os.path.join(project_dir, i[0])) assert file_name == os.path.join(project_dir, i[1]) diff --git a/mergin/utils.py b/mergin/utils.py index d8b86060..71d66d4f 100644 --- a/mergin/utils.py +++ b/mergin/utils.py @@ -148,7 +148,7 @@ def get_versions_with_file_changes( return [f"v{ver_nr}" for ver_nr in all_version_numbers[idx_from:idx_to + 1]] -def unique_file_name(path): +def unique_path_name(path): """ Generates an unique name for the given path. If the given path does not exist yet it will be returned unchanged, otherwise a sequntial @@ -180,7 +180,7 @@ def unique_file_name(path): return unique_path -def conflict_copy_file_name(path, user, version): +def conflicted_copy_file_name(path, user, version): """ Generates a file name for the conflict copy file in the following form (conflicted copy, v).gpkg. Example: