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")}
+
+ )}
+
}