diff --git a/src/Files.App/Actions/Content/Archives/Compress/BaseCompressArchiveAction.cs b/src/Files.App/Actions/Content/Archives/Compress/BaseCompressArchiveAction.cs index facfbb600954..3831bca97af6 100644 --- a/src/Files.App/Actions/Content/Archives/Compress/BaseCompressArchiveAction.cs +++ b/src/Files.App/Actions/Content/Archives/Compress/BaseCompressArchiveAction.cs @@ -6,6 +6,7 @@ namespace Files.App.Actions internal abstract class BaseCompressArchiveAction : BaseUIAction, IAction { protected readonly IContentPageContext context; + protected IStorageArchiveService StorageArchiveService { get; } = Ioc.Default.GetRequiredService(); public abstract string Label { get; } @@ -13,7 +14,7 @@ internal abstract class BaseCompressArchiveAction : BaseUIAction, IAction public override bool IsExecutable => IsContextPageTypeAdaptedToCommand() && - CompressHelper.CanCompress(context.SelectedItems) && + StorageArchiveService.CanCompress(context.SelectedItems) && UIHelpers.CanShowDialog; public BaseCompressArchiveAction() @@ -25,6 +26,26 @@ public BaseCompressArchiveAction() public abstract Task ExecuteAsync(object? parameter = null); + protected void GetDestination(out string[] sources, out string directory, out string fileName) + { + sources = context.SelectedItems.Select(item => item.ItemPath).ToArray(); + directory = string.Empty; + fileName = string.Empty; + + if (sources.Length is not 0) + { + // Get the current directory path + directory = context.ShellPage.FilesystemViewModel.WorkingDirectory.Normalize(); + + // Get the library save folder if the folder is library item + if (App.LibraryManager.TryGetLibrary(directory, out var library) && !library.IsEmpty) + directory = library.DefaultSaveFolder; + + // Gets the file name from the directory path + fileName = SystemIO.Path.GetFileName(sources.Length is 1 ? sources[0] : directory); + } + } + private bool IsContextPageTypeAdaptedToCommand() { return diff --git a/src/Files.App/Actions/Content/Archives/Compress/CompressIntoArchiveAction.cs b/src/Files.App/Actions/Content/Archives/Compress/CompressIntoArchiveAction.cs index 5d7a6bc15c9c..99524e36fdea 100644 --- a/src/Files.App/Actions/Content/Archives/Compress/CompressIntoArchiveAction.cs +++ b/src/Files.App/Actions/Content/Archives/Compress/CompressIntoArchiveAction.cs @@ -24,12 +24,9 @@ public override async Task ExecuteAsync(object? parameter = null) if (context.ShellPage is null) return; - var (sources, directory, fileName) = CompressHelper.GetCompressDestination(context.ShellPage); + GetDestination(out var sources, out var directory, out var fileName); - var dialog = new CreateArchiveDialog - { - FileName = fileName, - }; + var dialog = new CreateArchiveDialog() { FileName = fileName }; if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) dialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; @@ -39,7 +36,7 @@ public override async Task ExecuteAsync(object? parameter = null) if (!dialog.CanCreate || result != ContentDialogResult.Primary) return; - ICompressArchiveModel creator = new CompressArchiveModel( + ICompressArchiveModel compressionModel = new CompressArchiveModel( sources, directory, dialog.FileName, @@ -48,7 +45,7 @@ public override async Task ExecuteAsync(object? parameter = null) dialog.CompressionLevel, dialog.SplittingSize); - await CompressHelper.CompressArchiveAsync(creator); + await StorageArchiveService.CompressAsync(compressionModel); } } } diff --git a/src/Files.App/Actions/Content/Archives/Compress/CompressIntoSevenZipAction.cs b/src/Files.App/Actions/Content/Archives/Compress/CompressIntoSevenZipAction.cs index 1952bd232018..1d3414ab34c1 100644 --- a/src/Files.App/Actions/Content/Archives/Compress/CompressIntoSevenZipAction.cs +++ b/src/Files.App/Actions/Content/Archives/Compress/CompressIntoSevenZipAction.cs @@ -6,7 +6,7 @@ namespace Files.App.Actions internal sealed class CompressIntoSevenZipAction : BaseCompressArchiveAction { public override string Label - => string.Format("CreateNamedArchive".GetLocalizedResource(), $"{CompressHelper.DetermineArchiveNameFromSelection(context.SelectedItems)}.7z"); + => string.Format("CreateNamedArchive".GetLocalizedResource(), $"{StorageArchiveService.GenerateArchiveNameFromItems(context.SelectedItems)}.7z"); public override string Description => "CompressIntoSevenZipDescription".GetLocalizedResource(); @@ -20,15 +20,15 @@ public override Task ExecuteAsync(object? parameter = null) if (context.ShellPage is null) return Task.CompletedTask; - var (sources, directory, fileName) = CompressHelper.GetCompressDestination(context.ShellPage); + GetDestination(out var sources, out var directory, out var fileName); - ICompressArchiveModel creator = new CompressArchiveModel( + ICompressArchiveModel compressionModel = new CompressArchiveModel( sources, directory, fileName, fileFormat: ArchiveFormats.SevenZip); - return CompressHelper.CompressArchiveAsync(creator); + return StorageArchiveService.CompressAsync(compressionModel); } } } diff --git a/src/Files.App/Actions/Content/Archives/Compress/CompressIntoZipAction.cs b/src/Files.App/Actions/Content/Archives/Compress/CompressIntoZipAction.cs index a88353ebfa47..72e5ded7b1eb 100644 --- a/src/Files.App/Actions/Content/Archives/Compress/CompressIntoZipAction.cs +++ b/src/Files.App/Actions/Content/Archives/Compress/CompressIntoZipAction.cs @@ -6,7 +6,7 @@ namespace Files.App.Actions internal sealed class CompressIntoZipAction : BaseCompressArchiveAction { public override string Label - => string.Format("CreateNamedArchive".GetLocalizedResource(), $"{CompressHelper.DetermineArchiveNameFromSelection(context.SelectedItems)}.zip"); + => string.Format("CreateNamedArchive".GetLocalizedResource(), $"{StorageArchiveService.GenerateArchiveNameFromItems(context.SelectedItems)}.zip"); public override string Description => "CompressIntoZipDescription".GetLocalizedResource(); @@ -20,15 +20,15 @@ public override Task ExecuteAsync(object? parameter = null) if (context.ShellPage is null) return Task.CompletedTask; - var (sources, directory, fileName) = CompressHelper.GetCompressDestination(context.ShellPage); + GetDestination(out var sources, out var directory, out var fileName); - ICompressArchiveModel creator = new CompressArchiveModel( + ICompressArchiveModel compressionModel = new CompressArchiveModel( sources, directory, fileName, fileFormat: ArchiveFormats.Zip); - return CompressHelper.CompressArchiveAsync(creator); + return StorageArchiveService.CompressAsync(compressionModel); } } } diff --git a/src/Files.App/Actions/Content/Archives/Decompress/BaseDecompressArchiveAction.cs b/src/Files.App/Actions/Content/Archives/Decompress/BaseDecompressArchiveAction.cs index b4c78c1c507e..2f7dfa0cdad9 100644 --- a/src/Files.App/Actions/Content/Archives/Decompress/BaseDecompressArchiveAction.cs +++ b/src/Files.App/Actions/Content/Archives/Decompress/BaseDecompressArchiveAction.cs @@ -1,11 +1,20 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. +using Files.App.Dialogs; +using Microsoft.UI.Xaml.Controls; +using SevenZip; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Windows.Foundation.Metadata; +using Windows.Storage; + namespace Files.App.Actions { internal abstract class BaseDecompressArchiveAction : BaseUIAction, IAction { protected readonly IContentPageContext context; + protected IStorageArchiveService StorageArchiveService { get; } = Ioc.Default.GetRequiredService(); public abstract string Label { get; } @@ -16,7 +25,7 @@ public virtual HotKey HotKey public override bool IsExecutable => (IsContextPageTypeAdaptedToCommand() && - CompressHelper.CanDecompress(context.SelectedItems) || + StorageArchiveService.CanDecompress(context.SelectedItems) || CanDecompressInsideArchive()) && UIHelpers.CanShowDialog; @@ -37,6 +46,80 @@ protected bool IsContextPageTypeAdaptedToCommand() context.PageType != ContentPageTypes.None; } + protected async Task DecompressArchiveHereAsync(bool smart = false) + { + if (context.SelectedItems.Count is 0) + return; + + foreach (var selectedItem in context.SelectedItems) + { + var password = string.Empty; + BaseStorageFile archive = await StorageHelpers.ToStorageItem(selectedItem.ItemPath); + BaseStorageFolder currentFolder = await StorageHelpers.ToStorageItem(context.ShellPage?.FilesystemViewModel.CurrentFolder?.ItemPath ?? string.Empty); + + if (archive?.Path is null) + return; + + if (await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncryptedAsync(archive.Path))) + { + DecompressArchiveDialog decompressArchiveDialog = new(); + DecompressArchiveDialogViewModel decompressArchiveViewModel = new(archive) + { + IsArchiveEncrypted = true, + ShowPathSelection = false + }; + + decompressArchiveDialog.ViewModel = decompressArchiveViewModel; + + if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) + decompressArchiveDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; + + ContentDialogResult option = await decompressArchiveDialog.TryShowAsync(); + if (option != ContentDialogResult.Primary) + return; + + if (decompressArchiveViewModel.Password is not null) + password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password); + } + + BaseStorageFolder? destinationFolder = null; + + var isMultipleItems = await FilesystemTasks.Wrap(async () => + { + using SevenZipExtractor? zipFile = await StorageArchiveService.GetSevenZipExtractorAsync(archive.Path); + if (zipFile is null) + return true; + + return zipFile.ArchiveFileData.Select(file => + { + var pathCharIndex = file.FileName.IndexOfAny(['/', '\\']); + if (pathCharIndex == -1) + return file.FileName; + else + return file.FileName.Substring(0, pathCharIndex); + }) + .Distinct().Count() > 1; + }); + + if (smart && currentFolder is not null && isMultipleItems) + { + destinationFolder = + await FilesystemTasks.Wrap(() => + currentFolder.CreateFolderAsync( + SystemIO.Path.GetFileNameWithoutExtension(archive.Path), + CreationCollisionOption.GenerateUniqueName).AsTask()); + } + else + { + destinationFolder = currentFolder; + } + + // Operate decompress + var result = await FilesystemTasks.Wrap(() => + StorageArchiveService.DecompressAsync(selectedItem.ItemPath, destinationFolder?.Path ?? string.Empty, password)); + } + } + protected virtual bool CanDecompressInsideArchive() { return false; diff --git a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs index 70f9f160dfeb..41c8a17e5463 100644 --- a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs +++ b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchive.cs @@ -1,8 +1,13 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. +using Files.App.Dialogs; using Files.Shared.Helpers; +using Microsoft.UI.Xaml.Controls; using System.IO; +using System.Text; +using Windows.Foundation.Metadata; +using Windows.Storage; namespace Files.App.Actions { @@ -21,12 +26,56 @@ public DecompressArchive() { } - public override Task ExecuteAsync(object? parameter = null) + public override async Task ExecuteAsync(object? parameter = null) { if (context.ShellPage is null) - return Task.CompletedTask; + return; - return DecompressHelper.DecompressArchiveAsync(context.ShellPage); + BaseStorageFile archive = await StorageHelpers.ToStorageItem(context.SelectedItem?.ItemPath ?? string.Empty); + + if (archive?.Path is null) + return; + + var isArchiveEncrypted = await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncryptedAsync(archive.Path)); + var password = string.Empty; + + DecompressArchiveDialog decompressArchiveDialog = new(); + DecompressArchiveDialogViewModel decompressArchiveViewModel = new(archive) + { + IsArchiveEncrypted = isArchiveEncrypted, + ShowPathSelection = true + }; + decompressArchiveDialog.ViewModel = decompressArchiveViewModel; + + if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) + decompressArchiveDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; + + ContentDialogResult option = await decompressArchiveDialog.TryShowAsync(); + if (option != ContentDialogResult.Primary) + return; + + if (isArchiveEncrypted && decompressArchiveViewModel.Password is not null) + password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password); + + // Check if archive still exists + if (!StorageHelpers.Exists(archive.Path)) + return; + + BaseStorageFolder destinationFolder = decompressArchiveViewModel.DestinationFolder; + string destinationFolderPath = decompressArchiveViewModel.DestinationFolderPath; + + if (destinationFolder is null) + { + BaseStorageFolder parentFolder = await StorageHelpers.ToStorageItem(Path.GetDirectoryName(archive.Path) ?? string.Empty); + destinationFolder = await FilesystemTasks.Wrap(() => parentFolder.CreateFolderAsync(Path.GetFileName(destinationFolderPath), CreationCollisionOption.GenerateUniqueName).AsTask()); + } + + // Operate decompress + var result = await FilesystemTasks.Wrap(() => + StorageArchiveService.DecompressAsync(archive?.Path ?? string.Empty, destinationFolder?.Path ?? string.Empty, password)); + + if (decompressArchiveViewModel.OpenDestinationFolderOnCompletion) + await NavigationHelpers.OpenPath(destinationFolderPath, context.ShellPage, FilesystemItemType.Directory); } protected override bool CanDecompressInsideArchive() diff --git a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveHere.cs b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveHere.cs index bcbdbcd22888..212ad6cbf88e 100644 --- a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveHere.cs +++ b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveHere.cs @@ -17,7 +17,7 @@ public DecompressArchiveHere() public override Task ExecuteAsync(object? parameter = null) { - return DecompressHelper.DecompressArchiveHereAsync(context.ShellPage); + return DecompressArchiveHereAsync(); } } } diff --git a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveHereSmart.cs b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveHereSmart.cs index d29dd01f2817..496017294408 100644 --- a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveHereSmart.cs +++ b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveHereSmart.cs @@ -20,7 +20,7 @@ public DecompressArchiveHereSmart() public override Task ExecuteAsync(object? parameter = null) { - return DecompressHelper.DecompressArchiveHereAsync(context.ShellPage, true); + return DecompressArchiveHereAsync(true); } } } diff --git a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveToChildFolderAction.cs b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveToChildFolderAction.cs index 8cb973a81482..c7cd966391a3 100644 --- a/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveToChildFolderAction.cs +++ b/src/Files.App/Actions/Content/Archives/Decompress/DecompressArchiveToChildFolderAction.cs @@ -1,6 +1,12 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. +using Files.App.Dialogs; +using Microsoft.UI.Xaml.Controls; +using System.Text; +using Windows.Foundation.Metadata; +using Windows.Storage; + namespace Files.App.Actions { internal sealed class DecompressArchiveToChildFolderAction : BaseDecompressArchiveAction @@ -15,9 +21,49 @@ public DecompressArchiveToChildFolderAction() { } - public override Task ExecuteAsync(object? parameter = null) + public override async Task ExecuteAsync(object? parameter = null) { - return DecompressHelper.DecompressArchiveToChildFolderAsync(context.ShellPage); + if (context.SelectedItems.Count is 0) + return; + + foreach (var selectedItem in context.SelectedItems) + { + var password = string.Empty; + + BaseStorageFile archive = await StorageHelpers.ToStorageItem(selectedItem.ItemPath); + BaseStorageFolder currentFolder = await StorageHelpers.ToStorageItem(context.ShellPage?.FilesystemViewModel.CurrentFolder.ItemPath); + BaseStorageFolder destinationFolder = null; + + if (archive?.Path is null) + return; + + if (await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncryptedAsync(archive.Path))) + { + DecompressArchiveDialog decompressArchiveDialog = new(); + DecompressArchiveDialogViewModel decompressArchiveViewModel = new(archive) + { + IsArchiveEncrypted = true, + ShowPathSelection = false + }; + decompressArchiveDialog.ViewModel = decompressArchiveViewModel; + + if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) + decompressArchiveDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; + + ContentDialogResult option = await decompressArchiveDialog.TryShowAsync(); + if (option != ContentDialogResult.Primary) + return; + + password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password); + } + + if (currentFolder is not null) + destinationFolder = await FilesystemTasks.Wrap(() => currentFolder.CreateFolderAsync(SystemIO.Path.GetFileNameWithoutExtension(archive.Path), CreationCollisionOption.GenerateUniqueName).AsTask()); + + // Operate decompress + var result = await FilesystemTasks.Wrap(() => + StorageArchiveService.DecompressAsync(selectedItem.ItemPath, destinationFolder?.Path ?? string.Empty, password)); + } } protected override void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e) diff --git a/src/Files.App/Utils/Archives/ICompressArchiveModel.cs b/src/Files.App/Data/Contracts/ICompressArchiveModel.cs similarity index 97% rename from src/Files.App/Utils/Archives/ICompressArchiveModel.cs rename to src/Files.App/Data/Contracts/ICompressArchiveModel.cs index ccdebd7e0a9a..5d11ea5a85c0 100644 --- a/src/Files.App/Utils/Archives/ICompressArchiveModel.cs +++ b/src/Files.App/Data/Contracts/ICompressArchiveModel.cs @@ -1,7 +1,7 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -namespace Files.App.Utils.Archives +namespace Files.App.Data.Contracts { /// /// Represents an interface for archive creation support. diff --git a/src/Files.App/Data/Contracts/IStorageArchiveService.cs b/src/Files.App/Data/Contracts/IStorageArchiveService.cs new file mode 100644 index 000000000000..97b31b86176b --- /dev/null +++ b/src/Files.App/Data/Contracts/IStorageArchiveService.cs @@ -0,0 +1,65 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using SevenZip; + +namespace Files.App.Data.Contracts +{ + /// + /// Represents a service to manage storage archives, powered by 7zip and its C# wrapper SevenZipSharp. + /// + public interface IStorageArchiveService + { + /// + /// Gets the value that indicates whether specified items can be compressed. + /// + /// Items to check if they can be compressed. + /// True if can be compressed; otherwise, false. + bool CanCompress(IReadOnlyList items); + + /// + /// Gets the value that indicates whether specified items can be decompressed. + /// + /// Items to check if they can be decompressed. + /// True if can be decompressed; otherwise, false. + bool CanDecompress(IReadOnlyList items); + + /// + /// Compresses the specified items. + /// + /// A valid instance of . + /// True if the compression has done successfully; otherwise, false. + Task CompressAsync(ICompressArchiveModel compressionModel); + + /// + /// Decompresses the archive file specified by the path to the path specified by the path with password if applicable. + /// + /// The archive file path to decompress. + /// The destination folder path which the archive file will be decompressed to. + /// The password to decrypt the archive file if applicable. + /// True if the decompression has done successfully; otherwise, false. + Task DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = ""); + + /// + /// Generates the archive file name from item names. + /// + /// Item names to generate archive file name. + /// + string GenerateArchiveNameFromItems(IReadOnlyList items); + + /// + /// Gets the value that indicates whether the archive file is encrypted. + /// + /// The archive file path to check if the item is encrypted. + /// True if the archive file is encrypted; otherwise, false. + Task IsEncryptedAsync(string archiveFilePath); + + /// + /// Gets the instance from the archive file path. + /// + /// The archive file path to generate an instance. + /// The password to decrypt the archive file if applicable. + /// An instance of if the specified item is archive; otherwise null. + Task GetSevenZipExtractorAsync(string archiveFilePath, string password = ""); + } +} diff --git a/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs b/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs index e4f90c6589d8..eb7f4a865f6a 100644 --- a/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs +++ b/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs @@ -22,6 +22,7 @@ public static class ContentPageContextFlyoutFactory private static readonly IModifiableCommandManager ModifiableCommands = Ioc.Default.GetRequiredService(); private static readonly IAddItemService AddItemService = Ioc.Default.GetRequiredService(); private static readonly ICommandManager Commands = Ioc.Default.GetRequiredService(); + private static IStorageArchiveService StorageArchiveService { get; } = Ioc.Default.GetRequiredService(); public static List GetItemContextCommandsWithoutShellItems(CurrentInstanceViewModel currentInstanceViewModel, List selectedItems, BaseLayoutViewModel commandsViewModel, bool shiftPressed, SelectedItemsPropertiesViewModel? selectedItemsPropertiesViewModel, ItemViewModel? itemViewModel = null) { @@ -518,7 +519,7 @@ public static List GetBaseItemMenuItems( new ContextMenuFlyoutItemViewModelBuilder(Commands.CompressIntoZip).Build(), new ContextMenuFlyoutItemViewModelBuilder(Commands.CompressIntoSevenZip).Build(), ], - ShowItem = UserSettingsService.GeneralSettingsService.ShowCompressionOptions && itemsSelected && CompressHelper.CanCompress(selectedItems) + ShowItem = UserSettingsService.GeneralSettingsService.ShowCompressionOptions && itemsSelected && StorageArchiveService.CanCompress(selectedItems) }, new ContextMenuFlyoutItemViewModel { @@ -535,7 +536,7 @@ public static List GetBaseItemMenuItems( new ContextMenuFlyoutItemViewModelBuilder(Commands.DecompressArchiveHere).Build(), new ContextMenuFlyoutItemViewModelBuilder(Commands.DecompressArchiveToChildFolder).Build(), ], - ShowItem = UserSettingsService.GeneralSettingsService.ShowCompressionOptions && CompressHelper.CanDecompress(selectedItems) + ShowItem = UserSettingsService.GeneralSettingsService.ShowCompressionOptions && StorageArchiveService.CanDecompress(selectedItems) }, new ContextMenuFlyoutItemViewModel() { diff --git a/src/Files.App/Utils/Archives/CompressArchiveModel.cs b/src/Files.App/Data/Models/CompressArchiveModel.cs similarity index 99% rename from src/Files.App/Utils/Archives/CompressArchiveModel.cs rename to src/Files.App/Data/Models/CompressArchiveModel.cs index ef89b15644ea..8e4ff0ac45f6 100644 --- a/src/Files.App/Utils/Archives/CompressArchiveModel.cs +++ b/src/Files.App/Data/Models/CompressArchiveModel.cs @@ -6,7 +6,7 @@ using SevenZip; using System.IO; -namespace Files.App.Utils.Archives +namespace Files.App.Data.Models { /// /// Provides an archive creation support. diff --git a/src/Files.App/GlobalUsings.cs b/src/Files.App/GlobalUsings.cs index 90ba38009f1d..ee75e573644e 100644 --- a/src/Files.App/GlobalUsings.cs +++ b/src/Files.App/GlobalUsings.cs @@ -25,7 +25,6 @@ global using global::Files.App.Helpers; global using global::Files.App.Extensions; global using global::Files.App.Utils; -global using global::Files.App.Utils.Archives; global using global::Files.App.Utils.Cloud; global using global::Files.App.Utils.FileTags; global using global::Files.App.Utils.Git; diff --git a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs index 67fb24dc9542..833ed8e10d72 100644 --- a/src/Files.App/Helpers/Application/AppLifecycleHelper.cs +++ b/src/Files.App/Helpers/Application/AppLifecycleHelper.cs @@ -190,6 +190,7 @@ public static IHost ConfigureHost() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() // ViewModels .AddSingleton() diff --git a/src/Files.App/Services/Storage/StorageArchiveService.cs b/src/Files.App/Services/Storage/StorageArchiveService.cs new file mode 100644 index 000000000000..8b705f05acc3 --- /dev/null +++ b/src/Files.App/Services/Storage/StorageArchiveService.cs @@ -0,0 +1,225 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using Files.Shared.Helpers; +using SevenZip; +using System.IO; +using Windows.Storage; +using Windows.Win32; + +namespace Files.App.Services +{ + /// + public class StorageArchiveService : IStorageArchiveService + { + private StatusCenterViewModel StatusCenterViewModel { get; } = Ioc.Default.GetRequiredService(); + private IThreadingService ThreadingService { get; } = Ioc.Default.GetRequiredService(); + + /// + public bool CanCompress(IReadOnlyList items) + { + return + CanDecompress(items) is false || + items.Count > 1; + } + + /// + public bool CanDecompress(IReadOnlyList items) + { + return + items.Any() && + (items.All(x => x.IsArchive) || + items.All(x => + x.PrimaryItemAttribute == StorageItemTypes.File && + FileExtensionHelpers.IsZipFile(x.FileExtension))); + } + + /// + public async Task CompressAsync(ICompressArchiveModel compressionModel) + { + var archivePath = compressionModel.GetArchivePath(); + + int index = 1; + + while (SystemIO.File.Exists(archivePath) || SystemIO.Directory.Exists(archivePath)) + archivePath = compressionModel.GetArchivePath($" ({++index})"); + + compressionModel.ArchivePath = archivePath; + + var banner = StatusCenterHelper.AddCard_Compress( + compressionModel.Sources, + archivePath.CreateEnumerable(), + ReturnResult.InProgress, + compressionModel.Sources.Count()); + + compressionModel.Progress = banner.ProgressEventSource; + compressionModel.CancellationToken = banner.CancellationToken; + + bool isSuccess = await compressionModel.RunCreationAsync(); + + StatusCenterViewModel.RemoveItem(banner); + + if (isSuccess) + { + StatusCenterHelper.AddCard_Compress( + compressionModel.Sources, + archivePath.CreateEnumerable(), + ReturnResult.Success, + compressionModel.Sources.Count()); + } + else + { + PInvoke.DeleteFileFromApp(archivePath); + + StatusCenterHelper.AddCard_Compress( + compressionModel.Sources, + archivePath.CreateEnumerable(), + compressionModel.CancellationToken.IsCancellationRequested + ? ReturnResult.Cancelled + : ReturnResult.Failed, + compressionModel.Sources.Count()); + } + + return isSuccess; + } + + /// + public async Task DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "") + { + if (string.IsNullOrEmpty(archiveFilePath) || + string.IsNullOrEmpty(destinationFolderPath)) + return false; + + using var zipFile = await GetSevenZipExtractorAsync(archiveFilePath, password); + if (zipFile is null) + return false; + + // Initialize a new in-progress status card + var statusCard = StatusCenterHelper.AddCard_Decompress( + archiveFilePath.CreateEnumerable(), + destinationFolderPath.CreateEnumerable(), + ReturnResult.InProgress); + + // Check if the decompress operation canceled + if (statusCard.CancellationToken.IsCancellationRequested) + return false; + + StatusCenterItemProgressModel fsProgress = new( + statusCard.ProgressEventSource, + enumerationCompleted: true, + FileSystemStatusCode.InProgress, + zipFile.ArchiveFileData.Count(x => !x.IsDirectory)); + + fsProgress.TotalSize = zipFile.ArchiveFileData.Select(x => (long)x.Size).Sum(); + fsProgress.Report(); + + bool isSuccess = false; + + try + { + // TODO: Get this method return result + await zipFile.ExtractArchiveAsync(destinationFolderPath); + + isSuccess = true; + } + catch + { + isSuccess = false; + } + finally + { + // Remove the in-progress status card + StatusCenterViewModel.RemoveItem(statusCard); + + if (isSuccess) + { + // Successful + StatusCenterHelper.AddCard_Decompress( + archiveFilePath.CreateEnumerable(), + destinationFolderPath.CreateEnumerable(), + ReturnResult.Success); + } + else + { + // Error + StatusCenterHelper.AddCard_Decompress( + archiveFilePath.CreateEnumerable(), + destinationFolderPath.CreateEnumerable(), + statusCard.CancellationToken.IsCancellationRequested + ? ReturnResult.Cancelled + : ReturnResult.Failed); + } + } + + zipFile.Extracting += (s, e) => + { + if (fsProgress.TotalSize > 0) + fsProgress.Report(e.BytesProcessed / (double)fsProgress.TotalSize * 100); + }; + + zipFile.FileExtractionStarted += (s, e) => + { + if (statusCard.CancellationToken.IsCancellationRequested) + e.Cancel = true; + + if (!e.FileInfo.IsDirectory) + { + ThreadingService.ExecuteOnUiThreadAsync(() => + { + fsProgress.FileName = e.FileInfo.FileName; + fsProgress.Report(); + }); + } + }; + + zipFile.FileExtractionFinished += (s, e) => + { + if (!e.FileInfo.IsDirectory) + { + fsProgress.AddProcessedItemsCount(1); + fsProgress.Report(); + } + }; + + return isSuccess; + } + + /// + public string GenerateArchiveNameFromItems(IReadOnlyList items) + { + if (!items.Any()) + return string.Empty; + + return + SystemIO.Path.GetFileName( + items.Count is 1 + ? items[0].ItemPath + : SystemIO.Path.GetDirectoryName(items[0].ItemPath)) + ?? string.Empty; + } + + /// + public async Task IsEncryptedAsync(string archiveFilePath) + { + using SevenZipExtractor? zipFile = await GetSevenZipExtractorAsync(archiveFilePath); + if (zipFile is null) + return true; + + return zipFile.ArchiveFileData.Any(file => file.Encrypted || file.Method.Contains("Crypto") || file.Method.Contains("AES")); + } + + /// + public async Task GetSevenZipExtractorAsync(string archiveFilePath, string password = "") + { + return await FilesystemTasks.Wrap(async () => + { + BaseStorageFile archive = await StorageHelpers.ToStorageItem(archiveFilePath); + + var extractor = new SevenZipExtractor(await archive.OpenStreamForReadAsync(), password); + + // Force to load archive (1665013614u) + return extractor?.ArchiveFileData is null ? null : extractor; + }); + } + } +} diff --git a/src/Files.App/Services/StorageCacheService.cs b/src/Files.App/Services/Storage/StorageCacheService.cs similarity index 100% rename from src/Files.App/Services/StorageCacheService.cs rename to src/Files.App/Services/Storage/StorageCacheService.cs diff --git a/src/Files.App/Utils/Archives/CompressHelper.cs b/src/Files.App/Utils/Archives/CompressHelper.cs deleted file mode 100644 index 800e044ffb6e..000000000000 --- a/src/Files.App/Utils/Archives/CompressHelper.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using Files.Shared.Helpers; -using System.IO; -using Windows.Storage; -using Windows.Win32; - -namespace Files.App.Utils.Archives -{ - /// - /// Provides static helper for compressing archive. - /// - public static class CompressHelper - { - private readonly static StatusCenterViewModel _statusCenterViewModel = Ioc.Default.GetRequiredService(); - - public static bool CanDecompress(IReadOnlyList selectedItems) - { - return selectedItems.Any() && - (selectedItems.All(x => x.IsArchive) - || selectedItems.All(x => x.PrimaryItemAttribute == StorageItemTypes.File && FileExtensionHelpers.IsZipFile(x.FileExtension))); - } - - public static bool CanCompress(IReadOnlyList selectedItems) - { - return !CanDecompress(selectedItems) || selectedItems.Count > 1; - } - - public static string DetermineArchiveNameFromSelection(IReadOnlyList selectedItems) - { - if (!selectedItems.Any()) - return string.Empty; - - return Path.GetFileName( - selectedItems.Count is 1 - ? selectedItems[0].ItemPath - : Path.GetDirectoryName(selectedItems[0].ItemPath - )) ?? string.Empty; - } - - public static (string[] Sources, string directory, string fileName) GetCompressDestination(IShellPage associatedInstance) - { - string[] sources = associatedInstance.SlimContentPage.SelectedItems - .Select(item => item.ItemPath) - .ToArray(); - - if (sources.Length is 0) - return (sources, string.Empty, string.Empty); - - string directory = associatedInstance.FilesystemViewModel.WorkingDirectory.Normalize(); - - - if (App.LibraryManager.TryGetLibrary(directory, out var library) && !library.IsEmpty) - directory = library.DefaultSaveFolder; - - string fileName = Path.GetFileName(sources.Length is 1 ? sources[0] : directory); - - return (sources, directory, fileName); - } - - public static async Task CompressArchiveAsync(ICompressArchiveModel creator) - { - var archivePath = creator.GetArchivePath(); - - int index = 1; - - while (File.Exists(archivePath) || Directory.Exists(archivePath)) - archivePath = creator.GetArchivePath($" ({++index})"); - - creator.ArchivePath = archivePath; - - var banner = StatusCenterHelper.AddCard_Compress( - creator.Sources, - archivePath.CreateEnumerable(), - ReturnResult.InProgress, - creator.Sources.Count()); - - creator.Progress = banner.ProgressEventSource; - creator.CancellationToken = banner.CancellationToken; - - bool isSuccess = await creator.RunCreationAsync(); - - _statusCenterViewModel.RemoveItem(banner); - - if (isSuccess) - { - StatusCenterHelper.AddCard_Compress( - creator.Sources, - archivePath.CreateEnumerable(), - ReturnResult.Success, - creator.Sources.Count()); - } - else - { - PInvoke.DeleteFileFromApp(archivePath); - - StatusCenterHelper.AddCard_Compress( - creator.Sources, - archivePath.CreateEnumerable(), - creator.CancellationToken.IsCancellationRequested - ? ReturnResult.Cancelled - : ReturnResult.Failed, - creator.Sources.Count()); - } - } - } -} diff --git a/src/Files.App/Utils/Archives/DecompressHelper.cs b/src/Files.App/Utils/Archives/DecompressHelper.cs deleted file mode 100644 index 90503bd6bea1..000000000000 --- a/src/Files.App/Utils/Archives/DecompressHelper.cs +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright (c) 2024 Files Community -// Licensed under the MIT License. See the LICENSE. - -using Files.App.Dialogs; -using Files.App.ViewModels.Dialogs; -using Microsoft.Extensions.Logging; -using Microsoft.UI.Xaml.Controls; -using SevenZip; -using System.IO; -using System.Text; -using Windows.Foundation.Metadata; -using Windows.Storage; - -namespace Files.App.Utils.Archives -{ - public static class DecompressHelper - { - private readonly static StatusCenterViewModel _statusCenterViewModel = Ioc.Default.GetRequiredService(); - - private static IThreadingService _threadingService = Ioc.Default.GetRequiredService(); - - public static async Task DecompressArchiveAsync(BaseStorageFile archive, BaseStorageFolder destinationFolder, string password, IProgress progress, CancellationToken cancellationToken) - { - using SevenZipExtractor? zipFile = await GetZipFile(archive, password); - if (zipFile is null) - return false; - - // Check if the decompress operation canceled - if (cancellationToken.IsCancellationRequested) - return false; - - // Fill files - - byte[] buffer = new byte[4096]; - int entriesAmount = zipFile.ArchiveFileData.Count(x => !x.IsDirectory); - - StatusCenterItemProgressModel fsProgress = new( - progress, - enumerationCompleted: true, - FileSystemStatusCode.InProgress, - entriesAmount); - - fsProgress.TotalSize = zipFile.ArchiveFileData.Select(x => (long)x.Size).Sum(); - fsProgress.Report(); - - zipFile.Extracting += (s, e) => - { - if (fsProgress.TotalSize > 0) - fsProgress.Report(e.BytesProcessed / (double)fsProgress.TotalSize * 100); - }; - zipFile.FileExtractionStarted += (s, e) => - { - if (cancellationToken.IsCancellationRequested) - e.Cancel = true; - if (!e.FileInfo.IsDirectory) - { - _threadingService.ExecuteOnUiThreadAsync(() => - { - fsProgress.FileName = e.FileInfo.FileName; - fsProgress.Report(); - }); - } - }; - zipFile.FileExtractionFinished += (s, e) => - { - if (!e.FileInfo.IsDirectory) - { - fsProgress.AddProcessedItemsCount(1); - fsProgress.Report(); - } - }; - - try - { - // TODO: Get this method return result - await zipFile.ExtractArchiveAsync(destinationFolder.Path); - } - catch (Exception ex) - { - App.Logger.LogWarning(ex, $"Error extracting file: {archive.Name}"); - - return false; - } - - return true; - } - - private static async Task DecompressArchiveAsync(BaseStorageFile archive, BaseStorageFolder? destinationFolder, string password) - { - if (archive is null || destinationFolder is null) - return; - - // Initialize a new in-progress status card - var statusCard = StatusCenterHelper.AddCard_Decompress( - archive.Path.CreateEnumerable(), - destinationFolder.Path.CreateEnumerable(), - ReturnResult.InProgress); - - // Operate decompress - var result = await FilesystemTasks.Wrap(() => - DecompressArchiveAsync(archive, destinationFolder, password, statusCard.ProgressEventSource, statusCard.CancellationToken)); - - // Remove the in-progress status card - _statusCenterViewModel.RemoveItem(statusCard); - - if (result.Result) - { - // Successful - StatusCenterHelper.AddCard_Decompress( - archive.Path.CreateEnumerable(), - destinationFolder.Path.CreateEnumerable(), - ReturnResult.Success); - } - else - { - // Error - StatusCenterHelper.AddCard_Decompress( - archive.Path.CreateEnumerable(), - destinationFolder.Path.CreateEnumerable(), - statusCard.CancellationToken.IsCancellationRequested - ? ReturnResult.Cancelled - : ReturnResult.Failed); - } - } - - public static async Task DecompressArchiveAsync(IShellPage associatedInstance) - { - if (associatedInstance == null) - return; - - BaseStorageFile archive = await StorageHelpers.ToStorageItem(associatedInstance.SlimContentPage?.SelectedItems?.Count is null or 0 - ? associatedInstance.FilesystemViewModel.WorkingDirectory - : associatedInstance.SlimContentPage.SelectedItem.ItemPath); - - if (archive?.Path is null) - return; - - var isArchiveEncrypted = await FilesystemTasks.Wrap(() => DecompressHelper.IsArchiveEncrypted(archive)); - var password = string.Empty; - - DecompressArchiveDialog decompressArchiveDialog = new(); - DecompressArchiveDialogViewModel decompressArchiveViewModel = new(archive) - { - IsArchiveEncrypted = isArchiveEncrypted, - ShowPathSelection = true - }; - decompressArchiveDialog.ViewModel = decompressArchiveViewModel; - - if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) - decompressArchiveDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; - - ContentDialogResult option = await decompressArchiveDialog.TryShowAsync(); - if (option != ContentDialogResult.Primary) - return; - - if (isArchiveEncrypted) - password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password); - - // Check if archive still exists - if (!StorageHelpers.Exists(archive.Path)) - return; - - BaseStorageFolder destinationFolder = decompressArchiveViewModel.DestinationFolder; - string destinationFolderPath = decompressArchiveViewModel.DestinationFolderPath; - - if (destinationFolder is null) - { - BaseStorageFolder parentFolder = await StorageHelpers.ToStorageItem(Path.GetDirectoryName(archive.Path)); - destinationFolder = await FilesystemTasks.Wrap(() => parentFolder.CreateFolderAsync(Path.GetFileName(destinationFolderPath), CreationCollisionOption.GenerateUniqueName).AsTask()); - } - - await DecompressArchiveAsync(archive, destinationFolder, password); - - if (decompressArchiveViewModel.OpenDestinationFolderOnCompletion) - await NavigationHelpers.OpenPath(destinationFolderPath, associatedInstance, FilesystemItemType.Directory); - } - - public static async Task DecompressArchiveHereAsync(IShellPage associatedInstance, bool smart = false) - { - if (associatedInstance?.SlimContentPage?.SelectedItems == null) - return; - - foreach (var selectedItem in associatedInstance.SlimContentPage.SelectedItems) - { - var password = string.Empty; - BaseStorageFile archive = await StorageHelpers.ToStorageItem(selectedItem.ItemPath); - BaseStorageFolder currentFolder = await StorageHelpers.ToStorageItem(associatedInstance.FilesystemViewModel.CurrentFolder.ItemPath); - - if (archive?.Path is null) - return; - - if (await FilesystemTasks.Wrap(() => IsArchiveEncrypted(archive))) - { - DecompressArchiveDialog decompressArchiveDialog = new(); - DecompressArchiveDialogViewModel decompressArchiveViewModel = new(archive) - { - IsArchiveEncrypted = true, - ShowPathSelection = false - }; - - decompressArchiveDialog.ViewModel = decompressArchiveViewModel; - - if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) - decompressArchiveDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; - - ContentDialogResult option = await decompressArchiveDialog.TryShowAsync(); - if (option != ContentDialogResult.Primary) - return; - - password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password); - } - - if (smart && currentFolder is not null && await FilesystemTasks.Wrap(() => IsMultipleItems(archive))) - { - var destinationFolder = await FilesystemTasks.Wrap(() => currentFolder.CreateFolderAsync(Path.GetFileNameWithoutExtension(archive.Path), CreationCollisionOption.GenerateUniqueName).AsTask()); - await DecompressArchiveAsync(archive, destinationFolder, password); - } - else - await DecompressArchiveAsync(archive, currentFolder, password); - } - } - - public static async Task DecompressArchiveToChildFolderAsync(IShellPage associatedInstance) - { - if (associatedInstance?.SlimContentPage?.SelectedItems == null) - return; - - foreach (var selectedItem in associatedInstance.SlimContentPage.SelectedItems) - { - var password = string.Empty; - - BaseStorageFile archive = await StorageHelpers.ToStorageItem(selectedItem.ItemPath); - BaseStorageFolder currentFolder = await StorageHelpers.ToStorageItem(associatedInstance.FilesystemViewModel.CurrentFolder.ItemPath); - BaseStorageFolder destinationFolder = null; - - if (archive?.Path is null) - return; - - if (await FilesystemTasks.Wrap(() => DecompressHelper.IsArchiveEncrypted(archive))) - { - DecompressArchiveDialog decompressArchiveDialog = new(); - DecompressArchiveDialogViewModel decompressArchiveViewModel = new(archive) - { - IsArchiveEncrypted = true, - ShowPathSelection = false - }; - decompressArchiveDialog.ViewModel = decompressArchiveViewModel; - - if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) - decompressArchiveDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; - - ContentDialogResult option = await decompressArchiveDialog.TryShowAsync(); - if (option != ContentDialogResult.Primary) - return; - - password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password); - } - - if (currentFolder is not null) - destinationFolder = await FilesystemTasks.Wrap(() => currentFolder.CreateFolderAsync(Path.GetFileNameWithoutExtension(archive.Path), CreationCollisionOption.GenerateUniqueName).AsTask()); - - await DecompressArchiveAsync(archive, destinationFolder, password); - } - } - - private static async Task GetZipFile(BaseStorageFile archive, string password = "") - { - return await FilesystemTasks.Wrap(async () => - { - var arch = new SevenZipExtractor(await archive.OpenStreamForReadAsync(), password); - return arch?.ArchiveFileData is null ? null : arch; // Force load archive (1665013614u) - }); - } - - private static async Task IsArchiveEncrypted(BaseStorageFile archive) - { - using SevenZipExtractor? zipFile = await GetZipFile(archive); - if (zipFile is null) - return true; - - return zipFile.ArchiveFileData.Any(file => file.Encrypted || file.Method.Contains("Crypto") || file.Method.Contains("AES")); - } - - private static async Task IsMultipleItems(BaseStorageFile archive) - { - using SevenZipExtractor? zipFile = await GetZipFile(archive); - if (zipFile is null) - return true; - - return zipFile.ArchiveFileData.Select(file => - { - var pathCharIndex = file.FileName.IndexOfAny(['/', '\\']); - if (pathCharIndex == -1) - return file.FileName; - else - return file.FileName.Substring(0, pathCharIndex); - }).Distinct().Count() > 1; - } - } -}