IArchiveAsync

This commit is contained in:
Adam Hathcock
2026-01-08 09:14:46 +00:00
parent 60d42ca9c3
commit 541fd136d5
5 changed files with 249 additions and 14 deletions

View File

@@ -1,14 +1,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.IO;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public abstract class AbstractArchive<TEntry, TVolume> : IArchive
public abstract class AbstractArchive<TEntry, TVolume> : IArchive, IArchiveAsync
where TEntry : IArchiveEntry
where TVolume : IVolume
{
@@ -26,6 +25,8 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive
_sourceStream = sourceStream;
_lazyVolumes = new LazyReadOnlyCollection<TVolume>(LoadVolumes(_sourceStream));
_lazyEntries = new LazyReadOnlyCollection<TEntry>(LoadEntries(Volumes));
_lazyVolumesAsync = new LazyAsyncReadOnlyCollection<TVolume>(LoadVolumesAsync(_sourceStream));
_lazyEntriesAsync = new LazyAsyncReadOnlyCollection<TEntry>(LoadEntriesAsync(_lazyVolumesAsync));
}
internal AbstractArchive(ArchiveType type)
@@ -34,24 +35,16 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive
ReaderOptions = new();
_lazyVolumes = new LazyReadOnlyCollection<TVolume>(Enumerable.Empty<TVolume>());
_lazyEntries = new LazyReadOnlyCollection<TEntry>(Enumerable.Empty<TEntry>());
_lazyVolumesAsync = new LazyAsyncReadOnlyCollection<TVolume>(AsyncEnumerableEx.Empty<TVolume>());
_lazyEntriesAsync = new LazyAsyncReadOnlyCollection<TEntry>(AsyncEnumerableEx.Empty<TEntry>());
}
public ArchiveType Type { get; }
private static Stream CheckStreams(Stream stream)
{
if (!stream.CanSeek || !stream.CanRead)
{
throw new ArchiveException("Archive streams must be Readable and Seekable");
}
return stream;
}
/// <summary>
/// Returns an ReadOnlyCollection of all the RarArchiveEntries across the one or many parts of the RarArchive.
/// </summary>
public virtual ICollection<TEntry> Entries => _lazyEntries;
/// <summary>
/// Returns an ReadOnlyCollection of all the RarArchiveVolumes across the one or many parts of the RarArchive.
/// </summary>
@@ -72,6 +65,9 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive
protected abstract IEnumerable<TVolume> LoadVolumes(SourceStream sourceStream);
protected abstract IEnumerable<TEntry> LoadEntries(IEnumerable<TVolume> volumes);
protected abstract IAsyncEnumerable<TVolume> LoadVolumesAsync(SourceStream sourceStream);
protected abstract IAsyncEnumerable<TEntry> LoadEntriesAsync(IAsyncEnumerable<TVolume> volumes);
IEnumerable<IArchiveEntry> IArchive.Entries => Entries.Cast<IArchiveEntry>();
IEnumerable<IVolume> IArchive.Volumes => _lazyVolumes.Cast<IVolume>();
@@ -140,4 +136,67 @@ public abstract class AbstractArchive<TEntry, TVolume> : IArchive
return Entries.All(x => x.IsComplete);
}
}
#region Async Support
private readonly LazyAsyncReadOnlyCollection<TVolume> _lazyVolumesAsync;
private readonly LazyAsyncReadOnlyCollection<TEntry> _lazyEntriesAsync;
public virtual async ValueTask DisposeAsync()
{
if (!_disposed)
{
await foreach (var v in _lazyVolumesAsync)
{
v.Dispose();
}
foreach (var v in _lazyEntriesAsync.GetLoaded().Cast<Entry>())
{
v.Close();
}
_sourceStream?.Dispose();
_disposed = true;
}
}
private async ValueTask EnsureEntriesLoadedAsync()
{
await _lazyEntriesAsync.EnsureFullyLoaded();
await _lazyVolumesAsync.EnsureFullyLoaded();
}
public virtual IAsyncEnumerable<TEntry> EntriesAsync => _lazyEntriesAsync;
IAsyncEnumerable<IArchiveEntry> IArchiveAsync.EntriesAsync => EntriesAsync.Cast<TEntry, IArchiveEntry>();
public IAsyncEnumerable<IVolume> VolumesAsync => _lazyVolumesAsync.Cast<TVolume, IVolume>();
public async ValueTask<IReader> ExtractAllEntriesAsync()
{
if (!IsSolid && Type != ArchiveType.SevenZip)
{
throw new SharpCompressException(
"ExtractAllEntries can only be used on solid archives or 7Zip archives (which require random access)."
);
}
await EnsureEntriesLoadedAsync();
return await CreateReaderForSolidExtractionAsync();
}
protected abstract ValueTask<IReader> CreateReaderForSolidExtractionAsync();
public virtual ValueTask<bool> IsSolidAsync() => new (false);
public async ValueTask<bool> IsCompleteAsync()
{
await EnsureEntriesLoadedAsync();
return await EntriesAsync.All(x => x.IsComplete);
}
public async ValueTask<long> TotalSizeAsync() => await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.CompressedSize);
public async ValueTask<long> TotalUncompressSizeAsync() => await EntriesAsync.Aggregate(0L, (total, cf) => total + cf.Size);
#endregion
}

