feat(webdav): cache connection status and adjust auto-refresh behavior (#6129)

This commit is contained in:
Sline
2026-01-25 14:49:12 +08:00
committed by GitHub
parent b921098182
commit 93e7ac1bce
5 changed files with 179 additions and 46 deletions

View File

@@ -16,11 +16,16 @@ import { useTranslation } from "react-i18next";
import { useVerge } from "@/hooks/use-verge";
import { saveWebdavConfig, createWebdavBackup } from "@/services/cmds";
import { showNotice } from "@/services/notice-service";
import {
buildWebdavSignature,
getWebdavStatus,
setWebdavStatus,
} from "@/services/webdav-status";
import { isValidUrl } from "@/utils/helper";
interface BackupConfigViewerProps {
onBackupSuccess: () => Promise<void>;
onSaveSuccess: () => Promise<void>;
onSaveSuccess: (signature?: string) => Promise<void>;
onRefresh: () => Promise<void>;
onInit: () => Promise<void>;
setLoading: (loading: boolean) => void;
@@ -35,7 +40,7 @@ export const BackupConfigViewer = memo(
setLoading,
}: BackupConfigViewerProps) => {
const { t } = useTranslation();
const { verge } = useVerge();
const { verge, mutateVerge } = useVerge();
const { webdav_url, webdav_username, webdav_password } = verge || {};
const [showPassword, setShowPassword] = useState(false);
const usernameRef = useRef<HTMLInputElement>(null);
@@ -58,6 +63,10 @@ export const BackupConfigViewer = memo(
webdav_username !== username ||
webdav_password !== password;
const webdavSignature = buildWebdavSignature(verge);
const webdavStatus = getWebdavStatus(webdavSignature);
const shouldAutoInit = webdavStatus !== "failed";
const handleClickShowPassword = () => {
setShowPassword((prev) => !prev);
};
@@ -66,8 +75,11 @@ export const BackupConfigViewer = memo(
if (!webdav_url || !webdav_username || !webdav_password) {
return;
}
if (!shouldAutoInit) {
return;
}
void onInit();
}, [webdav_url, webdav_username, webdav_password, onInit]);
}, [webdav_url, webdav_username, webdav_password, onInit, shouldAutoInit]);
const checkForm = () => {
const username = usernameRef.current?.value;
@@ -97,18 +109,32 @@ export const BackupConfigViewer = memo(
const save = useLockFn(async (data: IWebDavConfig) => {
checkForm();
const signature = buildWebdavSignature({
webdav_url: data.url,
webdav_username: data.username,
webdav_password: data.password,
});
const trimmedUrl = data.url.trim();
const trimmedUsername = data.username.trim();
try {
setLoading(true);
await saveWebdavConfig(
data.url.trim(),
data.username.trim(),
data.password,
).then(() => {
showNotice.success(
"settings.modals.backup.messages.webdavConfigSaved",
);
onSaveSuccess();
});
await saveWebdavConfig(trimmedUrl, trimmedUsername, data.password);
await mutateVerge(
(current) =>
current
? {
...current,
webdav_url: trimmedUrl,
webdav_username: trimmedUsername,
webdav_password: data.password,
}
: current,
false,
);
setWebdavStatus(signature, "unknown");
showNotice.success("settings.modals.backup.messages.webdavConfigSaved");
await onSaveSuccess(signature);
} catch (error) {
showNotice.error(
"settings.modals.backup.messages.webdavConfigSaveFailed",
@@ -122,16 +148,24 @@ export const BackupConfigViewer = memo(
const handleBackup = useLockFn(async () => {
checkForm();
const signature = buildWebdavSignature({
webdav_url: url,
webdav_username: username,
webdav_password: password,
});
try {
setLoading(true);
await createWebdavBackup().then(async () => {
showNotice.success("settings.modals.backup.messages.backupCreated");
await onBackupSuccess();
});
setWebdavStatus(signature, "ready");
} catch (error) {
showNotice.error("settings.modals.backup.messages.backupFailed", {
error,
});
setWebdavStatus(signature, "failed");
} finally {
setLoading(false);
}

View File

@@ -36,6 +36,11 @@ import {
restoreWebDavBackup,
} from "@/services/cmds";
import { showNotice } from "@/services/notice-service";
import {
buildWebdavSignature,
getWebdavStatus,
setWebdavStatus,
} from "@/services/webdav-status";
dayjs.extend(customParseFormat);
dayjs.extend(relativeTime);
@@ -85,6 +90,8 @@ export const BackupHistoryViewer = ({
const isWebDavConfigured = Boolean(
verge?.webdav_url && verge?.webdav_username && verge?.webdav_password,
);
const webdavSignature = buildWebdavSignature(verge);
const webdavStatus = getWebdavStatus(webdavSignature);
const shouldSkipWebDav = !isLocal && !isWebDavConfigured;
const pageSize = 8;
const isBusy = loading || isRestoring || isRestarting;
@@ -128,33 +135,49 @@ export const BackupHistoryViewer = ({
[t],
);
const fetchRows = useCallback(async () => {
if (!open) return;
if (shouldSkipWebDav) {
setRows([]);
return;
}
setLoading(true);
try {
const list = isLocal ? await listLocalBackup() : await listWebDavBackup();
setRows(
list
.map((item) => buildRow(item))
.filter((item): item is BackupRow => item !== null)
.sort((a, b) =>
a.sort_value === b.sort_value
? b.filename.localeCompare(a.filename)
: b.sort_value - a.sort_value,
),
);
} catch (error) {
console.error(error);
setRows([]);
showNotice.error(error);
} finally {
setLoading(false);
}
}, [buildRow, isLocal, open, shouldSkipWebDav]);
const fetchRows = useCallback(
async (options?: { force?: boolean }) => {
if (!open) return;
if (shouldSkipWebDav) {
setRows([]);
return;
}
if (!isLocal && webdavStatus === "failed" && !options?.force) {
setRows([]);
return;
}
setLoading(true);
try {
const list = isLocal
? await listLocalBackup()
: await listWebDavBackup();
if (!isLocal) {
setWebdavStatus(webdavSignature, "ready");
}
setRows(
list
.map((item) => buildRow(item))
.filter((item): item is BackupRow => item !== null)
.sort((a, b) =>
a.sort_value === b.sort_value
? b.filename.localeCompare(a.filename)
: b.sort_value - a.sort_value,
),
);
} catch (error) {
if (!isLocal) {
setWebdavStatus(webdavSignature, "failed");
}
console.error(error);
setRows([]);
showNotice.error(error);
} finally {
setLoading(false);
}
},
[buildRow, isLocal, open, shouldSkipWebDav, webdavSignature, webdavStatus],
);
useEffect(() => {
void fetchRows();
@@ -169,7 +192,7 @@ export const BackupHistoryViewer = ({
);
const summary = useMemo(() => {
if (shouldSkipWebDav) {
if (shouldSkipWebDav || (!isLocal && webdavStatus === "failed")) {
return t("settings.modals.backup.manual.webdav");
}
if (!total) return t("settings.modals.backup.history.empty");
@@ -179,7 +202,7 @@ export const BackupHistoryViewer = ({
count: total,
recent,
});
}, [rows, shouldSkipWebDav, t, total]);
}, [isLocal, rows, shouldSkipWebDav, t, total, webdavStatus]);
const handleDelete = useLockFn(async (filename: string) => {
if (isRestarting) return;
@@ -241,7 +264,7 @@ export const BackupHistoryViewer = ({
const handleRefresh = () => {
if (isRestarting) return;
void fetchRows();
void fetchRows({ force: true });
};
return (

View File

@@ -14,12 +14,17 @@ import { useCallback, useImperativeHandle, useState } from "react";
import { useTranslation } from "react-i18next";
import { BaseDialog, DialogRef } from "@/components/base";
import { useVerge } from "@/hooks/use-verge";
import {
createLocalBackup,
createWebdavBackup,
importLocalBackup,
} from "@/services/cmds";
import { showNotice } from "@/services/notice-service";
import {
buildWebdavSignature,
setWebdavStatus,
} from "@/services/webdav-status";
import { AutoBackupSettings } from "./auto-backup-settings";
import { BackupHistoryViewer } from "./backup-history-viewer";
@@ -29,6 +34,7 @@ type BackupSource = "local" | "webdav";
export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
const { t } = useTranslation();
const { verge } = useVerge();
const [open, setOpen] = useState(false);
const [busyAction, setBusyAction] = useState<BackupSource | null>(null);
const [localImporting, setLocalImporting] = useState(false);
@@ -36,6 +42,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
const [historySource, setHistorySource] = useState<BackupSource>("local");
const [historyPage, setHistoryPage] = useState(0);
const [webdavDialogOpen, setWebdavDialogOpen] = useState(false);
const webdavSignature = buildWebdavSignature(verge);
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
@@ -59,6 +66,7 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
} else {
await createWebdavBackup();
showNotice.success("settings.modals.backup.messages.backupCreated");
setWebdavStatus(webdavSignature, "ready");
}
} catch (error) {
console.error(error);
@@ -68,6 +76,9 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
: "settings.modals.backup.messages.backupFailed",
target === "local" ? undefined : { error },
);
if (target === "webdav") {
setWebdavStatus(webdavSignature, "failed");
}
} finally {
setBusyAction(null);
}

View File

@@ -3,8 +3,13 @@ import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { BaseDialog, BaseLoadingOverlay } from "@/components/base";
import { useVerge } from "@/hooks/use-verge";
import { listWebDavBackup } from "@/services/cmds";
import { showNotice } from "@/services/notice-service";
import {
buildWebdavSignature,
setWebdavStatus,
} from "@/services/webdav-status";
import { BackupConfigViewer } from "./backup-config-viewer";
@@ -22,7 +27,9 @@ export const BackupWebdavDialog = ({
setBusy,
}: BackupWebdavDialogProps) => {
const { t } = useTranslation();
const { verge } = useVerge();
const [loading, setLoading] = useState(false);
const webdavSignature = buildWebdavSignature(verge);
const handleLoading = useCallback(
(value: boolean) => {
@@ -33,16 +40,19 @@ export const BackupWebdavDialog = ({
);
const refreshWebdav = useCallback(
async (options?: { silent?: boolean }) => {
async (options?: { silent?: boolean; signature?: string }) => {
const signature = options?.signature ?? webdavSignature;
handleLoading(true);
try {
await listWebDavBackup();
setWebdavStatus(signature, "ready");
if (!options?.silent) {
showNotice.success(
"settings.modals.backup.messages.webdavRefreshSuccess",
);
}
} catch (error) {
setWebdavStatus(signature, "failed");
showNotice.error(
"settings.modals.backup.messages.webdavRefreshFailed",
{ error },
@@ -51,11 +61,11 @@ export const BackupWebdavDialog = ({
handleLoading(false);
}
},
[handleLoading],
[handleLoading, webdavSignature],
);
const refreshSilently = useCallback(
() => refreshWebdav({ silent: true }),
(signature?: string) => refreshWebdav({ silent: true, signature }),
[refreshWebdav],
);

View File

@@ -0,0 +1,55 @@
export type WebdavStatus = "unknown" | "ready" | "failed";
interface WebdavStatusCache {
signature: string;
status: WebdavStatus;
updatedAt: number;
}
const WEBDAV_STATUS_KEY = "webdav_status_cache";
export const buildWebdavSignature = (
verge?: Pick<
IVergeConfig,
"webdav_url" | "webdav_username" | "webdav_password"
> | null,
) => {
const url = verge?.webdav_url?.trim() ?? "";
const username = verge?.webdav_username?.trim() ?? "";
const password = verge?.webdav_password ?? "";
if (!url && !username && !password) return "";
return JSON.stringify([url, username, password]);
};
const canUseStorage = () => typeof localStorage !== "undefined";
export const getWebdavStatus = (signature: string): WebdavStatus => {
if (!signature || !canUseStorage()) return "unknown";
const raw = localStorage.getItem(WEBDAV_STATUS_KEY);
if (!raw) return "unknown";
try {
const data = JSON.parse(raw) as Partial<WebdavStatusCache>;
if (!data || data.signature !== signature) return "unknown";
return data.status === "ready" || data.status === "failed"
? data.status
: "unknown";
} catch {
return "unknown";
}
};
export const setWebdavStatus = (signature: string, status: WebdavStatus) => {
if (!signature || !canUseStorage()) return;
const payload: WebdavStatusCache = {
signature,
status,
updatedAt: Date.now(),
};
localStorage.setItem(WEBDAV_STATUS_KEY, JSON.stringify(payload));
};