Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 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
169492d
Added to_version as latest project version to delta reponse for trans…
MarcelGeo Dec 12, 2025
decf2b1
Try to rename diff path to diff id
MarcelGeo Dec 12, 2025
81eb324
Some tweaks to comments
MarcelGeo Dec 12, 2025
ab7aeda
Merge pull request #547 from MerginMaps/provide-to-version
MarcelGeo Dec 16, 2025
2183811
Merge remote-tracking branch 'origin/develop' into dev-r85-v2-pull
MarcelGeo Jan 12, 2026
97a5a45
Merge branch 'master' into dev-r85-v2-pull
varmar05 Jan 19, 2026
2c745fa
Fix broken yaml file
varmar05 Jan 19, 2026
d8734fb
Add helper to map API field name to DB column
harminius Jan 22, 2026
4bd3ddc
Abort on invalid order param
harminius Jan 22, 2026
3c69bb6
Do not throw error on invalid order param
harminius Jan 22, 2026
a8a24b1
Address reviews
harminius Jan 23, 2026
b4ee9de
paginate once
harminius Jan 23, 2026
aae5d97
Review II - kwargs & fields to skip
harminius Jan 23, 2026
48a66a6
Merge pull request #565 from MerginMaps/api_to_db_fields_map
MarcelGeo Jan 26, 2026
ca81bd0
Update light theme fonts
xkello Jan 26, 2026
a7d566a
Add own name to CLA signed list
xkello Jan 27, 2026
9c7d1c5
Remove redundant comment
xkello Jan 27, 2026
f821257
Add more decimals to make line-height more precise
xkello Jan 27, 2026
fe057e4
Merge pull request #566 from MerginMaps/font-update-light-theme
MarcelGeo Jan 27, 2026
45d40b3
Merge pull request #568 from MerginMaps/master
MarcelGeo Jan 28, 2026
612ed7d
Merge remote-tracking branch 'origin/develop' into dev-r85-v2-pull
MarcelGeo Jan 29, 2026
923796d
Add Podman deployment scripts for community edition
MarcelGeo Jan 29, 2026
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 LICENSES/CLA-signed-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ C/ My company has custom contribution contract with Lutra Consulting Ltd. or I a
* luxusko, 25th August 2023
* jozef-budac, 30th January 2024
* fernandinand, 13th March 2025
* xkello, 27th January 2025
96 changes: 96 additions & 0 deletions deployment/community/run_podman.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/bin/bash
set -e

NETWORK="merginmaps"

# Create network if it doesn't exist
if ! podman network exists $NETWORK; then
echo "Creating network $NETWORK..."
podman network create $NETWORK
fi

# Function to run a container if it doesn't exist (optional cleanup can be added)
# For now, we assume this script starts the stack. Users should `podman rm -f` manually to restart.

echo "Starting Database..."
podman run -d \
--name merginmaps-db \
--network $NETWORK \
--network-alias db \
--restart always \
-e POSTGRES_DB=mergin \
-e POSTGRES_USER=postgres \
-e POSTGRES_PASSWORD=postgres \
-v ./mergin_db:/var/lib/postgresql/data \
postgres:14

echo "Starting Redis..."
podman run -d \
--name merginmaps-redis \
--network $NETWORK \
--network-alias redis \
--restart always \
redis:6.2.17

echo "Starting Server..."
# Note: Ensure .prod.env exists or provided variables match production requirements
podman run -d \
--name merginmaps-server \
--network $NETWORK \
--network-alias server \
--restart always \
--user 901:999 \
-v ./projects:/data \
-v ./diagnostic_logs:/diagnostic_logs \
--env-file .prod.env \
lutraconsulting/merginmaps-backend:2025.7.3 \
gunicorn --config config.py application:application

echo "Starting Celery Beat..."
podman run -d \
--name celery-beat \
--network $NETWORK \
--network-alias celery-beat \
--restart always \
--env-file .prod.env \
-e GEVENT_WORKER=0 \
-e NO_MONKEY_PATCH=1 \
lutraconsulting/merginmaps-backend:2025.7.3 \
celery -A application.celery beat --loglevel=info

echo "Starting Celery Worker..."
podman run -d \
--name celery-worker \
--network $NETWORK \
--network-alias celery-worker \
--restart always \
--user 901:999 \
--env-file .prod.env \
-e GEVENT_WORKER=0 \
-e NO_MONKEY_PATCH=1 \
-v ./projects:/data \
lutraconsulting/merginmaps-backend:2025.7.3 \
celery -A application.celery worker --loglevel=info

echo "Starting Web..."
podman run -d \
--name merginmaps-web \
--network $NETWORK \
--network-alias web \
--restart always \
--user 101:999 \
lutraconsulting/merginmaps-frontend:2025.7.3

echo "Starting Proxy..."
podman run -d \
--name merginmaps-proxy \
--network $NETWORK \
--network-alias proxy \
--restart always \
--user 101:999 \
-p 8080:8080 \
-v ./projects:/data \
-v ../common/nginx.conf:/etc/nginx/conf.d/default.conf \
nginxinc/nginx-unprivileged:1.27

echo "Stack started successfully with Podman."
14 changes: 14 additions & 0 deletions deployment/community/stop_podman.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
CONTAINERS="merginmaps-proxy merginmaps-web celery-worker celery-beat merginmaps-server merginmaps-redis merginmaps-db"

echo "Stopping containers..."
# Ignore errors if containers don't exist
podman stop $CONTAINERS 2>/dev/null || true

echo "Removing containers..."
podman rm $CONTAINERS 2>/dev/null || true

# Optional: Remove network
# podman network rm merginmaps 2>/dev/null || true

echo "Stack stopped and removed."
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,6 +75,8 @@ 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)
EXCLUDED_CLONE_FILENAMES = config(
"EXCLUDED_CLONE_FILENAMES", default="qgis_cfg.xml", cast=Csv()
)
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"
)
127 changes: 124 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,125 @@ 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"""

id: str


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

id = 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].id
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(id=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()))
to_version = fields.String(required=True)

class Meta:
unknown = EXCLUDE
Loading
Loading