View File

@@ -1,10 +1,48 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Readers;
namespace SharpCompress.Archives;
public interface IArchiveAsync : IAsyncDisposable
{
IAsyncEnumerable<IArchiveEntry> EntriesAsync { get; }
IAsyncEnumerable<IVolume> VolumesAsync { get; }
ArchiveType Type { get; }
/// <summary>
/// Use this method to extract all entries in an archive in order.
/// This is primarily for SOLID Rar Archives or 7Zip Archives as they need to be
/// extracted sequentially for the best performance.
/// </summary>
ValueTask<IReader> ExtractAllEntriesAsync();
/// <summary>
/// Archive is SOLID (this means the Archive saved bytes by reusing information which helps for archives containing many small files).
/// Rar Archives can be SOLID while all 7Zip archives are considered SOLID.
/// </summary>
ValueTask<bool> IsSolidAsync();
/// <summary>
/// This checks to see if all the known entries have IsComplete = true
/// </summary>
ValueTask<bool> IsCompleteAsync();
/// <summary>
/// The total size of the files compressed in the archive.
/// </summary>
ValueTask<long> TotalSizeAsync();
/// <summary>
/// The total size of the files as uncompressed in the archive.
/// </summary>
ValueTask<long> TotalUncompressSizeAsync();
}
public interface IArchive : IDisposable
{
IEnumerable<IArchiveEntry> Entries { get; }

View File

@@ -0,0 +1,96 @@
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace SharpCompress;
internal sealed class LazyAsyncReadOnlyCollection<T>(IAsyncEnumerable<T> source) : IAsyncEnumerable<T>
{
private readonly List<T> backing = new();
private readonly IAsyncEnumerator<T> source = source.GetAsyncEnumerator();
private bool fullyLoaded;
private class LazyLoader(LazyAsyncReadOnlyCollection<T> lazyReadOnlyCollection, CancellationToken cancellationToken) : IAsyncEnumerator<T>
{
private bool disposed;
private int index = -1;
public ValueTask DisposeAsync()
{
if (!disposed)
{
disposed = true;
}
return default;
}
public async ValueTask<bool> MoveNextAsync()
{
cancellationToken.ThrowIfCancellationRequested();
if (index + 1 < lazyReadOnlyCollection.backing.Count)
{
index++;
return true;
}
if (!lazyReadOnlyCollection.fullyLoaded && await lazyReadOnlyCollection.source.MoveNextAsync())
{
lazyReadOnlyCollection.backing.Add(lazyReadOnlyCollection.source.Current);
index++;
return true;
}
lazyReadOnlyCollection.fullyLoaded = true;
return false;
}
#region IEnumerator<T> Members
public T Current => lazyReadOnlyCollection.backing[index];
#endregion
#region IDisposable Members
public void Dispose()
{
if (!disposed)
{
disposed = true;
}
}
#endregion
}
internal async ValueTask EnsureFullyLoaded()
{
if (!fullyLoaded)
{
var loader = new LazyLoader(this, CancellationToken.None);
while (await loader.MoveNextAsync())
{
// Intentionally empty
}
fullyLoaded = true;
}
}
internal IEnumerable<T> GetLoaded() => backing;
#region ICollection<T> Members
public void Add(T item) => throw new NotSupportedException();
public void Clear() => throw new NotSupportedException();
public bool IsReadOnly => true;
public bool Remove(T item) => throw new NotSupportedException();
#endregion
public IAsyncEnumerator<T> GetAsyncEnumerator(CancellationToken cancellationToken = default) => new LazyLoader(this, cancellationToken);
}

View File

@@ -1,13 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SharpCompress;
public static class AsyncEnumerableEx
{
public static async IAsyncEnumerable<T> Empty<T>()
where T : notnull
{
await Task.CompletedTask;
yield break;
}
}
public static class AsyncEnumerableExtensions
{
extension<T>(IAsyncEnumerable<T> source)
where T : notnull
{
public async IAsyncEnumerable<TResult> Cast<TResult>()
where TResult : class
{
await foreach (var item in source)
{
yield return (item as TResult).NotNull();
}
}
public async ValueTask<bool> All(Func<T, bool> predicate)
{
await foreach (var item in source)
{
if (!predicate(item))
{
return false;
}
}
return true;
}
public async IAsyncEnumerable<T> Where(Func<T, bool> predicate)
{
await foreach (var item in source)
@@ -28,5 +60,15 @@ public static class AsyncEnumerableExtensions
return default; // Returns null/default if the stream is empty
}
public async ValueTask<TAccumulate> Aggregate<TAccumulate>(TAccumulate seed, Func<TAccumulate, T, TAccumulate> func)
{
TAccumulate result = seed;
await foreach (var element in source)
{
result = func(result, element);
}
return result;
}
}
}

View File

@@ -609,7 +609,7 @@ public class ArchiveTests : ReaderTests
{
try
{
foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
await foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory))
{
await entry.WriteToDirectoryAsync(
SCRATCH_FILES_PATH,