Add another convience method and tests

This commit is contained in:
Adam Hathcock
2026-06-06 15:33:03 +01:00
parent f2db00e8b3
commit 9d60394f8f
6 changed files with 307 additions and 0 deletions

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -10,6 +11,45 @@ namespace SharpCompress.Archives;
public static partial class ArchiveFactory
{
public static async ValueTask ExtractToDirectoryAsync(
string sourceArchive,
string destinationDirectory,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
)
{
sourceArchive.NotNullOrEmpty(nameof(sourceArchive));
await ExtractToDirectoryAsync(
new FileInfo(sourceArchive),
destinationDirectory,
options,
progress,
cancellationToken
)
.ConfigureAwait(false);
}
public static async ValueTask ExtractToDirectoryAsync(
FileInfo sourceArchive,
string destinationDirectory,
ExtractionOptions? options = null,
IProgress<ProgressReport>? progress = null,
CancellationToken cancellationToken = default
)
{
sourceArchive.NotNull(nameof(sourceArchive));
await using var archive = await OpenAsyncArchive(
sourceArchive,
ReaderOptions.ForFilePath,
cancellationToken
)
.ConfigureAwait(false);
await archive
.WriteToDirectoryAsync(destinationDirectory, options, progress, cancellationToken)
.ConfigureAwait(false);
}
public static async ValueTask<IAsyncArchive> OpenAsyncArchive(
Stream stream,
ReaderOptions? readerOptions = null,

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test;
public class ArchiveFactoryAsyncExtractionTests : ArchiveTests
{
public ArchiveFactoryAsyncExtractionTests() => UseExtensionInsteadOfNameToVerify = true;
[Fact]
public async ValueTask ExtractToDirectoryAsync_Zip_DefaultAuto_Extracts()
{
await ArchiveFactory.ExtractToDirectoryAsync(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"),
SCRATCH_FILES_PATH
);
VerifyFiles();
}
[Fact]
public async ValueTask ExtractToDirectoryAsync_Zip_FileInfo_RequireParallel_Extracts()
{
await ArchiveFactory.ExtractToDirectoryAsync(
new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip")),
SCRATCH_FILES_PATH,
new ExtractionOptions { Parallelism = ExtractionParallelism.RequireParallel }
);
VerifyFiles();
}
[Fact]
public async ValueTask ExtractToDirectoryAsync_Zip_SingleThreaded_Extracts()
{
await ArchiveFactory.ExtractToDirectoryAsync(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"),
SCRATCH_FILES_PATH,
new ExtractionOptions { Parallelism = ExtractionParallelism.SingleThreaded }
);
VerifyFiles();
}
[Fact]
public async ValueTask ExtractToDirectoryAsync_ForwardsProgress()
{
var progressReports = new List<ProgressReport>();
var progress = new SynchronousProgress<ProgressReport>(progressReports.Add);
await ArchiveFactory.ExtractToDirectoryAsync(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"),
SCRATCH_FILES_PATH,
progress: progress
);
VerifyFiles();
Assert.NotEmpty(progressReports);
}
private sealed class SynchronousProgress<T> : IProgress<T>
{
private readonly Action<T> handler;
internal SynchronousProgress(Action<T> handler) => this.handler = handler;
public void Report(T value) => handler(value);
}
}

View File

@@ -0,0 +1,40 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.GZip;
using SharpCompress.Common;
using SharpCompress.Writers.GZip;
using Xunit;
namespace SharpCompress.Test.GZip;
public class GZipArchiveAsyncFactoryExtractionTests : TestBase
{
[Fact]
public async ValueTask ExtractToDirectoryAsync_RequireParallel_Fails()
{
var archivePath = GetScratch2Path("single.gz");
var entryBytes = Encoding.UTF8.GetBytes("single file content");
await using (var archive = (GZipArchive)await GZipArchive.CreateAsyncArchive())
{
await archive.AddEntryAsync(
"single.txt",
new MemoryStream(entryBytes),
closeStream: true,
size: entryBytes.Length
);
await archive.SaveToAsync(archivePath, new GZipWriterOptions());
}
await Assert.ThrowsAsync<NotSupportedException>(async () =>
await ArchiveFactory.ExtractToDirectoryAsync(
archivePath,
SCRATCH_FILES_PATH,
new ExtractionOptions { Parallelism = ExtractionParallelism.RequireParallel }
)
);
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.Rar;
public class RarArchiveAsyncFactoryExtractionTests : ArchiveTests
{
[Fact]
public async ValueTask ExtractToDirectoryAsync_NonSolid_RequireParallel_Extracts()
{
await ArchiveFactory.ExtractToDirectoryAsync(
Path.Combine(TEST_ARCHIVES_PATH, "Rar.rar"),
SCRATCH_FILES_PATH,
new ExtractionOptions { Parallelism = ExtractionParallelism.RequireParallel }
);
VerifyFiles();
}
[Fact]
public async ValueTask ExtractToDirectoryAsync_Solid_RequireParallel_Fails()
{
await Assert.ThrowsAsync<NotSupportedException>(async () =>
await ArchiveFactory.ExtractToDirectoryAsync(
Path.Combine(TEST_ARCHIVES_PATH, "Rar.solid.rar"),
SCRATCH_FILES_PATH,
new ExtractionOptions { Parallelism = ExtractionParallelism.RequireParallel }
)
);
}
}

View File

@@ -0,0 +1,35 @@
using System;
using System.IO;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using Xunit;
namespace SharpCompress.Test.SevenZip;
public class SevenZipArchiveAsyncFactoryExtractionTests : ArchiveTests
{
[Fact]
public async ValueTask ExtractToDirectoryAsync_NonSolid_RequireParallel_Extracts()
{
await ArchiveFactory.ExtractToDirectoryAsync(
Path.Combine(TEST_ARCHIVES_PATH, "7Zip.nonsolid.7z"),
SCRATCH_FILES_PATH,
new ExtractionOptions { Parallelism = ExtractionParallelism.RequireParallel }
);
VerifyFiles();
}
[Fact]
public async ValueTask ExtractToDirectoryAsync_SolidSingleBlock_RequireParallel_Fails()
{
await Assert.ThrowsAsync<NotSupportedException>(async () =>
await ArchiveFactory.ExtractToDirectoryAsync(
Path.Combine(TEST_ARCHIVES_PATH, "7Zip.solid.1block.7z"),
SCRATCH_FILES_PATH,
new ExtractionOptions { Parallelism = ExtractionParallelism.RequireParallel }
)
);
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Common;
using SharpCompress.Writers;
using SharpCompress.Writers.Zip;
using Xunit;
namespace SharpCompress.Test.Zip;
public class ZipArchiveAsyncParallelExtractionTests : TestBase
{
[Fact]
public async ValueTask RequireParallel_DetectsDuplicateOutputPathsBeforeWriting()
{
var archivePath = GetScratch2Path("duplicate-output.zip");
CreateZip(archivePath, ("duplicate.txt", "first"), ("duplicate.txt", "second"));
var exception = await Assert.ThrowsAsync<ExtractionException>(async () =>
await ArchiveFactory.ExtractToDirectoryAsync(
archivePath,
SCRATCH_FILES_PATH,
new ExtractionOptions { Parallelism = ExtractionParallelism.RequireParallel }
)
);
Assert.Contains("multiple entries", exception.Message);
Assert.False(File.Exists(GetScratchPath("duplicate.txt")));
}
[Fact]
public async ValueTask RequireParallel_HonorsCancellation()
{
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.Cancel();
await Assert.ThrowsAnyAsync<OperationCanceledException>(async () =>
await ArchiveFactory.ExtractToDirectoryAsync(
Path.Combine(TEST_ARCHIVES_PATH, "Zip.deflate.zip"),
SCRATCH_FILES_PATH,
new ExtractionOptions { Parallelism = ExtractionParallelism.RequireParallel },
cancellationToken: cancellationTokenSource.Token
)
);
}
[Fact]
public async ValueTask RequireParallel_EntryFailureIncludesEntryName()
{
var archivePath = GetScratch2Path("overwrite-failure.zip");
CreateZip(archivePath, ("blocked.txt", "archive"));
File.WriteAllText(GetScratchPath("blocked.txt"), "existing");
var exception = await Assert.ThrowsAnyAsync<Exception>(async () =>
await ArchiveFactory.ExtractToDirectoryAsync(
archivePath,
SCRATCH_FILES_PATH,
new ExtractionOptions
{
Overwrite = false,
Parallelism = ExtractionParallelism.RequireParallel,
}
)
);
Assert.Contains("blocked.txt", exception.Message);
}
private static void CreateZip(string archivePath, params (string Key, string Content)[] entries)
{
using var stream = File.Create(archivePath);
using var writer = new ZipWriter(stream, new ZipWriterOptions(CompressionType.Deflate));
foreach (var entry in entries)
{
using var entryStream = new MemoryStream(Encoding.UTF8.GetBytes(entry.Content));
writer.Write(entry.Key, entryStream);
}
}
}