Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<PackageVersion Include="OwlCore.Storage" Version="0.12.2" />
<PackageVersion Include="Sentry" Version="5.1.1" />
<PackageVersion Include="SevenZipSharp" Version="1.0.2" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="SQLitePCLRaw.bundle_green" Version="2.1.10" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.7.250310001" />
<PackageVersion Include="Microsoft.Graphics.Win2D" Version="1.3.2" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,15 @@ public override async Task ExecuteAsync(object? parameter = null)
return;

var isArchiveEncrypted = await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncryptedAsync(archive.Path));
var isArchiveEncodingUndetermined = await FilesystemTasks.Wrap(() => StorageArchiveService.IsEncodingUndeterminedAsync(archive.Path));
var password = string.Empty;
Encoding? encoding = null;

DecompressArchiveDialog decompressArchiveDialog = new();
DecompressArchiveDialogViewModel decompressArchiveViewModel = new(archive)
{
IsArchiveEncrypted = isArchiveEncrypted,
IsArchiveEncodingUndetermined = isArchiveEncodingUndetermined,
ShowPathSelection = true
};
decompressArchiveDialog.ViewModel = decompressArchiveViewModel;
Expand All @@ -62,6 +65,8 @@ public override async Task ExecuteAsync(object? parameter = null)
if (isArchiveEncrypted && decompressArchiveViewModel.Password is not null)
password = Encoding.UTF8.GetString(decompressArchiveViewModel.Password);

encoding = decompressArchiveViewModel.SelectedEncoding.Encoding;

// Check if archive still exists
if (!StorageHelpers.Exists(archive.Path))
return;
Expand All @@ -77,7 +82,7 @@ public override async Task ExecuteAsync(object? parameter = null)

// Operate decompress
var result = await FilesystemTasks.Wrap(() =>
StorageArchiveService.DecompressAsync(archive?.Path ?? string.Empty, destinationFolder?.Path ?? string.Empty, password));
StorageArchiveService.DecompressAsync(archive?.Path ?? string.Empty, destinationFolder?.Path ?? string.Empty, password, encoding));

if (decompressArchiveViewModel.OpenDestinationFolderOnCompletion)
await NavigationHelpers.OpenPath(destinationFolderPath, context.ShellPage, FilesystemItemType.Directory);
Expand Down
11 changes: 10 additions & 1 deletion src/Files.App/Data/Contracts/IStorageArchiveService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Files Community
// Licensed under the MIT License.

using System.Text;
using SevenZip;

namespace Files.App.Data.Contracts
Expand Down Expand Up @@ -37,8 +38,9 @@ public interface IStorageArchiveService
/// <param name="archiveFilePath">The archive file path to decompress.</param>
/// <param name="destinationFolderPath">The destination folder path which the archive file will be decompressed to.</param>
/// <param name="password">The password to decrypt the archive file if applicable.</param>
/// <param name="encoding">The file name encoding to decrypt the archive file. If set to null, system default encoding will be used.</param>
/// <returns>True if the decompression has done successfully; otherwise, false.</returns>
Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "");
Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "", Encoding? encoding = null);

/// <summary>
/// Generates the archive file name from item names.
Expand All @@ -54,6 +56,13 @@ public interface IStorageArchiveService
/// <returns>True if the archive file is encrypted; otherwise, false.</returns>
Task<bool> IsEncryptedAsync(string archiveFilePath);

/// <summary>
/// Gets the value that indicates whether the archive file's encoding is undetermined.
/// </summary>
/// <param name="archiveFilePath">The archive file path to check if the item is encrypted.</param>
/// <returns>True if the archive file's encoding is undetermined; otherwise, false.</returns>
Task<bool> IsEncodingUndeterminedAsync(string archiveFilePath);

/// <summary>
/// Gets the <see cref="SevenZipExtractor"/> instance from the archive file path.
/// </summary>
Expand Down
41 changes: 41 additions & 0 deletions src/Files.App/Data/Items/EncodingItem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2024 Files Community
// Licensed under the MIT License. See the LICENSE.

using System.Text;

