diff --git a/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs b/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs index f00e889c..1cc115e4 100644 --- a/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs +++ b/src/SharpCompress/Archives/GZip/GZipArchiveEntry.cs @@ -1,5 +1,7 @@ using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common.GZip; namespace SharpCompress.Archives.GZip; @@ -20,6 +22,12 @@ public class GZipArchiveEntry : GZipEntry, IArchiveEntry return Parts.Single().GetCompressedStream().NotNull(); } + public virtual Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) + { + // GZip synchronous implementation is fast enough, just wrap it + return Task.FromResult(OpenEntryStream()); + } + #region IArchiveEntry Members public IArchive Archive { get; } diff --git a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs b/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs index 262d7cbe..18a87969 100644 --- a/src/SharpCompress/Archives/Rar/RarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Rar/RarArchiveEntry.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.Rar; using SharpCompress.Common.Rar.Headers; @@ -84,6 +86,9 @@ public class RarArchiveEntry : RarEntry, IArchiveEntry ); } + public Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) => + Task.FromResult(OpenEntryStream()); + public bool IsComplete { get diff --git a/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs b/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs index 9824ca47..754c8c63 100644 --- a/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs +++ b/src/SharpCompress/Archives/SevenZip/SevenZipArchiveEntry.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common.SevenZip; namespace SharpCompress.Archives.SevenZip; @@ -10,6 +12,9 @@ public class SevenZipArchiveEntry : SevenZipEntry, IArchiveEntry public Stream OpenEntryStream() => FilePart.GetCompressedStream(); + public Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) => + Task.FromResult(OpenEntryStream()); + public IArchive Archive { get; } public bool IsComplete => true; diff --git a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs index 770a7109..e3d2219b 100644 --- a/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs +++ b/src/SharpCompress/Archives/Tar/TarArchiveEntry.cs @@ -1,5 +1,7 @@ using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; using SharpCompress.Common.Tar; @@ -12,6 +14,9 @@ public class TarArchiveEntry : TarEntry, IArchiveEntry public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull(); + public virtual Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) => + Task.FromResult(OpenEntryStream()); + #region IArchiveEntry Members public IArchive Archive { get; } diff --git a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs index 4c980f91..5ab93836 100644 --- a/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs +++ b/src/SharpCompress/Archives/Zip/ZipArchiveEntry.cs @@ -1,5 +1,7 @@ using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common.Zip; namespace SharpCompress.Archives.Zip; @@ -11,6 +13,9 @@ public class ZipArchiveEntry : ZipEntry, IArchiveEntry public virtual Stream OpenEntryStream() => Parts.Single().GetCompressedStream().NotNull(); + public virtual Task OpenEntryStreamAsync(CancellationToken cancellationToken = default) => + Task.FromResult(OpenEntryStream()); + #region IArchiveEntry Members public IArchive Archive { get; } diff --git a/src/SharpCompress/Common/ExtractionMethods.cs b/src/SharpCompress/Common/ExtractionMethods.cs index 799d47b3..f81f82d4 100644 --- a/src/SharpCompress/Common/ExtractionMethods.cs +++ b/src/SharpCompress/Common/ExtractionMethods.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Common; @@ -116,4 +118,115 @@ internal static class ExtractionMethods entry.PreserveExtractionOptions(destinationFileName, options); } } + + public static async Task WriteEntryToDirectoryAsync( + IEntry entry, + string destinationDirectory, + ExtractionOptions? options, + Func writeAsync, + CancellationToken cancellationToken = default + ) + { + string destinationFileName; + var fullDestinationDirectoryPath = Path.GetFullPath(destinationDirectory); + + //check for trailing slash. + if ( + fullDestinationDirectoryPath[fullDestinationDirectoryPath.Length - 1] + != Path.DirectorySeparatorChar + ) + { + fullDestinationDirectoryPath += Path.DirectorySeparatorChar; + } + + if (!Directory.Exists(fullDestinationDirectoryPath)) + { + throw new ExtractionException( + $"Directory does not exist to extract to: {fullDestinationDirectoryPath}" + ); + } + + options ??= new ExtractionOptions() { Overwrite = true }; + + var file = Path.GetFileName(entry.Key.NotNull("Entry Key is null")).NotNull("File is null"); + file = Utility.ReplaceInvalidFileNameChars(file); + if (options.ExtractFullPath) + { + var folder = Path.GetDirectoryName(entry.Key.NotNull("Entry Key is null")) + .NotNull("Directory is null"); + var destdir = Path.GetFullPath(Path.Combine(fullDestinationDirectoryPath, folder)); + + if (!Directory.Exists(destdir)) + { + if (!destdir.StartsWith(fullDestinationDirectoryPath, StringComparison.Ordinal)) + { + throw new ExtractionException( + "Entry is trying to create a directory outside of the destination directory." + ); + } + + Directory.CreateDirectory(destdir); + } + destinationFileName = Path.Combine(destdir, file); + } + else + { + destinationFileName = Path.Combine(fullDestinationDirectoryPath, file); + } + + if (!entry.IsDirectory) + { + destinationFileName = Path.GetFullPath(destinationFileName); + + if ( + !destinationFileName.StartsWith( + fullDestinationDirectoryPath, + StringComparison.Ordinal + ) + ) + { + throw new ExtractionException( + "Entry is trying to write a file outside of the destination directory." + ); + } + await writeAsync(destinationFileName, options).ConfigureAwait(false); + } + else if (options.ExtractFullPath && !Directory.Exists(destinationFileName)) + { + Directory.CreateDirectory(destinationFileName); + } + } + + public static async Task WriteEntryToFileAsync( + IEntry entry, + string destinationFileName, + ExtractionOptions? options, + Func openAndWriteAsync, + CancellationToken cancellationToken = default + ) + { + if (entry.LinkTarget != null) + { + if (options?.WriteSymbolicLink is null) + { + throw new ExtractionException( + "Entry is a symbolic link but ExtractionOptions.WriteSymbolicLink delegate is null" + ); + } + options.WriteSymbolicLink(destinationFileName, entry.LinkTarget); + } + else + { + var fm = FileMode.Create; + options ??= new ExtractionOptions() { Overwrite = true }; + + if (!options.Overwrite) + { + fm = FileMode.CreateNew; + } + + await openAndWriteAsync(destinationFileName, fm).ConfigureAwait(false); + entry.PreserveExtractionOptions(destinationFileName, options); + } + } } diff --git a/src/SharpCompress/Readers/IReaderExtensions.cs b/src/SharpCompress/Readers/IReaderExtensions.cs index 0e51d72a..6480df1d 100644 --- a/src/SharpCompress/Readers/IReaderExtensions.cs +++ b/src/SharpCompress/Readers/IReaderExtensions.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; namespace SharpCompress.Readers; @@ -65,4 +67,64 @@ public static class IReaderExtensions reader.WriteEntryTo(fs); } ); + + /// + /// Extract to specific directory asynchronously, retaining filename + /// + public static async Task WriteEntryToDirectoryAsync( + this IReader reader, + string destinationDirectory, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + await ExtractionMethods + .WriteEntryToDirectoryAsync( + reader.Entry, + destinationDirectory, + options, + (fileName, opts) => reader.WriteEntryToFileAsync(fileName, opts, cancellationToken), + cancellationToken + ) + .ConfigureAwait(false); + + /// + /// Extract to specific file asynchronously + /// + public static async Task WriteEntryToFileAsync( + this IReader reader, + string destinationFileName, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) => + await ExtractionMethods + .WriteEntryToFileAsync( + reader.Entry, + destinationFileName, + options, + async (x, fm) => + { + using var fs = File.Open(destinationFileName, fm); + await reader.WriteEntryToAsync(fs, cancellationToken).ConfigureAwait(false); + }, + cancellationToken + ) + .ConfigureAwait(false); + + /// + /// Extract all remaining unread entries to specific directory asynchronously, retaining filename + /// + public static async Task WriteAllToDirectoryAsync( + this IReader reader, + string destinationDirectory, + ExtractionOptions? options = null, + CancellationToken cancellationToken = default + ) + { + while (reader.MoveToNextEntry()) + { + await reader + .WriteEntryToDirectoryAsync(destinationDirectory, options, cancellationToken) + .ConfigureAwait(false); + } + } } diff --git a/src/SharpCompress/Writers/AbstractWriter.cs b/src/SharpCompress/Writers/AbstractWriter.cs index 1820c986..2f71c0cb 100644 --- a/src/SharpCompress/Writers/AbstractWriter.cs +++ b/src/SharpCompress/Writers/AbstractWriter.cs @@ -1,5 +1,7 @@ using System; using System.IO; +using System.Threading; +using System.Threading.Tasks; using SharpCompress.Common; namespace SharpCompress.Writers; @@ -22,6 +24,19 @@ public abstract class AbstractWriter(ArchiveType type, WriterOptions writerOptio public abstract void Write(string filename, Stream source, DateTime? modificationTime); + public virtual async Task WriteAsync( + string filename, + Stream source, + DateTime? modificationTime, + CancellationToken cancellationToken = default + ) + { + // Default implementation calls synchronous version + // Derived classes should override for true async behavior + Write(filename, source, modificationTime); + await Task.CompletedTask.ConfigureAwait(false); + } + protected virtual void Dispose(bool isDisposing) { if (isDisposing) diff --git a/src/SharpCompress/Writers/IWriterExtensions.cs b/src/SharpCompress/Writers/IWriterExtensions.cs index 56f3966a..080312c7 100644 --- a/src/SharpCompress/Writers/IWriterExtensions.cs +++ b/src/SharpCompress/Writers/IWriterExtensions.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; namespace SharpCompress.Writers; @@ -52,4 +54,71 @@ public static class IWriterExtensions writer.Write(file.Substring(directory.Length), file); } } + + // Async extensions + public static Task WriteAsync( + this IWriter writer, + string entryPath, + Stream source, + CancellationToken cancellationToken = default + ) => writer.WriteAsync(entryPath, source, null, cancellationToken); + + public static async Task WriteAsync( + this IWriter writer, + string entryPath, + FileInfo source, + CancellationToken cancellationToken = default + ) + { + if (!source.Exists) + { + throw new ArgumentException("Source does not exist: " + source.FullName); + } + using var stream = source.OpenRead(); + await writer + .WriteAsync(entryPath, stream, source.LastWriteTime, cancellationToken) + .ConfigureAwait(false); + } + + public static Task WriteAsync( + this IWriter writer, + string entryPath, + string source, + CancellationToken cancellationToken = default + ) => writer.WriteAsync(entryPath, new FileInfo(source), cancellationToken); + + public static Task WriteAllAsync( + this IWriter writer, + string directory, + string searchPattern = "*", + SearchOption option = SearchOption.TopDirectoryOnly, + CancellationToken cancellationToken = default + ) => writer.WriteAllAsync(directory, searchPattern, null, option, cancellationToken); + + public static async Task WriteAllAsync( + this IWriter writer, + string directory, + string searchPattern = "*", + Func? fileSearchFunc = null, + SearchOption option = SearchOption.TopDirectoryOnly, + CancellationToken cancellationToken = default + ) + { + if (!Directory.Exists(directory)) + { + throw new ArgumentException("Directory does not exist: " + directory); + } + + fileSearchFunc ??= n => true; + foreach ( + var file in Directory + .EnumerateFiles(directory, searchPattern, option) + .Where(fileSearchFunc) + ) + { + await writer + .WriteAsync(file.Substring(directory.Length), file, cancellationToken) + .ConfigureAwait(false); + } + } }