Skip to content
Open
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
193 changes: 103 additions & 90 deletions Mergin/projects_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
UnsavedChangesStrategy,
write_project_variables,
bytes_to_human_size,
get_push_changes_batch,
SYNC_ATTEMPTS,
SYNC_ATTEMPT_WAIT,
)
from .utils_auth import get_stored_mergin_server_url

Expand Down Expand Up @@ -167,6 +170,7 @@ def create_project(self, project_name, project_dir, is_public, namespace):
return True

dlg = SyncDialog()
dlg.labelStatus.setText("Starting project upload...")
dlg.push_start(self.mc, project_dir, full_project_name)

dlg.exec() # blocks until success, failure or cancellation
Expand Down Expand Up @@ -366,102 +370,111 @@ def sync_project(self, project_dir, project_name=None):
)
return

dlg = SyncDialog()
dlg.pull_start(self.mc, project_dir, project_name)

dlg.exec() # blocks until success, failure or cancellation

if dlg.exception:
# pull failed for some reason
if isinstance(dlg.exception, LoginError):
login_error_message(dlg.exception)
elif isinstance(dlg.exception, ClientError):
QMessageBox.critical(None, "Project sync", "Client error: " + str(dlg.exception))
elif isinstance(dlg.exception, AuthTokenExpiredError):
self.plugin.auth_token_expired()
else:
unhandled_exception_message(
dlg.exception_details(),
"Project sync",
f"Something went wrong while synchronising your project {project_name}.",
self.mc,
)
return

# after pull project might be in the unfinished pull state. So we
# have to check and if this is the case, try to close project and
# finish pull. As in the result we will have conflicted copies created
# we stop and ask user to examine them.
if self.mc.has_unfinished_pull(project_dir):
self.close_project_and_fix_pull(project_dir)
return

if dlg.pull_conflicts:
self.report_conflicts(dlg.pull_conflicts)
return

if not dlg.is_complete:
# we were cancelled
return
has_push_changes = True
error_retries_attempts = 0
while has_push_changes:
dlg = SyncDialog()
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to create dlg in while loop?

Copy link
Contributor Author

@MarcelGeo MarcelGeo Sep 26, 2025

Choose a reason for hiding this comment

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

pull and push is creating own sync dialog. I think there were some issue as I know @wonder-sk , that we need to recreate SyncDialog. But of course, we can try version without recreating dialog. Not sure If exec will be ok there.

pull_timeout = 250
if error_retries_attempts > 0:
pull_timeout = SYNC_ATTEMPT_WAIT * 1000
dlg.labelStatus.setText("Starting project synchronisation...")
dlg.pull_start(self.mc, project_dir, project_name, pull_timeout)
dlg.exec() # blocks until success, failure or cancellation

if dlg.exception:
# pull failed for some reason
if isinstance(dlg.exception, LoginError):
login_error_message(dlg.exception)
elif isinstance(dlg.exception, ClientError):
QMessageBox.critical(None, "Project sync", "Client error: " + str(dlg.exception))
elif isinstance(dlg.exception, AuthTokenExpiredError):
self.plugin.auth_token_expired()
else:
unhandled_exception_message(
dlg.exception_details(),
"Project sync",
f"Something went wrong while synchronising your project {project_name}.",
self.mc,
)
return

# pull finished, start push
if any(push_changes.values()) and not self.mc.has_writing_permissions(project_name):
QMessageBox.information(
None,
"Project sync",
"You have no writing rights to this project",
QMessageBox.StandardButton.Close,
)
return
# after pull project might be in the unfinished pull state. So we
# have to check and if this is the case, try to close project and
# finish pull. As in the result we will have conflicted copies created
# we stop and ask user to examine them.
if self.mc.has_unfinished_pull(project_dir):
self.close_project_and_fix_pull(project_dir)
return

dlg = SyncDialog()
dlg.push_start(self.mc, project_dir, project_name)
dlg.exec() # blocks until success, failure or cancellation
if dlg.pull_conflicts:
self.report_conflicts(dlg.pull_conflicts)
return

qgis_proj_filename = os.path.normpath(QgsProject.instance().fileName())
qgis_proj_basename = os.path.basename(qgis_proj_filename)
qgis_proj_changed = False
for updated in pull_changes["updated"]:
if updated["path"] == qgis_proj_basename:
qgis_proj_changed = True
break
if qgis_proj_filename in find_qgis_files(project_dir) and qgis_proj_changed:
self.open_project(project_dir)
if not dlg.is_complete:
# we were cancelled
return

if dlg.exception:
# push failed for some reason
if isinstance(dlg.exception, LoginError):
login_error_message(dlg.exception)
elif isinstance(dlg.exception, ClientError):
if dlg.exception.http_error == 400 and "Another process" in dlg.exception.detail:
# To note we check for a string since error in flask doesn't return server error code
msg = "Somebody else is syncing, please try again later"
elif dlg.exception.server_code == ErrorCode.StorageLimitHit.value:
msg = f"{dlg.exception.detail}\nCurrent limit: {bytes_to_human_size(dlg.exception.server_response['storage_limit'])}"
dlg = SyncDialog()
dlg.labelStatus.setText("Preparing project upload...")
dlg.push_start(self.mc, project_dir, project_name)
dlg.exec() # blocks until success, failure or cancellation

qgis_proj_filename = os.path.normpath(QgsProject.instance().fileName())
qgis_proj_basename = os.path.basename(qgis_proj_filename)
qgis_proj_changed = False
for updated in pull_changes["updated"]:
if updated["path"] == qgis_proj_basename:
qgis_proj_changed = True
break
if qgis_proj_filename in find_qgis_files(project_dir) and qgis_proj_changed:
self.open_project(project_dir)

