From afcf76d0fe14221061d92a580d064cf7c9a12245 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 25 Feb 2026 14:06:59 +0100 Subject: [PATCH 1/3] Fix subfolders geopackages diffs - add sanitizations fo recurssion --- server/mergin/sync/models.py | 18 ++++++++- server/mergin/sync/public_api_v2.yaml | 6 +-- .../mergin/sync/public_api_v2_controller.py | 9 ++++- .../test_projects/test/test_dir/test.gpkg | Bin 0 -> 98304 bytes server/mergin/tests/test_public_api_v2.py | 36 ++++++++++++++++-- 5 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 server/mergin/tests/test_projects/test/test_dir/test.gpkg diff --git a/server/mergin/sync/models.py b/server/mergin/sync/models.py index 3358ba35..ad794ffa 100644 --- a/server/mergin/sync/models.py +++ b/server/mergin/sync/models.py @@ -411,6 +411,12 @@ def get_delta_changes( result.extend(DeltaChangeSchema(many=True).load(existing_delta.changes)) continue + if checkpoint.rank == 0: + raise ValueError( + f"Missing expected checkpoint of rank 0 for version {checkpoint.end}" + ) + return + # If higher rank delta checkopoint does not exists we need to create it if checkpoint.rank > 0: new_checkpoint = ProjectVersionDelta.create_checkpoint( @@ -918,6 +924,11 @@ def construct_checkpoint(self) -> bool: if os.path.exists(self.abs_path): return True + if self.rank == 0: + raise ValueError( + "Checkpoint of rank 0 should be created by user upload, cannot be constructed" + ) + # merged diffs can only be created for certain versions if self.version % LOG_BASE: return False @@ -1245,6 +1256,11 @@ def create_checkpoint( """ Creates and caches new checkpoint and any required FileDiff checkpoints recursively if needed. """ + if checkpoint.rank == 0: + raise ValueError( + f"Missing expected checkpoint of rank 0 for version {checkpoint.end}" + ) + delta_range = [] # our new checkpoint will be created by adding last individual delta to previous checkpoints expected_checkpoints = Checkpoint.get_checkpoints( @@ -1267,7 +1283,7 @@ def create_checkpoint( # make sure we have all components, if not, created them (recursively) for item in expected_checkpoints: existing_delta = existing_delta_map.get((item.rank, item.end)) - if not existing_delta: + if not existing_delta and item.rank > 0: existing_delta = cls.create_checkpoint(project_id, item) if existing_delta: diff --git a/server/mergin/sync/public_api_v2.yaml b/server/mergin/sync/public_api_v2.yaml index 67ca4523..b01f6781 100644 --- a/server/mergin/sync/public_api_v2.yaml +++ b/server/mergin/sync/public_api_v2.yaml @@ -280,7 +280,7 @@ paths: "404": $ref: "#/components/responses/NotFound" x-openapi-router-controller: mergin.sync.public_api_v2_controller - /projects/{id}/raw/diff/{file}: + /projects/{id}/raw/diff: get: tags: - project @@ -290,7 +290,7 @@ paths: - $ref: "#/components/parameters/ProjectId" - name: file required: true - in: path + in: query description: File id of the diff file to download schema: type: string @@ -443,7 +443,7 @@ paths: summary: List projects in the workspace operationId: list_workspace_projects parameters: - - $ref: '#/components/parameters/WorkspaceId' + - $ref: "#/components/parameters/WorkspaceId" - name: page in: query description: page number diff --git a/server/mergin/sync/public_api_v2_controller.py b/server/mergin/sync/public_api_v2_controller.py index 795f81aa..251fcf97 100644 --- a/server/mergin/sync/public_api_v2_controller.py +++ b/server/mergin/sync/public_api_v2_controller.py @@ -457,7 +457,14 @@ def get_project_delta(id: str, since: str, to: Optional[str] = None): 400, f"""The 'since' parameter must be less than or equal to the {"'to' parameter" if to_provided else 'latest project version'}""", ) - delta_changes = project.get_delta_changes(since_version, to_version) or [] + + try: + delta_changes = project.get_delta_changes(since_version, to_version) or [] + except ValueError: + logging.exception( + f"Failed to get delta changes for project {project.id} between versions {since_version} and {to_version}" + ) + abort(422) return ( DeltaChangeRespSchema().dump( diff --git a/server/mergin/tests/test_projects/test/test_dir/test.gpkg b/server/mergin/tests/test_projects/test/test_dir/test.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..90983bb549b1702fe198adfbd059f302df4ae892 GIT binary patch literal 98304 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 TcF}FW>~Q&qF8cHPF8coh|Kp|7 literal 0 HcmV?d00001 diff --git a/server/mergin/tests/test_public_api_v2.py b/server/mergin/tests/test_public_api_v2.py index 95654032..c93bdf1d 100644 --- a/server/mergin/tests/test_public_api_v2.py +++ b/server/mergin/tests/test_public_api_v2.py @@ -212,7 +212,9 @@ def test_file_diff_download(client, diff_project): file_path_id=gpkg_file.id, version=4, rank=0 ).first() - response = client.get(f"v2/projects/{diff_project.id}/raw/diff/{diff_file.path}") + response = client.get( + f"v2/projects/{diff_project.id}/raw/diff?file={diff_file.path}" + ) assert response.status_code == 200 assert response.content_type == "application/octet-stream" @@ -231,7 +233,7 @@ def test_file_diff_download(client, diff_project): assert not os.path.exists(diff.abs_path) # download merged diff with its reconstuction on the fly - response = client.get(f"v2/projects/{diff_project.id}/raw/diff/{diff.path}") + response = client.get(f"v2/projects/{diff_project.id}/raw/diff?file={diff.path}") assert response.status_code == 200 assert response.content_type == "application/octet-stream" assert os.path.exists(diff.abs_path) @@ -240,13 +242,39 @@ def test_file_diff_download(client, diff_project): with patch.object(FileDiff, "construct_checkpoint") as construct_checkpoint_mock: os.remove(diff.abs_path) construct_checkpoint_mock.return_value = False - response = client.get(f"v2/projects/{diff_project.id}/raw/diff/{diff.path}") + response = client.get( + f"v2/projects/{diff_project.id}/raw/diff?file={diff.path}" + ) assert response.status_code == 422 assert response.json["code"] == DiffDownloadError.code - response = client.get(f"v2/projects/{diff_project.id}/raw/diff/{diff.path}+1") + response = client.get(f"v2/projects/{diff_project.id}/raw/diff?file={diff.path}+1") assert response.status_code == 404 + subfolder_test_gpkg = os.path.join( + diff_project.storage.project_dir, "test_dir", "test.gpkg" + ) + os.makedirs(os.path.dirname(subfolder_test_gpkg), exist_ok=True) + shutil.copy( + os.path.join(test_project_dir, "base.gpkg"), + subfolder_test_gpkg, + ) + # shutil.copy( + # test_gpkg_file, + # os.path.join(diff_project.storage.project_dir, "v9", "test_dir", "test.gpkg"), + # ) + push_change( + diff_project, "added", "test_dir/test.gpkg", diff_project.storage.project_dir + ) + sql = f"UPDATE simple SET rating={1000}" + execute_query(subfolder_test_gpkg, sql) + pv = push_change( + diff_project, "updated", "test_dir/test.gpkg", diff_project.storage.project_dir + ) + fd = FileDiff.query.filter_by(version=pv.name, rank=0).first() + response = client.get(f"v2/projects/{diff_project.id}/raw/diff?file={fd.path}") + assert response.status_code == 200 + def test_create_diff_checkpoint(diff_project): """Test creation of diff checkpoints""" From 8496c9fb740aa88b60abbba086494a02b3ada073 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 25 Feb 2026 14:26:25 +0100 Subject: [PATCH 2/3] fix calling of raw diff with file --- .../tests/test_projects/test/test_dir/test.gpkg | Bin 98304 -> 0 bytes server/mergin/tests/test_public_api_v2.py | 6 ++++-- 2 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 server/mergin/tests/test_projects/test/test_dir/test.gpkg diff --git a/server/mergin/tests/test_projects/test/test_dir/test.gpkg b/server/mergin/tests/test_projects/test/test_dir/test.gpkg deleted file mode 100644 index 90983bb549b1702fe198adfbd059f302df4ae892..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98304 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 TcF}FW>~Q&qF8cHPF8coh|Kp|7 diff --git a/server/mergin/tests/test_public_api_v2.py b/server/mergin/tests/test_public_api_v2.py index c93bdf1d..528a45d0 100644 --- a/server/mergin/tests/test_public_api_v2.py +++ b/server/mergin/tests/test_public_api_v2.py @@ -766,7 +766,7 @@ def test_project_version_delta_changes(client, diff_project: Project): # check if checkpoint will be there response = client.get( - f"v2/projects/{diff_project.id}/raw/diff/{delta[0].diffs[0].id}" + f"v2/projects/{diff_project.id}/raw/diff?file={delta[0].diffs[0].id}" ) assert response.status_code == 200 @@ -1367,7 +1367,9 @@ def test_project_pull_diffs(client, diff_project): second_diff = delta[0]["diffs"][1] assert first_diff["id"] == current_diffs[0].path assert second_diff["id"] == current_diffs[1].path - response = client.get(f"v2/projects/{diff_project.id}/raw/diff/{first_diff['id']}") + response = client.get( + f"v2/projects/{diff_project.id}/raw/diff?file={first_diff['id']}" + ) assert response.status_code == 200 From 5b1733df498c1843057e43534e06ea515b4bf73e Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 25 Feb 2026 14:41:56 +0100 Subject: [PATCH 3/3] cleanup --- server/mergin/sync/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/server/mergin/sync/models.py b/server/mergin/sync/models.py index ad794ffa..bc01c523 100644 --- a/server/mergin/sync/models.py +++ b/server/mergin/sync/models.py @@ -415,7 +415,6 @@ def get_delta_changes( raise ValueError( f"Missing expected checkpoint of rank 0 for version {checkpoint.end}" ) - return # If higher rank delta checkopoint does not exists we need to create it if checkpoint.rank > 0: