mirror of
https://github.com/adamhathcock/sharpcompress.git
synced 2026-09-22 06:54:40 +00:00
Provide a better way to manage async disposable streams
This commit is contained in:
@@ -108,15 +108,12 @@ public static class IArchiveEntryExtensions
|
||||
throw new ExtractionException("Entry is a file directory and cannot be extracted.");
|
||||
}
|
||||
|
||||
#if LEGACY_DOTNET
|
||||
using var entryStream = await archiveEntry
|
||||
var entryStream = await archiveEntry
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#else
|
||||
await using var entryStream = await archiveEntry
|
||||
.OpenEntryStreamAsync(cancellationToken)
|
||||
await using var entryStreamScope = entryStream
|
||||
.DisposeAsyncScope()
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
var checkedStream = options is null
|
||||
? entryStream
|
||||
: IEntryExtensions.WrapWithChecksumValidation(archiveEntry, entryStream, options);
|
||||
|
||||
@@ -17,7 +17,6 @@ public partial class EntryStream
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
#if !LEGACY_DOTNET
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_isDisposed)
|
||||
@@ -43,9 +42,8 @@ public partial class EntryStream
|
||||
}
|
||||
}
|
||||
await base.DisposeAsync().ConfigureAwait(false);
|
||||
await _stream.DisposeAsync().ConfigureAwait(false);
|
||||
await _stream.DisposeAsyncCompat().ConfigureAwait(false);
|
||||
}
|
||||
#endif
|
||||
|
||||
public override async Task<int> ReadAsync(
|
||||
byte[] buffer,
|
||||
|
||||
@@ -8,7 +8,7 @@ using SharpCompress.Readers;
|
||||
|
||||
namespace SharpCompress.Common;
|
||||
|
||||
public partial class EntryStream : Stream
|
||||
public partial class EntryStream : AsyncDisposableStream
|
||||
{
|
||||
private readonly IReader _reader;
|
||||
private readonly Stream _stream;
|
||||
|
||||
36
src/SharpCompress/IO/AsyncDisposableStream.cs
Normal file
36
src/SharpCompress/IO/AsyncDisposableStream.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Stream"/> that is guaranteed to be asynchronously disposable on every target framework.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// On .NET Framework 4.8 and .NET Standard 2.0, <see cref="Stream"/> has no <c>DisposeAsync</c>.
|
||||
/// <c>Microsoft.Bcl.AsyncInterfaces</c> supplies the <see cref="IAsyncDisposable"/> interface on those
|
||||
/// targets but cannot retrofit it onto the BCL's <see cref="Stream"/>, and C# will not accept an
|
||||
/// extension method for the pattern - <c>await using</c> requires a reachable <em>instance</em>
|
||||
/// <c>DisposeAsync</c>. Deriving from this class instead of <see cref="Stream"/> therefore makes a type
|
||||
/// usable with <c>await using</c> uniformly, with no conditional compilation at the call site.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The fallback below is the same behaviour as the BCL's own default <see cref="Stream.DisposeAsync"/>,
|
||||
/// so a derived type may call <c>await base.DisposeAsync()</c> unconditionally on any target.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public abstract class AsyncDisposableStream : Stream
|
||||
#if NO_STREAM_DISPOSEASYNC
|
||||
, IAsyncDisposable
|
||||
#endif
|
||||
{
|
||||
#if NO_STREAM_DISPOSEASYNC
|
||||
public virtual ValueTask DisposeAsync()
|
||||
{
|
||||
Dispose();
|
||||
return default;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
36
src/SharpCompress/IO/AsyncDisposeScope.cs
Normal file
36
src/SharpCompress/IO/AsyncDisposeScope.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Makes any resource usable with <c>await using</c>, disposing it asynchronously when the runtime type
|
||||
/// supports it and synchronously otherwise.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Needed for locals whose <em>static</em> type is <see cref="System.IO.Stream"/> (or another type that
|
||||
/// only sometimes has <c>DisposeAsync</c>), where <c>await using</c> cannot bind directly on
|
||||
/// .NET Framework 4.8 / .NET Standard 2.0. Unlike a compile-time guard, this picks the asynchronous path
|
||||
/// based on the runtime type, so a stream that really is asynchronously disposable is disposed that way on
|
||||
/// every target framework. Prefer deriving from <see cref="AsyncDisposableStream"/> where the type is ours.
|
||||
/// </remarks>
|
||||
internal readonly struct AsyncDisposeScope(IDisposable? resource) : IAsyncDisposable
|
||||
{
|
||||
public ValueTask DisposeAsync()
|
||||
{
|
||||
if (resource is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
return asyncDisposable.DisposeAsync();
|
||||
}
|
||||
|
||||
resource?.Dispose();
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mirrors <c>ConfiguredAsyncDisposable</c> so <c>await using</c> can specify context capture without
|
||||
/// boxing this struct through <see cref="IAsyncDisposable"/>.
|
||||
/// </summary>
|
||||
public ConfiguredAsyncDisposeScope ConfigureAwait(bool continueOnCapturedContext) =>
|
||||
new(resource, continueOnCapturedContext);
|
||||
}
|
||||
19
src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs
Normal file
19
src/SharpCompress/IO/ConfiguredAsyncDisposeScope.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharpCompress.IO;
|
||||
|
||||
internal readonly struct ConfiguredAsyncDisposeScope(IDisposable? resource, bool continueOnCapturedContext)
|
||||
{
|
||||
public ConfiguredValueTaskAwaitable DisposeAsync()
|
||||
{
|
||||
if (resource is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
return asyncDisposable.DisposeAsync().ConfigureAwait(continueOnCapturedContext);
|
||||
}
|
||||
|
||||
resource?.Dispose();
|
||||
return default(ValueTask).ConfigureAwait(continueOnCapturedContext);
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,20 @@ public static class StreamExtensions
|
||||
|
||||
public void Skip() => stream.CopyTo(Stream.Null);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a scope that disposes this stream when awaited, asynchronously where the runtime type
|
||||
/// supports it. Lets <c>await using</c> be written against a <see cref="Stream"/>-typed local on
|
||||
/// every target framework.
|
||||
/// </summary>
|
||||
internal AsyncDisposeScope DisposeAsyncScope() => new(stream);
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this stream, asynchronously where the runtime type supports it. Use where the static
|
||||
/// type is <see cref="Stream"/>, which has no <c>DisposeAsync</c> on .NET Framework 4.8 /
|
||||
/// .NET Standard 2.0.
|
||||
/// </summary>
|
||||
internal ValueTask DisposeAsyncCompat() => new AsyncDisposeScope(stream).DisposeAsync();
|
||||
|
||||
public async ValueTask SkipAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -104,13 +104,8 @@ public abstract partial class AbstractReader<TEntry, TVolume>
|
||||
}
|
||||
}
|
||||
//don't know the size so we have to try to decompress to skip
|
||||
#if LEGACY_DOTNET
|
||||
using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
|
||||
#else
|
||||
await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await s.SkipEntryAsync(cancellationToken).ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
public async ValueTask WriteEntryToAsync(
|
||||
@@ -139,19 +134,11 @@ public abstract partial class AbstractReader<TEntry, TVolume>
|
||||
|
||||
private async ValueTask WriteAsync(Stream writeStream, CancellationToken cancellationToken)
|
||||
{
|
||||
#if LEGACY_DOTNET
|
||||
using Stream s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var s = await OpenEntryStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var sourceStream = WrapWithProgress(s, Entry);
|
||||
await sourceStream
|
||||
.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, Options.BufferSize, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
#endif
|
||||
}
|
||||
|
||||
public async ValueTask<EntryStream> OpenEntryStreamAsync(
|
||||
|
||||
@@ -108,15 +108,9 @@ public static class IAsyncReaderExtensions
|
||||
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 checkedStream = IEntryExtensions.WrapWithChecksumValidation(
|
||||
reader.Entry,
|
||||
entryStream,
|
||||
|
||||
Reference in New Issue
Block a user