Skip to content

Feature: Show modal when extracting password protected archives #10413

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Nov 14, 2022
96 changes: 65 additions & 31 deletions src/Files.App/Dialogs/DecompressArchiveDialog.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,79 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Files.App.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="{helpers:ResourceString Name=DecompressArchiveDialog/Title}"
Title="{helpers:ResourceString Name=ExtractArchive}"
CornerRadius="{StaticResource OverlayCornerRadius}"
DefaultButton="Primary"
PrimaryButtonText="Extract"
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
PrimaryButtonText="{helpers:ResourceString Name=Extract}"
RequestedTheme="{x:Bind helpers:ThemeHelper.RootTheme}"
SecondaryButtonText="{helpers:ResourceString Name=Cancel}"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">

<Grid
MinWidth="400"
Margin="0,16,0,0"
ColumnSpacing="6"
RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border MinWidth="400" Margin="0,16,0,0">
<StackPanel Orientation="Vertical" Spacing="12">
<!-- Extract To Path -->
<Grid
x:Name="ExtractPathGrid"
x:Load="{x:Bind ViewModel.ShowPathSelection}"
ColumnSpacing="8"
RowSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header -->
<TextBlock
Grid.Row="0"
Grid.ColumnSpan="2"
Text="{helpers:ResourceString Name=ExtractToPath}" />

<TextBox
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
IsReadOnly="True"
Text="{x:Bind ViewModel.DestinationFolderPath, Mode=OneWay}" />
<!-- Path Box -->
<TextBox
x:Name="DestinationFolderPath"
Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Stretch"
IsReadOnly="True"
Text="{x:Bind ViewModel.DestinationFolderPath, Mode=OneWay}" />
<Button
x:Name="SelectDestination"
Grid.Row="1"
Grid.Column="1"
Command="{x:Bind ViewModel.SelectDestinationCommand}"
Content="{helpers:ResourceString Name=Browse}" />
</Grid>

<Button
Grid.Column="1"
VerticalAlignment="Center"
Background="Transparent"
Command="{x:Bind ViewModel.SelectDestinationCommand}"
Content="{helpers:ResourceString Name=Browse}" />
<!-- Password -->
<StackPanel
x:Name="PasswordStackPanel"
x:Load="{x:Bind ViewModel.IsArchiveEncrypted}"
Orientation="Vertical"
Spacing="8">
<TextBlock
x:Name="PasswordHeader"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Text="{helpers:ResourceString Name=ArchivePassword}" />
<PasswordBox
x:Name="Password"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
PlaceholderText="{helpers:ResourceString Name=CredentialDialogPassword/PlaceholderText}" />
</StackPanel>

<CheckBox
Grid.Row="1"
Content="{helpers:ResourceString Name=DecompressArchiveDialogOpenDestinationWhenComplete/Content}"
IsChecked="{x:Bind ViewModel.OpenDestinationFolderOnCompletion, Mode=TwoWay}" />
</Grid>
<!-- Open when complete -->
<CheckBox
x:Name="OpenDestination"
Grid.Row="3"
x:Load="{x:Bind ViewModel.ShowPathSelection}"
Content="{helpers:ResourceString Name=DecompressArchiveDialogOpenDestinationWhenComplete/Content}"
IsChecked="{x:Bind ViewModel.OpenDestinationFolderOnCompletion, Mode=TwoWay}" />
</StackPanel>
</Border>
</ContentDialog>
8 changes: 8 additions & 0 deletions src/Files.App/Dialogs/DecompressArchiveDialog.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Files.App.ViewModels.Dialogs;
using Files.Backend.SecureStore;
using Microsoft.UI.Xaml.Controls;
using System.Text;

// The Content Dialog item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238

Expand All @@ -17,5 +19,11 @@ public DecompressArchiveDialog()
{
this.InitializeComponent();
}

private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
if (ViewModel.IsArchiveEncrypted)
ViewModel.PrimaryButtonClickCommand.Execute(new DisposableArray(Encoding.UTF8.GetBytes(Password.Password)));
}
}
}
159 changes: 83 additions & 76 deletions src/Files.App/Helpers/ZipHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ namespace Files.App.Helpers
{
public static class ZipHelpers
{
private static async Task<SevenZipExtractor?> 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<bool> CompressMultipleToArchive(string[] sourceFolders, string archive, IProgress<float> progressDelegate)
{
SevenZipCompressor compressor = new()
Expand Down Expand Up @@ -45,104 +54,102 @@ public static async Task<bool> CompressMultipleToArchive(string[] sourceFolders,
return noErrors;
}

public static async Task ExtractArchive(BaseStorageFile archive, BaseStorageFolder destinationFolder, IProgress<float> progressDelegate, CancellationToken cancellationToken)
public static async Task<bool> IsArchiveEncrypted(BaseStorageFile archive)
{
using (SevenZipExtractor zipFile = await Filesystem.FilesystemTasks.Wrap(async () =>
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"));
}

public static async Task ExtractArchive(BaseStorageFile archive, BaseStorageFolder destinationFolder, string password, IProgress<float> progressDelegate, CancellationToken cancellationToken)
{
using SevenZipExtractor? zipFile = await GetZipFile(archive, password);
if (zipFile is null)
return;
//zipFile.IsStreamOwner = true;
List<ArchiveFileInfo> directoryEntries = new List<ArchiveFileInfo>();
List<ArchiveFileInfo> fileEntries = new List<ArchiveFileInfo>();
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<string>();
try
{
if (zipFile is null)
return;
//zipFile.IsStreamOwner = true;
List<ArchiveFileInfo> directoryEntries = new List<ArchiveFileInfo>();
List<ArchiveFileInfo> fileEntries = new List<ArchiveFileInfo>();
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<string>();
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);
}
}
}
}
}
Loading