diff --git a/Mergin/plugin.py b/Mergin/plugin.py
index c1ccd045..4af81232 100644
--- a/Mergin/plugin.py
+++ b/Mergin/plugin.py
@@ -412,7 +412,7 @@ def set_current_workspace(self, workspace):
iface.messageBar().pushMessage(
"Mergin Maps",
"Your attention is required. Please visit the "
- f""
+ f""
"Mergin dashboard",
level=Qgis.Critical,
duration=0,
diff --git a/Mergin/project_settings_widget.py b/Mergin/project_settings_widget.py
index b1753a03..07e32da5 100644
--- a/Mergin/project_settings_widget.py
+++ b/Mergin/project_settings_widget.py
@@ -30,6 +30,7 @@
set_tracking_layer_flags,
is_experimental_plugin_enabled,
remove_prefix,
+ invalid_filename_character,
)
ui_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "ui", "ui_project_config.ui")
@@ -224,14 +225,21 @@ def update_preview(self, expression, layer, field_name):
exp = QgsExpression(expression)
exp.prepare(context)
if exp.hasParserError():
- self.label_preview.setText(f"{exp.parserErrorString()}")
+ self.label_preview.setText(f"{exp.parserErrorString()}")
return
val = exp.evaluate(context)
if exp.hasEvalError():
- self.label_preview.setText(f"{exp.evalErrorString()}")
+ self.label_preview.setText(f"{exp.evalErrorString()}")
return
-
+ if val:
+ # check if evaluated expression contains invalid filename characters
+ invalid_char = invalid_filename_character(val)
+ if invalid_char:
+ self.label_preview.setText(
+ f"The file name '{val}.jpg' contains an invalid character. Do not use '{invalid_char}' character in the file name."
+ )
+ return
config = layer.fields().field(field_name).editorWidgetSetup().config()
target_dir = resolve_target_dir(layer, config)
prefix = prefix_for_relative_path(
@@ -240,9 +248,9 @@ def update_preview(self, expression, layer, field_name):
target_dir,
)
if prefix:
- self.label_preview.setText(f"{remove_prefix(prefix, QgsProject.instance().homePath())}/{val}.jpg")
+ self.label_preview.setText(f"{remove_prefix(prefix, QgsProject.instance().homePath())}/{val}.jpg")
else:
- self.label_preview.setText(f"{val}.jpg")
+ self.label_preview.setText(f"{val}.jpg")
def check_project(self, state):
"""
diff --git a/Mergin/ui/ui_project_config.ui b/Mergin/ui/ui_project_config.ui
index cac9754e..d2da2177 100644
--- a/Mergin/ui/ui_project_config.ui
+++ b/Mergin/ui/ui_project_config.ui
@@ -193,6 +193,11 @@
0
+
+
+ true
+
+
@@ -214,14 +219,14 @@
-
- Preview
+ Preview:
-
- <html><head/><body><p><span style=" font-weight:600;">Photo name format</span></p><p>Set up custom photo names format based on an expression. Make sure that the name is unique for each photo.</p><p>Pro tip: use variables like <span style=" font-family:'Noto Sans Mono';">@mm_user_email</span>, <span style=" font-family:'Noto Sans Mono';">@layer_name</span> or layer fields in combination with <span style=" font-family:'Noto Sans Mono';">now()</span> (to get the current time) in order to generate unique photo names.</p><p>For further details, please refer to <a href="https://merginmaps.com/docs/layer/settingup_forms_photo/#customising-photo-name-format-with-expressions"><span style=" text-decoration: underline; color:#1d99f3;">our documentation</span></a>.</p></body></html>
+ <html><head/><body><p><span style=" font-weight:600;">Photo name format</span></p><p>Set up custom photo names format based on an expression. Make sure that the name is unique for each photo.</p><p>Pro tip: use variables like <span style=" font-family:'Noto Sans Mono';">@mm_user_email</span>, <span style=" font-family:'Noto Sans Mono';">@layer_name</span> or layer fields in combination with <span style=" font-family:'Noto Sans Mono';">now()</span> (to get the current time) in order to generate unique photo names.</p><p>For further details, please refer to <a href="https://merginmaps.com/docs/layer/settingup_forms_photo/#customising-photo-name-format-with-expressions"><span style=" text-decoration: underline; color:#1d99f3;">our documentation</span></a>.</p><p>File names may include letters (A–Z, a–z), numbers (0–9), spaces, dashes (-), and underscores (_).<br>Use '/' only to separate subfolders.</p></body></html>
true
diff --git a/Mergin/utils.py b/Mergin/utils.py
index da2c9c00..51395f2a 100644
--- a/Mergin/utils.py
+++ b/Mergin/utils.py
@@ -1656,3 +1656,12 @@ def is_experimental_plugin_enabled() -> bool:
else:
value = settings.value("plugin-manager/allow-experimental", False)
return value
+
+
+def invalid_filename_character(filename: str) -> str:
+ """Returns invalid character for the filename"""
+ illegal_filename_chars = re.compile(r'[\x00-\x19<>:|?*"]')
+
+ match = illegal_filename_chars.search(filename)
+ if match:
+ return match.group()
diff --git a/Mergin/validation.py b/Mergin/validation.py
index 32831611..0f2e3eb9 100644
--- a/Mergin/validation.py
+++ b/Mergin/validation.py
@@ -27,9 +27,10 @@
QGIS_NET_PROVIDERS,
is_versioned_file,
get_layer_by_path,
+ invalid_filename_character,
)
-INVALID_CHARS = re.compile('[\\\/\(\)\[\]\{\}"\n\r]')
+INVALID_FIELD_NAME_CHARS = re.compile('[\\\/\(\)\[\]\{\}"\n\r]')
PROJECT_VARS = re.compile("\@project_home|\@project_path|\@project_folder")
@@ -62,6 +63,7 @@ class Warning(Enum):
EDITOR_JSON_CONFIG_CHANGE = 26
EDITOR_DIFFBASED_FILE_REMOVED = 27
PROJECT_HOME_PATH = 28
+ INVALID_ADDED_FILENAME = 29
class MultipleLayersWarning:
@@ -128,6 +130,7 @@ def run_checks(self):
self.check_datum_shift_grids()
self.check_svgs_embedded()
self.check_editor_perms()
+ self.check_filenames()
return self.issues
@@ -345,7 +348,7 @@ def check_field_names(self):
if dp.storageType() == "GPKG":
fields = layer.fields()
for f in fields:
- if INVALID_CHARS.search(f.name()):
+ if INVALID_FIELD_NAME_CHARS.search(f.name()):
self.issues.append(SingleLayerWarning(lid, Warning.INCORRECT_FIELD_NAME))
def check_snapping(self):
@@ -452,6 +455,12 @@ def check_editor_perms(self):
url = f"reset_file?layer={path}"
self.issues.append(SingleLayerWarning(layer.id(), Warning.EDITOR_DIFFBASED_FILE_REMOVED, url))
+ def check_filenames(self):
+ """Checks that files to upload have valid filenames. Otherwise, push will be refused by the server."""
+ for file in self.changes["added"]:
+ if invalid_filename_character(file["path"]):
+ self.issues.append(MultipleLayersWarning(Warning.INVALID_ADDED_FILENAME, file["path"]))
+
def warning_display_string(warning_id, url=None):
"""Returns a display string for a corresponding warning"""
@@ -516,3 +525,5 @@ def warning_display_string(warning_id, url=None):
return f"You don't have permission to remove this layer. Reset the layer to be able to sync changes."
elif warning_id == Warning.PROJECT_HOME_PATH:
return "QGIS Project Home Path is specified. Quick fix the issue. (This will unset project home)"
+ elif warning_id == Warning.INVALID_ADDED_FILENAME:
+ return f"You cannot synchronize a file with invalid characters in it's name. Please sanitize the name of this file '{url}'"