diff --git a/desktop/crates/cloudreve-sync/src/drive/commands.rs b/desktop/crates/cloudreve-sync/src/drive/commands.rs index 1eec8630..0af5f462 100644 --- a/desktop/crates/cloudreve-sync/src/drive/commands.rs +++ b/desktop/crates/cloudreve-sync/src/drive/commands.rs @@ -132,6 +132,9 @@ pub enum MountCommand { source: PathBuf, destination: PathBuf, }, + /// Restart the remote event listener with a fresh backoff and kick a + /// full-hierarchy sync. User-triggered recovery for a lost connection. + Reconnect, } // SAFETY: Windows CFAPI is designed to allow callbacks from arbitrary threads. diff --git a/desktop/crates/cloudreve-sync/src/drive/mounts.rs b/desktop/crates/cloudreve-sync/src/drive/mounts.rs index 33049a8a..50ad6679 100644 --- a/desktop/crates/cloudreve-sync/src/drive/mounts.rs +++ b/desktop/crates/cloudreve-sync/src/drive/mounts.rs @@ -9,7 +9,7 @@ use crate::drive::commands::ManagerCommand; use crate::drive::commands::MountCommand; use crate::drive::event_blocker::EventBlocker; use crate::drive::ignore::IgnoreMatcher; -use crate::drive::sync::group_fs_events; +use crate::drive::sync::{SyncMode, group_fs_events}; #[cfg(windows)] use crate::drive::utils::recycle_bin_url; use crate::inventory::{DrivePropsUpdate, InventoryDb, TaskRecord}; @@ -653,6 +653,32 @@ impl Mount { } }); } + MountCommand::Reconnect => { + let s_clone = s.clone(); + let mount_id_clone = mount_id.clone(); + spawn(async move { + // Restart the event listener so it retries immediately + // instead of sitting in the long-retry sleep. + if let Some(handle) = + s_clone.remote_event_handle.lock().await.take() + { + handle.abort(); + let _ = handle.await; + } + s_clone.spawn_remote_event_processor(s_clone.clone()).await; + + let sync_path = { + let config = s_clone.config.read().await; + config.sync_path.clone() + }; + let _ = s_clone.command_tx.send(MountCommand::Sync { + local_paths: vec![sync_path], + mode: SyncMode::FullHierarchy, + user_initiated: true, + }); + tracing::info!(target: "drive::mounts", id = %mount_id_clone, "Reconnect requested"); + }); + } MountCommand::Renamed { source, destination, diff --git a/desktop/src-tauri/src/commands.rs b/desktop/src-tauri/src/commands.rs index 0ff98a30..238a2075 100644 --- a/desktop/src-tauri/src/commands.rs +++ b/desktop/src-tauri/src/commands.rs @@ -205,6 +205,28 @@ pub async fn remove_drive( Ok(result) } +/// Manually reconnect a drive: restart its remote event listener with a +/// fresh retry backoff and trigger a full sync. Recovers drives stuck in +/// the long-retry wait after repeated connection failures. +#[tauri::command] +pub async fn reconnect_drive( + state: State<'_, AppStateHandle>, + drive_id: String, +) -> CommandResult<()> { + let app_state = state + .get() + .ok_or_else(|| "App not yet initialized".to_string())?; + let mount = app_state + .drive_manager + .get_drive(&drive_id) + .await + .ok_or_else(|| "Drive not found".to_string())?; + mount + .command_tx + .send(cloudreve_sync::drive::commands::MountCommand::Reconnect) + .map_err(|e| e.to_string()) +} + /// Get ignore patterns for a drive #[tauri::command] pub async fn get_ignore_patterns( diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index 603fae00..6356e011 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -459,6 +459,7 @@ pub fn run() { commands::list_drives, commands::add_drive, commands::remove_drive, + commands::reconnect_drive, commands::get_ignore_patterns, commands::set_ignore_patterns, commands::get_sync_status, diff --git a/desktop/ui/public/locales/de/common.json b/desktop/ui/public/locales/de/common.json index c4c7ce12..02d46dad 100644 --- a/desktop/ui/public/locales/de/common.json +++ b/desktop/ui/public/locales/de/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "Die Synchronisierungsaufgabe wartet, bis die Datei nicht mehr geändert wird", "hideTrayIcon": "Infobereichssymbol ausblenden", "hideTrayIconDescription": "Im Hintergrund ohne Infobereichssymbol ausführen", + "reconnect": "Neu verbinden", "storage": "Speicher", "openFolder": "Ordner öffnen", "openSite": "Website öffnen", diff --git a/desktop/ui/public/locales/en-US/common.json b/desktop/ui/public/locales/en-US/common.json index af5050b8..2431712c 100644 --- a/desktop/ui/public/locales/en-US/common.json +++ b/desktop/ui/public/locales/en-US/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "Sync task wait until file stop changing", "hideTrayIcon": "Hide tray icon", "hideTrayIconDescription": "Run in the background without a system tray icon", + "reconnect": "Reconnect", "storage": "Storage", "openFolder": "Open folder", "openSite": "Open site", diff --git a/desktop/ui/public/locales/es/common.json b/desktop/ui/public/locales/es/common.json index a3d8628d..2f0ae630 100644 --- a/desktop/ui/public/locales/es/common.json +++ b/desktop/ui/public/locales/es/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "La tarea de sincronización espera hasta que el archivo deje de cambiar", "hideTrayIcon": "Ocultar icono de bandeja", "hideTrayIconDescription": "Ejecutar en segundo plano sin icono en la bandeja del sistema", + "reconnect": "Reconectar", "storage": "Almacenamiento", "openFolder": "Abrir carpeta", "openSite": "Abrir sitio", diff --git a/desktop/ui/public/locales/fr/common.json b/desktop/ui/public/locales/fr/common.json index cd2adc26..37161908 100644 --- a/desktop/ui/public/locales/fr/common.json +++ b/desktop/ui/public/locales/fr/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "La tâche de synchronisation attend que le fichier cesse d'être modifié", "hideTrayIcon": "Masquer l'icône de la zone de notification", "hideTrayIconDescription": "S'exécuter en arrière-plan sans icône dans la zone de notification", + "reconnect": "Reconnecter", "storage": "Stockage", "openFolder": "Ouvrir le dossier", "openSite": "Ouvrir le site", diff --git a/desktop/ui/public/locales/it/common.json b/desktop/ui/public/locales/it/common.json index c50d694a..35c1bc05 100644 --- a/desktop/ui/public/locales/it/common.json +++ b/desktop/ui/public/locales/it/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "L'attività di sincronizzazione attende che il file smetta di cambiare", "hideTrayIcon": "Nascondi icona nella barra delle applicazioni", "hideTrayIconDescription": "Esegui in background senza icona nell'area di notifica", + "reconnect": "Riconnetti", "storage": "Archiviazione", "openFolder": "Apri cartella", "openSite": "Apri sito", diff --git a/desktop/ui/public/locales/ja/common.json b/desktop/ui/public/locales/ja/common.json index 37974b71..8866ba0a 100644 --- a/desktop/ui/public/locales/ja/common.json +++ b/desktop/ui/public/locales/ja/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "同期タスクはファイルの変更が止まるまで待機します", "hideTrayIcon": "トレイアイコンを非表示", "hideTrayIconDescription": "システムトレイアイコンなしでバックグラウンド実行", + "reconnect": "再接続", "storage": "ストレージ", "openFolder": "フォルダを開く", "openSite": "サイトを開く", diff --git a/desktop/ui/public/locales/ko/common.json b/desktop/ui/public/locales/ko/common.json index 913cb36a..c00969b3 100644 --- a/desktop/ui/public/locales/ko/common.json +++ b/desktop/ui/public/locales/ko/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "동기화 작업은 파일 변경이 멈출 때까지 기다립니다", "hideTrayIcon": "트레이 아이콘 숨기기", "hideTrayIconDescription": "시스템 트레이 아이콘 없이 백그라운드에서 실행", + "reconnect": "다시 연결", "storage": "저장소", "openFolder": "폴더 열기", "openSite": "사이트 열기", diff --git a/desktop/ui/public/locales/pl/common.json b/desktop/ui/public/locales/pl/common.json index d07128d1..80b970d9 100644 --- a/desktop/ui/public/locales/pl/common.json +++ b/desktop/ui/public/locales/pl/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "Zadanie synchronizacji czeka, aż plik przestanie się zmieniać", "hideTrayIcon": "Ukryj ikonę w zasobniku", "hideTrayIconDescription": "Działaj w tle bez ikony w zasobniku systemowym", + "reconnect": "Połącz ponownie", "storage": "Pamięć", "openFolder": "Otwórz folder", "openSite": "Otwórz witrynę", diff --git a/desktop/ui/public/locales/ru/common.json b/desktop/ui/public/locales/ru/common.json index 3cb5fa0c..855c0e92 100644 --- a/desktop/ui/public/locales/ru/common.json +++ b/desktop/ui/public/locales/ru/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "Задача синхронизации ждёт, пока файл перестанет изменяться", "hideTrayIcon": "Скрыть значок в трее", "hideTrayIconDescription": "Работать в фоне без значка в системном трее", + "reconnect": "Переподключить", "storage": "Хранилище", "openFolder": "Открыть папку", "openSite": "Открыть сайт", diff --git a/desktop/ui/public/locales/zh-CN/common.json b/desktop/ui/public/locales/zh-CN/common.json index 03ded996..adc2c150 100644 --- a/desktop/ui/public/locales/zh-CN/common.json +++ b/desktop/ui/public/locales/zh-CN/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "同步任务等待文件停止更改后再上传", "hideTrayIcon": "隐藏托盘图标", "hideTrayIconDescription": "在后台运行而不显示系统托盘图标", + "reconnect": "重新连接", "storage": "存储空间", "openFolder": "打开文件夹", "openSite": "打开站点", diff --git a/desktop/ui/public/locales/zh-TW/common.json b/desktop/ui/public/locales/zh-TW/common.json index b7f4f17b..091cbfe7 100644 --- a/desktop/ui/public/locales/zh-TW/common.json +++ b/desktop/ui/public/locales/zh-TW/common.json @@ -93,6 +93,7 @@ "delaySyncDescription": "同步任務會等待檔案停止變更", "hideTrayIcon": "隱藏系統匣圖示", "hideTrayIconDescription": "在背景執行而不顯示系統匣圖示", + "reconnect": "重新連線", "storage": "儲存空間", "openFolder": "開啟資料夾", "openSite": "開啟網站", diff --git a/desktop/ui/src/pages/settings/DrivesSection.tsx b/desktop/ui/src/pages/settings/DrivesSection.tsx index 401166af..b047179a 100644 --- a/desktop/ui/src/pages/settings/DrivesSection.tsx +++ b/desktop/ui/src/pages/settings/DrivesSection.tsx @@ -23,6 +23,7 @@ import { Add as AddIcon, DeleteOutlineRounded, RefreshRounded, + SyncRounded, FilterListRounded, } from "@mui/icons-material"; import { useCallback, useEffect, useRef, useState } from "react"; @@ -114,6 +115,15 @@ export default function DrivesSection() { } }; + const handleReconnect = async (drive: DriveInfo) => { + try { + await invoke("reconnect_drive", { driveId: drive.id }); + fetchDrives(); + } catch (error) { + console.error("Failed to reconnect drive:", error); + } + }; + const handleOpenFolder = async (path: string) => { try { await invoke("show_file_in_explorer", { path }); @@ -411,6 +421,16 @@ export default function DrivesSection() { )} + {drive.status === "event_push_lost" && ( + } + onClick={() => handleReconnect(drive)} + > + {t("settings.reconnect")} + + )} + }