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)