From 7f78075a8c89f39c575e9cc75fb29c0fe832a524 Mon Sep 17 00:00:00 2001 From: Ferrario-Filippo Date: Sun, 6 Nov 2022 15:27:00 +0100 Subject: [PATCH 1/8] Feature: Show modal when extracting password protected archives --- .../Dialogs/ArchivePasswordDialog.xaml | 28 +++ .../Dialogs/ArchivePasswordDialog.xaml.cs | 27 +++ .../Dialogs/DecompressArchiveDialog.xaml | 2 +- src/Files.App/Helpers/ZipHelpers.cs | 161 +++++++++--------- .../BaseLayoutCommandImplementationModel.cs | 42 +++-- src/Files.App/Strings/en-US/Resources.resw | 3 + .../Dialogs/ArchivePasswordDialogViewModel.cs | 17 ++ 7 files changed, 191 insertions(+), 89 deletions(-) create mode 100644 src/Files.App/Dialogs/ArchivePasswordDialog.xaml create mode 100644 src/Files.App/Dialogs/ArchivePasswordDialog.xaml.cs create mode 100644 src/Files.App/ViewModels/Dialogs/ArchivePasswordDialogViewModel.cs diff --git a/src/Files.App/Dialogs/ArchivePasswordDialog.xaml b/src/Files.App/Dialogs/ArchivePasswordDialog.xaml new file mode 100644 index 000000000000..ff24c296cecb --- /dev/null +++ b/src/Files.App/Dialogs/ArchivePasswordDialog.xaml @@ -0,0 +1,28 @@ + + + + + + + + + + \ No newline at end of file diff --git a/src/Files.App/Dialogs/ArchivePasswordDialog.xaml.cs b/src/Files.App/Dialogs/ArchivePasswordDialog.xaml.cs new file mode 100644 index 000000000000..c2eb0a97e1fa --- /dev/null +++ b/src/Files.App/Dialogs/ArchivePasswordDialog.xaml.cs @@ -0,0 +1,27 @@ +using Files.App.ViewModels.Dialogs; +using Files.Backend.SecureStore; +using Microsoft.UI.Xaml.Controls; +using System.Text; + +namespace Files.App.Dialogs +{ + public sealed partial class ArchivePasswordDialog : ContentDialog + { + public ArchivePasswordDialogViewModel ViewModel + { + get => (ArchivePasswordDialogViewModel)DataContext; + set => DataContext = value; + } + + public ArchivePasswordDialog() + { + this.InitializeComponent(); + + } + + private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) + { + ViewModel.PrimaryButtonClickCommand.Execute(new DisposableArray(Encoding.UTF8.GetBytes(Password.Password))); + } + } +} diff --git a/src/Files.App/Dialogs/DecompressArchiveDialog.xaml b/src/Files.App/Dialogs/DecompressArchiveDialog.xaml index 2c66e46d95d9..309133fcf66f 100644 --- a/src/Files.App/Dialogs/DecompressArchiveDialog.xaml +++ b/src/Files.App/Dialogs/DecompressArchiveDialog.xaml @@ -8,7 +8,7 @@ Title="{helpers:ResourceString Name=DecompressArchiveDialog/Title}" CornerRadius="{StaticResource OverlayCornerRadius}" DefaultButton="Primary" - PrimaryButtonText="Extract" + PrimaryButtonText="{helpers:ResourceString Name=Extract}" RequestedTheme="{x:Bind helpers:ThemeHelper.RootTheme}" SecondaryButtonText="{helpers:ResourceString Name=Cancel}" Style="{StaticResource DefaultContentDialogStyle}" diff --git a/src/Files.App/Helpers/ZipHelpers.cs b/src/Files.App/Helpers/ZipHelpers.cs index 7746d938c8ee..d165d6312c7d 100644 --- a/src/Files.App/Helpers/ZipHelpers.cs +++ b/src/Files.App/Helpers/ZipHelpers.cs @@ -11,6 +11,15 @@ namespace Files.App.Helpers { public static class ZipHelpers { + private static async Task GetZipFile(BaseStorageFile archive, string password = "") + { + return await Filesystem.FilesystemTasks.Wrap(async () => + { + var arch = new SevenZipExtractor(await archive.OpenStreamForReadAsync(), password); + return arch?.ArchiveFileData is null ? null : arch; // Force load archive (1665013614u) + }); + } + public static async Task CompressMultipleToArchive(string[] sourceFolders, string archive, IProgress progressDelegate) { SevenZipCompressor compressor = new() @@ -20,7 +29,7 @@ public static async Task CompressMultipleToArchive(string[] sourceFolders, EventSynchronization = EventSynchronizationStrategy.AlwaysAsynchronous, FastCompression = true, IncludeEmptyDirectories = true, - PreserveDirectoryRoot = sourceFolders.Length > 1 + PreserveDirectoryRoot = sourceFolders.Length > 1, }; bool noErrors = true; @@ -45,104 +54,102 @@ public static async Task CompressMultipleToArchive(string[] sourceFolders, return noErrors; } - public static async Task ExtractArchive(BaseStorageFile archive, BaseStorageFolder destinationFolder, IProgress progressDelegate, CancellationToken cancellationToken) + public static async Task IsArchiveEncrypted(BaseStorageFile archive) { - using (SevenZipExtractor zipFile = await Filesystem.FilesystemTasks.Wrap(async () => + using SevenZipExtractor? zipFile = await GetZipFile(archive); + if (zipFile is null) + return false; + + return zipFile.ArchiveFileData.Any(file => file.Encrypted || file.Method.Contains("Crypto") || file.Method.Contains("AES")); + } + + public static async Task ExtractArchive(BaseStorageFile archive, BaseStorageFolder destinationFolder, string password, IProgress progressDelegate, CancellationToken cancellationToken) + { + using SevenZipExtractor? zipFile = await GetZipFile(archive, password); + if (zipFile is null) + return; + //zipFile.IsStreamOwner = true; + List directoryEntries = new List(); + List fileEntries = new List(); + foreach (ArchiveFileInfo entry in zipFile.ArchiveFileData) { - var arch = new SevenZipExtractor(await archive.OpenStreamForReadAsync()); - return arch?.ArchiveFileData is null ? null : arch; // Force load archive (1665013614u) - })) + if (!entry.IsDirectory) + fileEntries.Add(entry); + else + directoryEntries.Add(entry); + } + + if (cancellationToken.IsCancellationRequested) // Check if cancelled + return; + + var directories = new List(); + try { - if (zipFile is null) - return; - //zipFile.IsStreamOwner = true; - List directoryEntries = new List(); - List fileEntries = new List(); - foreach (ArchiveFileInfo entry in zipFile.ArchiveFileData) + directories.AddRange(directoryEntries.Select((entry) => entry.FileName)); + directories.AddRange(fileEntries.Select((entry) => Path.GetDirectoryName(entry.FileName))); + } + catch (Exception ex) + { + App.Logger.Warn(ex, $"Error transforming zip names into: {destinationFolder.Path}\n" + + $"Directories: {string.Join(", ", directoryEntries.Select(x => x.FileName))}\n" + + $"Files: {string.Join(", ", fileEntries.Select(x => x.FileName))}"); + return; + } + + foreach (var dir in directories.Distinct().OrderBy(x => x.Length)) + { + if (!NativeFileOperationsHelper.CreateDirectoryFromApp(dir, IntPtr.Zero)) { - if (!entry.IsDirectory) - fileEntries.Add(entry); - else - directoryEntries.Add(entry); + var dirName = destinationFolder.Path; + foreach (var component in dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)) + { + dirName = Path.Combine(dirName, component); + NativeFileOperationsHelper.CreateDirectoryFromApp(dirName, IntPtr.Zero); + } } - if (cancellationToken.IsCancellationRequested) // Check if cancelled + if (cancellationToken.IsCancellationRequested) // Check if canceled return; + } - var directories = new List(); - try - { - directories.AddRange(directoryEntries.Select((entry) => entry.FileName)); - directories.AddRange(fileEntries.Select((entry) => Path.GetDirectoryName(entry.FileName))); - } - catch (Exception ex) - { - App.Logger.Warn(ex, $"Error transforming zip names into: {destinationFolder.Path}\n" + - $"Directories: {string.Join(", ", directoryEntries.Select(x => x.FileName))}\n" + - $"Files: {string.Join(", ", fileEntries.Select(x => x.FileName))}"); - return; - } + if (cancellationToken.IsCancellationRequested) // Check if canceled + return; - foreach (var dir in directories.Distinct().OrderBy(x => x.Length)) - { - if (!NativeFileOperationsHelper.CreateDirectoryFromApp(dir, IntPtr.Zero)) - { - var dirName = destinationFolder.Path; - foreach (var component in dir.Split(Path.DirectorySeparatorChar, StringSplitOptions.RemoveEmptyEntries)) - { - dirName = Path.Combine(dirName, component); - NativeFileOperationsHelper.CreateDirectoryFromApp(dirName, IntPtr.Zero); - } - } + // Fill files - if (cancellationToken.IsCancellationRequested) // Check if canceled - return; - } + byte[] buffer = new byte[4096]; + int entriesAmount = fileEntries.Count; + int entriesFinished = 0; + foreach (var entry in fileEntries) + { if (cancellationToken.IsCancellationRequested) // Check if canceled return; - // Fill files + string filePath = Path.Combine(destinationFolder.Path, entry.FileName); - byte[] buffer = new byte[4096]; - int entriesAmount = fileEntries.Count; - int entriesFinished = 0; + var hFile = NativeFileOperationsHelper.CreateFileForWrite(filePath); + if (hFile.IsInvalid) + return; // TODO: handle error - foreach (var entry in fileEntries) + // We don't close hFile because FileStream.Dispose() already does that + using (FileStream destinationStream = new FileStream(hFile, FileAccess.Write)) { - if (cancellationToken.IsCancellationRequested) // Check if canceled - return; - if (entry.Encrypted) + try { - App.Logger.Info($"Skipped encrypted zip entry: {entry.FileName}"); - continue; // TODO: support password protected archives + await zipFile.ExtractFileAsync(entry.Index, destinationStream); } - - string filePath = Path.Combine(destinationFolder.Path, entry.FileName); - - var hFile = NativeFileOperationsHelper.CreateFileForWrite(filePath); - if (hFile.IsInvalid) - return; // TODO: handle error - - // We don't close hFile because FileStream.Dispose() already does that - using (FileStream destinationStream = new FileStream(hFile, FileAccess.Write)) + catch (Exception ex) { - try - { - await zipFile.ExtractFileAsync(entry.Index, destinationStream); - } - catch (Exception ex) - { - App.Logger.Warn(ex, $"Error extracting file: {filePath}"); - return; // TODO: handle error - } + App.Logger.Warn(ex, $"Error extracting file: {filePath}"); + return; // TODO: handle error } - - entriesFinished++; - float percentage = (float)((float)entriesFinished / (float)entriesAmount) * 100.0f; - progressDelegate?.Report(percentage); } + + entriesFinished++; + float percentage = (float)((float)entriesFinished / (float)entriesAmount) * 100.0f; + progressDelegate?.Report(percentage); } } } -} \ No newline at end of file +} diff --git a/src/Files.App/Interacts/BaseLayoutCommandImplementationModel.cs b/src/Files.App/Interacts/BaseLayoutCommandImplementationModel.cs index 51b541adcafa..c40b749d2c52 100644 --- a/src/Files.App/Interacts/BaseLayoutCommandImplementationModel.cs +++ b/src/Files.App/Interacts/BaseLayoutCommandImplementationModel.cs @@ -21,6 +21,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Text; using System.Threading; using System.Threading.Tasks; using Windows.ApplicationModel.DataTransfer; @@ -645,16 +646,14 @@ public async Task DecompressArchive() if (!StorageHelpers.Exists(archive.Path)) return; + BaseStorageFolder parentFolder = null; 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()); - } + parentFolder = await StorageHelpers.ToStorageItem(Path.GetDirectoryName(archive.Path)); - await ExtractArchive(archive, destinationFolder); + await ExtractArchive(archive, destinationFolder, parentFolder, Path.GetFileName(destinationFolderPath)); if (decompressArchiveViewModel.OpenDestinationFolderOnCompletion) await NavigationHelpers.OpenPath(destinationFolderPath, associatedInstance, FilesystemItemType.Directory); @@ -677,20 +676,41 @@ public async Task DecompressArchiveToChildFolder() { BaseStorageFile archive = await StorageHelpers.ToStorageItem(selectedItem.ItemPath); BaseStorageFolder currentFolder = await StorageHelpers.ToStorageItem(associatedInstance.FilesystemViewModel.CurrentFolder.ItemPath); - BaseStorageFolder destinationFolder = null; + string? destinationFolderPath = null; if (currentFolder is not null) - destinationFolder = await FilesystemTasks.Wrap(() => currentFolder.CreateFolderAsync(Path.GetFileNameWithoutExtension(archive.Path), CreationCollisionOption.GenerateUniqueName).AsTask()); + destinationFolderPath = Path.GetFileNameWithoutExtension(archive.Path); - await ExtractArchive(archive, destinationFolder); + await ExtractArchive(archive, null, currentFolder, destinationFolderPath); } } - private static async Task ExtractArchive(BaseStorageFile archive, BaseStorageFolder destinationFolder) + private static async Task ExtractArchive(BaseStorageFile archive, BaseStorageFolder? destinationFolder, BaseStorageFolder? destinationParent = null, string? destinationPath = null) { - if (archive is null || destinationFolder is null) + if (archive is null) return; + var password = string.Empty; + if (await FilesystemTasks.Wrap(() => ZipHelpers.IsArchiveEncrypted(archive))) + { + ArchivePasswordDialog dialog = new(); + ArchivePasswordDialogViewModel viewModel = new(); + dialog.ViewModel = viewModel; + ContentDialogResult result = await dialog.TryShowAsync(); + if (result != ContentDialogResult.Primary) + return; + + password = Encoding.UTF8.GetString(viewModel.Password); + } + + if (destinationFolder is null && destinationParent is not null) + { + destinationFolder = await FilesystemTasks.Wrap(() => destinationParent.CreateFolderAsync(destinationPath, CreationCollisionOption.GenerateUniqueName).AsTask()); + + if (destinationFolder is null) + return; + } + CancellationTokenSource extractCancellation = new(); PostedStatusBanner banner = App.OngoingTasksViewModel.PostOperationBanner( archive.Name.Length >= 30 ? archive.Name + "\n" : archive.Name, @@ -703,7 +723,7 @@ private static async Task ExtractArchive(BaseStorageFile archive, BaseStorageFol Stopwatch sw = new(); sw.Start(); - await FilesystemTasks.Wrap(() => ZipHelpers.ExtractArchive(archive, destinationFolder, banner.Progress, extractCancellation.Token)); + await FilesystemTasks.Wrap(() => ZipHelpers.ExtractArchive(archive, destinationFolder, password, banner.Progress, extractCancellation.Token)); sw.Stop(); banner.Remove(); diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index f8d466e892af..c89c24737b40 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -2856,4 +2856,7 @@ Failed to empty recycle bin + + Insert archive password + \ No newline at end of file diff --git a/src/Files.App/ViewModels/Dialogs/ArchivePasswordDialogViewModel.cs b/src/Files.App/ViewModels/Dialogs/ArchivePasswordDialogViewModel.cs new file mode 100644 index 000000000000..c074e20f3b1c --- /dev/null +++ b/src/Files.App/ViewModels/Dialogs/ArchivePasswordDialogViewModel.cs @@ -0,0 +1,17 @@ +using CommunityToolkit.Mvvm.Input; +using Files.Backend.SecureStore; + +namespace Files.App.ViewModels.Dialogs +{ + public class ArchivePasswordDialogViewModel + { + public DisposableArray? Password { get; private set; } + + public IRelayCommand PrimaryButtonClickCommand { get; } + + public ArchivePasswordDialogViewModel() + { + PrimaryButtonClickCommand = new RelayCommand(password => Password = password); + } + } +} From 323f119586f7bc35848a62d2e16bbfec64ba59c8 Mon Sep 17 00:00:00 2001 From: Ferrario-Filippo Date: Sun, 6 Nov 2022 15:37:10 +0100 Subject: [PATCH 2/8] Removed a comma --- src/Files.App/Helpers/ZipHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App/Helpers/ZipHelpers.cs b/src/Files.App/Helpers/ZipHelpers.cs index d165d6312c7d..901721d8e42d 100644 --- a/src/Files.App/Helpers/ZipHelpers.cs +++ b/src/Files.App/Helpers/ZipHelpers.cs @@ -29,7 +29,7 @@ public static async Task CompressMultipleToArchive(string[] sourceFolders, EventSynchronization = EventSynchronizationStrategy.AlwaysAsynchronous, FastCompression = true, IncludeEmptyDirectories = true, - PreserveDirectoryRoot = sourceFolders.Length > 1, + PreserveDirectoryRoot = sourceFolders.Length > 1 }; bool noErrors = true; From 07e0b98cb66221e1185743ec58c4b2ccaaadf2b2 Mon Sep 17 00:00:00 2001 From: ferrariofilippo Date: Sun, 6 Nov 2022 18:16:21 +0100 Subject: [PATCH 3/8] Merged into DecompressArchiveDialog --- .../Dialogs/ArchivePasswordDialog.xaml | 28 ------ .../Dialogs/ArchivePasswordDialog.xaml.cs | 27 ------ .../Dialogs/DecompressArchiveDialog.xaml | 24 +++++ .../Dialogs/DecompressArchiveDialog.xaml.cs | 8 ++ .../BaseLayoutCommandImplementationModel.cs | 87 ++++++++++++------- src/Files.App/Strings/en-US/Resources.resw | 2 +- .../Dialogs/ArchivePasswordDialogViewModel.cs | 17 ---- .../DecompressArchiveDialogViewModel.cs | 22 +++++ 8 files changed, 111 insertions(+), 104 deletions(-) delete mode 100644 src/Files.App/Dialogs/ArchivePasswordDialog.xaml delete mode 100644 src/Files.App/Dialogs/ArchivePasswordDialog.xaml.cs delete mode 100644 src/Files.App/ViewModels/Dialogs/ArchivePasswordDialogViewModel.cs diff --git a/src/Files.App/Dialogs/ArchivePasswordDialog.xaml b/src/Files.App/Dialogs/ArchivePasswordDialog.xaml deleted file mode 100644 index ff24c296cecb..000000000000 --- a/src/Files.App/Dialogs/ArchivePasswordDialog.xaml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/Files.App/Dialogs/ArchivePasswordDialog.xaml.cs b/src/Files.App/Dialogs/ArchivePasswordDialog.xaml.cs deleted file mode 100644 index c2eb0a97e1fa..000000000000 --- a/src/Files.App/Dialogs/ArchivePasswordDialog.xaml.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Files.App.ViewModels.Dialogs; -using Files.Backend.SecureStore; -using Microsoft.UI.Xaml.Controls; -using System.Text; - -namespace Files.App.Dialogs -{ - public sealed partial class ArchivePasswordDialog : ContentDialog - { - public ArchivePasswordDialogViewModel ViewModel - { - get => (ArchivePasswordDialogViewModel)DataContext; - set => DataContext = value; - } - - public ArchivePasswordDialog() - { - this.InitializeComponent(); - - } - - private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args) - { - ViewModel.PrimaryButtonClickCommand.Execute(new DisposableArray(Encoding.UTF8.GetBytes(Password.Password))); - } - } -} diff --git a/src/Files.App/Dialogs/DecompressArchiveDialog.xaml b/src/Files.App/Dialogs/DecompressArchiveDialog.xaml index 309133fcf66f..0bd3eb879062 100644 --- a/src/Files.App/Dialogs/DecompressArchiveDialog.xaml +++ b/src/Files.App/Dialogs/DecompressArchiveDialog.xaml @@ -8,6 +8,7 @@ Title="{helpers:ResourceString Name=DecompressArchiveDialog/Title}" CornerRadius="{StaticResource OverlayCornerRadius}" DefaultButton="Primary" + PrimaryButtonClick="ContentDialog_PrimaryButtonClick" PrimaryButtonText="{helpers:ResourceString Name=Extract}" RequestedTheme="{x:Bind helpers:ThemeHelper.RootTheme}" SecondaryButtonText="{helpers:ResourceString Name=Cancel}" @@ -22,6 +23,8 @@ + + @@ -29,12 +32,16 @@