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.
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
src/resources/images/icons8-cloud-80.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
BIN
src/resources/images/icons8-cloud-done-80.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/resources/images/icons8-cloud-error-80.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
src/resources/images/icons8-cloud-stop-80.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
BIN
src/resources/images/icons8-cloud-sync-80.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 654 B |
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -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
|
||||
|
||||