From fb3c282544fe9833d2d2e7cb083bdf34caf14817 Mon Sep 17 00:00:00 2001 From: Akisuke Date: Tue, 13 Dec 2022 02:27:30 +0100 Subject: [PATCH] added button to backup savedata added button to restore backup --- .../Ui/Controls/BackupSavedataCommand.cs | 73 +++++++++++ .../Ui/Controls/RestoreSavedataCommand.cs | 119 ++++++++++++++++++ .../Ui/ViewModels/UserProfileViewModel.cs | 8 +- 3 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 Ryujinx.Ava/Ui/Controls/BackupSavedataCommand.cs create mode 100644 Ryujinx.Ava/Ui/Controls/RestoreSavedataCommand.cs diff --git a/Ryujinx.Ava/Ui/Controls/BackupSavedataCommand.cs b/Ryujinx.Ava/Ui/Controls/BackupSavedataCommand.cs new file mode 100644 index 000000000..3b4f78e61 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/BackupSavedataCommand.cs @@ -0,0 +1,73 @@ +using Avalonia.Controls; +using Avalonia.Logging; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Ui.Windows; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using System; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; +using System.Windows.Input; +using Logger = Ryujinx.Common.Logging.Logger; +using Path = System.IO.Path; + +namespace Ryujinx.Ava.Ui.Controls +{ + internal class BackupSavedataCommand : ICommand + { + public event EventHandler CanExecuteChanged; + private readonly IControl parentControl; + + public BackupSavedataCommand(IControl parentControl) + { + this.parentControl = parentControl; + } + + public bool CanExecute(object parameter) + { + return true; + } + + public void Execute(object parameter) + { + SaveUserSaveDirectoryAsZip(); + } + + public async void SaveUserSaveDirectoryAsZip() + { + CreateBackupZip(await OpenFolderDialog()); + } + + private async Task OpenFolderDialog() + { + OpenFolderDialog dialog = new() + { + Title = LocaleManager.Instance["OpenFolderDialogTitle"] + }; + + return await dialog.ShowAsync(parentControl.VisualRoot as MainWindow); + } + + private void CreateBackupZip(string directoryPath) + { + if (!string.IsNullOrWhiteSpace(directoryPath) && Directory.Exists(directoryPath)) + { + string saveDir = Path.Combine(AppDataManager.BaseDirPath, AppDataManager.DefaultNandDir, "user", "save"); + + string zipFolderPath = Path.Combine(directoryPath, "Ryujinx_backup.zip"); + + Logger.Info.Value.Print(LogClass.Application, $"Start creating backup...", nameof(BackupSavedataCommand)); + + if (File.Exists(zipFolderPath)) + { + File.Delete(zipFolderPath); + } + + ZipFile.CreateFromDirectory(saveDir, zipFolderPath); + + Logger.Info.Value.Print(LogClass.Application, $"Backup done. Zip is locate under {directoryPath}", nameof(BackupSavedataCommand)); + } + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/RestoreSavedataCommand.cs b/Ryujinx.Ava/Ui/Controls/RestoreSavedataCommand.cs new file mode 100644 index 000000000..473b24cd1 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/RestoreSavedataCommand.cs @@ -0,0 +1,119 @@ +using Avalonia.Controls; +using Ryujinx.Ava.Common.Locale; +using Ryujinx.Ava.Ui.Windows; +using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using Path = System.IO.Path; + +namespace Ryujinx.Ava.Ui.Controls +{ + internal class RestoreSavedataCommand : ICommand + { + public event EventHandler CanExecuteChanged; + + private readonly IControl parentControl; + + public RestoreSavedataCommand(IControl parentControl) + { + this.parentControl = parentControl; + } + + private async Task ShowConditionMessage() + { + return await ContentDialogHelper.CreateChoiceDialog("Restore Backup", + "You have to start every game at least once to create a save directory for the game before you can Restore the backup save data!", + "Do you want to continue?"); + } + + public bool CanExecute(object parameter) + { + return true; + } + + public void Execute(object parameter) + { + RestoreSavedataBackup(); + } + + public async void RestoreSavedataBackup() + { + if (!(await ShowConditionMessage())) return; + + string[] backupZipFiles = await ShowFolderDialog(); + + ExtractBackupToSaveDirectory(backupZipFiles); + + Logger.Info.Value.Print(LogClass.Application, $"Done extracting savedata backup!", nameof(RestoreSavedataCommand)); + } + + private async Task ShowFolderDialog() + { + OpenFileDialog dialog = new() + { + Title = LocaleManager.Instance["OpenFileDialogTitle"], + AllowMultiple = false, + }; + + return await dialog.ShowAsync(parentControl.VisualRoot as MainWindow); + } + + private void ExtractBackupToSaveDirectory(string[] backupZipFiles) + { + if (!string.IsNullOrWhiteSpace(backupZipFiles.First()) && File.Exists(backupZipFiles.First())) + { + string tempZipExtractionPath = Path.GetTempPath(); + ZipFile.ExtractToDirectory(backupZipFiles.First(), tempZipExtractionPath, true); + + Logger.Info.Value.Print(LogClass.Application, $"Extracted Backup zip to temp path: {tempZipExtractionPath}", nameof(RestoreSavedataCommand)); + + string saveDir = Path.Combine(AppDataManager.BaseDirPath, AppDataManager.DefaultNandDir, "user", "save"); + ReplaceSavedataFilesWithBackupSaveFiles(Directory.GetDirectories(tempZipExtractionPath), saveDir); + } + } + + private void ReplaceSavedataFilesWithBackupSaveFiles(string[] backupSavedataPath, string saveDirectory) + { + //All current save files for later replacement + string[] userSaveFiles = GetSaveFilesInAllSubDirectories(saveDirectory); + + //Loops through every Title save folder and replaces all user save data files with the savedata files inside the extracted backup folder + //Logic to decide wich file is replaces is based on der filename and parent directory + foreach (string[] backupSaveFiles in backupSavedataPath.Select(GetSaveFilesInAllSubDirectories)) + { + foreach (string backupSaveFile in backupSaveFiles) + { + foreach (string userSaveFile in GetSaveFilesWithSameNameAndParentDir(userSaveFiles, backupSaveFile)) + { + try + { + File.Copy(backupSaveFile, userSaveFile, true); + Logger.Info.Value.Print(LogClass.Application, $"Copied Savedata {backupSaveFile} to {userSaveFile}", nameof(RestoreSavedataCommand)); + } + catch (Exception) + { + Logger.Error.Value.Print(LogClass.Application, $"Could not copy Savedata {backupSaveFile} to {userSaveFile}", nameof(RestoreSavedataCommand)); + } + } + } + } + } + + private string[] GetSaveFilesWithSameNameAndParentDir(string[] userSaveFiles, string backupSaveFile) + { + return userSaveFiles.Where(sf => Path.GetFileName(sf) == Path.GetFileName(backupSaveFile) && Directory.GetParent(sf).Name == Directory.GetParent(backupSaveFile).Name).ToArray(); + } + + private string[] GetSaveFilesInAllSubDirectories(string rootDirectory) + { + string[] unnecessarySaveFiles = { ".lock", "ExtraData0", "ExtraData1" }; + + return Directory.GetFiles(rootDirectory, "*", SearchOption.AllDirectories).Where(file => !unnecessarySaveFiles.Any(usf => file.EndsWith(usf))).ToArray(); + } + } +} \ No newline at end of file diff --git a/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs index eb9f69d63..378419bf3 100644 --- a/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs +++ b/Ryujinx.Ava/Ui/ViewModels/UserProfileViewModel.cs @@ -99,7 +99,7 @@ namespace Ryujinx.Ava.Ui.ViewModels default, saveDataId: default, index: default); using var saveDataIterator = new UniqueRef(); - + _owner.HorizonClient.Fs.OpenSaveDataIterator(ref saveDataIterator.Ref(), SaveDataSpaceId.User, in saveDataFilter).ThrowIfFailure(); Span saveDataInfo = stackalloc SaveDataInfo[10]; @@ -148,8 +148,10 @@ namespace Ryujinx.Ava.Ui.ViewModels ContentDialog contentDialog = new ContentDialog { Title = string.Format(LocaleManager.Instance["SaveManagerHeading"], userProfile.Name), - PrimaryButtonText = "", - SecondaryButtonText = "", + PrimaryButtonText = "Backup Savedata", + PrimaryButtonCommand = new BackupSavedataCommand(_owner), + SecondaryButtonText = "Restore Savedata", + SecondaryButtonCommand = new RestoreSavedataCommand(_owner), CloseButtonText = LocaleManager.Instance["UserProfilesClose"], Content = manager, Padding = new Thickness(0)