Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions server/mergin/sync/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,19 @@ def to_dict(self) -> Dict:
return data


class UnsupportedFilesDetected(UploadError):
code = "UnsupportedFilesDetected"

def __init__(self, error: str, unsupported_files: List[str]):
super().__init__(error)
self.unsupported_files = unsupported_files

def to_dict(self) -> Dict:
data = super().to_dict()
data["unsupported_files"] = self.unsupported_files
return data


class BigChunkError(ResponseError):
code = "BigChunkError"
detail = f"Chunk size exceeds maximum allowed size {MAX_CHUNK_SIZE} MB"
Expand Down
23 changes: 17 additions & 6 deletions server/mergin/sync/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ class UploadFileSchema(FileSchema):
diff = fields.Nested(FileSchema(), many=False, load_default=None)


class UnsupportedFileNamesError(ValidationError):
"""Upload changes contain file paths with invalid characters"""

def __init__(self, paths: List[str]):
self.paths = paths
files = ", ".join(f"'{path}'" for path in paths)
super().__init__(
f"Unsupported files detected: {files}. Please remove the invalid characters."
)


class ChangesSchema(ma.Schema):
"""Schema for upload changes"""

Expand Down Expand Up @@ -212,15 +223,14 @@ def validate(self, data, **kwargs):
raise ValidationError("Not unique changes")

# check if all files are valid
unsupported_files = []
for file in data["added"] + data["updated"]:
file_path = file["path"]
if is_versioned_file(file_path) and file["size"] == 0:
raise ValidationError("File is not valid")

if not is_valid_path(file_path):
raise ValidationError(
f"Unsupported file name detected: '{file_path}'. Please remove the invalid characters."
)
unsupported_files.append(file_path)

if not is_supported_extension(file_path):
raise ValidationError(
Expand All @@ -230,9 +240,10 @@ def validate(self, data, **kwargs):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave here that code and just raise specific error. Do not need specific loops. That can simplify this code.

like if diff and not is_valid_path():
raise UnsupportedFileName.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I put the validation in the existing loop. But I need to raise the specific error only when the loop finishes.

diff = file.get("diff")
if diff and not is_valid_path(diff["path"]):
raise ValidationError(
f"Unsupported file name detected: '{diff['path']}'. Please remove the invalid characters."
)
unsupported_files.append(diff["path"])

if unsupported_files:
raise UnsupportedFileNamesError(unsupported_files)
# new checks must restrict only new files not to block existing projects
for file in data["added"]:
file_path = file["path"]
Expand Down
16 changes: 16 additions & 0 deletions server/mergin/sync/public_api_v2.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ paths:
schema:
anyOf:
- $ref: "#/components/schemas/UploadError"
- $ref: "#/components/schemas/UnsupportedFilesDetected"
- $ref: "#/components/schemas/TrialExpired"
- $ref: "#/components/schemas/StorageLimitHit"
- $ref: "#/components/schemas/DataSyncError"
Expand Down Expand Up @@ -737,6 +738,21 @@ components:
example:
code: UploadError
detail: "Project version could not be created (UploadError)"
UnsupportedFilesDetected:
allOf:
- $ref: "#/components/schemas/CustomError"
type: object
properties:
unsupported_files:
type: array
items:
type: string
example:
code: UnsupportedFilesDetected
detail: "Unsupported files detected: 'notes:draft.txt', 'photos|old/tree.jpg'. Please remove the invalid characters. (UnsupportedFilesDetected)"
unsupported_files:
- "notes:draft.txt"
- "photos|old/tree.jpg"
BatchItemError:
type: object
properties:
Expand Down
10 changes: 9 additions & 1 deletion server/mergin/sync/public_api_v2_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@
ProjectLocked,
ProjectVersionExists,
StorageLimitHit,
UnsupportedFilesDetected,
UploadError,
)
from .files import ChangesSchema, DeltaChangeRespSchema, ProjectFileSchema
from .files import (
ChangesSchema,
DeltaChangeRespSchema,
ProjectFileSchema,
UnsupportedFileNamesError,
)
from .events import SyncEventType
from ..audit import emit
from ..audit.listeners import actor_context, audit_session_flags
Expand Down Expand Up @@ -279,6 +285,8 @@ def create_project_version(id):
try:
ChangesSchema().validate(changes)
upload_changes = ChangesSchema().dump(changes)
except UnsupportedFileNamesError as err:
return UnsupportedFilesDetected(err.messages[0], err.paths).response(422)
except ValidationError as err:
msg = err.messages[0] if type(err.messages) == list else "Invalid input data"
return UploadError(error=msg).response(422)
Expand Down
4 changes: 2 additions & 2 deletions server/mergin/tests/test_project_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -2712,7 +2712,7 @@ def test_filepath_manipulation(client):
assert resp.status_code == 400
assert (
resp.json["detail"]
== f"Unsupported file name detected: '{manipulated_path}'. Please remove the invalid characters."
== f"Unsupported files detected: '{manipulated_path}'. Please remove the invalid characters."
)


