From c3e24d7b96be565c64c0ae03881e1abad0746235 Mon Sep 17 00:00:00 2001 From: huzibaca Date: Fri, 8 Nov 2024 21:46:15 +0800 Subject: [PATCH] chore: update --- src-tauri/Cargo.lock | 247 +++++++++++++ src-tauri/Cargo.toml | 2 + src-tauri/src/cmds.rs | 32 ++ src-tauri/src/config/verge.rs | 8 + src-tauri/src/core/backup.rs | 118 +++++++ src-tauri/src/core/mod.rs | 1 + src-tauri/src/feat.rs | 40 +++ src-tauri/src/lib.rs | 6 +- src-tauri/src/utils/dirs.rs | 11 +- src/components/base/base-loading-overlay.tsx | 33 ++ src/components/base/index.ts | 1 + src/components/setting/mods/backup-viewer.tsx | 330 ++++++++++++++++++ src/components/setting/setting-verge.tsx | 8 + src/locales/en.json | 1 + src/locales/fa.json | 1 + src/locales/ru.json | 1 + src/locales/zh.json | 1 + src/services/cmds.ts | 23 ++ src/services/types.d.ts | 18 + src/utils/helper.ts | 9 + 20 files changed, 887 insertions(+), 4 deletions(-) create mode 100644 src-tauri/src/core/backup.rs create mode 100644 src/components/base/base-loading-overlay.tsx create mode 100644 src/components/setting/mods/backup-viewer.tsx create mode 100644 src/utils/helper.ts diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 05d461a..fc1d648 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -23,6 +23,17 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.11" @@ -841,6 +852,27 @@ dependencies = [ "serde", ] +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cairo-rs" version = "0.18.5" @@ -973,6 +1005,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clash-verge" version = "2.0.0" @@ -995,6 +1037,7 @@ dependencies = [ "percent-encoding", "port_scanner", "reqwest", + "reqwest_dav", "runas", "serde", "serde_json", @@ -1020,6 +1063,7 @@ dependencies = [ "warp", "window-shadows", "winreg 0.52.0", + "zip", ] [[package]] @@ -1156,6 +1200,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -1245,6 +1295,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.4.2" @@ -1420,6 +1485,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "deflate64" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" + [[package]] name = "delay_timer" version = "0.11.6" @@ -1555,6 +1626,20 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "crypto-common", + "subtle", +] + +[[package]] +name = "digest_auth" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3054f4e81d395e50822796c5e99ca522e6ba7be98947d6d4b0e5e61640bdb894" +dependencies = [ + "digest 0.10.7", + "hex", + "md-5", + "rand 0.8.5", + "sha2 0.10.8", ] [[package]] @@ -2622,6 +2707,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "home" version = "0.5.9" @@ -3077,6 +3171,15 @@ dependencies = [ "cfb", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -3461,6 +3564,12 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockfree-object-pool" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" + [[package]] name = "log" version = "0.4.22" @@ -3522,6 +3631,16 @@ dependencies = [ "hashbrown 0.15.0", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "mac" version = "0.1.1" @@ -3594,6 +3713,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.7.4" @@ -4459,6 +4588,16 @@ dependencies = [ "libc", ] +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -5294,6 +5433,26 @@ dependencies = [ "windows-registry 0.2.0", ] +[[package]] +name = "reqwest_dav" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea79cbb695b7cc877ae9c0f0eeb8468e36cd03dc9c41a93bcf237396357c7b42" +dependencies = [ + "async-trait", + "chrono", + "digest_auth", + "http 1.1.0", + "httpdate", + "reqwest", + "serde", + "serde-xml-rs", + "serde_derive", + "serde_json", + "tokio", + "url", +] + [[package]] name = "rfd" version = "0.15.0" @@ -5664,6 +5823,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde-xml-rs" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb3aa78ecda1ebc9ec9847d5d3aba7d618823446a049ba2491940506da6e2782" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + [[package]] name = "serde_derive" version = "1.0.213" @@ -8527,6 +8698,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xml-rs" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af310deaae937e48a26602b730250b4949e125f468f11e6990be3e5304ddd96f" + [[package]] name = "yoke" version = "0.7.4" @@ -8663,6 +8840,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] [[package]] name = "zerovec" @@ -8692,13 +8883,69 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" dependencies = [ + "aes", "arbitrary", + "bzip2", + "constant_time_eq", "crc32fast", "crossbeam-utils", + "deflate64", "displaydoc", + "flate2", + "hmac", "indexmap 2.6.0", + "lzma-rs", "memchr", + "pbkdf2", + "rand 0.8.5", + "sha1", "thiserror", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zopfli" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +dependencies = [ + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.13+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa" +dependencies = [ + "cc", + "pkg-config", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a235786..c751d6c 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -54,6 +54,8 @@ tauri-plugin-clipboard-manager = "2.0.1" tauri-plugin-deep-link = "2.0.1" tauri-plugin-devtools = "2.0.0-rc" url = "2.5.2" +zip = "2.2.0" +reqwest_dav = "0.1.14" [target.'cfg(windows)'.dependencies] runas = "=1.2.0" deelevate = "0.2.0" diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 8bd2437..d532aa6 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -11,6 +11,7 @@ use serde_yaml::Mapping; use std::collections::{HashMap, VecDeque}; use sysproxy::{Autoproxy, Sysproxy}; type CmdResult = Result; +use reqwest_dav::list_cmd::ListFile; use tauri::Manager; #[tauri::command] @@ -375,6 +376,37 @@ pub async fn exit_app() { feat::quit(Some(0)); } +#[tauri::command] +pub async fn save_webdav_config(url: String, username: String, password: String) -> CmdResult<()> { + let patch = IVerge { + webdav_url: Some(url), + webdav_username: Some(username), + webdav_password: Some(password), + ..IVerge::default() + }; + Config::verge().draft().patch_config(patch.clone()); + Config::verge().apply(); + Config::verge() + .data() + .save_file() + .map_err(|err| err.to_string())?; + backup::WebDavClient::global().reset(); + Ok(()) +} + +#[tauri::command] +pub async fn create_webdav_backup() -> CmdResult<()> { + feat::create_backup_and_upload_webdav() + .await + .map_err(|err| err.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn list_webdav_backup() -> CmdResult> { + feat::list_wevdav_backup().await.map_err(|e| e.to_string()) +} + pub mod service { use super::*; use crate::core::service; diff --git a/src-tauri/src/config/verge.rs b/src-tauri/src/config/verge.rs index b440e29..94c3e9c 100644 --- a/src-tauri/src/config/verge.rs +++ b/src-tauri/src/config/verge.rs @@ -147,6 +147,10 @@ pub struct IVerge { pub verge_port: Option, pub verge_http_enabled: Option, + + pub webdav_url: Option, + pub webdav_username: Option, + pub webdav_password: Option, } #[derive(Default, Debug, Clone, Deserialize, Serialize)] @@ -304,6 +308,10 @@ impl IVerge { patch!(proxy_layout_column); patch!(test_list); patch!(auto_log_clean); + + patch!(webdav_url); + patch!(webdav_username); + patch!(webdav_password); } /// 在初始化前尝试拿到单例端口的值 diff --git a/src-tauri/src/core/backup.rs b/src-tauri/src/core/backup.rs new file mode 100644 index 0000000..c160435 --- /dev/null +++ b/src-tauri/src/core/backup.rs @@ -0,0 +1,118 @@ +use crate::config::Config; +use crate::utils::dirs; +use anyhow::Error; +use once_cell::sync::OnceCell; +use parking_lot::Mutex; +use reqwest_dav::list_cmd::{ListEntity, ListFile}; +use std::env::{consts::OS, temp_dir}; +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use std::sync::Arc; +use zip::write::SimpleFileOptions; + +pub struct WebDavClient { + client: Arc>>, +} + +impl WebDavClient { + pub fn global() -> &'static WebDavClient { + static WEBDAV_CLIENT: OnceCell = OnceCell::new(); + WEBDAV_CLIENT.get_or_init(|| WebDavClient { + client: Arc::new(Mutex::new(None)), + }) + } + + async fn get_client(&self) -> Result { + if self.client.lock().is_none() { + let verge = Config::verge().latest().clone(); + if verge.webdav_url.is_none() + || verge.webdav_username.is_none() + || verge.webdav_password.is_none() + { + let msg = + "Unable to create web dav client, please make sure the webdav config is correct" + .to_string(); + log::error!(target: "app","{}",msg); + return Err(anyhow::Error::msg(msg)); + } + + let url = verge.webdav_url.unwrap_or_default(); + let username = verge.webdav_username.unwrap_or_default(); + let password = verge.webdav_password.unwrap_or_default(); + + let client = reqwest_dav::ClientBuilder::new() + .set_host(url.to_owned()) + .set_auth(reqwest_dav::Auth::Basic( + username.to_owned(), + password.to_owned(), + )) + .build()?; + *self.client.lock() = Some(client.clone()); + } + Ok(self.client.lock().clone().unwrap()) + } + + pub fn reset(&self) { + if !self.client.lock().is_none() { + self.client.lock().take(); + } + } + + pub async fn upload(&self, file_path: PathBuf, file_name: String) -> Result<(), Error> { + let client = self.get_client().await?; + if client.get(dirs::BACKUP_DIR).await.is_err() { + client.mkcol(dirs::BACKUP_DIR).await?; + } + + let webdav_path: String = format!("{}/{}", dirs::BACKUP_DIR, file_name); + client + .put(webdav_path.as_ref(), fs::read(file_path)?) + .await?; + Ok(()) + } + + pub async fn list_files(&self) -> Result, Error> { + let client = self.get_client().await?; + let files = client + .list(dirs::BACKUP_DIR, reqwest_dav::Depth::Number(1)) + .await?; + let mut final_files = Vec::new(); + for file in files { + if let ListEntity::File(file) = file { + final_files.push(file); + } + } + Ok(final_files) + } +} + +pub fn create_backup() -> Result<(String, PathBuf), Error> { + let now = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S").to_string(); + let zip_file_name = format!("{}-backup-{}.zip", OS, now); + let zip_path = temp_dir().join(&zip_file_name); + + let file = fs::File::create(&zip_path)?; + let mut zip = zip::ZipWriter::new(file); + zip.add_directory("profiles/", SimpleFileOptions::default())?; + let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored); + if let Ok(entries) = fs::read_dir(dirs::app_profiles_dir()?) { + for entry in entries { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_file() { + let backup_path = format!("profiles/{}", entry.file_name().to_str().unwrap()); + zip.start_file(backup_path, options)?; + zip.write_all(fs::read(path).unwrap().as_slice())?; + } + } + } + zip.start_file(dirs::CLASH_CONFIG, options)?; + zip.write_all(fs::read(dirs::clash_path()?)?.as_slice())?; + zip.start_file(dirs::VERGE_CONFIG, options)?; + zip.write_all(fs::read(dirs::verge_path()?)?.as_slice())?; + zip.start_file(dirs::PROFILE_YAML, options)?; + zip.write_all(fs::read(dirs::profiles_path()?)?.as_slice())?; + zip.finish()?; + Ok((zip_file_name, zip_path)) +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 52e8f27..32bb267 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,3 +1,4 @@ +pub mod backup; pub mod clash_api; #[allow(clippy::module_inception)] mod core; diff --git a/src-tauri/src/feat.rs b/src-tauri/src/feat.rs index 5b95fae..f5f954c 100644 --- a/src-tauri/src/feat.rs +++ b/src-tauri/src/feat.rs @@ -9,6 +9,7 @@ use crate::core::*; use crate::log_err; use crate::utils::resolve; use anyhow::{bail, Result}; +use reqwest_dav::list_cmd::ListFile; use serde_yaml::{Mapping, Value}; use tauri_plugin_clipboard_manager::ClipboardExt; @@ -401,3 +402,42 @@ pub async fn test_delay(url: String) -> Result { } } } + +pub async fn create_backup_and_upload_webdav() -> Result<()> { + if let Err(err) = async { + let (file_name, temp_file_path) = backup::create_backup().map_err(|err| { + log::error!(target: "app", "Failed to create backup: {:#?}", err); + err + })?; + + backup::WebDavClient::global() + .upload(temp_file_path.clone(), file_name) + .await + .map_err(|err| { + log::error!(target: "app", "Failed to upload to WebDAV: {:#?}", err); + err + })?; + + std::fs::remove_file(&temp_file_path).map_err(|err| { + log::warn!(target: "app", "Failed to remove temp file: {:#?}", err); + err + })?; + + Ok(()) + } + .await + { + return Err(err); + } + Ok(()) +} + +pub async fn list_wevdav_backup() -> Result> { + backup::WebDavClient::global() + .list_files() + .await + .map_err(|err| { + log::error!(target: "app", "Failed to list WebDAV backup files: {:#?}", err); + err + }) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 882665a..1d3ccec 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -123,7 +123,11 @@ pub fn run() { // service mode cmds::service::check_service, // clash api - cmds::clash_api_get_proxy_delay + cmds::clash_api_get_proxy_delay, + // backup + cmds::create_webdav_backup, + cmds::save_webdav_config, + cmds::list_webdav_backup, ]); #[cfg(debug_assertions)] diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index cf0f944..5000fd7 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -6,14 +6,19 @@ use tauri::Manager; #[cfg(not(feature = "verge-dev"))] pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev"; +#[cfg(not(feature = "verge-dev"))] +pub static BACKUP_DIR: &str = "clash-verge-rev-backup"; + #[cfg(feature = "verge-dev")] pub static APP_ID: &str = "io.github.clash-verge-rev.clash-verge-rev.dev"; +#[cfg(feature = "verge-dev")] +pub static BACKUP_DIR: &str = "clash-verge-rev-backup-dev"; pub static PORTABLE_FLAG: OnceCell = OnceCell::new(); -static CLASH_CONFIG: &str = "config.yaml"; -static VERGE_CONFIG: &str = "verge.yaml"; -static PROFILE_YAML: &str = "profiles.yaml"; +pub static CLASH_CONFIG: &str = "config.yaml"; +pub static VERGE_CONFIG: &str = "verge.yaml"; +pub static PROFILE_YAML: &str = "profiles.yaml"; /// init portable flag pub fn init_portable_flag() -> Result<()> { diff --git a/src/components/base/base-loading-overlay.tsx b/src/components/base/base-loading-overlay.tsx new file mode 100644 index 0000000..036250b --- /dev/null +++ b/src/components/base/base-loading-overlay.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import { Box, CircularProgress } from "@mui/material"; + +export interface BaseLoadingOverlayProps { + isLoading: boolean; +} + +export const BaseLoadingOverlay: React.FC = ({ + isLoading, +}) => { + if (!isLoading) return null; + + return ( + + + + ); +}; + +export default BaseLoadingOverlay; diff --git a/src/components/base/index.ts b/src/components/base/index.ts index 57303ed..3d17d59 100644 --- a/src/components/base/index.ts +++ b/src/components/base/index.ts @@ -5,3 +5,4 @@ export { BaseLoading } from "./base-loading"; export { BaseErrorBoundary } from "./base-error-boundary"; export { Notice } from "./base-notice"; export { Switch } from "./base-switch"; +export { BaseLoadingOverlay } from "./base-loading-overlay"; diff --git a/src/components/setting/mods/backup-viewer.tsx b/src/components/setting/mods/backup-viewer.tsx new file mode 100644 index 0000000..5392653 --- /dev/null +++ b/src/components/setting/mods/backup-viewer.tsx @@ -0,0 +1,330 @@ +import { forwardRef, useImperativeHandle, useState, useRef } from "react"; +import { useTranslation } from "react-i18next"; +import { useLockFn } from "ahooks"; +import { Typography } from "@mui/material"; +import { useForm } from "react-hook-form"; +import { useVerge } from "@/hooks/use-verge"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { isValidUrl } from "@/utils/helper"; +import { BaseLoadingOverlay } from "@/components/base"; +import { + TextField, + Button, + Grid, + Box, + Paper, + Stack, + IconButton, + InputAdornment, + Divider, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, +} from "@mui/material"; +import Visibility from "@mui/icons-material/Visibility"; +import VisibilityOff from "@mui/icons-material/VisibilityOff"; +import DeleteIcon from "@mui/icons-material/Delete"; +import RestoreIcon from "@mui/icons-material/Restore"; +import { createWebdavBackup, saveWebdavConfig } from "@/services/cmds"; +import { save } from "@tauri-apps/plugin-dialog"; + +export const BackupViewer = forwardRef((props, ref) => { + const { t } = useTranslation(); + const [open, setOpen] = useState(false); + + const { verge, mutateVerge } = useVerge(); + const { webdav_url, webdav_username, webdav_password } = verge || {}; + const [showPassword, setShowPassword] = useState(false); + const usernameRef = useRef(null); + const passwordRef = useRef(null); + const urlRef = useRef(null); + + const [isLoading, setIsLoading] = useState(false); + + const { register, handleSubmit, watch } = useForm({ + defaultValues: { + url: webdav_url, + username: webdav_username, + password: webdav_password, + }, + }); + + const url = watch("url"); + const username = watch("username"); + const password = watch("password"); + const webdavChanged = + webdav_url !== url || + webdav_username !== username || + webdav_password !== password; + + // const backups = [] as any[]; + const backups = [ + { name: "backup1.zip" }, + { name: "backup2.zip" }, + { name: "backup3.zip" }, + ]; + useImperativeHandle(ref, () => ({ + open: () => { + setOpen(true); + }, + close: () => setOpen(false), + })); + + const checkForm = () => { + const username = usernameRef.current?.value; + const password = passwordRef.current?.value; + const url = urlRef.current?.value; + + if (!url) { + Notice.error(t("Webdav url cannot be empty")); + urlRef.current?.focus(); + return; + } else if (!isValidUrl(url)) { + Notice.error(t("Webdav address must be url")); + urlRef.current?.focus(); + return; + } + if (!username) { + Notice.error(t("Username cannot be empty")); + usernameRef.current?.focus(); + return; + } + if (!password) { + Notice.error(t("Password cannot be empty")); + passwordRef.current?.focus(); + return; + } + }; + + const submit = async (data: IWebDavConfig) => { + checkForm(); + setIsLoading(true); + await saveWebdavConfig(data.url, data.username, data.password) + .then(() => { + mutateVerge( + { + webdav_url: data.url, + webdav_username: data.username, + webdav_password: data.password, + }, + false + ); + Notice.success(t("Webdav Config Saved Successfully"), 1500); + }) + .catch((e) => { + Notice.error(t("Webdav Config Save Failed", { error: e }), 3000); + }) + .finally(() => { + setIsLoading(false); + }); + }; + + const handleClickShowPassword = () => { + setShowPassword(!showPassword); + }; + + const handleBackup = useLockFn(async () => { + checkForm(); + setIsLoading(true); + await createWebdavBackup() + .then(() => { + Notice.success(t("Backup Successfully"), 1500); + }) + .finally(() => { + setIsLoading(false); + }) + .catch((e) => { + console.log(e, "backup failed"); + Notice.error(t("Backup Failed", { error: e }), 3000); + }); + }); + return ( + setOpen(false)} + onCancel={() => setOpen(false)} + > + + + +
+ + + + {/* WebDAV Server Address */} + + + + + {/* Username and Password */} + + + + + + + {showPassword ? ( + + ) : ( + + )} + + + ), + }} + /> + + + + + + + {webdavChanged || + webdav_url === null || + webdav_username == null || + webdav_password == null ? ( + + ) : ( + + )} + + + +
+ + + + + + 文件名称 + 操作 + + + + {backups.length > 0 ? ( + backups?.map((backup, index) => ( + + + {backup.name} + + + + + + + + + + + + + + + )) + ) : ( + + + + + 暂无备份 + + + + + )} + +
+
+
+
+
+ ); +}); diff --git a/src/components/setting/setting-verge.tsx b/src/components/setting/setting-verge.tsx index 1a4dd8d..b92b8ea 100644 --- a/src/components/setting/setting-verge.tsx +++ b/src/components/setting/setting-verge.tsx @@ -23,6 +23,7 @@ import { ThemeViewer } from "./mods/theme-viewer"; import { GuardState } from "./mods/guard-state"; import { LayoutViewer } from "./mods/layout-viewer"; import { UpdateViewer } from "./mods/update-viewer"; +import { BackupViewer } from "./mods/backup-viewer"; import getSystem from "@/utils/get-system"; import { routers } from "@/pages/_routers"; import { TooltipIcon } from "@/components/base/base-tooltip-icon"; @@ -52,6 +53,7 @@ const SettingVerge = ({ onError }: Props) => { const themeRef = useRef(null); const layoutRef = useRef(null); const updateRef = useRef(null); + const backupRef = useRef(null); const onChangeData = (patch: Partial) => { mutateVerge({ ...verge, ...patch }, false); @@ -83,6 +85,7 @@ const SettingVerge = ({ onError }: Props) => { + { label={t("Hotkey Setting")} /> + backupRef.current?.open()} + label={t("Backup Setting")} + /> + configRef.current?.open()} label={t("Runtime Config")} diff --git a/src/locales/en.json b/src/locales/en.json index 5540f61..261a38f 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -332,6 +332,7 @@ "clash_mode_direct": "Direct Mode", "toggle_system_proxy": "Enable/Disable System Proxy", "toggle_tun_mode": "Enable/Disable Tun Mode", + "Backup Setting": "Backup Setting", "Runtime Config": "Runtime Config", "Open Conf Dir": "Open Conf Dir", "Open Core Dir": "Open Core Dir", diff --git a/src/locales/fa.json b/src/locales/fa.json index 4c1ebeb..28ba77e 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -330,6 +330,7 @@ "clash_mode_direct": "حالت مستقیم", "toggle_system_proxy": "فعال/غیرفعال کردن پراکسی سیستم", "toggle_tun_mode": "فعال/غیرفعال کردن حالت Tun", + "Backup Setting": "تنظیمات پشتیبان", "Runtime Config": "پیکربندی زمان اجرا", "Open Conf Dir": "باز کردن پوشه برنامه", "Open Core Dir": "باز کردن پوشه هسته", diff --git a/src/locales/ru.json b/src/locales/ru.json index 4cabd8c..2c24b2c 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -330,6 +330,7 @@ "clash_mode_direct": "Прямой режим", "toggle_system_proxy": "Включить/Отключить системный прокси", "toggle_tun_mode": "Включить/Отключить режим туннеля", + "Backup Setting": "Настройки резервного копирования", "Runtime Config": "Используемый конфиг", "Open Conf Dir": "Открыть папку приложения", "Open Core Dir": "Открыть папку ядра", diff --git a/src/locales/zh.json b/src/locales/zh.json index f6790c7..0f0b161 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -332,6 +332,7 @@ "clash_mode_direct": "直连模式", "toggle_system_proxy": "打开/关闭系统代理", "toggle_tun_mode": "打开/关闭 Tun 模式", + "Backup Setting": "备份设置", "Runtime Config": "当前配置", "Open Conf Dir": "配置目录", "Open Core Dir": "内核目录", diff --git a/src/services/cmds.ts b/src/services/cmds.ts index c4e3058..57d45ef 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -236,3 +236,26 @@ export async function getNetworkInterfaces() { export async function getNetworkInterfacesInfo() { return invoke("get_network_interfaces_info"); } + +export async function createWebdavBackup() { + return invoke("create_webdav_backup"); +} +export async function saveWebdavConfig( + url: string, + username: string, + password: String +) { + return invoke("save_webdav_config", { + url, + username, + password, + }); +} + +export async function listWebDavBackup() { + let list: IWebDavFile[] = await invoke("list_webdav_backup"); + list.map((item) => { + item.filename = item.href.split("/").pop() as string; + }); + return list; +} diff --git a/src/services/types.d.ts b/src/services/types.d.ts index be2eb29..147323f 100644 --- a/src/services/types.d.ts +++ b/src/services/types.d.ts @@ -744,4 +744,22 @@ interface IVergeConfig { auto_log_clean?: 0 | 1 | 2 | 3; proxy_layout_column?: number; test_list?: IVergeTestItem[]; + webdav_url?: string; + webdav_username?: string; + webdav_password?: string; +} + +interface IWebDavFile { + filename: string; + href: string; + last_modified: string; + content_length: number; + content_type: string; + tag: string; +} + +interface IWebDavConfig { + url: string; + username: string; + password: string; } diff --git a/src/utils/helper.ts b/src/utils/helper.ts new file mode 100644 index 0000000..1a7d104 --- /dev/null +++ b/src/utils/helper.ts @@ -0,0 +1,9 @@ +export const isValidUrl = (url: string) => { + try { + new URL(url); + return true; + } catch (e) { + console.log(e); + return false; + } +};