mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 07:14:40 +08:00
feat(webdav): cache connection status and adjust auto-refresh behavior (#6129)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
);
|
||||
|
||||
|
||||
55
src/services/webdav-status.ts
Normal file
55
src/services/webdav-status.ts
Normal 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));
|
||||
};
|
||||
Reference in New Issue
Block a user