using System.Diagnostics; using System.Text; using System.Text.RegularExpressions; using Explorer.Application; using Explorer.Domain; namespace Explorer.Windows; public sealed class SevenZipArchiveExecutor : IArchiveExecutor { private static readonly Regex Percent = new(@"(\d{1,3})\s*%", RegexOptions.CultureInvariant); private readonly Func _configuredPath; public SevenZipArchiveExecutor(UiPreferencesStore preferences) => _configuredPath = () => preferences.Load().SevenZipPath; public bool IsAvailable => SevenZipLocator.Find(_configuredPath()) is not null; public string MissingHint => SevenZipLocator.MissingHint; public Task ExtractAsync( string archivePath, string destinationDirectory, IProgress? progress, CancellationToken cancellationToken) { Directory.CreateDirectory(PathRules.ToExtended(destinationDirectory)); return RunAsync( ["x", archivePath, "-o" + destinationDirectory, "-y", "-aoa", "-bb1", "-bsp1"], PathRules.Parent(archivePath), progress, cancellationToken); } public Task CompressAsync( IReadOnlyList sources, string archivePath, ArchiveFormat format, IProgress? progress, CancellationToken cancellationToken) { var type = format == ArchiveFormat.SevenZip ? "-t7z" : "-tzip"; var args = new List { "a", type, "-y", "-bb1", "-bsp1", archivePath }; args.AddRange(RelativeSources(sources, out var workDir)); return RunAsync(args, workDir, progress, cancellationToken); } public Task AddAsync( string archivePath, IReadOnlyList sources, IProgress? progress, CancellationToken cancellationToken) { var args = new List { "a", "-y", "-bb1", "-bsp1", archivePath }; args.AddRange(RelativeSources(sources, out var workDir)); return RunAsync(args, workDir, progress, cancellationToken); } public Task VerifyAsync( string archivePath, IProgress? progress, CancellationToken cancellationToken) => RunAsync( ["t", archivePath, "-bb1", "-bsp1"], PathRules.Parent(archivePath), progress, cancellationToken); private async Task RunAsync( IReadOnlyList arguments, string? workingDirectory, IProgress? progress, CancellationToken cancellationToken) { var exe = SevenZipLocator.Find(_configuredPath()) ?? throw new InvalidOperationException(MissingHint); var psi = new ProcessStartInfo { FileName = exe, WorkingDirectory = string.IsNullOrWhiteSpace(workingDirectory) ? Environment.CurrentDirectory : workingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8 }; foreach (var argument in arguments) { psi.ArgumentList.Add(argument); } using var process = new Process { StartInfo = psi, EnableRaisingEvents = true }; var errors = new StringBuilder(); var files = 0L; process.OutputDataReceived += (_, e) => { if (string.IsNullOrEmpty(e.Data)) { return; } var match = Percent.Match(e.Data); if (match.Success && int.TryParse(match.Groups[1].Value, out var pct)) { progress?.Report(new ArchiveProgress(Math.Clamp(pct, 0, 100), files, null)); } if (e.Data.StartsWith('+') || e.Data.StartsWith('-') || e.Data.StartsWith('T')) { files++; progress?.Report(new ArchiveProgress(0, files, e.Data.Trim())); } }; process.ErrorDataReceived += (_, e) => { if (!string.IsNullOrEmpty(e.Data)) { errors.AppendLine(e.Data); } }; if (!process.Start()) { throw new IOException("7-Zip could not be started."); } process.BeginOutputReadLine(); process.BeginErrorReadLine(); await using var kill = cancellationToken.Register(() => { try { process.Kill(entireProcessTree: true); } catch { /* already exited */ } }); await process.WaitForExitAsync(CancellationToken.None).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); if (process.ExitCode > 1) { var detail = errors.ToString().Trim(); throw new IOException(string.IsNullOrEmpty(detail) ? $"7-Zip failed ({process.ExitCode})." : detail); } } private static IEnumerable RelativeSources(IReadOnlyList sources, out string workDir) { if (sources.All(s => PathRules.Parent(s).Equals(PathRules.Parent(sources[0]), StringComparison.OrdinalIgnoreCase))) { workDir = PathRules.Parent(sources[0]); return sources.Select(PathRules.GetFileName).ToList(); } workDir = PathRules.Parent(sources[0]); return sources; } }