From c8b39e0c980ea58ffac94d0ba8c4a70a49ebbc0c Mon Sep 17 00:00:00 2001 From: Isaac Marovitz Date: Wed, 4 Oct 2023 10:03:57 -0400 Subject: [PATCH] Cleanup + Fix warnings --- .../SaveManager/BackupRequestOutcome.cs | 10 -- .../Common/SaveManager/ISaveManager.cs | 36 ---- .../Common/SaveManager/SaveManager.cs | 163 +++--------------- .../Common/SaveManager/SaveOptions.cs | 23 --- .../SaveManager/UserFriendlySaveMetadata.cs | 20 --- .../Views/User/UserSaveManagerView.axaml.cs | 118 +++++++------ 6 files changed, 89 insertions(+), 281 deletions(-) delete mode 100644 src/Ryujinx.Ava/Common/SaveManager/BackupRequestOutcome.cs delete mode 100644 src/Ryujinx.Ava/Common/SaveManager/ISaveManager.cs delete mode 100644 src/Ryujinx.Ava/Common/SaveManager/SaveOptions.cs delete mode 100644 src/Ryujinx.Ava/Common/SaveManager/UserFriendlySaveMetadata.cs diff --git a/src/Ryujinx.Ava/Common/SaveManager/BackupRequestOutcome.cs b/src/Ryujinx.Ava/Common/SaveManager/BackupRequestOutcome.cs deleted file mode 100644 index bbf61b852..000000000 --- a/src/Ryujinx.Ava/Common/SaveManager/BackupRequestOutcome.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace Ryujinx.Ava.Common.SaveManager -{ - public readonly record struct BackupRequestOutcome - { - public bool DidFail { get; init; } - public string Message { get; init; } - } -} diff --git a/src/Ryujinx.Ava/Common/SaveManager/ISaveManager.cs b/src/Ryujinx.Ava/Common/SaveManager/ISaveManager.cs deleted file mode 100644 index d9e9e1214..000000000 --- a/src/Ryujinx.Ava/Common/SaveManager/ISaveManager.cs +++ /dev/null @@ -1,36 +0,0 @@ - -using LibHac.Fs; -using Ryujinx.Ava.UI.ViewModels; -using Ryujinx.Ui.Common.Helper; -using Ryujinx.Ui.Common.SaveManager; -using System; -using System.Threading.Tasks; - -namespace Ryujinx.Ava.Common.SaveManager -{ - public interface ISaveManager - { - public event EventHandler BackupProgressUpdated; - public event EventHandler BackupImportSave; - - #region Backup - public Task BackupUserSaveDataToZip(UserId userId, - string location, - SaveOptions saveOptions = SaveOptions.Default); - - public Task BackupUserTitleSaveDataToZip(UserId userId, - ulong titleId, - string location, - SaveOptions saveOptions = SaveOptions.Default); - #endregion - - #region Restore - public Task RestoreUserSaveDataFromZip(UserId userId, - string sourceDataPath); - - public Task RestoreUserTitleSaveFromZip(UserId userId, - ulong titleId, - string sourceDataPath); - #endregion - } -} diff --git a/src/Ryujinx.Ava/Common/SaveManager/SaveManager.cs b/src/Ryujinx.Ava/Common/SaveManager/SaveManager.cs index ab63332ef..ae416023c 100644 --- a/src/Ryujinx.Ava/Common/SaveManager/SaveManager.cs +++ b/src/Ryujinx.Ava/Common/SaveManager/SaveManager.cs @@ -5,11 +5,9 @@ using LibHac.Fs; using LibHac.Fs.Shim; using LibHac.Ns; using Microsoft.IdentityModel.Tokens; -using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; -using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.SaveManager; using System; @@ -23,52 +21,32 @@ using Path = System.IO.Path; namespace Ryujinx.Ava.Common.SaveManager { - public class SaveManager : ISaveManager + public class SaveManager { // UI Metadata public event EventHandler BackupProgressUpdated; public event EventHandler BackupImportSave; - private readonly LoadingBarEventArgs _loadingEventArgs; + private readonly LoadingBarEventArgs _loadingEventArgs = new(); private readonly HorizonClient _horizonClient; - private readonly AccountManager _accountManager; - public SaveManager(HorizonClient hzClient, AccountManager acctManager) + public SaveManager(HorizonClient hzClient) { - _loadingEventArgs = new(); - _horizonClient = hzClient; - _accountManager = acctManager; } #region Backup - public Task BackupUserTitleSaveDataToZip(LibHacUserId userId, - ulong titleId, - string location, - SaveOptions saveOptions = SaveOptions.Default) + public async Task BackupUserSaveDataToZip(LibHacUserId userId, Uri savePath) { - throw new NotImplementedException(); - } - - public async Task BackupUserSaveDataToZip(LibHacUserId userId, - string location, - SaveOptions saveOptions = SaveOptions.Default) - { - // TODO: Eventually add cancellation source - - var userSaves = GetUserSaveData(userId, saveOptions); - if (userSaves.IsNullOrEmpty()) + var userSaves = GetUserSaveData(userId).ToArray(); + if (userSaves.Length == 0) { Logger.Warning?.Print(LogClass.Application, "No save data found"); - return new BackupRequestOutcome - { - DidFail = false, - Message = "No save data found" - }; + return false; } _loadingEventArgs.Curr = 0; - _loadingEventArgs.Max = userSaves.Count() + 1; // add one for metadata file + _loadingEventArgs.Max = userSaves.Length + 1; // Add one for metadata file BackupProgressUpdated?.Invoke(this, _loadingEventArgs); // Create the top level temp dir for the intermediate copies - ensure it's empty @@ -80,25 +58,13 @@ namespace Ryujinx.Ava.Common.SaveManager // Delete temp for good measure? _ = Directory.CreateDirectory(backupTempDir); - var outcome = await BatchCopySavesToTempDir(userId, userSaves, backupTempDir) - && CompleteBackup(location, userId, backupTempDir); - - return new BackupRequestOutcome - { - DidFail = !outcome, - Message = outcome - ? string.Empty - : "Failed to backup user saves" - }; + return await BatchCopySavesToTempDir(userSaves, backupTempDir) + && CreateOrReplaceZipFile(backupTempDir, savePath.LocalPath); } catch (Exception ex) { Logger.Error?.Print(LogClass.Application, $"Failed to backup user data - {ex.Message}"); - return new BackupRequestOutcome - { - DidFail = true, - Message = $"Failed to backup user data - {ex.Message}" - }; + return false; } finally { @@ -107,36 +73,20 @@ namespace Ryujinx.Ava.Common.SaveManager Directory.Delete(backupTempDir, true); } } - - // Produce the actual zip - bool CompleteBackup(string location, LibHacUserId userId, string backupTempDir) - { - var currDate = DateTime.UtcNow.ToString("yyyy-MM-dd"); - var profileName = _accountManager - .GetAllUsers() - .FirstOrDefault(u => u.UserId.ToLibHacUserId() == userId)?.Name; - - var backupFile = Path.Combine(location, $"{profileName}_{currDate}_saves.zip"); - return CreateOrReplaceZipFile(backupTempDir, backupFile); - } } - private IEnumerable GetUserSaveData(LibHacUserId userId, SaveOptions saveOptions) + private IEnumerable GetUserSaveData(LibHacUserId userId) { try { - // Almost all games have user savess + // Almost all games have user saves var userSaves = GetSaveData(userId, SaveDataType.Account) .ToList(); - var deviceSaves = saveOptions.HasFlag(SaveOptions.SaveTypeDevice) - ? GetSaveData(default, SaveDataType.Device) - : Enumerable.Empty(); + var deviceSaves = GetSaveData(default, SaveDataType.Device); userSaves.AddRange(deviceSaves); - var bcatSaves = saveOptions.HasFlag(SaveOptions.SaveTypeDevice) - ? GetSaveData(default, SaveDataType.Bcat) - : Enumerable.Empty(); + var bcatSaves = GetSaveData(default, SaveDataType.Bcat); userSaves.AddRange(bcatSaves); return userSaves; @@ -195,14 +145,11 @@ namespace Ryujinx.Ava.Common.SaveManager return saves; } - private async Task BatchCopySavesToTempDir(LibHacUserId userId, IEnumerable userSaves, string backupTempDir) + private async Task BatchCopySavesToTempDir(IEnumerable userSaves, string backupTempDir) { - // Generate a metadata item so users know what titleIds ares in case they're moving around, jksv, sanity, etc - Dictionary userFriendlyMetadataMap = new(); - try { - // batch intermediate copies so we don't overwhelm systems + // Batch intermediate copies so we don't overwhelm systems const int BATCH_SIZE = 5; List> tempCopyTasks = new(BATCH_SIZE); @@ -219,29 +166,11 @@ namespace Ryujinx.Ava.Common.SaveManager // Add backup task tempCopyTasks.Add(CopySaveDataToIntermediateDirectory(meta, backupTempDir)); - - // Add title metadata entry - might be dupe from bcat/device - if (!userFriendlyMetadataMap.ContainsKey(meta.TitleId.Value)) - { - var titleIdHex = meta.TitleId.Value.ToString("x16"); - - var appData = MainWindow.MainWindowViewModel.Applications - .FirstOrDefault(x => x.TitleId.Equals(titleIdHex, StringComparison.OrdinalIgnoreCase)); - - userFriendlyMetadataMap.Add(meta.TitleId.Value, new UserFriendlyAppData - { - Title = appData?.TitleName, - TitleId = meta.TitleId.Value, - TitleIdHex = titleIdHex - }); - } } // wait for any outstanding temp copies to complete _ = await Task.WhenAll(tempCopyTasks); - // finally, move the metadata tag file into the backup dir and track progress - await WriteMetadataFile(backupTempDir, userId, userFriendlyMetadataMap); _loadingEventArgs.Curr++; BackupProgressUpdated?.Invoke(this, _loadingEventArgs); @@ -253,34 +182,6 @@ namespace Ryujinx.Ava.Common.SaveManager } return false; - - #region LocalMethods - async Task WriteMetadataFile(string backupTempDir, - LibHacUserId userId, - Dictionary userFriendlyMetadataMap) - { - try - { - var userProfile = _accountManager.GetAllUsers() - .FirstOrDefault(u => u.UserId.ToLibHacUserId() == userId); - - var tagFile = Path.Combine(backupTempDir, "tag.json"); - - var completeMeta = System.Text.Json.JsonSerializer.Serialize(new UserFriendlySaveMetadata - { - UserId = userId.ToString(), - ProfileName = userProfile.Name, - CreationTimeUtc = DateTime.UtcNow, - ApplicationMap = userFriendlyMetadataMap.Values - }); - await File.WriteAllTextAsync(tagFile, completeMeta); - } - catch (Exception ex) - { - Logger.Error?.Print(LogClass.Application, $"Failed to write user friendly save metadata file - {ex.Message}"); - } - } - #endregion } private async Task CopySaveDataToIntermediateDirectory(BackupSaveMeta saveMeta, string destinationDir) @@ -322,14 +223,7 @@ namespace Ryujinx.Ava.Common.SaveManager #endregion #region Restore - public Task RestoreUserTitleSaveFromZip(LibHacUserId userId, - ulong titleId, - string sourceDataPath) - { - throw new NotImplementedException(); - } - - public async Task RestoreUserSaveDataFromZip(LibHacUserId userId, + public async Task RestoreUserSaveDataFromZip(LibHacUserId userId, string sourceDataPath) { var sourceInfo = new FileInfo(sourceDataPath); @@ -352,11 +246,7 @@ namespace Ryujinx.Ava.Common.SaveManager var error = $"Failed to extract save backup zip {ex.Message}"; Logger.Error?.Print(LogClass.Application, error); - return new() - { - DidFail = true, - Message = error - }; + return false; } } @@ -370,10 +260,7 @@ namespace Ryujinx.Ava.Common.SaveManager var identifiedTitleDirectories = GetTitleDirectories(sourceInfo); if (identifiedTitleDirectories.IsNullOrEmpty()) { - return new() - { - DidFail = true, - }; + return false; } // Start import @@ -403,18 +290,12 @@ namespace Ryujinx.Ava.Common.SaveManager // let the import complete _ = await Task.WhenAll(importBuffer); - return new BackupRequestOutcome - { - DidFail = false - }; + return true; } catch (Exception ex) { Logger.Error?.Print(LogClass.Application, $"Failed to import save data - {ex.Message}"); - return new BackupRequestOutcome - { - DidFail = false - }; + return false; } finally { diff --git a/src/Ryujinx.Ava/Common/SaveManager/SaveOptions.cs b/src/Ryujinx.Ava/Common/SaveManager/SaveOptions.cs deleted file mode 100644 index 0d1fe4243..000000000 --- a/src/Ryujinx.Ava/Common/SaveManager/SaveOptions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -namespace Ryujinx.Ava.Common.SaveManager -{ - [Flags] - public enum SaveOptions - { - // Save Data Types - SaveTypeAccount, - SaveTypeBcat, - SaveTypeDevice, - SaveTypeAll = SaveTypeAccount | SaveTypeBcat | SaveTypeDevice, - - // Request Semantics -- Not Implemented - SkipEmptyDirectories, - FlattenSaveStructure, - StopOnFirstFailure, - UseDateInName, - ObfuscateZipExtension, - - Default = SaveTypeAll - } -} diff --git a/src/Ryujinx.Ava/Common/SaveManager/UserFriendlySaveMetadata.cs b/src/Ryujinx.Ava/Common/SaveManager/UserFriendlySaveMetadata.cs deleted file mode 100644 index 514803373..000000000 --- a/src/Ryujinx.Ava/Common/SaveManager/UserFriendlySaveMetadata.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Ryujinx.Ava.Common.SaveManager -{ - internal readonly record struct UserFriendlyAppData - { - public ulong TitleId { get; init; } - public string Title { get; init; } - public string TitleIdHex { get; init; } - } - - internal readonly record struct UserFriendlySaveMetadata - { - public string UserId { get; init; } - public string ProfileName { get; init; } - public DateTime CreationTimeUtc { get; init; } - public IEnumerable ApplicationMap { get; init; } - } -} diff --git a/src/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs b/src/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs index b90b1b2d1..9941f1eb7 100644 --- a/src/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs +++ b/src/Ryujinx.Ava/UI/Views/User/UserSaveManagerView.axaml.cs @@ -1,6 +1,7 @@ using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Interactivity; +using Avalonia.Platform.Storage; using Avalonia.Threading; using Avalonia.VisualTree; using DynamicData; @@ -22,6 +23,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc; using Ryujinx.Ui.Common.Helper; using Ryujinx.Ui.Common.SaveManager; using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; @@ -36,9 +38,8 @@ namespace Ryujinx.Ava.UI.Views.User private AccountManager _accountManager; private HorizonClient _horizonClient; - private VirtualFileSystem _virtualFileSystem; private NavigationDialogHost _parent; - private ISaveManager _saveManager; + private SaveManager _saveManager; public UserSaveManagerView() { @@ -59,10 +60,10 @@ namespace Ryujinx.Ava.UI.Views.User switch (arg.NavigationMode) { case NavigationMode.New: - (_parent, _accountManager, _horizonClient, _virtualFileSystem) = + (_parent, _accountManager, _horizonClient, _) = ((NavigationDialogHost parent, AccountManager accountManager, HorizonClient client, VirtualFileSystem virtualFileSystem))arg.Parameter; - _saveManager = new SaveManager(_horizonClient, _accountManager); + _saveManager = new SaveManager(_horizonClient); _saveManager.BackupProgressUpdated += BackupManager_ProgressUpdate; _saveManager.BackupImportSave += BackupManager_ImportSave; @@ -79,11 +80,11 @@ namespace Ryujinx.Ava.UI.Views.User ViewModel.Saves.Clear(); var saves = new ObservableCollection(); var saveDataFilter = SaveDataFilter.Make( - programId: default, - saveType: SaveDataType.Account, + default, + SaveDataType.Account, new UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low), - saveDataId: default, - index: default); + default, + default); using var saveDataIterator = new UniqueRef(); @@ -143,7 +144,8 @@ namespace Ryujinx.Ava.UI.Views.User { if (button.DataContext is SaveModel saveModel) { - var result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DeleteUserSave], + var result = await ContentDialogHelper.CreateConfirmationDialog( + LocaleManager.Instance[LocaleKeys.DeleteUserSave], LocaleManager.Instance[LocaleKeys.IrreversibleActionNote], LocaleManager.Instance[LocaleKeys.InputDialogYes], LocaleManager.Instance[LocaleKeys.InputDialogNo], ""); @@ -160,16 +162,26 @@ namespace Ryujinx.Ava.UI.Views.User private async void GenerateProfileSaveBackup(object sender, RoutedEventArgs e) { - OpenFolderDialog dialog = new() - { - Title = LocaleManager.Instance[LocaleKeys.SaveManagerChooseBackupFolderTitle] - }; + var window = ((TopLevel)_parent.GetVisualRoot()) as Window; + var currDate = DateTime.UtcNow.ToString("yyyy-MM-dd"); + var profileName = _accountManager.LastOpenedUser.Name; + var fileName = $"{profileName}_{currDate}_saves"; - var backupDir = await dialog.ShowAsync(((TopLevel)_parent.GetVisualRoot()) as Window); - if (string.IsNullOrWhiteSpace(backupDir)) + var file = await window.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions { - return; - } + Title = LocaleManager.Instance[LocaleKeys.SaveManagerChooseBackupFolderTitle], + DefaultExtension = "zip", + SuggestedFileName = fileName, + FileTypeChoices = new List + { + new("zip") + { + Patterns = new[] { "*.zip" }, + AppleUniformTypeIdentifiers = new[] { "public.zip-archive" }, + MimeTypes = new[] { "application/zip" } + } + } + }); // Disable the user from doing anything until we complete ViewModel.IsGoBackEnabled = false; @@ -178,23 +190,21 @@ namespace Ryujinx.Ava.UI.Views.User { // Could potentially seed with existing saves already enumerated but we still need bcat and device data var result = await _saveManager.BackupUserSaveDataToZip( - userId: _accountManager.LastOpenedUser.UserId.ToLibHacUserId(), - location: backupDir, - saveOptions: SaveOptions.Default); + _accountManager.LastOpenedUser.UserId.ToLibHacUserId(), + file.Path); - var notificationType = result.DidFail - ? NotificationType.Error - : NotificationType.Success; + var notificationType = result + ? NotificationType.Success + : NotificationType.Error; - var message = result.DidFail - ? LocaleManager.Instance[LocaleKeys.SaveManagerBackupFailed] - : LocaleManager.Instance[LocaleKeys.SaveManagerBackupComplete]; + var message = result + ? LocaleManager.Instance[LocaleKeys.SaveManagerBackupComplete] + : LocaleManager.Instance[LocaleKeys.SaveManagerBackupFailed]; - NotificationHelper.Show(LocaleManager.Instance[LocaleKeys.NotificationBackupTitle], + NotificationHelper.Show( + LocaleManager.Instance[LocaleKeys.NotificationBackupTitle], message, notificationType); - - return; } catch (Exception ex) { @@ -209,37 +219,42 @@ namespace Ryujinx.Ava.UI.Views.User private async void ImportSaveBackup(object sender, RoutedEventArgs e) { - bool userConfirmation = await ContentDialogHelper.CreateChoiceDialog(LocaleManager.Instance[LocaleKeys.SaveManagerConfirmRestoreTitle], + bool userConfirmation = await ContentDialogHelper.CreateChoiceDialog( + LocaleManager.Instance[LocaleKeys.SaveManagerConfirmRestoreTitle], LocaleManager.Instance[LocaleKeys.SaveManagerChooseRestoreZipPrimaryMessage], LocaleManager.Instance[LocaleKeys.SaveManagerChooseRestoreZipSecondaryMessage], - primaryButtonKey: LocaleKeys.SaveMangerRestoreUserConfirm, - closeButtonKey: LocaleKeys.SaveMangerRestoreUserCancel); + LocaleKeys.SaveMangerRestoreUserConfirm, + LocaleKeys.SaveMangerRestoreUserCancel); if (!userConfirmation) { return; } - OpenFileDialog dialog = new() + var window = ((TopLevel)_parent.GetVisualRoot()) as Window; + + var fileResult = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = LocaleManager.Instance[LocaleKeys.SaveManagerChooseRestoreZipTitle], AllowMultiple = false, - Filters = { - new FileDialogFilter() { - Name = "Zip files", - Extensions = { "zip" }, + FileTypeFilter = new List + { + new("zip") + { + Patterns = new[] { "*.zip" }, + AppleUniformTypeIdentifiers = new[] { "public.zip-archive" }, + MimeTypes = new[] { "application/zip" } } } - }; + }); - var saveBackupZip = await dialog.ShowAsync(((TopLevel)_parent.GetVisualRoot()) as Window); - if (saveBackupZip is null - || saveBackupZip.Length == 0 - || string.IsNullOrWhiteSpace(saveBackupZip[0])) + if (fileResult.Count <= 0) { return; } + var saveBackupZip = fileResult[0].Path.LocalPath; + // Disable the user from doing anything until we complete ViewModel.IsGoBackEnabled = false; @@ -247,16 +262,16 @@ namespace Ryujinx.Ava.UI.Views.User { // Could potentially seed with existing saves already enumerated but we still need bcat and device data var result = await _saveManager.RestoreUserSaveDataFromZip( - userId: _accountManager.LastOpenedUser.UserId.ToLibHacUserId(), - sourceDataPath: saveBackupZip[0]); + _accountManager.LastOpenedUser.UserId.ToLibHacUserId(), + saveBackupZip); - var notificationType = result.DidFail - ? NotificationType.Error - : NotificationType.Success; + var notificationType = result + ? NotificationType.Success + : NotificationType.Error; - var message = result.DidFail - ? LocaleManager.Instance[LocaleKeys.SaveManagerRestoreFailed] - : LocaleManager.Instance[LocaleKeys.SaveManagerRestoreComplete]; + var message = result + ? LocaleManager.Instance[LocaleKeys.SaveManagerRestoreComplete] + : LocaleManager.Instance[LocaleKeys.SaveManagerRestoreFailed]; if (!string.IsNullOrWhiteSpace(ViewModel.Search)) { @@ -266,7 +281,8 @@ namespace Ryujinx.Ava.UI.Views.User }); } - NotificationHelper.Show(LocaleManager.Instance[LocaleKeys.NotificationBackupTitle], + NotificationHelper.Show( + LocaleManager.Instance[LocaleKeys.NotificationBackupTitle], message, notificationType); }