namespace Files.App.Data.Items
{
/// <summary>
/// Represents a text encoding in the application.
/// </summary>
public sealed class EncodingItem
{

public Encoding? Encoding { get; set; }

/// <summary>
/// Gets the encoding name. e.g. English (United States)
/// </summary>
public string Name { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="EncodingItem"/> class.
/// </summary>
/// <param name="code">The code of the language.</param>
public EncodingItem(string code)
{
if (string.IsNullOrEmpty(code))
{
Encoding = null;
Name = "ArchiveEncodingSystemDefault".GetLocalizedResource();
}
else
{
Encoding = Encoding.GetEncoding(code);
Name = Encoding.EncodingName;
}
}

public override string ToString() => Name;
}
}
30 changes: 30 additions & 0 deletions src/Files.App/Dialogs/DecompressArchiveDialog.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Files.App.Helpers"
xmlns:items="using:Files.App.Data.Items"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="using:Files.App.UserControls"
Title="{helpers:ResourceString Name=ExtractArchive}"
CornerRadius="{StaticResource OverlayCornerRadius}"
DefaultButton="Primary"
Expand Down Expand Up @@ -80,6 +82,34 @@

</StackPanel>

<!-- Encoding -->
<StackPanel
x:Name="EncodingStackPanel"
x:Load="{x:Bind ViewModel.IsArchiveEncodingUndetermined}"
Orientation="Vertical"
Spacing="8">

<TextBlock
x:Name="EncodingHeader"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Text="{helpers:ResourceString Name=ArchiveEncoding}" />

<uc:ComboBoxEx
x:Name="EncodingBox"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ItemsSource="{x:Bind ViewModel.EncodingOptions}"
SelectedItem="{x:Bind ViewModel.SelectedEncoding, Mode=TwoWay}">
<uc:ComboBoxEx.ItemTemplate>
<DataTemplate x:DataType="items:EncodingItem">
<TextBlock Text="{x:Bind Name}" />
</DataTemplate>
</uc:ComboBoxEx.ItemTemplate>
</uc:ComboBoxEx>

</StackPanel>

<!-- Open when complete -->
<CheckBox
x:Name="OpenDestination"
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/Files.App.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
<PackageReference Include="Sentry" />
<PackageReference Include="SevenZipSharp" />
<PackageReference Include="SharpZipLib" />
<PackageReference Include="SQLitePCLRaw.bundle_green" />
<PackageReference Include="Microsoft.WindowsAppSDK" />
<PackageReference Include="Microsoft.Graphics.Win2D" />
Expand Down
2 changes: 2 additions & 0 deletions src/Files.App/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using System.IO;
using System.Text;
using Windows.ApplicationModel.Activation;
using Windows.Storage;
using static Files.App.Helpers.Win32PInvoke;
Expand Down Expand Up @@ -55,6 +56,7 @@ static Program()
[STAThread]
private static void Main()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@oxygen-dioxide apologies only mentioning this now, but this should really go under AppLifecycleHelper.

WinRT.ComWrappersSupport.InitializeComWrappers();

// We are about to do the first WinRT server call, in case the WinRT server is hanging
Expand Down
159 changes: 158 additions & 1 deletion src/Files.App/Services/Storage/StorageArchiveService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
// Licensed under the MIT License.

using Files.Shared.Helpers;
using ICSharpCode.SharpZipLib.Core;
using ICSharpCode.SharpZipLib.Zip;
using SevenZip;
using System.IO;
using System.Linq;
using System.Text;
using Windows.Storage;
using Windows.Win32;

Expand Down Expand Up @@ -84,7 +88,17 @@ public async Task<bool> CompressAsync(ICompressArchiveModel compressionModel)
}

/// <inheritdoc/>
public async Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "")
public Task<bool> DecompressAsync(string archiveFilePath, string destinationFolderPath, string password = "", Encoding? encoding = null)
{
if(encoding == null){
return DecompressAsyncWithSevenZip(archiveFilePath, destinationFolderPath, password);
}
else
{
return DecompressAsyncWithSharpZipLib(archiveFilePath, destinationFolderPath, password, encoding);
}
}
async Task<bool> DecompressAsyncWithSevenZip(string archiveFilePath, string destinationFolderPath, string password = "")
{
if (string.IsNullOrEmpty(archiveFilePath) ||
string.IsNullOrEmpty(destinationFolderPath))
Expand Down Expand Up @@ -180,10 +194,134 @@ public async Task<bool> DecompressAsync(string archiveFilePath, string destinati
fsProgress.Report();
}
};
return isSuccess;
}

