From beddc71ed8240d77f3f101c3bf5a65440e9bd247 Mon Sep 17 00:00:00 2001 From: 2dust <31833384+2dust@users.noreply.github.com> Date: Sat, 7 Sep 2024 14:52:33 +0800 Subject: [PATCH] Add backup and restore --- v2rayN/ServiceLib/Common/FileManager.cs | 44 +++- v2rayN/ServiceLib/Handler/ConfigHandler.cs | 2 + v2rayN/ServiceLib/Handler/NoticeHandler.cs | 2 +- v2rayN/ServiceLib/Handler/WebDavHandler.cs | 163 ++++++++++++ v2rayN/ServiceLib/Models/Config.cs | 1 + v2rayN/ServiceLib/Models/ConfigItems.cs | 8 + v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 99 ++++++++ v2rayN/ServiceLib/Resx/ResUI.resx | 33 +++ v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 33 +++ v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 33 +++ v2rayN/ServiceLib/ServiceLib.csproj | 1 + .../ViewModels/BackupAndRestoreViewModel.cs | 151 +++++++++++ .../ViewModels/CheckUpdateViewModel.cs | 1 - v2rayN/v2rayN/Views/BackupAndRestoreView.xaml | 239 ++++++++++++++++++ .../v2rayN/Views/BackupAndRestoreView.xaml.cs | 63 +++++ v2rayN/v2rayN/Views/MainWindow.xaml | 8 +- v2rayN/v2rayN/Views/MainWindow.xaml.cs | 22 +- 17 files changed, 889 insertions(+), 14 deletions(-) create mode 100644 v2rayN/ServiceLib/Handler/WebDavHandler.cs create mode 100644 v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs create mode 100644 v2rayN/v2rayN/Views/BackupAndRestoreView.xaml create mode 100644 v2rayN/v2rayN/Views/BackupAndRestoreView.xaml.cs diff --git a/v2rayN/ServiceLib/Common/FileManager.cs b/v2rayN/ServiceLib/Common/FileManager.cs index 95a17adb3d..2e952bd8cd 100644 --- a/v2rayN/ServiceLib/Common/FileManager.cs +++ b/v2rayN/ServiceLib/Common/FileManager.cs @@ -106,7 +106,12 @@ public static bool CreateFromDirectory(string sourceDirectoryName, string destin { try { - ZipFile.CreateFromDirectory(sourceDirectoryName, destinationArchiveFileName); + if (File.Exists(destinationArchiveFileName)) + { + File.Delete(destinationArchiveFileName); + } + + ZipFile.CreateFromDirectory(sourceDirectoryName, destinationArchiveFileName, CompressionLevel.SmallestSize, true); } catch (Exception ex) { @@ -115,5 +120,42 @@ public static bool CreateFromDirectory(string sourceDirectoryName, string destin } return true; } + + public static void CopyDirectory(string sourceDir, string destinationDir, bool recursive, string ignoredName) + { + // Get information about the source directory + var dir = new DirectoryInfo(sourceDir); + + // Check if the source directory exists + if (!dir.Exists) + throw new DirectoryNotFoundException($"Source directory not found: {dir.FullName}"); + + // Cache directories before we start copying + DirectoryInfo[] dirs = dir.GetDirectories(); + + // Create the destination directory + Directory.CreateDirectory(destinationDir); + + // Get the files in the source directory and copy to the destination directory + foreach (FileInfo file in dir.GetFiles()) + { + if (!Utils.IsNullOrEmpty(ignoredName) && file.Name.Contains(ignoredName)) + { + continue; + } + string targetFilePath = Path.Combine(destinationDir, file.Name); + file.CopyTo(targetFilePath); + } + + // If recursive and copying subdirectories, recursively call this method + if (recursive) + { + foreach (DirectoryInfo subDir in dirs) + { + string newDestinationDir = Path.Combine(destinationDir, subDir.Name); + CopyDirectory(subDir.FullName, newDestinationDir, true, ignoredName); + } + } + } } } \ No newline at end of file diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index b55e1d9186..f949e5ce71 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -204,6 +204,8 @@ public static int LoadConfig(ref Config? config) }; } + config.webDavItem ??= new(); + return 0; } diff --git a/v2rayN/ServiceLib/Handler/NoticeHandler.cs b/v2rayN/ServiceLib/Handler/NoticeHandler.cs index 912eda1523..8efb12d781 100644 --- a/v2rayN/ServiceLib/Handler/NoticeHandler.cs +++ b/v2rayN/ServiceLib/Handler/NoticeHandler.cs @@ -22,7 +22,7 @@ public void SendMessage(string? content) MessageBus.Current.SendMessage(content, Global.CommandSendMsgView); } - public void SendMessageEx(string? content ) + public void SendMessageEx(string? content) { if (content.IsNullOrEmpty()) { diff --git a/v2rayN/ServiceLib/Handler/WebDavHandler.cs b/v2rayN/ServiceLib/Handler/WebDavHandler.cs new file mode 100644 index 0000000000..7473b28907 --- /dev/null +++ b/v2rayN/ServiceLib/Handler/WebDavHandler.cs @@ -0,0 +1,163 @@ +using System.Net; +using WebDav; + +namespace ServiceLib.Handler +{ + public sealed class WebDavHandler + { + private static readonly Lazy _instance = new(() => new()); + public static WebDavHandler Instance => _instance.Value; + + private Config? _config; + private WebDavClient? _client; + private string? _lastDescription; + private string _webDir = "mywebdir"; + private string _webFileName = "backup.zip"; + private string _logTitle = "WebDav--"; + + public WebDavHandler() + { + _config = LazyConfig.Instance.Config; + } + + private async Task GetClient() + { + try + { + if (_config.webDavItem.url.IsNullOrEmpty() + || _config.webDavItem.userName.IsNullOrEmpty() + || _config.webDavItem.password.IsNullOrEmpty()) + { + throw new ArgumentException("webdav parameter error or null"); + } + if (_client != null) + { + _client?.Dispose(); + _client = null; + } + + var clientParams = new WebDavClientParams + { + BaseAddress = new Uri(_config.webDavItem.url), + Credentials = new NetworkCredential(_config.webDavItem.userName, _config.webDavItem.password) + }; + _client = new WebDavClient(clientParams); + } + catch (Exception ex) + { + SaveLog(ex); + return false; + } + return await Task.FromResult(true); + } + + private async Task CheckProp() + { + if (_client is null) return false; + try + { + var result = await _client.Propfind(_webDir); + if (result.IsSuccessful) + { + return true; + } + var result2 = await _client.Mkcol(_webDir); // create a directory + if (result2.IsSuccessful) + { + return true; + } + SaveLog(result2.Description); + } + catch (Exception ex) + { + SaveLog(ex); + } + return false; + } + + private void SaveLog(string desc) + { + _lastDescription = desc; + Logging.SaveLog(_logTitle + desc); + } + + private void SaveLog(Exception ex) + { + _lastDescription = ex.Message; + Logging.SaveLog(_logTitle, ex); + } + + public async Task CheckConnection() + { + if (await GetClient() == false) + { + return false; + } + if (await CheckProp() == false) + { + return false; + } + + return true; + } + + public async Task PutFile(string fileName) + { + if (await GetClient() == false) + { + return false; + } + if (await CheckProp() == false) + { + return false; + } + + try + { + using var fs = File.OpenRead(fileName); + var result = await _client.PutFile($"{_webDir}/{_webFileName}", fs); // upload a resource + if (result.IsSuccessful) + { + return true; + } + + SaveLog(result.Description); + } + catch (Exception ex) + { + SaveLog(ex); + } + return false; + } + + public async Task GetRawFile(string fileName) + { + if (await GetClient() == false) + { + return false; + } + if (await CheckProp() == false) + { + return false; + } + try + { + var response = await _client.GetRawFile($"{_webDir}/{_webFileName}"); + if (!response.IsSuccessful) + { + SaveLog(response.Description); + } + using var outputFileStream = new FileStream(fileName, FileMode.Create); + response.Stream.CopyTo(outputFileStream); + return true; + } + catch (Exception ex) + { + SaveLog(ex); + } + return false; + } + + public string GetLastError() => _lastDescription ?? string.Empty; + } +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/Models/Config.cs b/v2rayN/ServiceLib/Models/Config.cs index 4062914c40..84de350ac0 100644 --- a/v2rayN/ServiceLib/Models/Config.cs +++ b/v2rayN/ServiceLib/Models/Config.cs @@ -47,6 +47,7 @@ public bool IsRunningCore(ECoreType type) public HysteriaItem hysteriaItem { get; set; } public ClashUIItem clashUIItem { get; set; } public SystemProxyItem systemProxyItem { get; set; } + public WebDavItem webDavItem { get; set; } public List inbound { get; set; } public List globalHotkeys { get; set; } public List coreTypeItem { get; set; } diff --git a/v2rayN/ServiceLib/Models/ConfigItems.cs b/v2rayN/ServiceLib/Models/ConfigItems.cs index b07c14423e..ec09ab6f93 100644 --- a/v2rayN/ServiceLib/Models/ConfigItems.cs +++ b/v2rayN/ServiceLib/Models/ConfigItems.cs @@ -248,4 +248,12 @@ public class SystemProxyItem public bool notProxyLocalAddress { get; set; } = true; public string systemProxyAdvancedProtocol { get; set; } } + + [Serializable] + public class WebDavItem + { + public string? url { get; set; } + public string? userName { get; set; } + public string? password { get; set; } + } } \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index 25f52f6bf3..dadb11ed6e 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -582,6 +582,42 @@ public static string LvUserAgent { } } + /// + /// 查找类似 WebDav Check 的本地化字符串。 + /// + public static string LvWebDavCheck { + get { + return ResourceManager.GetString("LvWebDavCheck", resourceCulture); + } + } + + /// + /// 查找类似 WebDav Password 的本地化字符串。 + /// + public static string LvWebDavPassword { + get { + return ResourceManager.GetString("LvWebDavPassword", resourceCulture); + } + } + + /// + /// 查找类似 WebDav Url 的本地化字符串。 + /// + public static string LvWebDavUrl { + get { + return ResourceManager.GetString("LvWebDavUrl", resourceCulture); + } + } + + /// + /// 查找类似 WebDav User Name 的本地化字符串。 + /// + public static string LvWebDavUserName { + get { + return ResourceManager.GetString("LvWebDavUserName", resourceCulture); + } + } + /// /// 查找类似 Add a custom configuration server 的本地化字符串。 /// @@ -690,6 +726,15 @@ public static string menuAddWireguardServer { } } + /// + /// 查找类似 Backup and Restore 的本地化字符串。 + /// + public static string menuBackupAndRestore { + get { + return ResourceManager.GetString("menuBackupAndRestore", resourceCulture); + } + } + /// /// 查找类似 Check Update 的本地化字符串。 /// @@ -861,6 +906,33 @@ public static string menuImportRulesFromUrl { } } + /// + /// 查找类似 Backup to local 的本地化字符串。 + /// + public static string menuLocalBackup { + get { + return ResourceManager.GetString("menuLocalBackup", resourceCulture); + } + } + + /// + /// 查找类似 Local 的本地化字符串。 + /// + public static string menuLocalBackupAndRestore { + get { + return ResourceManager.GetString("menuLocalBackupAndRestore", resourceCulture); + } + } + + /// + /// 查找类似 Restore from local 的本地化字符串。 + /// + public static string menuLocalRestore { + get { + return ResourceManager.GetString("menuLocalRestore", resourceCulture); + } + } + /// /// 查找类似 One-click multi test Latency and speed (Ctrl+E) 的本地化字符串。 /// @@ -1095,6 +1167,33 @@ public static string menuReload { } } + /// + /// 查找类似 Backup to remote (WebDAV) 的本地化字符串。 + /// + public static string menuRemoteBackup { + get { + return ResourceManager.GetString("menuRemoteBackup", resourceCulture); + } + } + + /// + /// 查找类似 Remote (WebDAV) 的本地化字符串。 + /// + public static string menuRemoteBackupAndRestore { + get { + return ResourceManager.GetString("menuRemoteBackupAndRestore", resourceCulture); + } + } + + /// + /// 查找类似 Restore from remote (WebDAV) 的本地化字符串。 + /// + public static string menuRemoteRestore { + get { + return ResourceManager.GetString("menuRemoteRestore", resourceCulture); + } + } + /// /// 查找类似 Remove duplicate servers 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index e40c318f91..87971fbc9c 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1279,4 +1279,37 @@ Custom config socks port + + Backup and Restore + + + Backup to local + + + Restore from local + + + Backup to remote (WebDAV) + + + Restore from remote (WebDAV) + + + Local + + + Remote (WebDAV) + + + WebDav Url + + + WebDav User Name + + + WebDav Password + + + WebDav Check + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index c0e9deb98e..d6ae1fc2cd 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1276,4 +1276,37 @@ 自定义配置的Socks端口 + + 备份和还原 + + + 备份到本地 + + + 从本地恢复 + + + 备份到远程 (WebDAV) + + + 从远程恢复 (WebDAV) + + + 本地 + + + 远程 (WebDAV) + + + WebDav 账户 + + + WebDav 可用检查 + + + WebDav 密码 + + + WebDav 服务器地址 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 67679f3d03..571243c911 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1156,4 +1156,37 @@ 自訂配置的Socks端口 + + 備份和還原 + + + 備份到本地 + + + 從本地恢復 + + + 備份到遠端 (WebDAV) + + + 從遠端恢復 (WebDAV) + + + 本地 + + + 遠端 (WebDAV) + + + WebDav 賬戶 + + + WebDav 可用檢查 + + + WebDav 密碼 + + + WebDav 服務器地址 + \ No newline at end of file diff --git a/v2rayN/ServiceLib/ServiceLib.csproj b/v2rayN/ServiceLib/ServiceLib.csproj index 8185bec9d4..79955b48ec 100644 --- a/v2rayN/ServiceLib/ServiceLib.csproj +++ b/v2rayN/ServiceLib/ServiceLib.csproj @@ -13,6 +13,7 @@ + diff --git a/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs new file mode 100644 index 0000000000..1e3d0d0703 --- /dev/null +++ b/v2rayN/ServiceLib/ViewModels/BackupAndRestoreViewModel.cs @@ -0,0 +1,151 @@ +using ReactiveUI; +using ReactiveUI.Fody.Helpers; +using Splat; +using System.Reactive; + +namespace ServiceLib.ViewModels +{ + public class BackupAndRestoreViewModel : MyReactiveObject + { + public ReactiveCommand RemoteBackupCmd { get; } + public ReactiveCommand RemoteRestoreCmd { get; } + public ReactiveCommand WebDavCheckCmd { get; } + + [Reactive] + public WebDavItem SelectedSource { get; set; } + + [Reactive] + public string OperationMsg { get; set; } + + public BackupAndRestoreViewModel(Func>? updateView) + { + _config = LazyConfig.Instance.Config; + _updateView = updateView; + _noticeHandler = Locator.Current.GetService(); + + WebDavCheckCmd = ReactiveCommand.CreateFromTask(async () => + { + await WebDavCheck(); + }); + + RemoteBackupCmd = ReactiveCommand.CreateFromTask(async () => + { + await RemoteBackup(); + }); + RemoteRestoreCmd = ReactiveCommand.CreateFromTask(async () => + { + await RemoteRestore(); + }); + + SelectedSource = JsonUtils.DeepCopy(_config.webDavItem); + } + + private void DisplayOperationMsg(string msg = "") + { + OperationMsg = msg; + } + + private async Task WebDavCheck() + { + DisplayOperationMsg(); + _config.webDavItem = SelectedSource; + ConfigHandler.SaveConfig(_config); + + var result = await WebDavHandler.Instance.CheckConnection(); + if (result) + { + DisplayOperationMsg(ResUI.OperationSuccess); + } + else + { + DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + } + } + + private async Task RemoteBackup() + { + DisplayOperationMsg(); + var fileName = Utils.GetBackupPath($"backup_{DateTime.Now:yyyyMMddHHmmss}.zip"); + var result = await CreateZipFileFromDirectory(fileName); + if (result) + { + var result2 = await WebDavHandler.Instance.PutFile(fileName); + if (result2) + { + DisplayOperationMsg(ResUI.OperationSuccess); + return; + } + } + + DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + } + + private async Task RemoteRestore() + { + DisplayOperationMsg(); + var fileName = Utils.GetTempPath(Utils.GetGUID()); + var result = await WebDavHandler.Instance.GetRawFile(fileName); + if (result) + { + await LocalRestore(fileName); + return; + } + + DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + } + + public async Task LocalBackup(string fileName) + { + DisplayOperationMsg(); + var result = await CreateZipFileFromDirectory(fileName); + if (result) + { + DisplayOperationMsg(ResUI.OperationSuccess); + } + else + { + DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + } + + return result; + } + + public async Task LocalRestore(string fileName) + { + DisplayOperationMsg(); + if (Utils.IsNullOrEmpty(fileName)) + { + return; + } + + //backup first + var fileBackup = Utils.GetBackupPath($"backup_{DateTime.Now:yyyyMMddHHmmss}.zip"); + var result = await CreateZipFileFromDirectory(fileBackup); + if (result) + { + Locator.Current.GetService()?.V2rayUpgrade(fileName); + } + else + { + DisplayOperationMsg(WebDavHandler.Instance.GetLastError()); + } + } + + private async Task CreateZipFileFromDirectory(string fileName) + { + if (Utils.IsNullOrEmpty(fileName)) + { + return false; + } + + var configDir = Utils.GetConfigPath(); + var configDirZipTemp = Utils.GetTempPath($"v2rayN_{DateTime.Now:yyyyMMddHHmmss}"); + var configDirTemp = Path.Combine(configDirZipTemp, "guiConfigs"); + + await Task.Run(() => FileManager.CopyDirectory(configDir, configDirTemp, true, "cache.db")); + var ret = await Task.Run(() => FileManager.CreateFromDirectory(configDirZipTemp, fileName)); + await Task.Run(() => Directory.Delete(configDirZipTemp, true)); + return ret; + } + } +} \ No newline at end of file diff --git a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs index 813c3b9bf7..b0966221b7 100644 --- a/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/CheckUpdateViewModel.cs @@ -3,7 +3,6 @@ using ReactiveUI; using ReactiveUI.Fody.Helpers; using Splat; -using System.Diagnostics; using System.Reactive; namespace ServiceLib.ViewModels diff --git a/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml b/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml new file mode 100644 index 0000000000..fc4a0bb622 --- /dev/null +++ b/v2rayN/v2rayN/Views/BackupAndRestoreView.xaml @@ -0,0 +1,239 @@ + + + + + + + + + + +