diff --git a/UPDATELOG.md b/UPDATELOG.md index 722430efc..8bbcb9867 100644 --- a/UPDATELOG.md +++ b/UPDATELOG.md @@ -9,6 +9,7 @@ - 监听关机事件,自动关闭系统代理 - 主界面“当前节点”卡片新增“延迟测试”按钮 - 新增批量选择配置文件功能 +- 新增本地备份功能 ### 🚀 优化改进 @@ -27,6 +28,7 @@ - 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核 - 修改内核默认日志级别为 Info - 支持通过桌面快捷方式重新打开应用 +- 主界面“当前节点”卡片每 5 分钟自动测试延迟 ### 🐞 修复问题 diff --git a/src-tauri/src/cmd/backup.rs b/src-tauri/src/cmd/backup.rs new file mode 100644 index 000000000..f583ef438 --- /dev/null +++ b/src-tauri/src/cmd/backup.rs @@ -0,0 +1,33 @@ +use super::CmdResult; +use crate::{feat, wrap_err}; +use feat::LocalBackupFile; + +/// Create a local backup +#[tauri::command] +pub fn create_local_backup() -> CmdResult<()> { + wrap_err!(feat::create_local_backup()) +} + +/// List local backups +#[tauri::command] +pub fn list_local_backup() -> CmdResult> { + wrap_err!(feat::list_local_backup()) +} + +/// Delete local backup +#[tauri::command] +pub fn delete_local_backup(filename: String) -> CmdResult<()> { + wrap_err!(feat::delete_local_backup(filename)) +} + +/// Restore local backup +#[tauri::command] +pub async fn restore_local_backup(filename: String) -> CmdResult<()> { + wrap_err!(feat::restore_local_backup(filename).await) +} + +/// Export local backup to a user selected destination +#[tauri::command] +pub fn export_local_backup(filename: String, destination: String) -> CmdResult<()> { + wrap_err!(feat::export_local_backup(filename, destination)) +} diff --git a/src-tauri/src/cmd/mod.rs b/src-tauri/src/cmd/mod.rs index 2d9e65bd8..7af3b596b 100644 --- a/src-tauri/src/cmd/mod.rs +++ b/src-tauri/src/cmd/mod.rs @@ -4,6 +4,7 @@ pub type CmdResult = Result; // Command modules pub mod app; +pub mod backup; pub mod clash; pub mod lightweight; pub mod media_unlock_checker; @@ -21,6 +22,7 @@ pub mod webdav; // Re-export all command functions for backwards compatibility pub use app::*; +pub use backup::*; pub use clash::*; pub use lightweight::*; pub use media_unlock_checker::*; diff --git a/src-tauri/src/feat/backup.rs b/src-tauri/src/feat/backup.rs index f5c9687e6..72df0cebf 100644 --- a/src-tauri/src/feat/backup.rs +++ b/src-tauri/src/feat/backup.rs @@ -2,11 +2,24 @@ use crate::{ config::{Config, IVerge}, core::backup, logging_error, - utils::{dirs::app_home_dir, logging::Type}, + utils::{ + dirs::{app_home_dir, local_backup_dir}, + logging::Type, + }, }; -use anyhow::Result; +use anyhow::{Result, anyhow}; +use chrono::Utc; use reqwest_dav::list_cmd::ListFile; -use std::fs; +use serde::Serialize; +use std::{fs, path::PathBuf}; + +#[derive(Debug, Serialize)] +pub struct LocalBackupFile { + pub filename: String, + pub path: String, + pub last_modified: String, + pub content_length: u64, +} /// Create a backup and upload to WebDAV pub async fn create_backup_and_upload_webdav() -> Result<()> { @@ -90,3 +103,147 @@ pub async fn restore_webdav_backup(filename: String) -> Result<()> { fs::remove_file(backup_storage_path)?; Ok(()) } + +/// Create a backup and save to local storage +pub fn create_local_backup() -> Result<()> { + let (file_name, temp_file_path) = backup::create_backup().map_err(|err| { + log::error!(target: "app", "Failed to create local backup: {err:#?}"); + err + })?; + + let backup_dir = local_backup_dir()?; + let target_path = backup_dir.join(&file_name); + + if let Err(err) = move_file(temp_file_path.clone(), target_path.clone()) { + log::error!(target: "app", "Failed to move local backup file: {err:#?}"); + // 清理临时文件 + if let Err(clean_err) = std::fs::remove_file(&temp_file_path) { + log::warn!( + target: "app", + "Failed to remove temp backup file after move error: {clean_err:#?}" + ); + } + return Err(err); + } + + Ok(()) +} + +fn move_file(from: PathBuf, to: PathBuf) -> Result<()> { + if let Some(parent) = to.parent() { + fs::create_dir_all(parent)?; + } + + match fs::rename(&from, &to) { + Ok(_) => Ok(()), + Err(rename_err) => { + // Attempt copy + remove as fallback, covering cross-device moves + log::warn!( + target: "app", + "Failed to rename backup file directly, fallback to copy/remove: {rename_err:#?}" + ); + fs::copy(&from, &to).map_err(|err| anyhow!("Failed to copy backup file: {err:#?}"))?; + fs::remove_file(&from) + .map_err(|err| anyhow!("Failed to remove temp backup file: {err:#?}"))?; + Ok(()) + } + } +} + +/// List local backups +pub fn list_local_backup() -> Result> { + let backup_dir = local_backup_dir()?; + if !backup_dir.exists() { + return Ok(vec![]); + } + + let mut backups = Vec::new(); + for entry in fs::read_dir(&backup_dir)? { + let entry = entry?; + let path = entry.path(); + if !path.is_file() { + continue; + } + + let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let metadata = entry.metadata()?; + let last_modified = metadata + .modified() + .map(|time| chrono::DateTime::::from(time).to_rfc3339()) + .unwrap_or_default(); + backups.push(LocalBackupFile { + filename: file_name.to_string(), + path: path.to_string_lossy().to_string(), + last_modified, + content_length: metadata.len(), + }); + } + + backups.sort_by(|a, b| b.filename.cmp(&a.filename)); + Ok(backups) +} + +/// Delete local backup +pub fn delete_local_backup(filename: String) -> Result<()> { + let backup_dir = local_backup_dir()?; + let target_path = backup_dir.join(&filename); + if !target_path.exists() { + log::warn!(target: "app", "Local backup file not found: {}", filename); + return Ok(()); + } + fs::remove_file(target_path)?; + Ok(()) +} + +/// Restore local backup +pub async fn restore_local_backup(filename: String) -> Result<()> { + let backup_dir = local_backup_dir()?; + let target_path = backup_dir.join(&filename); + if !target_path.exists() { + return Err(anyhow!("Backup file not found: {}", filename)); + } + + let verge = Config::verge().await; + let verge_data = verge.latest_ref().clone(); + let webdav_url = verge_data.webdav_url.clone(); + let webdav_username = verge_data.webdav_username.clone(); + let webdav_password = verge_data.webdav_password.clone(); + + let mut zip = zip::ZipArchive::new(fs::File::open(&target_path)?)?; + zip.extract(app_home_dir()?)?; + logging_error!( + Type::Backup, + super::patch_verge( + IVerge { + webdav_url, + webdav_username, + webdav_password, + ..IVerge::default() + }, + false + ) + .await + ); + Ok(()) +} + +/// Export local backup file to user selected destination +pub fn export_local_backup(filename: String, destination: String) -> Result<()> { + let backup_dir = local_backup_dir()?; + let source_path = backup_dir.join(&filename); + if !source_path.exists() { + return Err(anyhow!("Backup file not found: {}", filename)); + } + + let dest_path = PathBuf::from(destination); + if let Some(parent) = dest_path.parent() { + fs::create_dir_all(parent)?; + } + + fs::copy(&source_path, &dest_path) + .map(|_| ()) + .map_err(|err| anyhow!("Failed to export backup file: {err:#?}"))?; + Ok(()) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index db107a15d..00a815a62 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -204,6 +204,11 @@ mod app_init { cmd::script_validate_notice, cmd::validate_script_file, // Backup and WebDAV + cmd::create_local_backup, + cmd::list_local_backup, + cmd::delete_local_backup, + cmd::restore_local_backup, + cmd::export_local_backup, cmd::create_webdav_backup, cmd::save_webdav_config, cmd::list_webdav_backup, diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index 503048563..0b581a4df 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -120,6 +120,13 @@ pub fn app_logs_dir() -> Result { Ok(app_home_dir()?.join("logs")) } +/// local backups dir +pub fn local_backup_dir() -> Result { + let dir = app_home_dir()?.join(BACKUP_DIR); + fs::create_dir_all(&dir)?; + Ok(dir) +} + pub fn clash_path() -> Result { Ok(app_home_dir()?.join(CLASH_CONFIG)) } diff --git a/src/components/setting/mods/backup-table-viewer.tsx b/src/components/setting/mods/backup-table-viewer.tsx index e68da158c..fdb0de812 100644 --- a/src/components/setting/mods/backup-table-viewer.tsx +++ b/src/components/setting/mods/backup-table-viewer.tsx @@ -1,4 +1,5 @@ import DeleteIcon from "@mui/icons-material/Delete"; +import DownloadIcon from "@mui/icons-material/Download"; import RestoreIcon from "@mui/icons-material/Restore"; import { Box, @@ -14,22 +15,20 @@ import { TablePagination, } from "@mui/material"; import { Typography } from "@mui/material"; +import { save } from "@tauri-apps/plugin-dialog"; import { useLockFn } from "ahooks"; import { Dayjs } from "dayjs"; import { SVGProps, memo } from "react"; import { useTranslation } from "react-i18next"; -import { - deleteWebdavBackup, - restoreWebDavBackup, - restartApp, -} from "@/services/cmds"; +import { restartApp } from "@/services/cmds"; import { showNotice } from "@/services/noticeService"; -export type BackupFile = IWebDavFile & { +export type BackupFile = { platform: string; backup_time: Dayjs; allow_apply: boolean; + filename: string; }; export const DEFAULT_ROWS_PER_PAGE = 5; @@ -43,6 +42,9 @@ interface BackupTableViewerProps { ) => void; total: number; onRefresh: () => Promise; + onDelete: (filename: string) => Promise; + onRestore: (filename: string) => Promise; + onExport?: (filename: string, destination: string) => Promise; } export const BackupTableViewer = memo( @@ -52,21 +54,43 @@ export const BackupTableViewer = memo( onPageChange, total, onRefresh, + onDelete, + onRestore, + onExport, }: BackupTableViewerProps) => { const { t } = useTranslation(); const handleDelete = useLockFn(async (filename: string) => { - await deleteWebdavBackup(filename); + await onDelete(filename); await onRefresh(); }); const handleRestore = useLockFn(async (filename: string) => { - await restoreWebDavBackup(filename).then(() => { + await onRestore(filename).then(() => { showNotice("success", t("Restore Success, App will restart in 1s")); }); await restartApp(); }); + const handleExport = useLockFn(async (filename: string) => { + if (!onExport) { + return; + } + try { + const savePath = await save({ + defaultPath: filename, + }); + if (!savePath || Array.isArray(savePath)) { + return; + } + await onExport(filename, savePath); + showNotice("success", t("Local Backup Exported")); + } catch (error) { + console.error(error); + showNotice("error", t("Local Backup Export Failed")); + } + }); + return ( @@ -102,6 +126,27 @@ export const BackupTableViewer = memo( justifyContent: "flex-end", }} > + {onExport && ( + <> + { + e.preventDefault(); + await handleExport(file.filename); + }} + > + + + + + )} }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); + const contentRef = useRef(null); + const [dialogPaper, setDialogPaper] = useState(null); + const [closeButtonPosition, setCloseButtonPosition] = useState<{ + top: number; + left: number; + } | null>(null); const [isLoading, setIsLoading] = useState(false); const [backupFiles, setBackupFiles] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(0); + const [source, setSource] = useState("local"); useImperativeHandle(ref, () => ({ open: () => { @@ -43,7 +68,51 @@ export function BackupViewer({ ref }: { ref?: Ref }) { [], ); - const fetchAndSetBackupFiles = async () => { + const handleChangeSource = useCallback( + (_event: React.SyntheticEvent, newSource: string) => { + setSource(newSource as BackupSource); + setPage(0); + }, + [], + ); + + const buildBackupFile = useCallback((filename: string) => { + const platform = filename.split("-")[0]; + const fileBackupTimeStr = filename.match(FILENAME_PATTERN); + + if (fileBackupTimeStr === null) { + return null; + } + + const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT); + const allowApply = true; + return { + filename, + platform, + backup_time: backupTime, + allow_apply: allowApply, + } as BackupFile; + }, []); + + const getAllBackupFiles = useCallback(async (): Promise => { + if (source === "local") { + const files = await listLocalBackup(); + return files + .map((file) => buildBackupFile(file.filename)) + .filter((item): item is BackupFile => item !== null) + .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); + } + + const files = await listWebDavBackup(); + return files + .map((file) => { + return buildBackupFile(file.filename); + }) + .filter((item): item is BackupFile => item !== null) + .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); + }, [buildBackupFile, source]); + + const fetchAndSetBackupFiles = useCallback(async () => { try { setIsLoading(true); const files = await getAllBackupFiles(); @@ -57,31 +126,109 @@ export function BackupViewer({ ref }: { ref?: Ref }) { } finally { setIsLoading(false); } - }; + }, [getAllBackupFiles]); - const getAllBackupFiles = async () => { - const files = await listWebDavBackup(); - return files - .map((file) => { - const platform = file.filename.split("-")[0]; - const fileBackupTimeStr = file.filename.match(FILENAME_PATTERN)!; + useEffect(() => { + if (open) { + fetchAndSetBackupFiles(); + const paper = contentRef.current?.closest(".MuiPaper-root"); + setDialogPaper((paper as HTMLElement) ?? null); + } else { + setDialogPaper(null); + } + }, [open, fetchAndSetBackupFiles]); - if (fileBackupTimeStr === null) { - return null; - } + useEffect(() => { + if (!open || dialogPaper) { + return; + } + const frame = requestAnimationFrame(() => { + const paper = contentRef.current?.closest(".MuiPaper-root"); + setDialogPaper((paper as HTMLElement) ?? null); + }); + return () => cancelAnimationFrame(frame); + }, [open, dialogPaper]); - const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT); - const allowApply = true; - return { - ...file, - platform, - backup_time: backupTime, - allow_apply: allowApply, - } as BackupFile; - }) - .filter((item) => item !== null) - .sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1)); - }; + useEffect(() => { + if (!dialogPaper) { + setCloseButtonPosition(null); + return; + } + if (typeof window === "undefined") { + return; + } + + const updatePosition = () => { + const rect = dialogPaper.getBoundingClientRect(); + setCloseButtonPosition({ + top: rect.bottom - 16, + left: rect.right - 24, + }); + }; + + updatePosition(); + + let resizeObserver: ResizeObserver | null = null; + if (typeof ResizeObserver !== "undefined") { + resizeObserver = new ResizeObserver(() => { + updatePosition(); + }); + resizeObserver.observe(dialogPaper); + } + + const scrollTargets: EventTarget[] = []; + const addScrollListener = (target: EventTarget | null) => { + if (!target) { + return; + } + target.addEventListener("scroll", updatePosition, true); + scrollTargets.push(target); + }; + + addScrollListener(window); + addScrollListener(dialogPaper); + const dialogContent = dialogPaper.querySelector(".MuiDialogContent-root"); + addScrollListener(dialogContent); + + window.addEventListener("resize", updatePosition); + + return () => { + resizeObserver?.disconnect(); + scrollTargets.forEach((target) => { + target.removeEventListener("scroll", updatePosition, true); + }); + window.removeEventListener("resize", updatePosition); + }; + }, [dialogPaper]); + + const handleDelete = useCallback( + async (filename: string) => { + if (source === "local") { + await deleteLocalBackup(filename); + } else { + await deleteWebdavBackup(filename); + } + }, + [source], + ); + + const handleRestore = useCallback( + async (filename: string) => { + if (source === "local") { + await restoreLocalBackup(filename); + } else { + await restoreWebDavBackup(filename); + } + }, + [source], + ); + + const handleExport = useCallback( + async (filename: string, destination: string) => { + await exportLocalBackup(filename, destination); + }, + [], + ); const dataSource = useMemo( () => @@ -96,33 +243,105 @@ export function BackupViewer({ ref }: { ref?: Ref }) { setOpen(false)} - onCancel={() => setOpen(false)} - disableOk + disableFooter > - + - - + + + + + + {source === "local" ? ( + + ) : ( + + )} - + + + + {dialogPaper && + closeButtonPosition && + createPortal( + theme.zIndex.modal + 1, + }} + > + + , + dialogPaper, + )} ); } diff --git a/src/components/setting/mods/local-backup-actions.tsx b/src/components/setting/mods/local-backup-actions.tsx new file mode 100644 index 000000000..ff4ed4e11 --- /dev/null +++ b/src/components/setting/mods/local-backup-actions.tsx @@ -0,0 +1,78 @@ +import { Button, Grid, Stack, Typography } from "@mui/material"; +import { useLockFn } from "ahooks"; +import { memo } from "react"; +import { useTranslation } from "react-i18next"; + +import { createLocalBackup } from "@/services/cmds"; +import { showNotice } from "@/services/noticeService"; + +interface LocalBackupActionsProps { + onBackupSuccess: () => Promise; + onRefresh: () => Promise; + setLoading: (loading: boolean) => void; +} + +export const LocalBackupActions = memo( + ({ onBackupSuccess, onRefresh, setLoading }: LocalBackupActionsProps) => { + const { t } = useTranslation(); + + const handleBackup = useLockFn(async () => { + try { + setLoading(true); + await createLocalBackup(); + showNotice("success", t("Local Backup Created")); + await onBackupSuccess(); + } catch (error) { + console.error(error); + showNotice("error", t("Local Backup Failed")); + } finally { + setLoading(false); + } + }); + + const handleRefresh = useLockFn(async () => { + setLoading(true); + try { + await onRefresh(); + } finally { + setLoading(false); + } + }); + + return ( + + + + {t("Local Backup Info")} + + + + + + + + + + ); + }, +); diff --git a/src/locales/en.json b/src/locales/en.json index de33d703f..fc8859424 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -387,7 +387,7 @@ "toggle_tun_mode": "Enable/Disable Tun Mode", "entry_lightweight_mode": "Entry Lightweight Mode", "Backup Setting": "Backup Setting", - "Backup Setting Info": "Support WebDAV backup configuration files", + "Backup Setting Info": "Support local or WebDAV backup of configuration files", "Runtime Config": "Runtime Config", "Open Conf Dir": "Open Conf Dir", "Open Conf Dir Info": "If the software runs abnormally, BACKUP and delete all files in this folder then restart the software", @@ -448,9 +448,13 @@ "Username": "Username", "Password": "Password", "Backup": "Backup", + "Local Backup": "Local backup", + "WebDAV Backup": "WebDAV backup", + "Select Backup Target": "Select backup target", "Filename": "Filename", "Actions": "Actions", "Restore": "Restore", + "Export": "Export", "No Backups": "No backups available", "WebDAV URL Required": "WebDAV URL cannot be empty", "Invalid WebDAV URL": "Invalid WebDAV URL format", @@ -461,7 +465,13 @@ "WebDAV Config Save Failed": "Failed to save WebDAV configuration: {{error}}", "Backup Created": "Backup created successfully", "Backup Failed": "Backup failed: {{error}}", + "Local Backup Created": "Local backup created successfully", + "Local Backup Failed": "Local backup failed", + "Local Backup Exported": "Local backup exported successfully", + "Local Backup Export Failed": "Failed to export local backup", + "Local Backup Info": "Backups are stored locally in the application data directory. Use the list below to restore or delete backups.", "Delete Backup": "Delete Backup", + "Export Backup": "Export Backup", "Restore Backup": "Restore Backup", "Backup Time": "Backup Time", "Confirm to delete this backup file?": "Confirm to delete this backup file?", diff --git a/src/locales/zh.json b/src/locales/zh.json index bb5d92c25..98d900d5c 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -387,7 +387,7 @@ "toggle_tun_mode": "打开/关闭 TUN 模式", "entry_lightweight_mode": "进入轻量模式", "Backup Setting": "备份设置", - "Backup Setting Info": "支持 WebDAV 备份配置文件", + "Backup Setting Info": "支持本地或 WebDAV 方式备份配置文件", "Runtime Config": "当前配置", "Open Conf Dir": "配置目录", "Open Conf Dir Info": "如果软件运行异常,!备份!并删除此文件夹下的所有文件,重启软件", @@ -448,9 +448,13 @@ "Username": "用户名", "Password": "密码", "Backup": "备份", + "Local Backup": "本地备份", + "WebDAV Backup": "WebDAV 备份", + "Select Backup Target": "选择备份目标", "Filename": "文件名称", "Actions": "操作", "Restore": "恢复", + "Export": "导出", "No Backups": "暂无备份", "WebDAV URL Required": "WebDAV 服务器地址不能为空", "Invalid WebDAV URL": "无效的 WebDAV 服务器地址格式", @@ -461,7 +465,13 @@ "WebDAV Config Save Failed": "保存 WebDAV 配置失败: {{error}}", "Backup Created": "备份创建成功", "Backup Failed": "备份失败: {{error}}", + "Local Backup Created": "本地备份创建成功", + "Local Backup Failed": "本地备份失败", + "Local Backup Exported": "本地备份导出成功", + "Local Backup Export Failed": "本地备份导出失败", + "Local Backup Info": "在应用数据目录中创建本地备份,您可以通过下方列表进行恢复或删除。", "Delete Backup": "删除备份", + "Export Backup": "导出备份", "Restore Backup": "恢复备份", "Backup Time": "备份时间", "Confirm to delete this backup file?": "确认删除此备份文件吗?", diff --git a/src/locales/zhtw.json b/src/locales/zhtw.json index dcd09b05b..ece171b92 100644 --- a/src/locales/zhtw.json +++ b/src/locales/zhtw.json @@ -344,7 +344,7 @@ "toggle_tun_mode": "打開/關閉 TUN 模式", "entry_lightweight_mode": "進入輕量模式", "Backup Setting": "備份設置", - "Backup Setting Info": "支持 WebDAV 備份配置文件", + "Backup Setting Info": "支持本地或 WebDAV 方式備份配置文件", "Runtime Config": "當前配置", "Open Conf Dir": "配置目錄", "Open Conf Dir Info": "如果軟件運行異常,!備份!並刪除此文件夾下的所有文件,重啟軟件", @@ -393,9 +393,13 @@ "Username": "用戶名", "Password": "密碼", "Backup": "備份", + "Local Backup": "本地備份", + "WebDAV Backup": "WebDAV 備份", + "Select Backup Target": "選擇備份目標", "Filename": "文件名稱", "Actions": "操作", "Restore": "恢復", + "Export": "匯出", "No Backups": "暫無備份", "WebDAV URL Required": "WebDAV 服務器地址不能為空", "Invalid WebDAV URL": "無效的 WebDAV 服務器地址格式", @@ -405,7 +409,13 @@ "WebDAV Config Save Failed": "保存 WebDAV 配置失敗: {{error}}", "Backup Created": "備份創建成功", "Backup Failed": "備份失敗: {{error}}", + "Local Backup Created": "本地備份創建成功", + "Local Backup Failed": "本地備份失敗", + "Local Backup Exported": "本地備份匯出成功", + "Local Backup Export Failed": "本地備份匯出失敗", + "Local Backup Info": "在應用資料目錄中建立本地備份,您可以透過下方列表進行恢復或刪除。", "Delete Backup": "刪除備份", + "Export Backup": "匯出備份", "Restore Backup": "恢復備份", "Backup Time": "備份時間", "Restore Success, App will restart in 1s": "恢復成功,應用將在 1 秒後重啟", diff --git a/src/services/cmds.ts b/src/services/cmds.ts index dd6b7ad09..e1e686bd3 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -440,14 +440,30 @@ export async function createWebdavBackup() { return invoke("create_webdav_backup"); } +export async function createLocalBackup() { + return invoke("create_local_backup"); +} + export async function deleteWebdavBackup(filename: string) { return invoke("delete_webdav_backup", { filename }); } +export async function deleteLocalBackup(filename: string) { + return invoke("delete_local_backup", { filename }); +} + export async function restoreWebDavBackup(filename: string) { return invoke("restore_webdav_backup", { filename }); } +export async function restoreLocalBackup(filename: string) { + return invoke("restore_local_backup", { filename }); +} + +export async function exportLocalBackup(filename: string, destination: string) { + return invoke("export_local_backup", { filename, destination }); +} + export async function saveWebdavConfig( url: string, username: string, @@ -468,6 +484,10 @@ export async function listWebDavBackup() { return list; } +export async function listLocalBackup() { + return invoke("list_local_backup"); +} + export async function scriptValidateNotice(status: string, msg: string) { return invoke("script_validate_notice", { status, msg }); } diff --git a/src/services/types.d.ts b/src/services/types.d.ts index 254f75904..a34764d48 100644 --- a/src/services/types.d.ts +++ b/src/services/types.d.ts @@ -870,6 +870,13 @@ interface IWebDavFile { tag: string; } +interface ILocalBackupFile { + filename: string; + path: string; + last_modified: string; + content_length: number; +} + interface IWebDavConfig { url: string; username: string;