Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
d7bbfc0
Introduce file diff table
varmar05 Jul 15, 2025
f50eb96
Add alembic migration for file diff table
varmar05 Jul 17, 2025
32b3c5b
Merge pull request #482 from MerginMaps/new_diffs_table
MarcelGeo Jul 23, 2025
feeafd9
Merge branch 'dev-r65-pull' into dev-r85-v2-pull
varmar05 Aug 26, 2025
8c18891
Utils for generating cached version levels from versions
varmar05 Aug 29, 2025
afb42e5
Add db hook to trigger caching on project version created
varmar05 Sep 1, 2025
b14e55b
create celery task to generate diff checkpoint
varmar05 Sep 4, 2025
4461ec9
Use diff checkpoint in gpkg restore function
varmar05 Sep 5, 2025
dd6463e
API: Add new v2 endpoint to download diff file
varmar05 Sep 25, 2025
85ef488
Remove celery caching job and trigger on project push
varmar05 Sep 25, 2025
4ab69f6
Initial migration
MarcelGeo Oct 1, 2025
71f9f0f
Modify changes table in name
MarcelGeo Oct 1, 2025
a9d1119
Merge branch 'dev-r84-concurrent-push' into dev-r85-v2-pull
varmar05 Oct 2, 2025
0025b3b
Cosmetic changes
varmar05 Oct 2, 2025
30be795
Merge branch 'dev-r85-v2-pull' into merged-diffs
varmar05 Oct 2, 2025
3c2153a
Return custom error on failed diff download + small functions renamin…
varmar05 Oct 3, 2025
202eae7
Merge pull request #504 from MerginMaps/merged-diffs
varmar05 Oct 3, 2025
83f858f
Initial version for merging diffs
MarcelGeo Oct 3, 2025
0bfd6b5
Merge remote-tracking branch 'origin/dev-r85-v2-pull' into cache-vers…
MarcelGeo Oct 3, 2025
3db1550
Adapt merge versions
MarcelGeo Oct 3, 2025
03f5098
Delta endpoints + logic improvements:
MarcelGeo Oct 7, 2025
4fb5c83
extract method for create checkpoint
MarcelGeo Oct 8, 2025
63100a6
Final fixes and changing schema
MarcelGeo Oct 9, 2025
29a9eef
Fix missing import
MarcelGeo Oct 9, 2025
c53b928
Fix tests and add checkpoints just i UPDATE_DIFF
MarcelGeo Oct 9, 2025
dce0e11
add safe check for dwonloading
MarcelGeo Oct 9, 2025
832603d
Imporve tests
MarcelGeo Oct 9, 2025
a9140e8
Address comments @varmar05 1. part
MarcelGeo Oct 10, 2025
8f13613
Fix alembic migration for file diff
varmar05 Oct 13, 2025
c6b60ce
Merge pull request #521 from MerginMaps/update_migration
MarcelGeo Oct 14, 2025
d0ef271
enhancements v2
MarcelGeo Oct 16, 2025
8ceee04
Address disscussions:
MarcelGeo Oct 24, 2025
99194b0
add mechanism for handling previous history files.
MarcelGeo Oct 28, 2025
2077d89
fix integrity test
MarcelGeo Oct 28, 2025
3685e43
Upgrade logic
MarcelGeo Oct 29, 2025
2c9bdad
Merge pull request #520 from MerginMaps/cache-versions
MarcelGeo Oct 30, 2025
02ff027
API: add 'v' prefix to version in delta endpoint
varmar05 Nov 6, 2025
f01986e
Merge pull request #532 from MerginMaps/delta_add_v_prefix
varmar05 Nov 6, 2025
f4f00f0
Make construct diff method recursive
varmar05 Nov 14, 2025
21dfb2d
Make delta project function to create missing checkpoints recursively
varmar05 Nov 14, 2025
f7da890
Make diff checkoint validation check more robust
varmar05 Nov 25, 2025
2938e21
Add more tests
varmar05 Nov 25, 2025
a9986b1
Add cli command to trigger checkpoints caching
varmar05 Nov 25, 2025
2b872e5
Fix failing tests with random 504
varmar05 Nov 25, 2025
3a51545
Merge pull request #535 from MerginMaps/create_checkpoint_recursively
varmar05 Nov 26, 2025
54a6f16
Merge branch 'develop' into dev-r85-v2-pull
varmar05 Nov 26, 2025
80adef2
Publish v2 pull enabled flag
MarcelGeo Nov 27, 2025
bac1498
Merge pull request #539 from MerginMaps/v2-pull-flag
MarcelGeo Nov 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/.test.env
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ SECURITY_BEARER_SALT='bearer'
SECURITY_EMAIL_SALT='email'
SECURITY_PASSWORD_SALT='password'
DIAGNOSTIC_LOGS_DIR=/tmp/diagnostic_logs
GEVENT_WORKER=0
47 changes: 47 additions & 0 deletions server/mergin/sync/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,50 @@ def remove(project_name):
project.removed_by = None
db.session.commit()
click.secho("Project removed", fg="green")

@project.command()
@click.argument("project-name", callback=normalize_input(lowercase=False))
@click.option("--since", type=int, required=False)
@click.option("--to", type=int, required=False)
def create_checkpoint(project_name, since=None, to=None):
"""Create project delta checkpoint, corresponding lower checkpoints and merged diffs for project"""
ws, name = split_project_path(project_name)
workspace = current_app.ws_handler.get_by_name(ws)
if not workspace:
click.secho("ERROR: Workspace does not exist", fg="red", err=True)
sys.exit(1)
project = (
Project.query.filter_by(workspace_id=workspace.id, name=name)
.filter(Project.storage_params.isnot(None))
.first()
)
if not project:
click.secho("ERROR: Project does not exist", fg="red", err=True)
sys.exit(1)