async Task<bool> DecompressAsyncWithSharpZipLib(string archiveFilePath, string destinationFolderPath, string password, Encoding encoding)
{
if (string.IsNullOrEmpty(archiveFilePath) ||
string.IsNullOrEmpty(destinationFolderPath))
return false;
using var zipFile = new ZipFile(archiveFilePath, StringCodec.FromEncoding(encoding));
if(zipFile is null)
return false;

if(!string.IsNullOrEmpty(password))
zipFile.Password = password;

// 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.Cast<ZipEntry>().Count<ZipEntry>(x => !x.IsDirectory));
fsProgress.TotalSize = zipFile.Cast<ZipEntry>().Select(x => (long)x.Size).Sum();
fsProgress.Report();

bool isSuccess = false;

try
{
long processedBytes = 0;
int processedFiles = 0;

foreach (ZipEntry zipEntry in zipFile)
{
if (statusCard.CancellationToken.IsCancellationRequested)
{
isSuccess = false;
break;
}

if (!zipEntry.IsFile)
{
continue; // Ignore directories
}

string entryFileName = zipEntry.Name;
string fullZipToPath = Path.Combine(destinationFolderPath, entryFileName);
string directoryName = Path.GetDirectoryName(fullZipToPath);

if (!Directory.Exists(directoryName))
{
Directory.CreateDirectory(directoryName);
}

byte[] buffer = new byte[4096]; // 4K is a good default
using (Stream zipStream = zipFile.GetInputStream(zipEntry))
using (FileStream streamWriter = File.Create(fullZipToPath))
{
await ThreadingService.ExecuteOnUiThreadAsync(() =>
{
fsProgress.FileName = entryFileName;
fsProgress.Report();
});

StreamUtils.Copy(zipStream, streamWriter, buffer);
}
processedBytes += zipEntry.Size;
if (fsProgress.TotalSize > 0)
{
fsProgress.Report(processedBytes / (double)fsProgress.TotalSize * 100);
}
processedFiles++;
fsProgress.AddProcessedItemsCount(1);
fsProgress.Report();
}

if (!statusCard.CancellationToken.IsCancellationRequested)
{
isSuccess = true;
}
}
catch (Exception ex)
{
isSuccess = false;
Console.WriteLine($"Error during decompression: {ex.Message}");
}
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);
}

if (zipFile != null)
{
zipFile.IsStreamOwner = true; // Makes close also close the underlying stream
zipFile.Close();
}
}
return isSuccess;
}


/// <inheritdoc/>
public string GenerateArchiveNameFromItems(IReadOnlyList<ListedItem> items)
{
Expand All @@ -208,6 +346,25 @@ public async Task<bool> IsEncryptedAsync(string archiveFilePath)
return zipFile.ArchiveFileData.Any(file => file.Encrypted || file.Method.Contains("Crypto") || file.Method.Contains("AES"));
}

/// <inheritdoc/>
public async Task<bool> IsEncodingUndeterminedAsync(string archiveFilePath)
{
if (archiveFilePath is null) return false;
if (Path.GetExtension(archiveFilePath) != ".zip") return false;
try
{
using (ZipFile zipFile = new ZipFile(archiveFilePath))
{
return !zipFile.Cast<ZipEntry>().All(entry=>entry.IsUnicodeText);
}
}
catch (Exception ex)
{
Console.WriteLine($"SharpZipLib error: {ex.Message}");
return true;
}
}

/// <inheritdoc/>
public async Task<SevenZipExtractor?> GetSevenZipExtractorAsync(string archiveFilePath, string password = "")
{
Expand Down
6 changes: 6 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2096,6 +2096,12 @@
<data name="ArchivePassword" xml:space="preserve">
<value>Archive password</value>
</data>
<data name="ArchiveEncoding" xml:space="preserve">
<value>Archive encoding</value>
</data>
<data name="ArchiveEncodingSystemDefault" xml:space="preserve">
<value>Windows default</value>
</data>
<data name="ExtractToPath" xml:space="preserve">
<value>Path</value>
</data>
Expand Down
Loading