diff --git a/server/mergin/sync/files.py b/server/mergin/sync/files.py index 5e28ed70..f0ee09a1 100644 --- a/server/mergin/sync/files.py +++ b/server/mergin/sync/files.py @@ -254,13 +254,13 @@ def patch_field(self, data, **kwargs): class DeltaDiffFile: """Diff file path in diffs list""" - path: str + id: str class DeltaChangeDiffFileSchema(ma.Schema): """Schema for diff file path in diffs list""" - path = fields.String(required=True) + id = fields.String(required=True) @dataclass @@ -287,7 +287,7 @@ def to_data_delta(self): version=self.version, ) if self.diffs: - result.diff = self.diffs[0].path + result.diff = self.diffs[0].id return result @@ -307,7 +307,7 @@ def to_merged(self) -> DeltaChangeMerged: version=self.version, ) if self.diff: - result.diffs = [DeltaDiffFile(path=self.diff)] + result.diffs = [DeltaDiffFile(id=self.diff)] return result @@ -356,6 +356,7 @@ class DeltaChangeRespSchema(ma.Schema): """Schema for list of delta changes wrapped in items field""" items = fields.List(fields.Nested(DeltaChangeItemSchema())) + to_version = fields.String(required=True) class Meta: unknown = EXCLUDE diff --git a/server/mergin/sync/public_api_v2.yaml b/server/mergin/sync/public_api_v2.yaml index b4d76944..d272b046 100644 --- a/server/mergin/sync/public_api_v2.yaml +++ b/server/mergin/sync/public_api_v2.yaml @@ -292,7 +292,7 @@ paths: - name: file required: true in: path - description: File path + description: File id of the diff file to download schema: type: string example: survey.gpkg-diff-1b9fe848-d2e4-4c53-958d-3dd97e5486f6 @@ -666,7 +666,7 @@ components: description: List of files in the project items: allOf: - - $ref: '#/components/schemas/File' + - $ref: "#/components/schemas/File" - type: object properties: mtime: @@ -958,14 +958,17 @@ components: items: type: object properties: - path: + id: type: string example: survey.gpkg-diff-1 ProjectDeltaResponse: type: object required: - items + - to_version properties: + to_version: + $ref: "#/components/schemas/VersionName" items: type: array items: diff --git a/server/mergin/sync/public_api_v2_controller.py b/server/mergin/sync/public_api_v2_controller.py index 1f9848cc..ddaf403f 100644 --- a/server/mergin/sync/public_api_v2_controller.py +++ b/server/mergin/sync/public_api_v2_controller.py @@ -424,20 +424,30 @@ def get_project_delta(id: str, since: str, to: Optional[str] = None): """Get project changes (delta) between two versions""" project: Project = require_project_by_uuid(id, ProjectPermissions.Read) - since = ProjectVersion.from_v_name(since) - to = project.latest_version if to is None else ProjectVersion.from_v_name(to) - if since < 0 or to < 1: + since_version = ProjectVersion.from_v_name(since) + to_provided = to is not None + to_version = ( + project.latest_version if not to_provided else ProjectVersion.from_v_name(to) + ) + if since_version < 0 or to_version < 0: abort( 400, - "Invalid version number, minimum version for 'since' is 0 and minimum version for 'to' is 1", + "Invalid version number, minimum version for 'since' is 0 and minimum version for 'to' is 0", ) - if to > project.latest_version: - abort(400, "'to' version exceeds latest project version") - - if since >= to: - abort(400, "'since' version must be less than 'to' version") + if to_version > project.latest_version: + abort(400, "The 'to' parameter exceeds latest project version") - delta_changes = project.get_delta_changes(since, to) or [] + if since_version > to_version: + abort( + 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 [] - return DeltaChangeRespSchema().dump({"items": delta_changes}), 200 + return ( + DeltaChangeRespSchema().dump( + {"to_version": f"v{to_version}", "items": delta_changes} + ), + 200, + ) diff --git a/server/mergin/tests/test_public_api_v2.py b/server/mergin/tests/test_public_api_v2.py index 1db387c0..25adbc89 100644 --- a/server/mergin/tests/test_public_api_v2.py +++ b/server/mergin/tests/test_public_api_v2.py @@ -456,7 +456,7 @@ def test_delta_merge_changes(): assert merged[0].version == update_diff2.version assert merged[0].size == update_diff2.size assert merged[0].checksum == update_diff2.checksum - assert [d.path for d in merged[0].diffs] == ["diff1", "diff2"] + assert [d.id for d in merged[0].diffs] == ["diff1", "diff2"] # case when trying to delete already existing file in history # copy create with new version number @@ -608,14 +608,14 @@ def test_project_version_delta_changes(client, diff_project: Project): delta = diff_project.get_delta_changes(12, latest_version.name + 6) assert len(delta) == 1 assert len(delta[0].diffs) == 1 - assert delta[0].diffs[0].path == test_gpkg_checkpoint.path + assert delta[0].diffs[0].id == test_gpkg_checkpoint.path assert delta[0].change == PushChangeType.UPDATE_DIFF assert delta[0].checksum == fh.checksum assert delta[0].size == fh.size # check if checkpoint will be there response = client.get( - f"v2/projects/{diff_project.id}/raw/diff/{delta[0].diffs[0].path}" + f"v2/projects/{diff_project.id}/raw/diff/{delta[0].diffs[0].id}" ) assert response.status_code == 200 @@ -1078,6 +1078,7 @@ def test_project_delta(client, diff_project): assert len(changes) == 1 assert changes[0]["change"] == PushChangeType.CREATE.value assert changes[0]["version"] == "v1" + assert response.json.get("to_version") == "v1" # remove the file and get changes from 0 -> 2 where base gpgkg is removed -> transparent push_change(initial_project, "removed", "base.gpkg", working_dir) @@ -1086,6 +1087,22 @@ def test_project_delta(client, diff_project): changes = response.json["items"] assert len(changes) == 0 + # get delta from 0 -> 1 where file was created + response = client.get(f"v2/projects/{initial_project.id}/delta?since=v0&to=v1") + assert response.status_code == 200 + changes = response.json["items"] + assert len(changes) == 1 + assert changes[0]["change"] == PushChangeType.CREATE.value + assert changes[0]["version"] == "v1" + assert response.json.get("to_version") == "v1" + + # get delta from 1 -> 1, no changes detected + response = client.get(f"v2/projects/{initial_project.id}/delta?since=v1&to=v1") + assert response.status_code == 200 + changes = response.json["items"] + assert len(changes) == 0 + assert response.json.get("to_version") == "v1" + # non valid cases response = client.get(f"v2/projects/{diff_project.id}/delta") assert response.status_code == 400 @@ -1098,9 +1115,6 @@ def test_project_delta(client, diff_project): # exceeding latest version response = client.get(f"v2/projects/{diff_project.id}/delta?since=v0&to=v2000") assert response.status_code == 400 - # no changes between versions with same number - response = client.get(f"v2/projects/{diff_project.id}/delta?since=v1&to=v1") - assert response.status_code == 400 # since 1 to latest version response = client.get(f"v2/projects/{diff_project.id}/delta?since=v1") @@ -1117,6 +1131,8 @@ def test_project_delta(client, diff_project): assert changes[1]["version"] == "v9" assert changes[1]["path"] == "test.gpkg" assert changes[1]["size"] == 98304 + # there is version without changes in v10, but exists in server + assert response.json.get("to_version") == "v10" # simple update response = client.get(f"v2/projects/{diff_project.id}/delta?since=v4&to=v8") @@ -1127,6 +1143,7 @@ def test_project_delta(client, diff_project): # version is new latest version of the change assert changes[0]["version"] == "v7" assert not changes[0].get("diffs") + assert response.json.get("to_version") == "v8" def test_project_pull_diffs(client, diff_project): @@ -1149,9 +1166,7 @@ def test_project_pull_diffs(client, diff_project): assert delta[0]["version"] == "v7" first_diff = delta[0]["diffs"][0] second_diff = delta[0]["diffs"][1] - assert first_diff["path"] == current_diffs[0].path - assert second_diff["path"] == current_diffs[1].path - response = client.get( - f"v2/projects/{diff_project.id}/raw/diff/{first_diff['path']}" - ) + 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']}") assert response.status_code == 200