/
githubmirror
/
Files
Обзор
Документация
Войти
/
githubmirror
/
Files
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Files.App/Services/Storage/StorageArchiveService.cs
458 строк
13 KB
oxygen dioxide
Feature: Add ZIP encoding selection support for browsing archives with non-UTF-8 filenames (#18529)
05 авг 2026, 18:17
Не верифицирован
05 авг 2026, 18:17
8d5546e
Код
Авторство
О чём код?
// Copyright (c) Files Community // Licensed under the MIT License. using Files.Shared.Helpers; using ICSharpCode.SharpZipLib.Core; using ICSharpCode.SharpZipLib.Zip; using Microsoft.Extensions.Logging; using SevenZip; using System.Collections.Concurrent; using System.IO; using System.Text; using UtfUnknown; using Windows.Storage; using Windows.Win32; namespace Files.App.Services { /// <inheritdoc cref="IStorageArchiveService"/> public class StorageArchiveService : IStorageArchiveService { private StatusCenterViewModel StatusCenterViewModel { get; } = Ioc.Default.GetRequiredService<StatusCenterViewModel>(); // Archive paths currently being written. ConcurrentDictionary used as a set; the byte value // is unused. Lookups are case-insensitive to match Windows path semantics. private readonly ConcurrentDictionary<string, byte> inProgressArchives = new(StringComparer.OrdinalIgnoreCase); /// <inheritdoc/> public event EventHandler<string>? CompressionCompleted; /// <inheritdoc/> public bool IsCompressionInProgress(string archivePath) => inProgressArchives.ContainsKey(archivePath); /// <inheritdoc/> public bool CanCompress(IReadOnlyList<ListedItem> items) { return CanDecompress(items) is false || items.Count > 1; } /// <inheritdoc/> public bool CanDecompress(IReadOnlyList<ListedItem> items) { return items.Any() && (items.All(x => x.IsArchive) || items.All(x => x.PrimaryItemAttribute == StorageItemTypes.File && FileExtensionHelpers.IsZipFile(x.FileExtension))); } /// <inheritdoc/> public async Task<bool> 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; inProgressArchives.TryAdd(archivePath, 0); bool isSuccess; try { isSuccess = await compressionModel.RunCreationAsync(); } finally { inProgressArchives.TryRemove(archivePath, out _); } StatusCenterViewModel.RemoveItem(banner); if (isSuccess) { StatusCenterHelper.AddCard_Compress( compressionModel.Sources, archivePath.CreateEnumerable(), ReturnResult.Success, compressionModel.Sources.Count()); CompressionCompleted?.Invoke(this, archivePath); } else { PInvoke.DeleteFileFromApp(archivePath); StatusCenterHelper.AddCard_Compress( compressionModel.Sources, archivePath.CreateEnumerable(), compressionModel.CancellationToken.IsCancellationRequested || compressionModel.IsCancelled ? ReturnResult.Cancelled : ReturnResult.Failed, compressionModel.Sources.Count()); } return isSuccess; } /// <inheritdoc/> 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)) 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(); 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) { fsProgress.FileName = ArchiveEntryHelpers.GetEntryName(e.FileInfo, archiveFilePath); fsProgress.Report(); } }; zipFile.FileExtractionFinished += (s, e) => { if (!e.FileInfo.IsDirectory) { fsProgress.AddProcessedItemsCount(1); fsProgress.Report(); } }; bool isSuccess = false; try { var files = zipFile.ArchiveFileData.Where(x => !x.IsDirectory).ToList(); var resolvedName = files.Count is 1 ? ArchiveEntryHelpers.GetEntryName(files[0], archiveFilePath) : null; if (resolvedName is not null && resolvedName != files[0].FileName) { // ExtractArchive would write the entry's "[no name]" placeholder to disk Directory.CreateDirectory(destinationFolderPath); await using var destinationStream = File.Create(Path.Combine(destinationFolderPath, resolvedName)); await zipFile.ExtractFileAsync(files[0].Index, destinationStream); } else { // TODO: Get this method return result await zipFile.ExtractArchiveAsync(destinationFolderPath); } if (!statusCard.CancellationToken.IsCancellationRequested) isSuccess = true; } catch (Exception ex) { isSuccess = false; App.Logger.LogError(ex, "SevenZipLib error extracting archive file."); } 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); } } 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; await Task.Run(() => { 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)) { 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; App.Logger.LogError(ex, "SharpZipLib error during decompression."); } 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) { 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; } /// <inheritdoc/> public async Task<bool> 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")); } /// <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 || entry.Name.All(c => char.IsAscii(c)) ); } } catch (Exception ex) { App.Logger.LogError(ex, "SharpZipLib error."); return true; } } public async Task<Encoding?> DetectEncodingAsync(string archiveFilePath) { //Temporarily using cp437 to decode zip file //because SharpZipLib requires an encoding when decoding //and cp437 contains all bytes as character //which means that we can store any byte array as cp437 string losslessly var cp437 = Encoding.GetEncoding(437); try { using (ZipFile zipFile = new ZipFile(archiveFilePath, StringCodec.FromEncoding(cp437))) { var fileNameBytes = cp437.GetBytes( String.Join("\n", zipFile.Cast<ZipEntry>() .Where(e => !e.IsUnicodeText) .Select(e => e.Name) ) ); var detectionResult = CharsetDetector.DetectFromBytes(fileNameBytes); if (detectionResult.Detected != null && detectionResult.Detected.Confidence > 0.5) { return detectionResult.Detected.Encoding; } else { return null; } } } catch (Exception ex) { App.Logger.LogError(ex, "SharpZipLib error detecting encoding."); return null; } } /// <inheritdoc/> public async Task<SevenZipExtractor?> GetSevenZipExtractorAsync(string archiveFilePath, string password = "") { return await FilesystemTasks.Wrap(async () => { BaseStorageFile archive = await StorageHelpers.ToStorageItem<BaseStorageFile>(archiveFilePath); var extractor = string.IsNullOrEmpty(password) ? new SevenZipExtractor(archive.Path) : new SevenZipExtractor(archive.Path, password); // Force to load archive (1665013614u) return extractor?.ArchiveFileData is null ? null : extractor; }); } } }