if dlg.exception:
# push failed for some reason
if isinstance(dlg.exception, LoginError):
login_error_message(dlg.exception)
elif isinstance(dlg.exception, ClientError):
if error_retries_attempts < SYNC_ATTEMPTS - 1 and dlg.exception.is_retryable_sync():
error_retries_attempts += 1
continue # try again
if (
dlg.exception.http_error == 400
and "Another process" in dlg.exception.detail
or dlg.exception.server_code == ErrorCode.AnotherUploadRunning.value
):
# To note we check for a string since error in flask doesn't return server error code
msg = "Somebody else is syncing, please try again later"
elif dlg.exception.server_code == ErrorCode.StorageLimitHit.value:
msg = f"{dlg.exception.detail}\nCurrent limit: {bytes_to_human_size(dlg.exception.server_response['storage_limit'])}"
else:
msg = str(dlg.exception)
QMessageBox.critical(None, "Project sync", "Client error: \n" + msg)
elif isinstance(dlg.exception, AuthTokenExpiredError):
self.plugin.auth_token_expired()
else:
msg = str(dlg.exception)
QMessageBox.critical(None, "Project sync", "Client error: \n" + msg)
elif isinstance(dlg.exception, AuthTokenExpiredError):
self.plugin.auth_token_expired()
else:
unhandled_exception_message(
dlg.exception_details(),
"Project sync",
f"Something went wrong while synchronising your project {project_name}.",
self.mc,
)
return
unhandled_exception_message(
dlg.exception_details(),
"Project sync",
f"Something went wrong while synchronising your project {project_name}.",
self.mc,
)
return

if dlg.is_complete:
# TODO: report success only when we have actually done anything
msg = "Mergin Maps project {} synchronised successfully".format(project_name)
QMessageBox.information(None, "Project sync", msg, QMessageBox.StandardButton.Close)
# clear canvas cache so any changes become immediately visible to users
self.iface.mapCanvas().clearCache()
self.iface.mapCanvas().refresh()
else:
# we were cancelled - but no need to show a message box about that...?
pass
if not dlg.is_complete:
# we were cancelled
return
_, has_push_changes = get_push_changes_batch(self.mc, project_dir)
error_retries_attempts = 0
if not has_push_changes:
# TODO: report success only when we have actually done anything
msg = "Mergin Maps project {} synchronised successfully".format(project_name)
QMessageBox.information(None, "Project sync", msg, QMessageBox.StandardButton.Close)
# clear canvas cache so any changes become immediately visible to users
self.iface.mapCanvas().clearCache()
self.iface.mapCanvas().refresh()
else:
# we were cancelled - but no need to show a message box about that...?
pass

def submit_logs(self, project_dir):
logs_path = os.path.join(project_dir, ".mergin", "client-log.txt")
Expand Down
12 changes: 4 additions & 8 deletions Mergin/sync_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,17 +161,15 @@ def download_cancel(self):
else:
self.cancel_sync_operation("Cancelling download...", download_project_cancel)

def push_start(self, mergin_client, target_dir, project_name):
def push_start(self, mergin_client, target_dir, project_name, timeout=250):
self.operation = self.PUSH
self.mergin_client = mergin_client
self.target_dir = target_dir
self.project_name = project_name

self.labelStatus.setText("Querying project...")

# we would like to get the dialog displayed at least for a bit
# with low timeout (or zero) it may not even appear before it is closed
QTimer.singleShot(250, self.push_start_internal)
QTimer.singleShot(timeout, self.push_start_internal)

def push_start_internal(self):
with OverrideCursor(Qt.CursorShape.WaitCursor):
Expand Down Expand Up @@ -227,17 +225,15 @@ def push_cancel(self):
else:
self.cancel_sync_operation("Cancelling sync...", push_project_cancel)

def pull_start(self, mergin_client, target_dir, project_name):
def pull_start(self, mergin_client, target_dir, project_name, timeout=250):
self.operation = self.PULL
self.mergin_client = mergin_client
self.target_dir = target_dir
self.project_name = project_name

self.labelStatus.setText("Querying project...")

# we would like to get the dialog displayed at least for a bit
# with low timeout (or zero) it may not even appear before it is closed
QTimer.singleShot(250, self.pull_start_internal)
QTimer.singleShot(timeout, self.pull_start_internal)

def pull_start_internal(self):
with OverrideCursor(Qt.CursorShape.WaitCursor):
Expand Down
6 changes: 4 additions & 2 deletions Mergin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
from .mergin.merginproject import MerginProject

try:
from .mergin.common import ClientError, ErrorCode, LoginError, InvalidProject
from .mergin.common import ClientError, ErrorCode, LoginError, InvalidProject, SYNC_ATTEMPTS, SYNC_ATTEMPT_WAIT
from .mergin.client import MerginClient, ServerType
from .mergin.client_pull import (
download_project_async,
Expand All @@ -95,6 +95,7 @@
push_project_is_running,
push_project_finalize,
push_project_cancel,
get_push_changes_batch,
)
from .mergin.report import create_report
from .mergin.deps import pygeodiff
Expand All @@ -105,7 +106,7 @@
path = os.path.join(this_dir, "mergin_client.whl")
sys.path.append(path)
from mergin.client import MerginClient, ServerType
from mergin.common import ClientError, InvalidProject, LoginError
from mergin.common import ClientError, InvalidProject, LoginError, PUSH_ATTEMPTS, PUSH_ATTEMPT_WAIT

from mergin.client_pull import (
download_project_async,
Expand All @@ -124,6 +125,7 @@
push_project_is_running,
push_project_finalize,
push_project_cancel,
get_push_changes_batch,
)
from .mergin.report import create_report
from .mergin.deps import pygeodiff
Expand Down
Loading