|
5 | 5 | from urllib.parse import urlparse |
6 | 6 | from pathlib import Path |
7 | 7 | import posixpath |
| 8 | +import json |
8 | 9 |
|
9 | 10 | from qgis.core import QgsProject, Qgis, QgsApplication |
10 | 11 | from qgis.utils import iface, OverrideCursor |
|
34 | 35 | UnsavedChangesStrategy, |
35 | 36 | write_project_variables, |
36 | 37 | bytes_to_human_size, |
| 38 | + push_error_message, |
37 | 39 | ) |
38 | 40 | from .utils_auth import get_stored_mergin_server_url |
39 | 41 |
|
@@ -111,12 +113,31 @@ def create_project(self, project_name, project_dir, is_public, namespace): |
111 | 113 | "Please try renaming the project." |
112 | 114 | ) |
113 | 115 | elif e.server_code == ErrorCode.ProjectsLimitHit.value: |
| 116 | + data = e.server_response |
| 117 | + if isinstance(data, str): |
| 118 | + try: |
| 119 | + data = json.loads(data) # convert string to json |
| 120 | + except json.JSONDecodeError: |
| 121 | + data = {} |
| 122 | + |
| 123 | + quota = data.get("projects_quota", "unknown") |
| 124 | + |
114 | 125 | msg = ( |
115 | 126 | "Maximum number of projects reached. Please upgrade your subscription to create new projects.\n" |
116 | | - f"Projects quota: {e.server_response['projects_quota']}" |
| 127 | + f"Projects quota: {quota}" |
117 | 128 | ) |
118 | 129 | elif e.server_code == ErrorCode.StorageLimitHit.value: |
119 | | - msg = f"{e.detail}\nCurrent limit: {bytes_to_human_size(e.server_response['storage_limit'])}" |
| 130 | + data = e.server_response |
| 131 | + if isinstance(data, str): |
| 132 | + try: |
| 133 | + data = json.loads(data) |
| 134 | + except json.JSONDecodeError: |
| 135 | + data = {} |
| 136 | + |
| 137 | + storage_limit = data.get("storage_limit") |
| 138 | + human_limit = bytes_to_human_size(storage_limit) if storage_limit is not None else "unknown" |
| 139 | + |
| 140 | + msg = f"{e.detail}\nCurrent limit: {human_limit}" |
120 | 141 |
|
121 | 142 | QMessageBox.critical( |
122 | 143 | None, |
@@ -172,18 +193,7 @@ def create_project(self, project_name, project_dir, is_public, namespace): |
172 | 193 | dlg.exec() # blocks until success, failure or cancellation |
173 | 194 |
|
174 | 195 | if dlg.exception: |
175 | | - # push failed for some reason |
176 | | - if isinstance(dlg.exception, LoginError): |
177 | | - login_error_message(dlg.exception) |
178 | | - elif isinstance(dlg.exception, ClientError): |
179 | | - QMessageBox.critical(None, "Project sync", "Client error: " + str(dlg.exception)) |
180 | | - else: |
181 | | - unhandled_exception_message( |
182 | | - dlg.exception_details(), |
183 | | - "Project sync", |
184 | | - f"Something went wrong while synchronising your project {project_name}.", |
185 | | - self.mc, |
186 | | - ) |
| 196 | + push_error_message(dlg, project_name, self.plugin, self.mc) |
187 | 197 | return True |
188 | 198 |
|
189 | 199 | if not dlg.is_complete: |
@@ -298,32 +308,48 @@ def reset_local_changes(self, project_dir: str, files_to_reset=None): |
298 | 308 |
|
299 | 309 | current_project_filename = os.path.normpath(QgsProject.instance().fileName()) |
300 | 310 | current_project_path = os.path.normpath(QgsProject.instance().absolutePath()) |
| 311 | + |
| 312 | + # Windows-specific behavior: |
| 313 | + # When a project is opened from this directory, QGIS may keep GPKG file handles |
| 314 | + # for a short time after closing the project. The same workaround is used in |
| 315 | + # `close_project_and_fix_pull()` (unfinished pull handling). |
| 316 | + delay = 0 |
301 | 317 | if current_project_path == os.path.normpath(project_dir): |
302 | 318 | QgsProject.instance().clear() |
| 319 | + QApplication.processEvents() |
| 320 | + delay = 2500 # allow OS to release locked GPKG handles |
303 | 321 |
|
304 | | - try: |
305 | | - self.mc.reset_local_changes(project_dir, files_to_reset) |
306 | | - if files_to_reset: |
307 | | - msg = f"File {files_to_reset} was successfully reset" |
308 | | - else: |
309 | | - msg = "Project local changes were successfully reset" |
310 | | - QMessageBox.information( |
311 | | - None, |
312 | | - "Project reset local changes", |
313 | | - msg, |
314 | | - QMessageBox.StandardButton.Close, |
315 | | - ) |
| 322 | + def do_reset(): |
| 323 | + try: |
| 324 | + self.mc.reset_local_changes(project_dir, files_to_reset) |
316 | 325 |
|
317 | | - except Exception as e: |
318 | | - msg = f"Failed to reset local changes:\n\n{str(e)}" |
319 | | - QMessageBox.critical( |
320 | | - None, |
321 | | - "Project reset local changes", |
322 | | - msg, |
323 | | - QMessageBox.StandardButton.Close, |
324 | | - ) |
| 326 | + if files_to_reset: |
| 327 | + msg = f"File {files_to_reset} was successfully reset" |
| 328 | + else: |
| 329 | + msg = "Project local changes were successfully reset" |
| 330 | + |
| 331 | + QMessageBox.information( |
| 332 | + None, |
| 333 | + "Project reset local changes", |
| 334 | + msg, |
| 335 | + QMessageBox.StandardButton.Close, |
| 336 | + ) |
| 337 | + |
| 338 | + except Exception as e: |
| 339 | + msg = f"Failed to reset local changes:\n\n{str(e)}" |
| 340 | + QMessageBox.critical( |
| 341 | + None, |
| 342 | + "Project reset local changes", |
| 343 | + msg, |
| 344 | + QMessageBox.StandardButton.Close, |
| 345 | + ) |
| 346 | + |
| 347 | + # Reopen the project after successful or failed reset |
| 348 | + self.open_project(os.path.dirname(current_project_filename)) |
325 | 349 |
|
326 | | - self.open_project(os.path.dirname(current_project_filename)) |
| 350 | + # Run the reset after delay (0 ms on Linux/macOS, 2500 ms on Windows) |
| 351 | + # This mirrors the pattern from unfinished pull resolution. |
| 352 | + QTimer.singleShot(delay, do_reset) |
327 | 353 |
|
328 | 354 | def sync_project(self, project_dir, project_name=None): |
329 | 355 | if not project_dir: |
@@ -429,27 +455,7 @@ def sync_project(self, project_dir, project_name=None): |
429 | 455 | self.open_project(project_dir) |
430 | 456 |
|
431 | 457 | if dlg.exception: |
432 | | - # push failed for some reason |
433 | | - if isinstance(dlg.exception, LoginError): |
434 | | - login_error_message(dlg.exception) |
435 | | - elif isinstance(dlg.exception, ClientError): |
436 | | - if dlg.exception.http_error == 400 and "Another process" in dlg.exception.detail: |
437 | | - # To note we check for a string since error in flask doesn't return server error code |
438 | | - msg = "Somebody else is syncing, please try again later" |
439 | | - elif dlg.exception.server_code == ErrorCode.StorageLimitHit.value: |
440 | | - msg = f"{dlg.exception.detail}\nCurrent limit: {bytes_to_human_size(dlg.exception.server_response['storage_limit'])}" |
441 | | - else: |
442 | | - msg = str(dlg.exception) |
443 | | - QMessageBox.critical(None, "Project sync", "Client error: \n" + msg) |
444 | | - elif isinstance(dlg.exception, AuthTokenExpiredError): |
445 | | - self.plugin.auth_token_expired() |
446 | | - else: |
447 | | - unhandled_exception_message( |
448 | | - dlg.exception_details(), |
449 | | - "Project sync", |
450 | | - f"Something went wrong while synchronising your project {project_name}.", |
451 | | - self.mc, |
452 | | - ) |
| 458 | + push_error_message(dlg, project_name, self.plugin, self.mc) |
453 | 459 | return |
454 | 460 |
|
455 | 461 | if dlg.is_complete: |
|
0 commit comments