diff --git a/src/SharpCompress/Archives/AbstractWritableArchive.cs b/src/SharpCompress/Archives/AbstractWritableArchive.cs index 082b9631..b72c2dff 100644 --- a/src/SharpCompress/Archives/AbstractWritableArchive.cs +++ b/src/SharpCompress/Archives/AbstractWritableArchive.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; using SharpCompress.Writers; @@ -141,6 +143,18 @@ public abstract class AbstractWritableArchive SaveTo(stream, options, OldEntries, newEntries); } + public async Task SaveToAsync( + Stream stream, + WriterOptions options, + CancellationToken cancellationToken = default + ) + { + //reset streams of new entries + newEntries.Cast().ForEach(x => x.Stream.Seek(0, SeekOrigin.Begin)); + await SaveToAsync(stream, options, OldEntries, newEntries, cancellationToken) + .ConfigureAwait(false); + } + protected TEntry CreateEntry( string key, Stream source, @@ -173,6 +187,14 @@ public abstract class AbstractWritableArchive IEnumerable newEntries ); + protected abstract Task SaveToAsync( + Stream stream, + WriterOptions options, + IEnumerable oldEntries, + IEnumerable newEntries, + CancellationToken cancellationToken = default + ); + public override void Dispose() { base.Dispose(); diff --git a/src/SharpCompress/Archives/GZip/GZipArchive.cs b/src/SharpCompress/Archives/GZip/GZipArchive.cs index a0345cdf..f4eb2805 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchive.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchive.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.GZip; using SharpCompress.IO; @@ -136,6 +138,16 @@ public class GZipArchive : AbstractWritableArchive SaveTo(stream, new WriterOptions(CompressionType.GZip)); } + public Task SaveToAsync(string filePath, CancellationToken cancellationToken = default) => + SaveToAsync(new FileInfo(filePath), cancellationToken); + + public async Task SaveToAsync(FileInfo fileInfo, CancellationToken cancellationToken = default) + { + using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + await SaveToAsync(stream, new WriterOptions(CompressionType.GZip), cancellationToken) + .ConfigureAwait(false); + } + public static bool IsGZipFile(Stream stream) { // read the header on the first read @@ -196,6 +208,28 @@ public class GZipArchive : AbstractWritableArchive } } + protected override async Task SaveToAsync( + Stream stream, + WriterOptions options, + IEnumerable oldEntries, + IEnumerable newEntries, + CancellationToken cancellationToken = default + ) + { + if (Entries.Count > 1) + { + throw new InvalidFormatException("Only one entry is allowed in a GZip Archive"); + } + using var writer = new GZipWriter(stream, new GZipWriterOptions(options)); + foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + await writer + .WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken) + .ConfigureAwait(false); + } + } + protected override IEnumerable LoadEntries(IEnumerable volumes) { var stream = volumes.Single().Stream; diff --git a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs index bda1e358..0ca119b2 100644 --- a/src/SharpCompress/Archives/IArchiveEntryExtensions.cs +++ b/src/SharpCompress/Archives/IArchiveEntryExtensions.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.IO; @@ -30,6 +32,34 @@ public static class IArchiveEntryExtensions streamListener.FireEntryExtractionEnd(archiveEntry); } + public static async Task WriteToAsync( + this IArchiveEntry archiveEntry, + Stream streamToWriteTo, + CancellationToken cancellationToken = default + ) + { + if (archiveEntry.IsDirectory) + { + throw new ExtractionException("Entry is a file directory and cannot be extracted."); + } + + var streamListener = (IArchiveExtractionListener)archiveEntry.Archive; + streamListener.EnsureEntriesLoaded(); + streamListener.FireEntryExtractionBegin(archiveEntry); + streamListener.FireFilePartExtractionBegin( + archiveEntry.Key ?? "Key", + archiveEntry.Size, + archiveEntry.CompressedSize + ); + var entryStream = archiveEntry.OpenEntryStream(); + using (entryStream) + { + using Stream s = new ListeningStream(streamListener, entryStream); + await s.CopyToAsync(streamToWriteTo, 81920, cancellationToken).ConfigureAwait(false); + } + streamListener.FireEntryExtractionEnd(archiveEntry); + } + /// /// Extract to specific directory, retaining filename /// @@ -63,4 +93,24 @@ public static class IArchiveEntryExtensions entry.WriteTo(fs); } ); + + /// + /// Extract to specific file asynchronously + /// + public static Task WriteToFileAsync( + this IArchiveEntry entry, + string destinationFileName, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + ExtractionMethods.WriteEntryToFileAsync( + entry, + destinationFileName, + options, + async (x, fm) => + { + using var fs = File.Open(destinationFileName, fm); + await entry.WriteToAsync(fs, cancellationToken).ConfigureAwait(false); + } + ); } diff --git a/src/SharpCompress/Archives/IWritableArchive.cs b/src/SharpCompress/Archives/IWritableArchive.cs index 37b84aa0..5529ec3b 100644 --- a/src/SharpCompress/Archives/IWritableArchive.cs +++ b/src/SharpCompress/Archives/IWritableArchive.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Writers; namespace SharpCompress.Archives; @@ -18,6 +20,12 @@ public interface IWritableArchive : IArchive void SaveTo(Stream stream, WriterOptions options); + Task SaveToAsync( + Stream stream, + WriterOptions options, + CancellationToken cancellationToken = default + ); + /// /// Use this to pause entry rebuilding when adding large collections of entries. Dispose when complete. A using statement is recommended. /// diff --git a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs index 8f531d41..4defe604 100644 --- a/src/SharpCompress/Archives/IWritableArchiveExtensions.cs +++ b/src/SharpCompress/Archives/IWritableArchiveExtensions.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Writers; namespace SharpCompress.Archives; @@ -42,6 +44,24 @@ public static class IWritableArchiveExtensions writableArchive.SaveTo(stream, options); } + public static Task SaveToAsync( + this IWritableArchive writableArchive, + string filePath, + WriterOptions options, + CancellationToken cancellationToken = default + ) => writableArchive.SaveToAsync(new FileInfo(filePath), options, cancellationToken); + + public static async Task SaveToAsync( + this IWritableArchive writableArchive, + FileInfo fileInfo, + WriterOptions options, + CancellationToken cancellationToken = default + ) + { + using var stream = fileInfo.Open(FileMode.Create, FileAccess.Write); + await writableArchive.SaveToAsync(stream, options, cancellationToken).ConfigureAwait(false); + } + public static void AddAllFromDirectory( this IWritableArchive writableArchive, string filePath, diff --git a/src/SharpCompress/Archives/Tar/TarArchive.cs b/src/SharpCompress/Archives/Tar/TarArchive.cs index 39f0fce6..a7f277bc 100644 --- a/src/SharpCompress/Archives/Tar/TarArchive.cs +++ b/src/SharpCompress/Archives/Tar/TarArchive.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Tar; using SharpCompress.Common.Tar.Headers; @@ -242,6 +244,24 @@ public class TarArchive : AbstractWritableArchive } } + protected override async Task SaveToAsync( + Stream stream, + WriterOptions options, + IEnumerable oldEntries, + IEnumerable newEntries, + CancellationToken cancellationToken = default + ) + { + using var writer = new TarWriter(stream, new TarWriterOptions(options)); + foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + await writer + .WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken) + .ConfigureAwait(false); + } + } + protected override IReader CreateReaderForSolidExtraction() { var stream = Volumes.Single().Stream; diff --git a/src/SharpCompress/Archives/Zip/ZipArchive.cs b/src/SharpCompress/Archives/Zip/ZipArchive.cs index 35b8e0cb..955e9649 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchive.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchive.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Zip; using SharpCompress.Common.Zip.Headers; @@ -317,6 +319,24 @@ public class ZipArchive : AbstractWritableArchive } } + protected override async Task SaveToAsync( + Stream stream, + WriterOptions options, + IEnumerable oldEntries, + IEnumerable newEntries, + CancellationToken cancellationToken = default + ) + { + using var writer = new ZipWriter(stream, new ZipWriterOptions(options)); + foreach (var entry in oldEntries.Concat(newEntries).Where(x => !x.IsDirectory)) + { + using var entryStream = entry.OpenEntryStream(); + await writer + .WriteAsync(entry.Key.NotNull("Entry Key is null"), entryStream, cancellationToken) + .ConfigureAwait(false); + } + } + protected override ZipArchiveEntry CreateEntryInternal( string filePath, Stream source, diff --git a/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs new file mode 100644 index 00000000..016c6fc0 --- /dev/null +++ b/tests/SharpCompress.Test/GZip/GZipArchiveAsyncTests.cs @@ -0,0 +1,127 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using SharpCompress.Archives; +using SharpCompress.Archives.GZip; +using SharpCompress.Archives.Tar; +using SharpCompress.Common; +using Xunit; + +namespace SharpCompress.Test.GZip; + +public class GZipArchiveAsyncTests : ArchiveTests +{ + public GZipArchiveAsyncTests() => UseExtensionInsteadOfNameToVerify = true; + + [Fact] + public async Task GZip_Archive_Generic_Async() + { + using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) + using (var archive = ArchiveFactory.Open(stream)) + { + var entry = archive.Entries.First(); + await entry.WriteToFileAsync(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull())); + + var size = entry.Size; + var scratch = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar")); + var test = new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")); + + Assert.Equal(size, scratch.Length); + Assert.Equal(size, test.Length); + } + CompareArchivesByPath( + Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"), + Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar") + ); + } + + [Fact] + public async Task GZip_Archive_Async() + { + using (Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) + using (var archive = GZipArchive.Open(stream)) + { + var entry = archive.Entries.First(); + await entry.WriteToFileAsync(Path.Combine(SCRATCH_FILES_PATH, entry.Key.NotNull())); + + var size = entry.Size; + var scratch = new FileInfo(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar")); + var test = new FileInfo(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")); + + Assert.Equal(size, scratch.Length); + Assert.Equal(size, test.Length); + } + CompareArchivesByPath( + Path.Combine(SCRATCH_FILES_PATH, "Tar.tar"), + Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar") + ); + } + + [Fact] + public async Task GZip_Archive_NoAdd_Async() + { + var jpg = Path.Combine(ORIGINAL_FILES_PATH, "jpg", "test.jpg"); + using Stream stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); + using var archive = GZipArchive.Open(stream); + Assert.Throws(() => archive.AddEntry("jpg\\test.jpg", jpg)); + await archive.SaveToAsync(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz")); + } + + [Fact] + public async Task GZip_Archive_Multiple_Reads_Async() + { + var inputStream = new MemoryStream(); + using (var fileStream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz"))) + { + await fileStream.CopyToAsync(inputStream); + inputStream.Position = 0; + } + using var archive = GZipArchive.Open(inputStream); + var archiveEntry = archive.Entries.First(); + + MemoryStream tarStream; + using (var entryStream = archiveEntry.OpenEntryStream()) + { + tarStream = new MemoryStream(); + await entryStream.CopyToAsync(tarStream); + } + var size = tarStream.Length; + using (var entryStream = archiveEntry.OpenEntryStream()) + { + tarStream = new MemoryStream(); + await entryStream.CopyToAsync(tarStream); + } + Assert.Equal(size, tarStream.Length); + using (var entryStream = archiveEntry.OpenEntryStream()) + { + var result = TarArchive.IsTarFile(entryStream); + Assert.True(result); + } + Assert.Equal(size, tarStream.Length); + using (var entryStream = archiveEntry.OpenEntryStream()) + { + tarStream = new MemoryStream(); + await entryStream.CopyToAsync(tarStream); + } + Assert.Equal(size, tarStream.Length); + } + + [Fact] + public void TestGzCrcWithMostSignificantBitNotNegative_Async() + { + using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); + using var archive = GZipArchive.Open(stream); + foreach (var entry in archive.Entries.Where(entry => !entry.IsDirectory)) + { + Assert.InRange(entry.Crc, 0L, 0xFFFFFFFFL); + } + } + + [Fact] + public void TestGzArchiveTypeGzip_Async() + { + using var stream = File.OpenRead(Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz")); + using var archive = GZipArchive.Open(stream); + Assert.Equal(archive.Type, ArchiveType.GZip); + } +} diff --git a/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs b/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs new file mode 100644 index 00000000..ead377ed --- /dev/null +++ b/tests/SharpCompress.Test/GZip/GZipWriterAsyncTests.cs @@ -0,0 +1,83 @@ +using System.IO; +using System.Threading.Tasks; +using SharpCompress.Common; +using SharpCompress.Writers; +using SharpCompress.Writers.GZip; +using Xunit; + +namespace SharpCompress.Test.GZip; + +public class GZipWriterAsyncTests : WriterTests +{ + public GZipWriterAsyncTests() + : base(ArchiveType.GZip) => UseExtensionInsteadOfNameToVerify = true; + + [Fact] + public async Task GZip_Writer_Generic_Async() + { + using ( + Stream stream = File.Open( + Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"), + FileMode.OpenOrCreate, + FileAccess.Write + ) + ) + using (var writer = WriterFactory.Open(stream, ArchiveType.GZip, CompressionType.GZip)) + { + await writer.WriteAsync("Tar.tar", Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")); + } + CompareArchivesByPath( + Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"), + Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz") + ); + } + + [Fact] + public async Task GZip_Writer_Async() + { + using ( + Stream stream = File.Open( + Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"), + FileMode.OpenOrCreate, + FileAccess.Write + ) + ) + using (var writer = new GZipWriter(stream)) + { + await writer.WriteAsync("Tar.tar", Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar")); + } + CompareArchivesByPath( + Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"), + Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz") + ); + } + + [Fact] + public void GZip_Writer_Generic_Bad_Compression_Async() => + Assert.Throws(() => + { + using Stream stream = File.OpenWrite(Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz")); + using var writer = WriterFactory.Open(stream, ArchiveType.GZip, CompressionType.BZip2); + }); + + [Fact] + public async Task GZip_Writer_Entry_Path_With_Dir_Async() + { + using ( + Stream stream = File.Open( + Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"), + FileMode.OpenOrCreate, + FileAccess.Write + ) + ) + using (var writer = new GZipWriter(stream)) + { + var path = Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar"); + await writer.WriteAsync(path, path); + } + CompareArchivesByPath( + Path.Combine(SCRATCH_FILES_PATH, "Tar.tar.gz"), + Path.Combine(TEST_ARCHIVES_PATH, "Tar.tar.gz") + ); + } +}