since = since if since is not None else 0
to = to if to is not None else project.latest_version
if since < 0 or to < 1:
click.secho(
"ERROR: Invalid version number, minimum version for 'since' is 0 and minimum version for 'to' is 1",
fg="red",
err=True,
)
sys.exit(1)

if to > project.latest_version:
click.secho(
"ERROR: 'to' version exceeds latest project version", fg="red", err=True
)
sys.exit(1)

if since >= to:
click.secho(
"ERROR: 'since' version must be less than 'to' version",
fg="red",
err=True,
)
sys.exit(1)

project.get_delta_changes(since, to)
click.secho("Project checkpoint(s) created", fg="green")
2 changes: 2 additions & 0 deletions server/mergin/sync/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,5 @@ class Configuration(object):
UPLOAD_CHUNKS_EXPIRATION = config(
"UPLOAD_CHUNKS_EXPIRATION", default=86400, cast=int
)
# whether client can pull using v2 apis
V2_PULL_ENABLED = config("V2_PULL_ENABLED", default=True, cast=bool)
7 changes: 7 additions & 0 deletions server/mergin/sync/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,10 @@ def to_dict(self) -> Dict:
class BigChunkError(ResponseError):
code = "BigChunkError"
detail = f"Chunk size exceeds maximum allowed size {MAX_CHUNK_SIZE} MB"


class DiffDownloadError(ResponseError):
code = "DiffDownloadError"
detail = (
"Required diff file could not be downloaded as it could not be reconstructed"
)
126 changes: 123 additions & 3 deletions server/mergin/sync/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@
import datetime
from enum import Enum
import os
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Optional, List
import uuid
from flask import current_app
from marshmallow import ValidationError, fields, EXCLUDE, post_dump, validates_schema
from marshmallow import (
ValidationError,
fields,
EXCLUDE,
post_dump,
validates_schema,
post_load,
)
from pathvalidate import sanitize_filename

from .utils import (
Expand Down Expand Up @@ -231,11 +238,124 @@ def validate(self, data, **kwargs):

class ProjectFileSchema(FileSchema):
mtime = DateTimeWithZ()
diff = fields.Nested(FileSchema())
diff = fields.Nested(
FileSchema(),
)

@post_dump
def patch_field(self, data, **kwargs):
# drop 'diff' key entirely if empty or None as clients would expect
if not data.get("diff"):
data.pop("diff", None)
return data


@dataclass
class DeltaDiffFile:
"""Diff file path in diffs list"""

path: str


class DeltaChangeDiffFileSchema(ma.Schema):
"""Schema for diff file path in diffs list"""

path = fields.String(required=True)


@dataclass
class DeltaChangeBase(File):
"""Base class for changes stored in json list or returned from delta endpoint"""

change: PushChangeType
version: int


@dataclass
class DeltaChangeMerged(DeltaChangeBase):
"""Delta item with merged diffs to list of multiple diff files"""

diffs: List[DeltaDiffFile] = field(default_factory=list)

def to_data_delta(self):
"""Convert DeltaMerged to DeltaData with single diff"""
result = DeltaChange(
path=self.path,
size=self.size,
checksum=self.checksum,
change=self.change,
version=self.version,
)
if self.diffs:
result.diff = self.diffs[0].path
return result


@dataclass
class DeltaChange(DeltaChangeBase):
"""Change items stored in database as list of this item with single diff file"""

diff: Optional[str] = None

def to_merged(self) -> DeltaChangeMerged:
"""Convert to DeltaMerged with multiple diffs"""
result = DeltaChangeMerged(
path=self.path,
size=self.size,
checksum=self.checksum,
change=self.change,
version=self.version,
)
if self.diff:
result.diffs = [DeltaDiffFile(path=self.diff)]
return result


class DeltaChangeBaseSchema(ma.Schema):
"""Base schema for delta json and response from delta endpoint"""

path = fields.String(required=True)
size = fields.Integer(required=True)
checksum = fields.String(required=True)
version = fields.Integer(required=True)
change = fields.Enum(PushChangeType, by_value=True, required=True)


class DeltaChangeSchema(DeltaChangeBaseSchema):
"""Schema for change data in changes column"""

diff = fields.String(required=False)

@post_load
def make_object(self, data, **kwargs):
return DeltaChange(**data)

@post_dump
def patch_field(self, data, **kwargs):
# drop 'diff' key entirely if empty or None as database would expect
if not data.get("diff"):
data.pop("diff", None)
return data


class DeltaChangeItemSchema(DeltaChangeBaseSchema):
"""Schema for delta changes response"""

version = fields.Function(lambda obj: f"v{obj.version}")
diffs = fields.List(fields.Nested(DeltaChangeDiffFileSchema()))

@post_dump
def patch_field(self, data, **kwargs):
# drop 'diffs' key entirely if empty or None as clients would expect
if not data.get("diffs"):
data.pop("diffs", None)
return data


class DeltaChangeRespSchema(ma.Schema):
"""Schema for list of delta changes wrapped in items field"""

items = fields.List(fields.Nested(DeltaChangeItemSchema()))

class Meta:
unknown = EXCLUDE
Loading
Loading