From 408d2e6663cdc16d47986c162dcd668c799fe8df Mon Sep 17 00:00:00 2001 From: Adam Hathcock Date: Sun, 18 Jan 2026 14:57:01 +0000 Subject: [PATCH] Async add entry --- .../Archives/AbstractWritableArchive.cs | 83 ++++++++++++++++++- .../Archives/IWritableArchive.cs | 28 ++++++- .../IWritableArchiveCommonExtensions.cs | 59 ------------- .../Archives/IWritableArchiveExtensions.cs | 50 +++++++++++ .../IWritableAsyncArchiveExtensions.cs | 52 ++++++++++++ .../GZip/GZipArchiveAsyncTests.cs | 19 +++-- .../Tar/TarArchiveAsyncTests.cs | 4 +- .../Zip/ZipArchiveAsyncTests.cs | 2 +- 8 files changed, 223 insertions(+), 74 deletions(-) delete mode 100644 src/SharpCompress/Archives/IWritableArchiveCommonExtensions.cs diff --git a/src/SharpCompress/Archives/AbstractWritableArchive.cs b/src/SharpCompress/Archives/AbstractWritableArchive.cs index 6e482f23..0ae869b4 100644 --- a/src/SharpCompress/Archives/AbstractWritableArchive.cs +++ b/src/SharpCompress/Archives/AbstractWritableArchive.cs @@ -89,7 +89,7 @@ public abstract class AbstractWritableArchive public TEntry AddEntry(string key, Stream source, long size = 0, DateTime? modified = null) => AddEntry(key, source, false, size, modified); - IArchiveEntry IWritableArchiveCommon.AddEntry( + IArchiveEntry IWritableArchive.AddEntry( string key, Stream source, bool closeStream, @@ -97,7 +97,7 @@ public abstract class AbstractWritableArchive DateTime? modified ) => AddEntry(key, source, closeStream, size, modified); - IArchiveEntry IWritableArchiveCommon.AddDirectoryEntry(string key, DateTime? modified) => + IArchiveEntry IWritableArchive.AddDirectoryEntry(string key, DateTime? modified) => AddDirectoryEntry(key, modified); public TEntry AddEntry( @@ -140,6 +140,65 @@ public abstract class AbstractWritableArchive return false; } + private async ValueTask DoesKeyMatchExistingAsync( + string key, + CancellationToken cancellationToken + ) + { + await foreach ( + var entry in EntriesAsync.WithCancellation(cancellationToken).ConfigureAwait(false) + ) + { + var path = entry.Key; + if (path is null) + { + continue; + } + var p = path.Replace('/', '\\'); + if (p.Length > 0 && p[0] == '\\') + { + p = p.Substring(1); + } + return string.Equals(p, key, StringComparison.OrdinalIgnoreCase); + } + return false; + } + async ValueTask IWritableAsyncArchive.AddEntryAsync( + string key, + Stream source, + bool closeStream, + long size, + DateTime? modified, + CancellationToken cancellationToken + ) => await AddEntryAsync(key, source, closeStream, size, modified, cancellationToken); + + async ValueTask IWritableAsyncArchive.AddDirectoryEntryAsync(string key, DateTime? modified, + CancellationToken cancellationToken) => + await AddDirectoryEntryAsync(key, modified, cancellationToken); + + public async ValueTask AddEntryAsync( + string key, + Stream source, + bool closeStream, + long size = 0, + DateTime? modified = null, + CancellationToken cancellationToken = default + ) + { + if (key.Length > 0 && key[0] is '/' or '\\') + { + key = key.Substring(1); + } + if (await DoesKeyMatchExistingAsync(key, cancellationToken).ConfigureAwait(false)) + { + throw new ArchiveException("Cannot add entry with duplicate key: " + key); + } + var entry = CreateEntry(key, source, size, modified, closeStream); + newEntries.Add(entry); + RebuildModifiedCollection(); + return entry; + } + public TEntry AddDirectoryEntry(string key, DateTime? modified = null) { if (key.Length > 0 && key[0] is '/' or '\\') @@ -156,6 +215,26 @@ public abstract class AbstractWritableArchive return entry; } + public async ValueTask AddDirectoryEntryAsync( + string key, + DateTime? modified = null, + CancellationToken cancellationToken = default + ) + { + if (key.Length > 0 && key[0] is '/' or '\\') + { + key = key.Substring(1); + } + if (await DoesKeyMatchExistingAsync(key, cancellationToken).ConfigureAwait(false)) + { + throw new ArchiveException("Cannot add entry with duplicate key: " + key); + } + var entry = CreateDirectoryEntry(key, modified); + newEntries.Add(entry); + RebuildModifiedCollection(); + return entry; + } + public void SaveTo(Stream stream, WriterOptions options) { //reset streams of new entries diff --git a/src/SharpCompress/Archives/IWritableArchive.cs b/src/SharpCompress/Archives/IWritableArchive.cs index 28496b48..663723d0 100644 --- a/src/SharpCompress/Archives/IWritableArchive.cs +++ b/src/SharpCompress/Archives/IWritableArchive.cs @@ -18,6 +18,10 @@ public interface IWritableArchiveCommon /// Removes the specified entry from the archive. /// void RemoveEntry(IArchiveEntry entry); +} + +public interface IWritableArchive : IArchive, IWritableArchiveCommon +{ IArchiveEntry AddEntry( string key, @@ -28,10 +32,7 @@ public interface IWritableArchiveCommon ); IArchiveEntry AddDirectoryEntry(string key, DateTime? modified = null); -} -public interface IWritableArchive : IArchive, IWritableArchiveCommon -{ /// /// Saves the archive to the specified stream using the given writer options. /// @@ -48,4 +49,25 @@ public interface IWritableAsyncArchive : IAsyncArchive, IWritableArchiveCommon WriterOptions options, CancellationToken cancellationToken = default ); + + /// + /// Asynchronously adds an entry to the archive with the specified key, source stream, and options. + /// + ValueTask AddEntryAsync( + string key, + Stream source, + bool closeStream, + long size = 0, + DateTime? modified = null, + CancellationToken cancellationToken = default + ); + + /// + /// Asynchronously adds a directory entry to the archive with the specified key and modification time. + /// + ValueTask AddDirectoryEntryAsync( + string key, + DateTime? modified = null, + CancellationToken cancellationToken = default + ); } diff --git a/src/SharpCompress/Archives/IWritableArchiveCommonExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveCommonExtensions.cs deleted file mode 100644 index 7f276ece..00000000 --- a/src/SharpCompress/Archives/IWritableArchiveCommonExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.IO; - -namespace SharpCompress.Archives; - -public static class IWritableArchiveCommonExtensions -{ - extension(IWritableArchiveCommon writableArchive) - { - public void AddAllFromDirectory( - string filePath, - string searchPattern = "*.*", - SearchOption searchOption = SearchOption.AllDirectories - ) - { - using (writableArchive.PauseEntryRebuilding()) - { - foreach ( - var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption) - ) - { - var fileInfo = new FileInfo(path); - writableArchive.AddEntry( - path.Substring(filePath.Length), - fileInfo.OpenRead(), - true, - fileInfo.Length, - fileInfo.LastWriteTime - ); - } - } - } - - public IArchiveEntry AddEntry(string key, string file) => - writableArchive.AddEntry(key, new FileInfo(file)); - - public IArchiveEntry AddEntry( - string key, - Stream source, - long size = 0, - DateTime? modified = null - ) => writableArchive.AddEntry(key, source, false, size, modified); - - public IArchiveEntry AddEntry(string key, FileInfo fileInfo) - { - if (!fileInfo.Exists) - { - throw new ArgumentException("FileInfo does not exist."); - } - return writableArchive.AddEntry( - key, - fileInfo.OpenRead(), - true, - fileInfo.Length, - fileInfo.LastWriteTime - ); - } - } -} diff --git a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs index 60971bfc..f383ec87 100644 --- a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using SharpCompress.Common; using SharpCompress.Writers; @@ -8,6 +9,55 @@ public static class IWritableArchiveExtensions { extension(IWritableArchive writableArchive) { + public void AddAllFromDirectory( + string filePath, + string searchPattern = "*.*", + SearchOption searchOption = SearchOption.AllDirectories + ) + { + using (writableArchive.PauseEntryRebuilding()) + { + foreach ( + var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption) + ) + { + var fileInfo = new FileInfo(path); + writableArchive.AddEntry( + path.Substring(filePath.Length), + fileInfo.OpenRead(), + true, + fileInfo.Length, + fileInfo.LastWriteTime + ); + } + } + } + + public IArchiveEntry AddEntry(string key, string file) => + writableArchive.AddEntry(key, new FileInfo(file)); + + public IArchiveEntry AddEntry( + string key, + Stream source, + long size = 0, + DateTime? modified = null + ) => writableArchive.AddEntry(key, source, false, size, modified); + + public IArchiveEntry AddEntry(string key, FileInfo fileInfo) + { + if (!fileInfo.Exists) + { + throw new ArgumentException("FileInfo does not exist."); + } + return writableArchive.AddEntry( + key, + fileInfo.OpenRead(), + true, + fileInfo.Length, + fileInfo.LastWriteTime + ); + } + public void SaveTo(string filePath, WriterOptions? options = null) => writableArchive.SaveTo(new FileInfo(filePath), options ?? new(CompressionType.Deflate)); diff --git a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs index a69bfc0a..38461fe9 100644 --- a/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableAsyncArchiveExtensions.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -8,8 +9,59 @@ namespace SharpCompress.Archives; public static class IWritableAsyncArchiveExtensions { + extension(IWritableAsyncArchive writableArchive) { + public async ValueTask AddAllFromDirectoryAsync( + string filePath, + string searchPattern = "*.*", + SearchOption searchOption = SearchOption.AllDirectories + ) + { + using (writableArchive.PauseEntryRebuilding()) + { + foreach ( + var path in Directory.EnumerateFiles(filePath, searchPattern, searchOption) + ) + { + var fileInfo = new FileInfo(path); + await writableArchive.AddEntryAsync( + path.Substring(filePath.Length), + fileInfo.OpenRead(), + true, + fileInfo.Length, + fileInfo.LastWriteTime + ); + } + } + } + + public ValueTask AddEntryAsync(string key, string file) => + writableArchive.AddEntryAsync(key, new FileInfo(file)); + + public ValueTask AddEntryAsync( + string key, + Stream source, + long size = 0, + DateTime? modified = null + ) => writableArchive.AddEntryAsync(key, source, false, size, modified); + + public ValueTask AddEntryAsync(string key, FileInfo fileInfo) + { + if (!fileInfo.Exists) + { + throw new ArgumentException("FileInfo does not exist."); + } + return writableArchive.AddEntryAsync( + key, + fileInfo.OpenRead(), + true, + fileInfo.Length, + fileInfo.LastWriteTime + ); + } + + public ValueTask SaveToAsync( string filePath, WriterOptions? options = null, diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs index 7ebf2425..04f1a673 100644 --- a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs @@ -1,3 +1,4 @@ +using System; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -22,9 +23,11 @@ public class GZipArchiveAsyncTests : ArchiveTests #else await using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) #endif - using (var archive = ArchiveFactory.OpenArchive(new AsyncOnlyStream(stream))) + await using ( + var archive = await ArchiveFactory.OpenAsyncArchive(new AsyncOnlyStream(stream)) + ) { - var entry = archive.Entries.First(); + var entry = await archive.EntriesAsync.FirstAsync(); await entry.WriteToFileAsync(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull())); var size = entry.Size; @@ -79,7 +82,9 @@ public class GZipArchiveAsyncTests : ArchiveTests #endif await using (var archive = GZipArchive.OpenAsyncArchive(new AsyncOnlyStream(stream))) { - Assert.Throws(() => archive.AddEntry("jpg\\test.jpg", jpg)); + await Assert.ThrowsAsync(async () => + await archive.AddEntryAsync("jpg\\test.jpg", File.OpenRead(jpg), closeStream: true) + ); await archive.SaveToAsync(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz")); } } @@ -98,8 +103,8 @@ public class GZipArchiveAsyncTests : ArchiveTests inputStream.Position = 0; } - using var archive = GZipArchive.OpenArchive(new AsyncOnlyStream(inputStream)); - var archiveEntry = archive.Entries.First(); + await using var archive = GZipArchive.OpenAsyncArchive(new AsyncOnlyStream(inputStream)); + var archiveEntry = await archive.EntriesAsync.FirstAsync(); MemoryStream tarStream; #if NETFRAMEWORK @@ -147,7 +152,7 @@ public class GZipArchiveAsyncTests : ArchiveTests [Fact] public async Task TestGzCrcWithMostSignificantBitNotNegative_Async() { - using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); + await using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); await using var archive = GZipArchive.OpenAsyncArchive(new AsyncOnlyStream(stream)); await foreach (var entry in archive.EntriesAsync.Where(entry => !entry.IsDirectory)) { @@ -158,7 +163,7 @@ public class GZipArchiveAsyncTests : ArchiveTests [Fact] public async Task TestGzArchiveTypeGzip_Async() { - using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); + await using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); await using var archive = GZipArchive.OpenAsyncArchive(new AsyncOnlyStream(stream)); Assert.Equal(archive.Type, ArchiveType.GZip); } diff --git a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs index b21fe278..baeb1e14 100644 --- a/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Tar/TarArchiveAsyncTests.cs @@ -139,7 +139,7 @@ public class TarArchiveAsyncTests : ArchiveTests await using (var archive = TarArchive.CreateAsyncArchive()) { - archive.AddAllFromDirectory(ORIGINAL_FILES_PATH); + await archive.AddAllFromDirectoryAsync(ORIGINAL_FILES_PATH); var twopt = new TarWriterOptions(CompressionType.None, true); twopt.ArchiveEncoding = new ArchiveEncoding { Default = Encoding.GetEncoding(866) }; await archive.SaveToAsync(scratchPath, twopt); @@ -157,7 +157,7 @@ public class TarArchiveAsyncTests : ArchiveTests await using (var archive = TarArchive.OpenAsyncArchive(unmodified)) { - archive.AddEntry("jpg\\test.jpg", jpg); + await archive.AddEntryAsync("jpg\\test.jpg", jpg); await archive.SaveToAsync(scratchPath, new WriterOptions(CompressionType.None)); } CompareArchivesByPath(modified, scratchPath); diff --git a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs index ffea0b92..1b084c7f 100644 --- a/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs +++ b/tests/SharpCompress.Test/Zip/ZipArchiveAsyncTests.cs @@ -151,7 +151,7 @@ public class ZipArchiveAsyncTests : ArchiveTests await using (var archive = ZipArchive.OpenAsyncArchive(unmodified)) { - archive.AddEntry("jpg\\test.jpg", jpg); + await archive.AddEntryAsync("jpg\\test.jpg", jpg); WriterOptions writerOptions = new ZipWriterOptions(CompressionType.Deflate); writerOptions.ArchiveEncoding.Default = Encoding.GetEncoding(866);