Expand Down Expand Up @@ -2749,7 +2749,7 @@ def test_diff_filepath_manipulation(client):
assert resp.status_code == 400
assert (
resp.json["detail"]
== f"Unsupported file name detected: '{manipulated_diff_path}'. Please remove the invalid characters."
== f"Unsupported files detected: '{manipulated_diff_path}'. Please remove the invalid characters."
)


Expand Down
39 changes: 37 additions & 2 deletions server/mergin/tests/test_public_api_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
ProjectVersionExists,
AnotherUploadRunning,
StorageLimitHit,
UnsupportedFilesDetected,
UploadError,
)
from mergin.sync.files import ChangesSchema
Expand Down Expand Up @@ -920,7 +921,7 @@ def _get_changes_with_diff_added(project_dir):
"changes": _get_changes_with_diff_updated(test_project_dir),
},
422,
UploadError.code,
UnsupportedFilesDetected.code,
),
# contains already uploaded file
(
Expand Down Expand Up @@ -971,7 +972,7 @@ def _get_changes_with_diff_added(project_dir):
(
{"version": "v1", "changes": _get_changes_with_diff_added(test_project_dir)},
422,
UploadError.code,
UnsupportedFilesDetected.code,
),
(
{
Expand Down Expand Up @@ -1052,6 +1053,40 @@ def test_create_version(client, data, expected, err_code):
assert failure.error_type == "project_push"


def test_create_version_unsupported_file_names(client):
"""Test all files with unsupported names are listed in the error response"""
project = Project.query.filter_by(
workspace_id=test_workspace_id, name=test_project
).first()
changes = _get_changes_with_diff_updated(test_project_dir)
changes["added"] = [
{
"path": path,
"size": 1234,
"checksum": "9adb76bf81a34880209040ffe5ee262a090b62ab",
"chunks": [],
}
for path in ("notes.txt", "notes:draft.txt", "photos|old/tree.jpg")
]
invalid_diff_path = changes["updated"][2]["diff"]["path"]

response = client.post(
f"v2/projects/{project.id}/versions",
json={"version": "v1", "changes": changes, "check_only": True},
)
assert response.status_code == 422
assert response.json["code"] == UnsupportedFilesDetected.code
assert response.json["unsupported_files"] == [
"notes:draft.txt",
"photos|old/tree.jpg",
invalid_diff_path,
]
assert response.json["detail"] == (
f"Unsupported files detected: 'notes:draft.txt', 'photos|old/tree.jpg', '{invalid_diff_path}'. "
"Please remove the invalid characters. (UnsupportedFilesDetected)"
)


def test_create_version_failures(client):
"""Test various project push failures beyond invalid payload"""
project = Project.query.filter_by(
Expand Down
Loading