mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-29 00:35:38 +08:00
feat: local backup (#5054)
* feat: local backup * refactor(backup): make local backup helpers synchronous and clean up redundant checks - Converted local backup helpers to synchronous functions to remove unused async warnings and align command signatures. - Updated list/delete/export commands to call the sync feature functions directly without awaits while preserving behavior. - Simplified destination directory creation to always ensure parent folders exist without redundant checks, satisfying Clippy.
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
- 监听关机事件,自动关闭系统代理
|
- 监听关机事件,自动关闭系统代理
|
||||||
- 主界面“当前节点”卡片新增“延迟测试”按钮
|
- 主界面“当前节点”卡片新增“延迟测试”按钮
|
||||||
- 新增批量选择配置文件功能
|
- 新增批量选择配置文件功能
|
||||||
|
- 新增本地备份功能
|
||||||
|
|
||||||
### 🚀 优化改进
|
### 🚀 优化改进
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核
|
- 改进 Windows 和 Unix 的 服务连接方式以及权限,避免无法连接服务或内核
|
||||||
- 修改内核默认日志级别为 Info
|
- 修改内核默认日志级别为 Info
|
||||||
- 支持通过桌面快捷方式重新打开应用
|
- 支持通过桌面快捷方式重新打开应用
|
||||||
|
- 主界面“当前节点”卡片每 5 分钟自动测试延迟
|
||||||
|
|
||||||
### 🐞 修复问题
|
### 🐞 修复问题
|
||||||
|
|
||||||
|
|||||||
33
src-tauri/src/cmd/backup.rs
Normal file
33
src-tauri/src/cmd/backup.rs
Normal file
@@ -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<Vec<LocalBackupFile>> {
|
||||||
|
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))
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ pub type CmdResult<T = ()> = Result<T, String>;
|
|||||||
|
|
||||||
// Command modules
|
// Command modules
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
pub mod backup;
|
||||||
pub mod clash;
|
pub mod clash;
|
||||||
pub mod lightweight;
|
pub mod lightweight;
|
||||||
pub mod media_unlock_checker;
|
pub mod media_unlock_checker;
|
||||||
@@ -21,6 +22,7 @@ pub mod webdav;
|
|||||||
|
|
||||||
// Re-export all command functions for backwards compatibility
|
// Re-export all command functions for backwards compatibility
|
||||||
pub use app::*;
|
pub use app::*;
|
||||||
|
pub use backup::*;
|
||||||
pub use clash::*;
|
pub use clash::*;
|
||||||
pub use lightweight::*;
|
pub use lightweight::*;
|
||||||
pub use media_unlock_checker::*;
|
pub use media_unlock_checker::*;
|
||||||
|
|||||||
@@ -2,11 +2,24 @@ use crate::{
|
|||||||
config::{Config, IVerge},
|
config::{Config, IVerge},
|
||||||
core::backup,
|
core::backup,
|
||||||
logging_error,
|
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 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
|
/// Create a backup and upload to WebDAV
|
||||||
pub async fn create_backup_and_upload_webdav() -> Result<()> {
|
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)?;
|
fs::remove_file(backup_storage_path)?;
|
||||||
Ok(())
|
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<Vec<LocalBackupFile>> {
|
||||||
|
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::<Utc>::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(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -204,6 +204,11 @@ mod app_init {
|
|||||||
cmd::script_validate_notice,
|
cmd::script_validate_notice,
|
||||||
cmd::validate_script_file,
|
cmd::validate_script_file,
|
||||||
// Backup and WebDAV
|
// 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::create_webdav_backup,
|
||||||
cmd::save_webdav_config,
|
cmd::save_webdav_config,
|
||||||
cmd::list_webdav_backup,
|
cmd::list_webdav_backup,
|
||||||
|
|||||||
@@ -120,6 +120,13 @@ pub fn app_logs_dir() -> Result<PathBuf> {
|
|||||||
Ok(app_home_dir()?.join("logs"))
|
Ok(app_home_dir()?.join("logs"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// local backups dir
|
||||||
|
pub fn local_backup_dir() -> Result<PathBuf> {
|
||||||
|
let dir = app_home_dir()?.join(BACKUP_DIR);
|
||||||
|
fs::create_dir_all(&dir)?;
|
||||||
|
Ok(dir)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clash_path() -> Result<PathBuf> {
|
pub fn clash_path() -> Result<PathBuf> {
|
||||||
Ok(app_home_dir()?.join(CLASH_CONFIG))
|
Ok(app_home_dir()?.join(CLASH_CONFIG))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import RestoreIcon from "@mui/icons-material/Restore";
|
import RestoreIcon from "@mui/icons-material/Restore";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
@@ -14,22 +15,20 @@ import {
|
|||||||
TablePagination,
|
TablePagination,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { Typography } from "@mui/material";
|
import { Typography } from "@mui/material";
|
||||||
|
import { save } from "@tauri-apps/plugin-dialog";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Dayjs } from "dayjs";
|
import { Dayjs } from "dayjs";
|
||||||
import { SVGProps, memo } from "react";
|
import { SVGProps, memo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import {
|
import { restartApp } from "@/services/cmds";
|
||||||
deleteWebdavBackup,
|
|
||||||
restoreWebDavBackup,
|
|
||||||
restartApp,
|
|
||||||
} from "@/services/cmds";
|
|
||||||
import { showNotice } from "@/services/noticeService";
|
import { showNotice } from "@/services/noticeService";
|
||||||
|
|
||||||
export type BackupFile = IWebDavFile & {
|
export type BackupFile = {
|
||||||
platform: string;
|
platform: string;
|
||||||
backup_time: Dayjs;
|
backup_time: Dayjs;
|
||||||
allow_apply: boolean;
|
allow_apply: boolean;
|
||||||
|
filename: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_ROWS_PER_PAGE = 5;
|
export const DEFAULT_ROWS_PER_PAGE = 5;
|
||||||
@@ -43,6 +42,9 @@ interface BackupTableViewerProps {
|
|||||||
) => void;
|
) => void;
|
||||||
total: number;
|
total: number;
|
||||||
onRefresh: () => Promise<void>;
|
onRefresh: () => Promise<void>;
|
||||||
|
onDelete: (filename: string) => Promise<void>;
|
||||||
|
onRestore: (filename: string) => Promise<void>;
|
||||||
|
onExport?: (filename: string, destination: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BackupTableViewer = memo(
|
export const BackupTableViewer = memo(
|
||||||
@@ -52,21 +54,43 @@ export const BackupTableViewer = memo(
|
|||||||
onPageChange,
|
onPageChange,
|
||||||
total,
|
total,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
onDelete,
|
||||||
|
onRestore,
|
||||||
|
onExport,
|
||||||
}: BackupTableViewerProps) => {
|
}: BackupTableViewerProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleDelete = useLockFn(async (filename: string) => {
|
const handleDelete = useLockFn(async (filename: string) => {
|
||||||
await deleteWebdavBackup(filename);
|
await onDelete(filename);
|
||||||
await onRefresh();
|
await onRefresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleRestore = useLockFn(async (filename: string) => {
|
const handleRestore = useLockFn(async (filename: string) => {
|
||||||
await restoreWebDavBackup(filename).then(() => {
|
await onRestore(filename).then(() => {
|
||||||
showNotice("success", t("Restore Success, App will restart in 1s"));
|
showNotice("success", t("Restore Success, App will restart in 1s"));
|
||||||
});
|
});
|
||||||
await restartApp();
|
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 (
|
return (
|
||||||
<TableContainer component={Paper}>
|
<TableContainer component={Paper}>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -102,6 +126,27 @@ export const BackupTableViewer = memo(
|
|||||||
justifyContent: "flex-end",
|
justifyContent: "flex-end",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{onExport && (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
aria-label={t("Export")}
|
||||||
|
size="small"
|
||||||
|
title={t("Export Backup")}
|
||||||
|
onClick={async (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
await handleExport(file.filename);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Divider
|
||||||
|
orientation="vertical"
|
||||||
|
flexItem
|
||||||
|
sx={{ mx: 1, height: 24 }}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
color="secondary"
|
color="secondary"
|
||||||
aria-label={t("Delete")}
|
aria-label={t("Delete")}
|
||||||
|
|||||||
@@ -1,12 +1,28 @@
|
|||||||
import { Box, Divider, Paper } from "@mui/material";
|
import { Box, Button, Divider, Paper, Tab, Tabs } from "@mui/material";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
import type { Ref } from "react";
|
import type { Ref } from "react";
|
||||||
import { useCallback, useImperativeHandle, useMemo, useState } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { BaseDialog, BaseLoadingOverlay, DialogRef } from "@/components/base";
|
import { BaseDialog, BaseLoadingOverlay, DialogRef } from "@/components/base";
|
||||||
import { listWebDavBackup } from "@/services/cmds";
|
import {
|
||||||
|
deleteLocalBackup,
|
||||||
|
deleteWebdavBackup,
|
||||||
|
listLocalBackup,
|
||||||
|
listWebDavBackup,
|
||||||
|
exportLocalBackup,
|
||||||
|
restoreLocalBackup,
|
||||||
|
restoreWebDavBackup,
|
||||||
|
} from "@/services/cmds";
|
||||||
|
|
||||||
import { BackupConfigViewer } from "./backup-config-viewer";
|
import { BackupConfigViewer } from "./backup-config-viewer";
|
||||||
import {
|
import {
|
||||||
@@ -14,19 +30,28 @@ import {
|
|||||||
BackupTableViewer,
|
BackupTableViewer,
|
||||||
DEFAULT_ROWS_PER_PAGE,
|
DEFAULT_ROWS_PER_PAGE,
|
||||||
} from "./backup-table-viewer";
|
} from "./backup-table-viewer";
|
||||||
|
import { LocalBackupActions } from "./local-backup-actions";
|
||||||
dayjs.extend(customParseFormat);
|
dayjs.extend(customParseFormat);
|
||||||
|
|
||||||
const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss";
|
const DATE_FORMAT = "YYYY-MM-DD_HH-mm-ss";
|
||||||
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
|
const FILENAME_PATTERN = /\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}/;
|
||||||
|
type BackupSource = "local" | "webdav";
|
||||||
|
|
||||||
export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const contentRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const [dialogPaper, setDialogPaper] = useState<HTMLElement | null>(null);
|
||||||
|
const [closeButtonPosition, setCloseButtonPosition] = useState<{
|
||||||
|
top: number;
|
||||||
|
left: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
|
const [backupFiles, setBackupFiles] = useState<BackupFile[]>([]);
|
||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
|
const [source, setSource] = useState<BackupSource>("local");
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
open: () => {
|
open: () => {
|
||||||
@@ -43,7 +68,51 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
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<BackupFile[]> => {
|
||||||
|
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 {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const files = await getAllBackupFiles();
|
const files = await getAllBackupFiles();
|
||||||
@@ -57,32 +126,110 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}, [getAllBackupFiles]);
|
||||||
|
|
||||||
const getAllBackupFiles = async () => {
|
useEffect(() => {
|
||||||
const files = await listWebDavBackup();
|
if (open) {
|
||||||
return files
|
fetchAndSetBackupFiles();
|
||||||
.map((file) => {
|
const paper = contentRef.current?.closest(".MuiPaper-root");
|
||||||
const platform = file.filename.split("-")[0];
|
setDialogPaper((paper as HTMLElement) ?? null);
|
||||||
const fileBackupTimeStr = file.filename.match(FILENAME_PATTERN)!;
|
} else {
|
||||||
|
setDialogPaper(null);
|
||||||
|
}
|
||||||
|
}, [open, fetchAndSetBackupFiles]);
|
||||||
|
|
||||||
if (fileBackupTimeStr === null) {
|
useEffect(() => {
|
||||||
return null;
|
if (!open || dialogPaper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const frame = requestAnimationFrame(() => {
|
||||||
|
const paper = contentRef.current?.closest(".MuiPaper-root");
|
||||||
|
setDialogPaper((paper as HTMLElement) ?? null);
|
||||||
|
});
|
||||||
|
return () => cancelAnimationFrame(frame);
|
||||||
|
}, [open, dialogPaper]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialogPaper) {
|
||||||
|
setCloseButtonPosition(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof window === "undefined") {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const backupTime = dayjs(fileBackupTimeStr[0], DATE_FORMAT);
|
const updatePosition = () => {
|
||||||
const allowApply = true;
|
const rect = dialogPaper.getBoundingClientRect();
|
||||||
return {
|
setCloseButtonPosition({
|
||||||
...file,
|
top: rect.bottom - 16,
|
||||||
platform,
|
left: rect.right - 24,
|
||||||
backup_time: backupTime,
|
});
|
||||||
allow_apply: allowApply,
|
|
||||||
} as BackupFile;
|
|
||||||
})
|
|
||||||
.filter((item) => item !== null)
|
|
||||||
.sort((a, b) => (a.backup_time.isAfter(b.backup_time) ? -1 : 1));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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<BackupFile[]>(
|
const dataSource = useMemo<BackupFile[]>(
|
||||||
() =>
|
() =>
|
||||||
backupFiles.slice(
|
backupFiles.slice(
|
||||||
@@ -96,16 +243,49 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
<BaseDialog
|
<BaseDialog
|
||||||
open={open}
|
open={open}
|
||||||
title={t("Backup Setting")}
|
title={t("Backup Setting")}
|
||||||
// contentSx={{ width: 600, maxHeight: 800 }}
|
contentSx={{
|
||||||
okBtn={t("")}
|
minWidth: { xs: 320, sm: 620 },
|
||||||
cancelBtn={t("Close")}
|
maxWidth: "unset",
|
||||||
|
minHeight: 460,
|
||||||
|
}}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
onCancel={() => setOpen(false)}
|
disableFooter
|
||||||
disableOk
|
>
|
||||||
|
<Box
|
||||||
|
ref={contentRef}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
|
||||||
<BaseLoadingOverlay isLoading={isLoading} />
|
<BaseLoadingOverlay isLoading={isLoading} />
|
||||||
<Paper elevation={2} sx={{ padding: 2 }}>
|
<Paper
|
||||||
|
elevation={2}
|
||||||
|
sx={{
|
||||||
|
padding: 2,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
flexGrow: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
value={source}
|
||||||
|
onChange={handleChangeSource}
|
||||||
|
aria-label={t("Select Backup Target")}
|
||||||
|
sx={{ mb: 2 }}
|
||||||
|
>
|
||||||
|
<Tab value="local" label={t("Local Backup")} />
|
||||||
|
<Tab value="webdav" label={t("WebDAV Backup")} />
|
||||||
|
</Tabs>
|
||||||
|
{source === "local" ? (
|
||||||
|
<LocalBackupActions
|
||||||
|
setLoading={setIsLoading}
|
||||||
|
onBackupSuccess={fetchAndSetBackupFiles}
|
||||||
|
onRefresh={fetchAndSetBackupFiles}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<BackupConfigViewer
|
<BackupConfigViewer
|
||||||
setLoading={setIsLoading}
|
setLoading={setIsLoading}
|
||||||
onBackupSuccess={fetchAndSetBackupFiles}
|
onBackupSuccess={fetchAndSetBackupFiles}
|
||||||
@@ -113,16 +293,55 @@ export function BackupViewer({ ref }: { ref?: Ref<DialogRef> }) {
|
|||||||
onRefresh={fetchAndSetBackupFiles}
|
onRefresh={fetchAndSetBackupFiles}
|
||||||
onInit={fetchAndSetBackupFiles}
|
onInit={fetchAndSetBackupFiles}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Divider sx={{ marginY: 2 }} />
|
<Divider sx={{ marginY: 2 }} />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: "auto",
|
||||||
|
minHeight: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<BackupTableViewer
|
<BackupTableViewer
|
||||||
datasource={dataSource}
|
datasource={dataSource}
|
||||||
page={page}
|
page={page}
|
||||||
onPageChange={handleChangePage}
|
onPageChange={handleChangePage}
|
||||||
total={total}
|
total={total}
|
||||||
onRefresh={fetchAndSetBackupFiles}
|
onRefresh={fetchAndSetBackupFiles}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onRestore={handleRestore}
|
||||||
|
onExport={source === "local" ? handleExport : undefined}
|
||||||
/>
|
/>
|
||||||
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
{dialogPaper &&
|
||||||
|
closeButtonPosition &&
|
||||||
|
createPortal(
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "fixed",
|
||||||
|
top: closeButtonPosition.top,
|
||||||
|
left: closeButtonPosition.left,
|
||||||
|
transform: "translate(-100%, -100%)",
|
||||||
|
pointerEvents: "none",
|
||||||
|
zIndex: (theme) => theme.zIndex.modal + 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
sx={{
|
||||||
|
pointerEvents: "auto",
|
||||||
|
boxShadow: (theme) => theme.shadows[3],
|
||||||
|
backgroundColor: (theme) => theme.palette.background.paper,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("Close")}
|
||||||
|
</Button>
|
||||||
|
</Box>,
|
||||||
|
dialogPaper,
|
||||||
|
)}
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
78
src/components/setting/mods/local-backup-actions.tsx
Normal file
78
src/components/setting/mods/local-backup-actions.tsx
Normal file
@@ -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<void>;
|
||||||
|
onRefresh: () => Promise<void>;
|
||||||
|
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 (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid size={{ xs: 12, sm: 9 }}>
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
{t("Local Backup Info")}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
<Grid size={{ xs: 12, sm: 3 }}>
|
||||||
|
<Stack
|
||||||
|
direction="column"
|
||||||
|
alignItems="stretch"
|
||||||
|
spacing={1.5}
|
||||||
|
sx={{ height: "100%" }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="success"
|
||||||
|
onClick={handleBackup}
|
||||||
|
type="button"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{t("Backup")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
type="button"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{t("Refresh")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -387,7 +387,7 @@
|
|||||||
"toggle_tun_mode": "Enable/Disable Tun Mode",
|
"toggle_tun_mode": "Enable/Disable Tun Mode",
|
||||||
"entry_lightweight_mode": "Entry Lightweight Mode",
|
"entry_lightweight_mode": "Entry Lightweight Mode",
|
||||||
"Backup Setting": "Backup Setting",
|
"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",
|
"Runtime Config": "Runtime Config",
|
||||||
"Open Conf Dir": "Open Conf Dir",
|
"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",
|
"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",
|
"Username": "Username",
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
"Backup": "Backup",
|
"Backup": "Backup",
|
||||||
|
"Local Backup": "Local backup",
|
||||||
|
"WebDAV Backup": "WebDAV backup",
|
||||||
|
"Select Backup Target": "Select backup target",
|
||||||
"Filename": "Filename",
|
"Filename": "Filename",
|
||||||
"Actions": "Actions",
|
"Actions": "Actions",
|
||||||
"Restore": "Restore",
|
"Restore": "Restore",
|
||||||
|
"Export": "Export",
|
||||||
"No Backups": "No backups available",
|
"No Backups": "No backups available",
|
||||||
"WebDAV URL Required": "WebDAV URL cannot be empty",
|
"WebDAV URL Required": "WebDAV URL cannot be empty",
|
||||||
"Invalid WebDAV URL": "Invalid WebDAV URL format",
|
"Invalid WebDAV URL": "Invalid WebDAV URL format",
|
||||||
@@ -461,7 +465,13 @@
|
|||||||
"WebDAV Config Save Failed": "Failed to save WebDAV configuration: {{error}}",
|
"WebDAV Config Save Failed": "Failed to save WebDAV configuration: {{error}}",
|
||||||
"Backup Created": "Backup created successfully",
|
"Backup Created": "Backup created successfully",
|
||||||
"Backup Failed": "Backup failed: {{error}}",
|
"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",
|
"Delete Backup": "Delete Backup",
|
||||||
|
"Export Backup": "Export Backup",
|
||||||
"Restore Backup": "Restore Backup",
|
"Restore Backup": "Restore Backup",
|
||||||
"Backup Time": "Backup Time",
|
"Backup Time": "Backup Time",
|
||||||
"Confirm to delete this backup file?": "Confirm to delete this backup file?",
|
"Confirm to delete this backup file?": "Confirm to delete this backup file?",
|
||||||
|
|||||||
@@ -387,7 +387,7 @@
|
|||||||
"toggle_tun_mode": "打开/关闭 TUN 模式",
|
"toggle_tun_mode": "打开/关闭 TUN 模式",
|
||||||
"entry_lightweight_mode": "进入轻量模式",
|
"entry_lightweight_mode": "进入轻量模式",
|
||||||
"Backup Setting": "备份设置",
|
"Backup Setting": "备份设置",
|
||||||
"Backup Setting Info": "支持 WebDAV 备份配置文件",
|
"Backup Setting Info": "支持本地或 WebDAV 方式备份配置文件",
|
||||||
"Runtime Config": "当前配置",
|
"Runtime Config": "当前配置",
|
||||||
"Open Conf Dir": "配置目录",
|
"Open Conf Dir": "配置目录",
|
||||||
"Open Conf Dir Info": "如果软件运行异常,!备份!并删除此文件夹下的所有文件,重启软件",
|
"Open Conf Dir Info": "如果软件运行异常,!备份!并删除此文件夹下的所有文件,重启软件",
|
||||||
@@ -448,9 +448,13 @@
|
|||||||
"Username": "用户名",
|
"Username": "用户名",
|
||||||
"Password": "密码",
|
"Password": "密码",
|
||||||
"Backup": "备份",
|
"Backup": "备份",
|
||||||
|
"Local Backup": "本地备份",
|
||||||
|
"WebDAV Backup": "WebDAV 备份",
|
||||||
|
"Select Backup Target": "选择备份目标",
|
||||||
"Filename": "文件名称",
|
"Filename": "文件名称",
|
||||||
"Actions": "操作",
|
"Actions": "操作",
|
||||||
"Restore": "恢复",
|
"Restore": "恢复",
|
||||||
|
"Export": "导出",
|
||||||
"No Backups": "暂无备份",
|
"No Backups": "暂无备份",
|
||||||
"WebDAV URL Required": "WebDAV 服务器地址不能为空",
|
"WebDAV URL Required": "WebDAV 服务器地址不能为空",
|
||||||
"Invalid WebDAV URL": "无效的 WebDAV 服务器地址格式",
|
"Invalid WebDAV URL": "无效的 WebDAV 服务器地址格式",
|
||||||
@@ -461,7 +465,13 @@
|
|||||||
"WebDAV Config Save Failed": "保存 WebDAV 配置失败: {{error}}",
|
"WebDAV Config Save Failed": "保存 WebDAV 配置失败: {{error}}",
|
||||||
"Backup Created": "备份创建成功",
|
"Backup Created": "备份创建成功",
|
||||||
"Backup Failed": "备份失败: {{error}}",
|
"Backup Failed": "备份失败: {{error}}",
|
||||||
|
"Local Backup Created": "本地备份创建成功",
|
||||||
|
"Local Backup Failed": "本地备份失败",
|
||||||
|
"Local Backup Exported": "本地备份导出成功",
|
||||||
|
"Local Backup Export Failed": "本地备份导出失败",
|
||||||
|
"Local Backup Info": "在应用数据目录中创建本地备份,您可以通过下方列表进行恢复或删除。",
|
||||||
"Delete Backup": "删除备份",
|
"Delete Backup": "删除备份",
|
||||||
|
"Export Backup": "导出备份",
|
||||||
"Restore Backup": "恢复备份",
|
"Restore Backup": "恢复备份",
|
||||||
"Backup Time": "备份时间",
|
"Backup Time": "备份时间",
|
||||||
"Confirm to delete this backup file?": "确认删除此备份文件吗?",
|
"Confirm to delete this backup file?": "确认删除此备份文件吗?",
|
||||||
|
|||||||
@@ -344,7 +344,7 @@
|
|||||||
"toggle_tun_mode": "打開/關閉 TUN 模式",
|
"toggle_tun_mode": "打開/關閉 TUN 模式",
|
||||||
"entry_lightweight_mode": "進入輕量模式",
|
"entry_lightweight_mode": "進入輕量模式",
|
||||||
"Backup Setting": "備份設置",
|
"Backup Setting": "備份設置",
|
||||||
"Backup Setting Info": "支持 WebDAV 備份配置文件",
|
"Backup Setting Info": "支持本地或 WebDAV 方式備份配置文件",
|
||||||
"Runtime Config": "當前配置",
|
"Runtime Config": "當前配置",
|
||||||
"Open Conf Dir": "配置目錄",
|
"Open Conf Dir": "配置目錄",
|
||||||
"Open Conf Dir Info": "如果軟件運行異常,!備份!並刪除此文件夾下的所有文件,重啟軟件",
|
"Open Conf Dir Info": "如果軟件運行異常,!備份!並刪除此文件夾下的所有文件,重啟軟件",
|
||||||
@@ -393,9 +393,13 @@
|
|||||||
"Username": "用戶名",
|
"Username": "用戶名",
|
||||||
"Password": "密碼",
|
"Password": "密碼",
|
||||||
"Backup": "備份",
|
"Backup": "備份",
|
||||||
|
"Local Backup": "本地備份",
|
||||||
|
"WebDAV Backup": "WebDAV 備份",
|
||||||
|
"Select Backup Target": "選擇備份目標",
|
||||||
"Filename": "文件名稱",
|
"Filename": "文件名稱",
|
||||||
"Actions": "操作",
|
"Actions": "操作",
|
||||||
"Restore": "恢復",
|
"Restore": "恢復",
|
||||||
|
"Export": "匯出",
|
||||||
"No Backups": "暫無備份",
|
"No Backups": "暫無備份",
|
||||||
"WebDAV URL Required": "WebDAV 服務器地址不能為空",
|
"WebDAV URL Required": "WebDAV 服務器地址不能為空",
|
||||||
"Invalid WebDAV URL": "無效的 WebDAV 服務器地址格式",
|
"Invalid WebDAV URL": "無效的 WebDAV 服務器地址格式",
|
||||||
@@ -405,7 +409,13 @@
|
|||||||
"WebDAV Config Save Failed": "保存 WebDAV 配置失敗: {{error}}",
|
"WebDAV Config Save Failed": "保存 WebDAV 配置失敗: {{error}}",
|
||||||
"Backup Created": "備份創建成功",
|
"Backup Created": "備份創建成功",
|
||||||
"Backup Failed": "備份失敗: {{error}}",
|
"Backup Failed": "備份失敗: {{error}}",
|
||||||
|
"Local Backup Created": "本地備份創建成功",
|
||||||
|
"Local Backup Failed": "本地備份失敗",
|
||||||
|
"Local Backup Exported": "本地備份匯出成功",
|
||||||
|
"Local Backup Export Failed": "本地備份匯出失敗",
|
||||||
|
"Local Backup Info": "在應用資料目錄中建立本地備份,您可以透過下方列表進行恢復或刪除。",
|
||||||
"Delete Backup": "刪除備份",
|
"Delete Backup": "刪除備份",
|
||||||
|
"Export Backup": "匯出備份",
|
||||||
"Restore Backup": "恢復備份",
|
"Restore Backup": "恢復備份",
|
||||||
"Backup Time": "備份時間",
|
"Backup Time": "備份時間",
|
||||||
"Restore Success, App will restart in 1s": "恢復成功,應用將在 1 秒後重啟",
|
"Restore Success, App will restart in 1s": "恢復成功,應用將在 1 秒後重啟",
|
||||||
|
|||||||
@@ -440,14 +440,30 @@ export async function createWebdavBackup() {
|
|||||||
return invoke<void>("create_webdav_backup");
|
return invoke<void>("create_webdav_backup");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createLocalBackup() {
|
||||||
|
return invoke<void>("create_local_backup");
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteWebdavBackup(filename: string) {
|
export async function deleteWebdavBackup(filename: string) {
|
||||||
return invoke<void>("delete_webdav_backup", { filename });
|
return invoke<void>("delete_webdav_backup", { filename });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteLocalBackup(filename: string) {
|
||||||
|
return invoke<void>("delete_local_backup", { filename });
|
||||||
|
}
|
||||||
|
|
||||||
export async function restoreWebDavBackup(filename: string) {
|
export async function restoreWebDavBackup(filename: string) {
|
||||||
return invoke<void>("restore_webdav_backup", { filename });
|
return invoke<void>("restore_webdav_backup", { filename });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function restoreLocalBackup(filename: string) {
|
||||||
|
return invoke<void>("restore_local_backup", { filename });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function exportLocalBackup(filename: string, destination: string) {
|
||||||
|
return invoke<void>("export_local_backup", { filename, destination });
|
||||||
|
}
|
||||||
|
|
||||||
export async function saveWebdavConfig(
|
export async function saveWebdavConfig(
|
||||||
url: string,
|
url: string,
|
||||||
username: string,
|
username: string,
|
||||||
@@ -468,6 +484,10 @@ export async function listWebDavBackup() {
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listLocalBackup() {
|
||||||
|
return invoke<ILocalBackupFile[]>("list_local_backup");
|
||||||
|
}
|
||||||
|
|
||||||
export async function scriptValidateNotice(status: string, msg: string) {
|
export async function scriptValidateNotice(status: string, msg: string) {
|
||||||
return invoke<void>("script_validate_notice", { status, msg });
|
return invoke<void>("script_validate_notice", { status, msg });
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/services/types.d.ts
vendored
7
src/services/types.d.ts
vendored
@@ -870,6 +870,13 @@ interface IWebDavFile {
|
|||||||
tag: string;
|
tag: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ILocalBackupFile {
|
||||||
|
filename: string;
|
||||||
|
path: string;
|
||||||
|
last_modified: string;
|
||||||
|
content_length: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface IWebDavConfig {
|
interface IWebDavConfig {
|
||||||
url: string;
|
url: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user