mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-07-08 18:16:30 +00:00
Merge remote-tracking branch 'origin/master' into adam/xz-crc-check
This commit is contained in:
@@ -3,11 +3,11 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"csharpier": {
|
||||
"version": "1.2.6",
|
||||
"version": "1.3.0",
|
||||
"commands": [
|
||||
"csharpier"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
18
docs/API.md
18
docs/API.md
@@ -92,7 +92,8 @@ using (var archive = GZipArchive.CreateArchive())
|
||||
// With fluent options (preferred)
|
||||
var options = WriterOptions.ForZip()
|
||||
.WithCompressionLevel(9)
|
||||
.WithLeaveStreamOpen(false);
|
||||
.WithLeaveStreamOpen(false)
|
||||
.WithBufferSize(131072);
|
||||
using (var archive = ZipArchive.CreateArchive())
|
||||
{
|
||||
archive.SaveTo("output.zip", options);
|
||||
@@ -102,10 +103,13 @@ using (var archive = ZipArchive.CreateArchive())
|
||||
var options2 = new WriterOptions(CompressionType.Deflate)
|
||||
{
|
||||
CompressionLevel = 9,
|
||||
LeaveStreamOpen = false
|
||||
LeaveStreamOpen = false,
|
||||
BufferSize = 131072
|
||||
};
|
||||
```
|
||||
|
||||
`WriterOptions.BufferSize` controls stream copy buffers used while writing archive entries. If it is not set, SharpCompress falls back to `Constants.BufferSize`.
|
||||
|
||||
---
|
||||
|
||||
## Archive API Methods
|
||||
@@ -315,6 +319,9 @@ var safeOptions = ExtractionOptions.SafeExtract; // No overwrite
|
||||
var flatOptions = ExtractionOptions.FlatExtract; // No directory structure
|
||||
var metadataOptions = ExtractionOptions.PreserveMetadata; // Keep timestamps and attributes
|
||||
|
||||
// Tune extraction copy buffering
|
||||
var extractionOptions = new ExtractionOptions { BufferSize = 131072 };
|
||||
|
||||
// Factory defaults:
|
||||
// - file path / FileInfo overloads use LeaveStreamOpen = false
|
||||
// - stream overloads use LeaveStreamOpen = true
|
||||
@@ -334,6 +341,13 @@ var options = new ReaderOptions
|
||||
BufferSize = 81920,
|
||||
RewindableBufferSize = 1_048_576,
|
||||
};
|
||||
|
||||
var extractionOptions = new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true,
|
||||
BufferSize = 131072,
|
||||
};
|
||||
```
|
||||
|
||||
### WriterOptions
|
||||
|
||||
@@ -100,7 +100,12 @@ using (var archive = RarArchive.OpenArchive("Test.rar", ReaderOptions.ForFilePat
|
||||
// Simple extraction with RarArchive; this WriteToDirectory pattern works for all archive types
|
||||
archive.WriteToDirectory(
|
||||
@"D:\temp",
|
||||
new ExtractionOptions { ExtractFullPath = true, Overwrite = true }
|
||||
new ExtractionOptions
|
||||
{
|
||||
ExtractFullPath = true,
|
||||
Overwrite = true,
|
||||
BufferSize = 131072,
|
||||
}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.300",
|
||||
"rollForward": "latestPatch"
|
||||
"rollForward": "disable"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,14 @@ public static class IArchiveEntryExtensions
|
||||
/// </summary>
|
||||
/// <param name="streamToWriteTo">The stream to write the entry content to.</param>
|
||||
/// <param name="progress">Optional progress reporter for tracking extraction progress.</param>
|
||||
public void WriteTo(Stream streamToWriteTo, IProgress<ProgressReport>? progress = null)
|
||||
public void WriteTo(Stream streamToWriteTo, IProgress<ProgressReport>? progress = null) =>
|
||||
archiveEntry.WriteTo(streamToWriteTo, null, progress);
|
||||
|
||||
private void WriteTo(
|
||||
Stream streamToWriteTo,
|
||||
int? bufferSize,
|
||||
IProgress<ProgressReport>? progress = null
|
||||
)
|
||||
{
|
||||
if (archiveEntry.IsDirectory)
|
||||
{
|
||||
@@ -26,7 +33,7 @@ public static class IArchiveEntryExtensions
|
||||
|
||||
using var entryStream = archiveEntry.OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
|
||||
sourceStream.CopyTo(streamToWriteTo, Constants.BufferSize);
|
||||
sourceStream.CopyTo(streamToWriteTo, bufferSize ?? Constants.BufferSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,6 +47,18 @@ public static class IArchiveEntryExtensions
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
await archiveEntry
|
||||
.WriteToAsync(streamToWriteTo, Constants.BufferSize, progress, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async ValueTask WriteToAsync(
|
||||
Stream streamToWriteTo,
|
||||
int? bufferSize,
|
||||
IProgress<ProgressReport>? progress = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
if (archiveEntry.IsDirectory)
|
||||
{
|
||||
@@ -57,7 +76,7 @@ public static class IArchiveEntryExtensions
|
||||
#endif
|
||||
var sourceStream = WrapWithProgress(entryStream, archiveEntry, progress);
|
||||
await sourceStream
|
||||
.CopyToAsync(streamToWriteTo, Constants.BufferSize, cancellationToken)
|
||||
.CopyToAsync(streamToWriteTo, bufferSize ?? Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -133,16 +152,18 @@ public static class IArchiveEntryExtensions
|
||||
/// <summary>
|
||||
/// Extract to specific file
|
||||
/// </summary>
|
||||
public void WriteToFile(string destinationFileName, ExtractionOptions? options = null) =>
|
||||
public void WriteToFile(string destinationFileName, ExtractionOptions? options = null)
|
||||
{
|
||||
entry.WriteEntryToFile(
|
||||
destinationFileName,
|
||||
options,
|
||||
(x, fm) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
entry.WriteTo(fs);
|
||||
using var fs = File.Open(x, fm);
|
||||
entry.WriteTo(fs, options?.BufferSize ?? Constants.BufferSize);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extract to specific file asynchronously
|
||||
@@ -158,8 +179,10 @@ public static class IArchiveEntryExtensions
|
||||
options,
|
||||
async (x, fm, ct) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
await entry.WriteToAsync(fs, null, ct).ConfigureAwait(false);
|
||||
using var fs = File.Open(x, fm);
|
||||
await entry
|
||||
.WriteToAsync(fs, options?.BufferSize, null, ct)
|
||||
.ConfigureAwait(false);
|
||||
},
|
||||
cancellationToken
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ public static class Constants
|
||||
/// The default buffer size for stream operations, matching .NET's Stream.CopyTo default of 81920 bytes.
|
||||
/// This can be modified globally at runtime.
|
||||
/// </summary>
|
||||
// TODO: Revisit remaining non-extraction usages after extraction buffering moves to ExtractionOptions.
|
||||
public static int BufferSize { get; set; } = 81920;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -38,6 +38,11 @@ public sealed record ExtractionOptions : IExtractionOptions
|
||||
/// </summary>
|
||||
public bool PreserveAttributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer size for extraction stream copy operations.
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; } = Constants.BufferSize;
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for writing symbolic links to disk.
|
||||
/// The first parameter is the source path (where the symlink is created).
|
||||
|
||||
@@ -30,6 +30,11 @@ public interface IExtractionOptions
|
||||
/// </summary>
|
||||
bool PreserveAttributes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer size for extraction stream copy operations.
|
||||
/// </summary>
|
||||
int BufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Delegate for writing symbolic links to disk.
|
||||
/// The first parameter is the source path (where the symlink is created).
|
||||
|
||||
@@ -19,6 +19,11 @@ public interface IWriterOptions : IStreamOptions, IEncodingOptions, IProgressOpt
|
||||
/// </summary>
|
||||
int CompressionLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer size for writer stream copy operations.
|
||||
/// </summary>
|
||||
int BufferSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Registry of compression providers.
|
||||
/// Defaults to <see cref="CompressionProviderRegistry.Default" /> but can be replaced with custom providers, such as
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Readers;
|
||||
|
||||
@@ -138,19 +137,19 @@ public abstract partial class AbstractReader<TEntry, TVolume>
|
||||
_wroteCurrentEntry = true;
|
||||
}
|
||||
|
||||
internal async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken)
|
||||
private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
await sourceStream
|
||||
.CopyToAsync(writeStream, Constants.BufferSize, cancellationToken)
|
||||
.CopyToAsync(writeStream, Options.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
await sourceStream
|
||||
.CopyToAsync(writeStream, Constants.BufferSize, cancellationToken)
|
||||
.CopyToAsync(writeStream, Options.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
@@ -187,9 +186,7 @@ public abstract partial class AbstractReader<TEntry, TVolume>
|
||||
/// <summary>
|
||||
/// Moves the current async enumerator to the next entry.
|
||||
/// </summary>
|
||||
internal virtual ValueTask<bool> NextEntryForCurrentStreamAsync(
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
private ValueTask<bool> NextEntryForCurrentStreamAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_entriesForCurrentReadStreamAsync is not null)
|
||||
{
|
||||
@@ -202,6 +199,9 @@ public abstract partial class AbstractReader<TEntry, TVolume>
|
||||
// Async iterator method
|
||||
protected virtual async IAsyncEnumerable<TEntry> GetEntriesAsync(Stream stream)
|
||||
{
|
||||
#pragma warning disable VSTHRD111
|
||||
await Task.CompletedTask;
|
||||
#pragma warning restore VSTHRD111
|
||||
foreach (var entry in GetEntries(stream))
|
||||
{
|
||||
yield return entry;
|
||||
|
||||
@@ -179,7 +179,10 @@ public abstract partial class AbstractReader<TEntry, TVolume> : IReader, IAsyncR
|
||||
s.SkipEntry();
|
||||
}
|
||||
|
||||
public void WriteEntryTo(Stream writableStream)
|
||||
public void WriteEntryTo(Stream writableStream) =>
|
||||
WriteEntryTo(writableStream, Options.BufferSize);
|
||||
|
||||
private void WriteEntryTo(Stream writableStream, int bufferSize)
|
||||
{
|
||||
if (_wroteCurrentEntry)
|
||||
{
|
||||
@@ -194,15 +197,17 @@ public abstract partial class AbstractReader<TEntry, TVolume> : IReader, IAsyncR
|
||||
);
|
||||
}
|
||||
|
||||
Write(writableStream);
|
||||
Write(writableStream, bufferSize);
|
||||
_wroteCurrentEntry = true;
|
||||
}
|
||||
|
||||
internal void Write(Stream writeStream)
|
||||
internal void Write(Stream writeStream) => Write(writeStream, Options.BufferSize);
|
||||
|
||||
internal void Write(Stream writeStream, int bufferSize)
|
||||
{
|
||||
using Stream s = OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
sourceStream.CopyTo(writeStream, Constants.BufferSize);
|
||||
sourceStream.CopyTo(writeStream, bufferSize);
|
||||
}
|
||||
|
||||
private Stream WrapWithProgress(Stream source, Entry entry)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Readers;
|
||||
|
||||
@@ -42,7 +44,8 @@ public static class IAsyncReaderExtensions
|
||||
async (x, fm, ct) =>
|
||||
{
|
||||
using var fs = File.Open(x, fm);
|
||||
await reader.WriteEntryToAsync(fs, ct).ConfigureAwait(false);
|
||||
await CopyEntryToAsync(reader, fs, options?.BufferSize, ct)
|
||||
.ConfigureAwait(false);
|
||||
},
|
||||
cancellationToken
|
||||
)
|
||||
@@ -77,7 +80,8 @@ public static class IAsyncReaderExtensions
|
||||
async (x, fm, ct) =>
|
||||
{
|
||||
using var fs = File.Open(x, fm);
|
||||
await reader.WriteEntryToAsync(fs, ct).ConfigureAwait(false);
|
||||
await CopyEntryToAsync(reader, fs, options?.BufferSize, ct)
|
||||
.ConfigureAwait(false);
|
||||
},
|
||||
cancellationToken
|
||||
)
|
||||
@@ -92,4 +96,58 @@ public static class IAsyncReaderExtensions
|
||||
.WriteEntryToAsync(destinationFileInfo.FullName, options, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async ValueTask CopyEntryToAsync(
|
||||
IAsyncReader reader,
|
||||
Stream writableStream,
|
||||
int? bufferSize,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await reader
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await using var entryStream = await reader
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
var sourceStream = WrapWithProgress(entryStream, reader.Entry);
|
||||
await sourceStream
|
||||
.CopyToAsync(writableStream, bufferSize ?? Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static Stream WrapWithProgress(Stream source, IEntry entry)
|
||||
{
|
||||
var progress = entry.Options.Progress;
|
||||
if (progress is null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
var entryPath = entry.Key ?? string.Empty;
|
||||
var totalBytes = GetEntrySizeSafe(entry);
|
||||
return new ProgressReportingStream(
|
||||
source,
|
||||
progress,
|
||||
entryPath,
|
||||
totalBytes,
|
||||
leaveOpen: true
|
||||
);
|
||||
}
|
||||
|
||||
private static long? GetEntrySizeSafe(IEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
var size = entry.Size;
|
||||
return size >= 0 ? size : null;
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using SharpCompress.Common;
|
||||
using SharpCompress.IO;
|
||||
|
||||
namespace SharpCompress.Readers;
|
||||
|
||||
@@ -58,9 +60,48 @@ public static class IReaderExtensions
|
||||
options,
|
||||
(x, fm) =>
|
||||
{
|
||||
using var fs = File.Open(destinationFileName, fm);
|
||||
reader.WriteEntryTo(fs);
|
||||
using var fs = File.Open(x, fm);
|
||||
CopyEntryTo(reader, fs, options?.BufferSize ?? Constants.BufferSize);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static void CopyEntryTo(IReader reader, Stream writableStream, int bufferSize)
|
||||
{
|
||||
using var entryStream = reader.OpenEntryStream();
|
||||
var sourceStream = WrapWithProgress(entryStream, reader.Entry);
|
||||
sourceStream.CopyTo(writableStream, bufferSize);
|
||||
}
|
||||
|
||||
private static Stream WrapWithProgress(Stream source, IEntry entry)
|
||||
{
|
||||
var progress = entry.Options.Progress;
|
||||
if (progress is null)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
var entryPath = entry.Key ?? string.Empty;
|
||||
var totalBytes = GetEntrySizeSafe(entry);
|
||||
return new ProgressReportingStream(
|
||||
source,
|
||||
progress,
|
||||
entryPath,
|
||||
totalBytes,
|
||||
leaveOpen: true
|
||||
);
|
||||
}
|
||||
|
||||
private static long? GetEntrySizeSafe(IEntry entry)
|
||||
{
|
||||
try
|
||||
{
|
||||
var size = entry.Size;
|
||||
return size >= 0 ? size : null;
|
||||
}
|
||||
catch (NotImplementedException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +65,14 @@ internal static partial class Utility
|
||||
public async ValueTask<long> TransferToAsync(
|
||||
Stream destination,
|
||||
long maxLength,
|
||||
int? bufferSize = null,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
// Use ReadOnlySubStream to limit reading and leverage framework's CopyToAsync
|
||||
using var limitedStream = new IO.ReadOnlySubStream(source, maxLength);
|
||||
await limitedStream
|
||||
.CopyToAsync(destination, Constants.BufferSize, cancellationToken)
|
||||
.CopyToAsync(destination, bufferSize ?? Constants.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
return limitedStream.Position;
|
||||
}
|
||||
|
||||
@@ -150,11 +150,11 @@ internal static partial class Utility
|
||||
|
||||
extension(Stream source)
|
||||
{
|
||||
public long TransferTo(Stream destination, long maxLength)
|
||||
public long TransferTo(Stream destination, long maxLength, int? bufferSize)
|
||||
{
|
||||
// Use ReadOnlySubStream to limit reading and leverage framework's CopyTo
|
||||
using var limitedStream = new IO.ReadOnlySubStream(source, maxLength);
|
||||
limitedStream.CopyTo(destination, Constants.BufferSize);
|
||||
limitedStream.CopyTo(destination, bufferSize ?? Constants.BufferSize);
|
||||
return limitedStream.Position;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ public sealed partial class GZipWriter : AbstractWriter
|
||||
}
|
||||
|
||||
var progressStream = WrapWithProgress(source, filename);
|
||||
progressStream.CopyTo(OutputStream.NotNull(), Constants.BufferSize);
|
||||
progressStream.CopyTo(OutputStream.NotNull(), WriterOptions.BufferSize);
|
||||
_wroteToStream = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,6 +68,11 @@ public sealed record GZipWriterOptions : IWriterOptions
|
||||
/// </summary>
|
||||
public IProgress<ProgressReport>? Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer size for writer stream copy operations.
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; } = Constants.BufferSize;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of compression providers.
|
||||
/// Defaults to <see cref="CompressionProviderRegistry.Default" /> but can be replaced with custom implementations, such as
|
||||
@@ -115,6 +120,7 @@ public sealed record GZipWriterOptions : IWriterOptions
|
||||
LeaveStreamOpen = options.LeaveStreamOpen;
|
||||
ArchiveEncoding = options.ArchiveEncoding;
|
||||
Progress = options.Progress;
|
||||
BufferSize = options.BufferSize;
|
||||
Providers = options.Providers;
|
||||
}
|
||||
|
||||
@@ -128,6 +134,7 @@ public sealed record GZipWriterOptions : IWriterOptions
|
||||
LeaveStreamOpen = options.LeaveStreamOpen;
|
||||
ArchiveEncoding = options.ArchiveEncoding;
|
||||
Progress = options.Progress;
|
||||
BufferSize = options.BufferSize;
|
||||
Providers = options.Providers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,11 @@ public sealed record SevenZipWriterOptions : IWriterOptions
|
||||
/// </summary>
|
||||
public IProgress<ProgressReport>? Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer size for writer stream copy operations.
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; } = Constants.BufferSize;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of compression providers.
|
||||
/// Defaults to <see cref="CompressionProviderRegistry.Default" /> but can be replaced with custom implementations.
|
||||
@@ -103,6 +108,7 @@ public sealed record SevenZipWriterOptions : IWriterOptions
|
||||
LeaveStreamOpen = options.LeaveStreamOpen;
|
||||
ArchiveEncoding = options.ArchiveEncoding;
|
||||
Progress = options.Progress;
|
||||
BufferSize = options.BufferSize;
|
||||
Providers = options.Providers;
|
||||
}
|
||||
|
||||
@@ -117,6 +123,7 @@ public sealed record SevenZipWriterOptions : IWriterOptions
|
||||
LeaveStreamOpen = options.LeaveStreamOpen;
|
||||
ArchiveEncoding = options.ArchiveEncoding;
|
||||
Progress = options.Progress;
|
||||
BufferSize = options.BufferSize;
|
||||
Providers = options.Providers;
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +104,12 @@ public partial class TarWriter
|
||||
await header.WriteAsync(OutputStream.NotNull(), cancellationToken).ConfigureAwait(false);
|
||||
var progressStream = WrapWithProgress(source, filename);
|
||||
var written = await progressStream
|
||||
.TransferToAsync(OutputStream.NotNull(), realSize, cancellationToken)
|
||||
.TransferToAsync(
|
||||
OutputStream.NotNull(),
|
||||
realSize,
|
||||
WriterOptions.BufferSize,
|
||||
cancellationToken
|
||||
)
|
||||
.ConfigureAwait(false);
|
||||
await PadTo512Async(written, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -133,7 +133,11 @@ public partial class TarWriter : AbstractWriter
|
||||
header.Size = realSize;
|
||||
header.Write(OutputStream.NotNull());
|
||||
var progressStream = WrapWithProgress(source, filename);
|
||||
size = progressStream.TransferTo(OutputStream.NotNull(), realSize);
|
||||
size = progressStream.TransferTo(
|
||||
OutputStream.NotNull(),
|
||||
realSize,
|
||||
WriterOptions.BufferSize
|
||||
);
|
||||
PadTo512(size.Value);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,11 @@ public sealed record TarWriterOptions : IWriterOptions
|
||||
/// </summary>
|
||||
public IProgress<ProgressReport>? Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer size for writer stream copy operations.
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; } = Constants.BufferSize;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of compression providers.
|
||||
/// Defaults to <see cref="CompressionProviderRegistry.Default" /> but can be replaced with custom implementations.
|
||||
@@ -104,6 +109,7 @@ public sealed record TarWriterOptions : IWriterOptions
|
||||
LeaveStreamOpen = options.LeaveStreamOpen;
|
||||
ArchiveEncoding = options.ArchiveEncoding;
|
||||
Progress = options.Progress;
|
||||
BufferSize = options.BufferSize;
|
||||
Providers = options.Providers;
|
||||
}
|
||||
|
||||
@@ -118,6 +124,7 @@ public sealed record TarWriterOptions : IWriterOptions
|
||||
LeaveStreamOpen = options.LeaveStreamOpen;
|
||||
ArchiveEncoding = options.ArchiveEncoding;
|
||||
Progress = options.Progress;
|
||||
BufferSize = options.BufferSize;
|
||||
Providers = options.Providers;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,11 @@ public sealed record WriterOptions : IWriterOptions
|
||||
/// </summary>
|
||||
public IProgress<ProgressReport>? Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer size for writer stream copy operations.
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; } = Constants.BufferSize;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of compression providers.
|
||||
/// Defaults to <see cref="CompressionProviderRegistry.Default" /> but can be replaced with custom implementations, such as
|
||||
|
||||
@@ -53,6 +53,15 @@ public static class WriterOptionsExtensions
|
||||
),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy with the specified buffer size.
|
||||
/// </summary>
|
||||
public static WriterOptions WithBufferSize(this WriterOptions options, int bufferSize) =>
|
||||
options with
|
||||
{
|
||||
BufferSize = bufferSize,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy with the specified compression level.
|
||||
/// </summary>
|
||||
|
||||
@@ -16,7 +16,6 @@ using SharpCompress.Compressors.PPMd;
|
||||
using SharpCompress.Compressors.ZStandard;
|
||||
using SharpCompress.IO;
|
||||
using SharpCompress.Providers;
|
||||
using Constants = SharpCompress.Common.Constants;
|
||||
|
||||
namespace SharpCompress.Writers.Zip;
|
||||
|
||||
@@ -89,7 +88,7 @@ public partial class ZipWriter : AbstractWriter
|
||||
{
|
||||
using var output = WriteToStream(entryPath, zipWriterEntryOptions);
|
||||
var progressStream = WrapWithProgress(source, entryPath);
|
||||
progressStream.CopyTo(output, Constants.BufferSize);
|
||||
progressStream.CopyTo(output, WriterOptions.BufferSize);
|
||||
}
|
||||
|
||||
public Stream WriteToStream(string entryPath, ZipWriterEntryOptions options)
|
||||
|
||||
@@ -61,6 +61,11 @@ public sealed record ZipWriterOptions : IWriterOptions
|
||||
/// </summary>
|
||||
public IProgress<ProgressReport>? Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Buffer size for writer stream copy operations.
|
||||
/// </summary>
|
||||
public int BufferSize { get; set; } = Constants.BufferSize;
|
||||
|
||||
/// <summary>
|
||||
/// Registry of compression providers.
|
||||
/// Defaults to <see cref="CompressionProviderRegistry.Default" /> but can be replaced with custom implementations.
|
||||
@@ -128,6 +133,7 @@ public sealed record ZipWriterOptions : IWriterOptions
|
||||
LeaveStreamOpen = options.LeaveStreamOpen;
|
||||
ArchiveEncoding = options.ArchiveEncoding;
|
||||
Progress = options.Progress;
|
||||
BufferSize = options.BufferSize;
|
||||
Providers = options.Providers;
|
||||
}
|
||||
|
||||
@@ -141,6 +147,7 @@ public sealed record ZipWriterOptions : IWriterOptions
|
||||
LeaveStreamOpen = options.LeaveStreamOpen;
|
||||
ArchiveEncoding = options.ArchiveEncoding;
|
||||
Progress = options.Progress;
|
||||
BufferSize = options.BufferSize;
|
||||
Providers = options.Providers;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#if !LEGACY_DOTNET
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Common;
|
||||
@@ -10,6 +12,7 @@ using SharpCompress.Readers;
|
||||
using SharpCompress.Test.Mocks;
|
||||
using SharpCompress.Writers;
|
||||
using SharpCompress.Writers.GZip;
|
||||
using SharpCompress.Writers.Tar;
|
||||
using SharpCompress.Writers.Zip;
|
||||
using Xunit;
|
||||
|
||||
@@ -132,11 +135,16 @@ public class OptionsUsabilityTests : TestBase
|
||||
[Fact]
|
||||
public void WriterOptions_Fluent_Methods_Modify_Correctly()
|
||||
{
|
||||
var options = WriterOptions.ForZip().WithLeaveStreamOpen(false).WithCompressionLevel(9);
|
||||
var options = WriterOptions
|
||||
.ForZip()
|
||||
.WithLeaveStreamOpen(false)
|
||||
.WithCompressionLevel(9)
|
||||
.WithBufferSize(65536);
|
||||
|
||||
Assert.Equal(CompressionType.Deflate, options.CompressionType);
|
||||
Assert.Equal(9, options.CompressionLevel);
|
||||
Assert.False(options.LeaveStreamOpen);
|
||||
Assert.Equal(65536, options.BufferSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -160,6 +168,73 @@ public class OptionsUsabilityTests : TestBase
|
||||
Assert.Equal(factoryApproach.LeaveStreamOpen, constructorApproach.LeaveStreamOpen);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriterOptions_Default_BufferSize_Uses_Constants_BufferSize()
|
||||
{
|
||||
Assert.Equal(Constants.BufferSize, WriterOptions.ForZip().BufferSize);
|
||||
Assert.Equal(
|
||||
Constants.BufferSize,
|
||||
new ZipWriterOptions(CompressionType.Deflate).BufferSize
|
||||
);
|
||||
Assert.Equal(
|
||||
Constants.BufferSize,
|
||||
new TarWriterOptions(CompressionType.None, true).BufferSize
|
||||
);
|
||||
Assert.Equal(Constants.BufferSize, new GZipWriterOptions().BufferSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Format_WriterOptions_Copy_BufferSize()
|
||||
{
|
||||
var options = WriterOptions.ForZip().WithBufferSize(12345);
|
||||
|
||||
Assert.Equal(12345, new ZipWriterOptions(options).BufferSize);
|
||||
Assert.Equal(12345, new TarWriterOptions(options).BufferSize);
|
||||
Assert.Equal(12345, new GZipWriterOptions(options).BufferSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZipWriter_Uses_WriterOptions_BufferSize()
|
||||
{
|
||||
using var source = new TrackingReadStream(new byte[100]);
|
||||
using var destination = new MemoryStream();
|
||||
using var writer = new ZipWriter(
|
||||
destination,
|
||||
new ZipWriterOptions(CompressionType.None) { BufferSize = 17 }
|
||||
);
|
||||
|
||||
writer.Write("buffer-size.txt", source, DateTime.Now);
|
||||
|
||||
Assert.Equal(17, source.CopyBufferSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GZipWriter_Uses_WriterOptions_BufferSize()
|
||||
{
|
||||
using var source = new TrackingReadStream(new byte[100]);
|
||||
using var destination = new MemoryStream();
|
||||
using var writer = new GZipWriter(destination, new GZipWriterOptions { BufferSize = 19 });
|
||||
|
||||
writer.Write("buffer-size.txt", source, DateTime.Now);
|
||||
|
||||
Assert.Equal(19, source.CopyBufferSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TarWriter_Uses_WriterOptions_BufferSize_ForTransfer()
|
||||
{
|
||||
using var source = new MemoryStream(new byte[100]);
|
||||
using var destination = new MemoryStream();
|
||||
using var writer = new TarWriter(
|
||||
destination,
|
||||
new TarWriterOptions(CompressionType.None, true) { BufferSize = 0 }
|
||||
);
|
||||
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
writer.Write("buffer-size.txt", source, DateTime.Now)
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReaderOptions_Fluent_Methods_Modify_Correctly()
|
||||
{
|
||||
@@ -229,6 +304,30 @@ public class OptionsUsabilityTests : TestBase
|
||||
var preserveMetadata = ExtractionOptions.PreserveMetadata;
|
||||
Assert.True(preserveMetadata.PreserveFileTime);
|
||||
Assert.True(preserveMetadata.PreserveAttributes);
|
||||
|
||||
Assert.Equal(Constants.BufferSize, new ExtractionOptions().BufferSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reader_WriteEntryToFile_Uses_ExtractionOptions_BufferSize()
|
||||
{
|
||||
using var reader = new TrackingReader();
|
||||
var destination = Path.Combine(SCRATCH_FILES_PATH, "reader-buffer-size.txt");
|
||||
|
||||
reader.WriteEntryToFile(destination, new ExtractionOptions { BufferSize = 11 });
|
||||
|
||||
Assert.Equal(11, reader.EntryStreamCopyBufferSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reader_WriteEntryToFileAsync_Uses_ExtractionOptions_BufferSize()
|
||||
{
|
||||
await using var reader = new TrackingReader();
|
||||
var destination = Path.Combine(SCRATCH_FILES_PATH, "reader-buffer-size-async.txt");
|
||||
|
||||
await reader.WriteEntryToFileAsync(destination, new ExtractionOptions { BufferSize = 13 });
|
||||
|
||||
Assert.Equal(13, reader.EntryStreamCopyBufferSize);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -321,4 +420,128 @@ public class OptionsUsabilityTests : TestBase
|
||||
Assert.Null(noPassword.Password);
|
||||
Assert.Equal(1_048_576, noPassword.RewindableBufferSize);
|
||||
}
|
||||
|
||||
private sealed class TestArchiveEntry(Stream source) : IArchiveEntry
|
||||
{
|
||||
public CompressionType CompressionType => CompressionType.None;
|
||||
public DateTime? ArchivedTime => null;
|
||||
public long CompressedSize => source.Length;
|
||||
public long Crc => 0;
|
||||
public DateTime? CreatedTime => null;
|
||||
public string? Key => "buffer-size.txt";
|
||||
public string? LinkTarget => null;
|
||||
public bool IsDirectory => false;
|
||||
public bool IsEncrypted => false;
|
||||
public bool IsSplitAfter => false;
|
||||
public bool IsSolid => false;
|
||||
public int VolumeIndexFirst => 0;
|
||||
public int VolumeIndexLast => 0;
|
||||
public DateTime? LastAccessedTime => null;
|
||||
public DateTime? LastModifiedTime => null;
|
||||
public long Size => source.Length;
|
||||
public int? Attrib => null;
|
||||
public SharpCompress.Common.Options.IReaderOptions Options =>
|
||||
ReaderOptions.ForExternalStream;
|
||||
public bool IsComplete => true;
|
||||
public IArchive Archive => throw new NotSupportedException();
|
||||
|
||||
public Stream OpenEntryStream()
|
||||
{
|
||||
source.Position = 0;
|
||||
return source;
|
||||
}
|
||||
|
||||
public ValueTask<Stream> OpenEntryStreamAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
source.Position = 0;
|
||||
return new ValueTask<Stream>(source);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TrackingReadStream(byte[] data) : MemoryStream(data)
|
||||
{
|
||||
public int? CopyBufferSize { get; private set; }
|
||||
|
||||
public override void CopyTo(Stream destination, int bufferSize)
|
||||
{
|
||||
CopyBufferSize = bufferSize;
|
||||
base.CopyTo(destination, bufferSize);
|
||||
}
|
||||
|
||||
public override Task CopyToAsync(
|
||||
Stream destination,
|
||||
int bufferSize,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
CopyBufferSize = bufferSize;
|
||||
return base.CopyToAsync(destination, bufferSize, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TrackingReader : IReader, IAsyncReader
|
||||
{
|
||||
public ArchiveType Type => ArchiveType.Zip;
|
||||
public MemoryStream Source { get; } = new(new byte[100]);
|
||||
public IEntry Entry => new TestArchiveEntry(Source);
|
||||
public bool Cancelled => false;
|
||||
public int? EntryStreamCopyBufferSize { get; private set; }
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
public ValueTask DisposeAsync() => default;
|
||||
|
||||
public void WriteEntryTo(Stream writableStream) => throw new NotSupportedException();
|
||||
|
||||
public ValueTask WriteEntryToAsync(
|
||||
Stream writableStream,
|
||||
CancellationToken cancellationToken = default
|
||||
) => throw new NotSupportedException();
|
||||
|
||||
public void Cancel() { }
|
||||
|
||||
public bool MoveToNextEntry() => false;
|
||||
|
||||
public ValueTask<bool> MoveToNextEntryAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
) => new(false);
|
||||
|
||||
public EntryStream OpenEntryStream()
|
||||
{
|
||||
Source.Position = 0;
|
||||
return new TrackingEntryStream(
|
||||
this,
|
||||
Source,
|
||||
bufferSize => EntryStreamCopyBufferSize = bufferSize
|
||||
);
|
||||
}
|
||||
|
||||
public ValueTask<EntryStream> OpenEntryStreamAsync(
|
||||
CancellationToken cancellationToken = default
|
||||
) => new(OpenEntryStream());
|
||||
}
|
||||
|
||||
private sealed class TrackingEntryStream(
|
||||
IReader reader,
|
||||
Stream stream,
|
||||
Action<int> copyBufferSize
|
||||
) : EntryStream(reader, stream)
|
||||
{
|
||||
public override void CopyTo(Stream destination, int bufferSize)
|
||||
{
|
||||
copyBufferSize(bufferSize);
|
||||
base.CopyTo(destination, bufferSize);
|
||||
}
|
||||
|
||||
public override Task CopyToAsync(
|
||||
Stream destination,
|
||||
int bufferSize,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
copyBufferSize(bufferSize);
|
||||
return base.CopyToAsync(destination, bufferSize, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -579,7 +579,7 @@ public class UtilityTests
|
||||
using var source = new MemoryStream(sourceData);
|
||||
using var destination = new MemoryStream();
|
||||
|
||||
var transferred = source.TransferTo(destination, 5);
|
||||
var transferred = source.TransferTo(destination, 5, null);
|
||||
|
||||
Assert.Equal(5, transferred);
|
||||
Assert.Equal(new byte[] { 1, 2, 3, 4, 5 }, destination.ToArray());
|
||||
@@ -592,7 +592,7 @@ public class UtilityTests
|
||||
using var source = new MemoryStream(sourceData);
|
||||
using var destination = new MemoryStream();
|
||||
|
||||
var transferred = source.TransferTo(destination, 100);
|
||||
var transferred = source.TransferTo(destination, 100, null);
|
||||
|
||||
Assert.Equal(3, transferred);
|
||||
Assert.Equal(sourceData, destination.ToArray());
|
||||
@@ -604,7 +604,7 @@ public class UtilityTests
|
||||
using var source = new MemoryStream();
|
||||
using var destination = new MemoryStream();
|
||||
|
||||
var transferred = source.TransferTo(destination, 100);
|
||||
var transferred = source.TransferTo(destination, 100, null);
|
||||
|
||||
Assert.Equal(0, transferred);
|
||||
Assert.Empty(destination.ToArray());
|
||||
|
||||
Reference in New Issue
Block a user