Add Git overlay, operation tools, and virtualized preview so large folders stay responsive.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
155
src/Explorer.Windows/SevenZipArchiveExecutor.cs
Normal file
155
src/Explorer.Windows/SevenZipArchiveExecutor.cs
Normal file
@@ -0,0 +1,155 @@
|
||||
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<string?> _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<ArchiveProgress>? 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<string> sources,
|
||||
string archivePath,
|
||||
ArchiveFormat format,
|
||||
IProgress<ArchiveProgress>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var type = format == ArchiveFormat.SevenZip ? "-t7z" : "-tzip";
|
||||
var args = new List<string> { "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<string> sources,
|
||||
IProgress<ArchiveProgress>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var args = new List<string> { "a", "-y", "-bb1", "-bsp1", archivePath };
|
||||
args.AddRange(RelativeSources(sources, out var workDir));
|
||||
return RunAsync(args, workDir, progress, cancellationToken);
|
||||
}
|
||||
|
||||
public Task VerifyAsync(
|
||||
string archivePath,
|
||||
IProgress<ArchiveProgress>? progress,
|
||||
CancellationToken cancellationToken)
|
||||
=> RunAsync(
|
||||
["t", archivePath, "-bb1", "-bsp1"],
|
||||
PathRules.Parent(archivePath),
|
||||
progress,
|
||||
cancellationToken);
|
||||
|
||||
private async Task RunAsync(
|
||||
IReadOnlyList<string> arguments,
|
||||
string? workingDirectory,
|
||||
IProgress<ArchiveProgress>? 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<string> RelativeSources(IReadOnlyList<string> 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user