feat(desktop): manual reconnect for drives with lost event push (#156)

When the remote-event subscription dies (server outage, network drop)
the auto-retry backs off to a 1h dead zone with no user recourse —
online-only files stay unusable.

Add MountCommand::Reconnect which aborts the stale event processor,
respawns it with fresh retry state, and queues a full-hierarchy sync.
Exposed via reconnect_drive Tauri command and a Reconnect button on
drive cards showing event_push_lost.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
pull/3587/head
Tomas Dvorak 2 weeks ago
parent def78e9c04
commit d8b0a87870

@ -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.

@ -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,

@ -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(

@ -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,

@ -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",

@ -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",

@ -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",

@ -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",

@ -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",

@ -93,6 +93,7 @@
"delaySyncDescription": "同期タスクはファイルの変更が止まるまで待機します",
"hideTrayIcon": "トレイアイコンを非表示",
"hideTrayIconDescription": "システムトレイアイコンなしでバックグラウンド実行",
"reconnect": "再接続",
"storage": "ストレージ",
"openFolder": "フォルダを開く",
"openSite": "サイトを開く",

@ -93,6 +93,7 @@
"delaySyncDescription": "동기화 작업은 파일 변경이 멈출 때까지 기다립니다",
"hideTrayIcon": "트레이 아이콘 숨기기",
"hideTrayIconDescription": "시스템 트레이 아이콘 없이 백그라운드에서 실행",
"reconnect": "다시 연결",
"storage": "저장소",
"openFolder": "폴더 열기",
"openSite": "사이트 열기",

@ -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ę",

@ -93,6 +93,7 @@
"delaySyncDescription": "Задача синхронизации ждёт, пока файл перестанет изменяться",
"hideTrayIcon": "Скрыть значок в трее",
"hideTrayIconDescription": "Работать в фоне без значка в системном трее",
"reconnect": "Переподключить",
"storage": "Хранилище",
"openFolder": "Открыть папку",
"openSite": "Открыть сайт",

@ -93,6 +93,7 @@
"delaySyncDescription": "同步任务等待文件停止更改后再上传",
"hideTrayIcon": "隐藏托盘图标",
"hideTrayIconDescription": "在后台运行而不显示系统托盘图标",
"reconnect": "重新连接",
"storage": "存储空间",
"openFolder": "打开文件夹",
"openSite": "打开站点",

@ -93,6 +93,7 @@
"delaySyncDescription": "同步任務會等待檔案停止變更",
"hideTrayIcon": "隱藏系統匣圖示",
"hideTrayIconDescription": "在背景執行而不顯示系統匣圖示",
"reconnect": "重新連線",
"storage": "儲存空間",
"openFolder": "開啟資料夾",
"openSite": "開啟網站",

@ -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() {
</SecondaryButton>
)}
{drive.status === "event_push_lost" && (
<SecondaryButton
size="small"
startIcon={<SyncRounded />}
onClick={() => handleReconnect(drive)}
>
{t("settings.reconnect")}
</SecondaryButton>
)}
<SecondaryButton
size="small"
startIcon={<FilterListRounded />}

Loading…
Cancel
Save