1
0
mirror of https://github.com/bpozdena/OneDriveGUI.git synced 2026-10-24 04:59:17 +03:00

Refactor OneDrive sync error handling and update image resources

- Enhanced error reporting for failed file uploads/downloads in the WorkerThread class.
- Introduced a mechanism to collect and display detailed information about failed files.
- Removed unused cloud icon images and added new icons for better visual representation.
- Updated the OneDrive sync completion status messages to reflect the number of failed files.
This commit is contained in:
Boris Pozdena
2025-11-21 14:35:32 +01:00
parent 373c6e8333
commit 332d07373e
15 changed files with 231 additions and 6 deletions

View File

@@ -86,3 +86,6 @@ A GUI for [Linux OneDrive Client](https://github.com/abraunegg/onedrive) with mu
- The list of managed OneDrive profiles is stored in `~/.config/onedrive-gui/profiles`. You can manually rename your profile or path to the config file there.
- Newly created OneDrive config files are stored in `~/.config/onedrive/accounts/<profile_name>`.
- Debug logs are saved in ``. Logging can be changed in the GUI settings.
# Attribution
- Icons by <a target="_blank" href="https://icons8.com">Icons8</a>

View File

@@ -19,7 +19,7 @@ from global_config import DIR_PATH
app = QApplication(sys.argv)
app.setApplicationName("OneDriveGUI")
app.setDesktopFileName("OneDriveGUI")
app.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png"))
app.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-cloud-80.png"))
from options import gui_settings, global_config, version

View File

@@ -74,7 +74,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
super(MainWindow, self).__init__()
self.setupUi(self)
self.setWindowTitle(f"OneDriveGUI v{version}")
self.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png"))
self.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-cloud-80.png"))
if gui_settings.get("frameless_window") == "True":
self.setWindowFlags(Qt.FramelessWindowHint)
@@ -150,7 +150,8 @@ class MainWindow(QMainWindow, Ui_MainWindow):
# System Tray
self.tray = QSystemTrayIcon()
if self.tray.isSystemTrayAvailable():
icon = QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png")
# Initial icon will be updated by update_tray_icon() after profiles are loaded
icon = QIcon(DIR_PATH + "/resources/images/icons8-cloud-80.png")
menu = QMenu()
show_action = menu.addAction("Show/Hide")
@@ -165,6 +166,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.tray.setIcon(icon)
self.tray.setContextMenu(menu)
self.tray.show()
# Initial tooltip - will be updated by update_tray_icon()
self.tray.setToolTip("OneDriveGUI")
else:
@@ -510,6 +512,162 @@ class MainWindow(QMainWindow, Ui_MainWindow):
profile_status_page.pushButton_start_stop.clicked.disconnect()
profile_status_page.pushButton_start_stop.clicked.connect(profile_status_page.stop_monitor)
# Update system tray icon based on aggregate status
self.update_tray_icon()
def is_ignorable_error(self, error_text):
"""
Check if an error message should be ignored for tray icon status.
Returns True if the error should be ignored.
"""
if not error_text:
return True
# List of error patterns to ignore
ignored_patterns = ["inotify_add_watch failed", "The local file system returned an error with the following message"]
error_lower = error_text.lower()
for pattern in ignored_patterns:
if pattern.lower() in error_lower:
return True
return False
def aggregate_tray_status(self):
"""
Aggregate status across all profiles to determine system tray icon state.
Returns: tuple (status_state, running_count, total_count, profile_statuses)
status_state: 'ERROR', 'STOPPED', 'SYNCING', or 'IDLE'
Priority: ERROR > STOPPED > SYNCING > IDLE
"""
if not global_config:
return ("IDLE", 0, 0, {})
total_count = len(global_config)
running_count = 0
profile_statuses = {}
has_error = False
has_stopped = False
has_syncing = False
for profile_name in global_config:
# Check if worker is running
is_running = profile_name in workers and workers[profile_name].isRunning()
# Get status message from profile status page
if profile_name in self.profile_status_pages:
status_msg = self.profile_status_pages[profile_name].label_onedrive_status.text()
error_tooltip = self.profile_status_pages[profile_name].label_error_icon.toolTip()
else:
status_msg = ""
error_tooltip = ""
# Categorize this profile's state
if not is_running:
profile_state = "STOPPED"
has_stopped = True
elif (not self.is_ignorable_error(error_tooltip) and not self.is_ignorable_error(status_msg)) and (
error_tooltip or "Error:" in status_msg or "error" in status_msg.lower() or "failed" in status_msg.lower() or "crashed" in status_msg.lower()
):
profile_state = "ERROR"
has_error = True
running_count += 1
elif "sync in progress" in status_msg.lower() or "processing" in status_msg.lower() or "starting" in status_msg.lower() or "initializing" in status_msg.lower():
profile_state = "SYNCING"
has_syncing = True
running_count += 1
elif is_running:
profile_state = "IDLE"
running_count += 1
else:
profile_state = "STOPPED"
has_stopped = True
profile_statuses[profile_name] = {"state": profile_state, "status_msg": status_msg if status_msg else "stopped", "is_running": is_running}
# Determine overall state with priority
if has_error:
overall_state = "ERROR"
elif has_stopped:
overall_state = "STOPPED"
elif has_syncing:
overall_state = "SYNCING"
else:
overall_state = "IDLE"
return (overall_state, running_count, total_count, profile_statuses)
def generate_tray_tooltip(self, overall_state, running_count, total_count, profile_statuses):
"""
Generate detailed tooltip showing status of all profiles.
"""
if not profile_statuses:
return "OneDriveGUI - No profiles configured"
# Summary line
if running_count == total_count and overall_state != "ERROR":
summary = f"All {total_count} profile(s) running"
elif running_count == 0:
summary = f"All {total_count} profile(s) stopped"
else:
summary = f"{running_count} of {total_count} profile(s) running"
# Add overall state indicator
state_indicators = {"ERROR": "Sync error detected", "STOPPED": "Sync stopped", "SYNCING": "Syncing...", "IDLE": "All syncs complete"}
summary += f" - {state_indicators.get(overall_state, '')}"
# Build detailed list
lines = [summary, ""]
for profile_name, status_info in profile_statuses.items():
state = status_info["state"]
status_msg = status_info["status_msg"]
# Add text indicator for each profile
if state == "ERROR":
indicator = "[ERROR]"
elif state == "STOPPED":
indicator = "[STOPPED]"
elif state == "SYNCING":
indicator = "[SYNCING]"
else:
indicator = "[IDLE]"
# Shorten status message if too long
if len(status_msg) > 50:
status_msg = status_msg[:47] + "..."
lines.append(f"{indicator} {profile_name}: {status_msg}")
return "\n".join(lines)
def update_tray_icon(self):
"""
Update system tray icon and tooltip based on current sync status.
"""
if not self.tray:
return
# Get aggregated status
overall_state, running_count, total_count, profile_statuses = self.aggregate_tray_status()
# Select icon based on state
icon_map = {
"ERROR": DIR_PATH + "/resources/images/icons8-cloud-error-80.png",
"STOPPED": DIR_PATH + "/resources/images/icons8-cloud-stop-80.png",
"SYNCING": DIR_PATH + "/resources/images/icons8-cloud-sync-80.png",
"IDLE": DIR_PATH + "/resources/images/icons8-cloud-done-80.png",
}
icon_path = icon_map.get(overall_state, DIR_PATH + "/resources/images/icons8-cloud-80.png")
icon = QIcon(icon_path)
self.tray.setIcon(icon)
# Generate and set tooltip
tooltip = self.generate_tray_tooltip(overall_state, running_count, total_count, profile_statuses)
self.tray.setToolTip(tooltip)
def autostart_monitor(self):
# Auto-start sync if compatible version of OneDrive client is installed.
for profile_name in global_config:
@@ -632,6 +790,9 @@ class MainWindow(QMainWindow, Ui_MainWindow):
self.profile_status_pages[profile].label_error_icon.clear()
self.profile_status_pages[profile].label_error_icon.setToolTip("")
# Update system tray icon when profile status changes
self.update_tray_icon()
def event_update_progress(self, data, profile):
"""
data:
@@ -768,7 +929,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
def show_login(self, profile):
# Show login window with QT WebEngine
self.window1 = QWidget()
self.window1.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png"))
self.window1.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-cloud-80.png"))
self.lw = Ui_LoginWindow()
self.lw.setupUi(self.window1)
self.window1.show()
@@ -792,7 +953,7 @@ class MainWindow(QMainWindow, Ui_MainWindow):
def show_external_login(self, profile):
# Show external login window
self.window2 = QWidget()
self.window2.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-clouds-80-dark-edge.png"))
self.window2.setWindowIcon(QIcon(DIR_PATH + "/resources/images/icons8-cloud-80.png"))
self.lw2 = Ui_ExternalLoginWindow()
self.lw2.setupUi(self.window2)
self.window2.show()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -95,6 +95,11 @@ class WorkerThread(QThread):
# Track pending error for multi-line error messages
self.pending_error = None
# Track failed files for detailed error reporting
self.failed_files = []
self.failed_files_count = 0
self.collecting_failed_files = False # Flag to indicate we're collecting failed file lines
self.profile_status["status_message"] = "OneDrive sync is starting..."
self.update_profile_status.emit(self.profile_status, self.profile_name)
@@ -159,6 +164,32 @@ class WorkerThread(QThread):
self.profile_status["error_message"] = full_error_message # Full error for tooltip
self.update_profile_status.emit(self.profile_status, self.profile_name)
def _emit_failed_files_status(self):
"""Format and emit status for failed file uploads/downloads."""
if not self.failed_files and self.failed_files_count == 0:
return
# Use the actual count from the summary line if available, otherwise use list length
count = self.failed_files_count if self.failed_files_count > 0 else len(self.failed_files)
# Status message (truncated for main UI)
status_msg = f"Sync completed with {count} failed file(s)"
# Tooltip message (detailed list)
tooltip_lines = [f"Failed items to upload to/from Microsoft OneDrive: {count}"]
tooltip_lines.extend(self.failed_files)
# If there are more failures than we captured, add indicator
if count > len(self.failed_files):
remaining = count - len(self.failed_files)
tooltip_lines.append(f"... and {remaining} more")
full_tooltip = "\n".join(tooltip_lines)
self.profile_status["status_message"] = status_msg
self.profile_status["error_message"] = full_tooltip
self.update_profile_status.emit(self.profile_status, self.profile_name)
def read_stdout(self):
stdout = self.onedrive_process.stdout.readline().strip()
if stdout != "":
@@ -170,6 +201,12 @@ class WorkerThread(QThread):
self._emit_error_status(self.pending_error)
self.pending_error = 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:
# We're done collecting, emit the status
self._emit_failed_files_status()
self.collecting_failed_files = False
if "Calling Function: testNetwork()" in stdout:
self.profile_status["status_message"] = "Cannot connect to Microsoft OneDrive Service"
self.update_profile_status.emit(self.profile_status, self.profile_name)
@@ -180,7 +217,7 @@ class WorkerThread(QThread):
self.update_profile_status.emit(self.profile_status, self.profile_name)
self.update_credentials.emit(self.profile_name)
elif any(msg in stdout for msg in ["Sync with Microsoft OneDrive is complete", "Total number of local file(s) added or changed", "Scanning the local file system"]):
elif any(msg in stdout for msg in ["Sync with Microsoft OneDrive is complete", "Total number of local file(s) added or changed"]):
self.profile_status["status_message"] = "OneDrive sync is complete"
self.update_profile_status.emit(self.profile_status, self.profile_name)
@@ -216,6 +253,10 @@ class WorkerThread(QThread):
# save_global_config()
elif "Initializing the OneDrive API" in stdout:
# Clear failed files list at start of new sync cycle
self.failed_files = []
self.failed_files_count = 0
self.collecting_failed_files = False
self.profile_status["status_message"] = "Initializing the OneDrive API"
self.update_profile_status.emit(self.profile_status, self.profile_name)
@@ -365,6 +406,26 @@ class WorkerThread(QThread):
# Store as pending error to check if next line has "Error Message:"
self.pending_error = error_text
elif "Failed items to upload to/from Microsoft OneDrive:" in stdout:
# Extract the count of failed items
match = re.search(r"Failed items .+?: (\d+)", stdout)
if match:
self.failed_files_count = int(match.group(1))
# Start collecting failed file lines (they come after this summary line)
self.collecting_failed_files = True
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)
if match:
operation = match.group(1)
file_path = match.group(2).strip()
failed_entry = f"Failed to {operation}: {file_path}"
# Add to list, limiting to 25 entries
if len(self.failed_files) < 25:
self.failed_files.append(failed_entry)
else:
# logging.debug(f"No rule matched: {stdout}")
pass