From 4ccea940e8de68c496920503bf574986cd02881d Mon Sep 17 00:00:00 2001 From: Boris Pozdena Date: Sun, 18 Oct 2026 10:24:26 +0200 Subject: [PATCH] Report local Recycle Bin move failures Client v2.5.12 reports online deletions that could not be moved to the configured local Recycle Bin, both as an error line ("ERROR: Move of local file to the configured Recycle Bin failed for : ") and in the end-of-cycle summary ("Failed local items to move to the configured Recycle Bin: N" followed by one line per path). These failures are now included in the failed-files count, status tooltip and tray notification, and each affected path gets a "Move to Recycle Bin failed" row in the file operation list with the reason in its tooltip. Failed rows for deletions and Recycle Bin moves are no longer rendered as completed "Deleted"/"Trashed" entries. Refs #305 --- src/onedrive_gui/main_window.py | 16 +++++++++------ src/onedrive_gui/workers.py | 36 ++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/onedrive_gui/main_window.py b/src/onedrive_gui/main_window.py index 02058bb..65b296c 100644 --- a/src/onedrive_gui/main_window.py +++ b/src/onedrive_gui/main_window.py @@ -62,6 +62,10 @@ except ImportError: window.hide() +# Label of a failed file operation in the file operation list. +FAILED_OPERATION_LABELS = {"Uploading": "Upload", "Downloading": "Download", "Moving": "Move to Recycle Bin"} + + class MainWindow(QMainWindow, Ui_MainWindow): def __init__(self): # Expose this instance as main_window.main_window_instance so other modules @@ -1066,7 +1070,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): item_widget.set_icon(file_path) item_widget.hide_progress_bar(transfer_complete or transfer_failed) - if file_operation == "Deleting": + if file_operation == "Deleting" and not transfer_failed: item_widget.set_label_1(f"Deleted from {parent_dir}") item_widget.set_custom_icon(self.trash_icon) item_widget.set_label_2(f"") @@ -1078,7 +1082,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): else: item_widget.set_timestamp("") - elif file_operation == "Moving": + elif file_operation == "Moving" and not transfer_failed: item_widget.set_label_1(f"Trashed from {parent_dir}") item_widget.set_label_2(f"") # Store timestamp and display for moved files @@ -1097,7 +1101,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): # is deliberately left empty so repeated attempts of the # same file update this row rather than adding duplicates, # and a later successful upload flips it back to "Uploaded". - failed_operation = "Upload" if file_operation == "Uploading" else "Download" + failed_operation = FAILED_OPERATION_LABELS.get(file_operation, "Download") shortened_path = shorten_path(relative_path_display, 32) item_widget.set_label_1(f"{failed_operation} failed - {shortened_path}") item_widget.set_label_2(f"") @@ -1181,7 +1185,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): myQCustomQWidget.set_icon(file_path) myQCustomQWidget.hide_progress_bar(transfer_complete or transfer_failed) - if file_operation == "Deleting": + if file_operation == "Deleting" and not transfer_failed: myQCustomQWidget.set_label_1(f"Deleted from {parent_dir}") myQCustomQWidget.set_custom_icon(self.trash_icon) myQCustomQWidget.set_label_2(f"") @@ -1193,7 +1197,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): else: myQCustomQWidget.set_timestamp("") - elif file_operation == "Moving": + elif file_operation == "Moving" and not transfer_failed: myQCustomQWidget.set_label_1(f"Trashed from {parent_dir}") myQCustomQWidget.set_label_2(f"") # Store timestamp and display for moved files @@ -1207,7 +1211,7 @@ class MainWindow(QMainWindow, Ui_MainWindow): elif transfer_failed: # The transfer failed before an item existed - create it in the failed # state so the failure is visible in the file operation list. - failed_operation = "Upload" if file_operation == "Uploading" else "Download" + failed_operation = FAILED_OPERATION_LABELS.get(file_operation, "Download") shortened_path = shorten_path(relative_path_display, 32) myQCustomQWidget.set_label_1(f"{failed_operation} failed - {shortened_path}") myQCustomQWidget.set_label_2(f"") diff --git a/src/onedrive_gui/workers.py b/src/onedrive_gui/workers.py index b6f8d4a..2abf181 100644 --- a/src/onedrive_gui/workers.py +++ b/src/onedrive_gui/workers.py @@ -315,7 +315,7 @@ class WorkerThread(QThread): self.pending_error_path = None # Check if we were collecting failed files and this line is not a "Failed to" line - if self.collecting_failed_files and "Failed to upload:" not in stdout and "Failed to download:" not in stdout: + if self.collecting_failed_files and not any(m in stdout for m in ("Failed to upload:", "Failed to download:", "Failed to move to the configured Recycle Bin:")): # We're done collecting, emit the status self._emit_failed_files_status() self.collecting_failed_files = False @@ -647,6 +647,16 @@ class WorkerThread(QThread): if path_value and path_value != "(not available)": self.pending_error_path = path_value + elif "to the configured Recycle Bin failed for " in stdout: + # Client v2.5.12+: 'ERROR: Move of local file to the configured Recycle Bin + # failed for : ' - single line, no 'Path:' continuation. + error_text = stdout.split("ERROR:", 1)[-1].strip() + match = re.search(r"failed for (.+?): ([^:]+)$", error_text) + if match: + self.file_errors[_path_key(match.group(1).strip())] = error_text + logging.error(f"[{self.profile_name}] {error_text}") + self._emit_error_status(error_text) + elif "ERROR:" in stdout: # Extract error message after "ERROR:" prefix error_parts = stdout.split("ERROR:", 1) @@ -673,6 +683,30 @@ class WorkerThread(QThread): # Start collecting failed file lines (they come after this summary line) self.collecting_failed_files = True + elif stdout.startswith("Failed local items to move to the configured Recycle Bin:"): + # Client v2.5.12+: online deletions that could not be moved to the + # local Recycle Bin are summarised after the transfer failures. + match = re.search(r": (\d+)$", stdout) + if match: + self.failed_files_count += int(match.group(1)) + self.collecting_failed_files = True + + elif stdout.startswith("Failed to move to the configured Recycle Bin:"): + file_path = stdout.split(":", 1)[1].strip() + if len(self.failed_files) < 25: + self.failed_files.append(f"Failed to move to Recycle Bin: {file_path}") + transfer_progress_new = { + "file_operation": "Moving", + "file_path": file_path, + "progress": "0", + "transfer_complete": False, + "transfer_failed": True, + "error_message": self.file_errors.get(_path_key(file_path)), + "timestamp": None, + } + logging.debug(transfer_progress_new) + self.update_progress_new.emit(transfer_progress_new, self.profile_name) + elif "Failed to upload:" in stdout or "Failed to download:" in stdout: # Extract the operation and file path match = re.search(r"Failed to (upload|download): (.+)$", stdout)