diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f67c023b..ec07825a4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,6 +26,7 @@ + @@ -90,4 +91,4 @@ - \ No newline at end of file + diff --git a/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs b/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs index 85c69b524..5b2705c25 100644 --- a/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs +++ b/src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs @@ -3,40 +3,101 @@ using System.IO; using System.IO.Compression; using System.Linq; +using Fallout.Common.Utilities.Collections; using ICSharpCode.SharpZipLib.BZip2; using ICSharpCode.SharpZipLib.GZip; using ICSharpCode.SharpZipLib.Tar; using ICSharpCode.SharpZipLib.Zip; -using Fallout.Common.Utilities.Collections; +using SharpCompress.Common; +using SharpCompress.Readers; +using ZipFile = ICSharpCode.SharpZipLib.Zip.ZipFile; namespace Fallout.Common.IO; public static class CompressionExtensions { + /// + /// Compresses into , choosing the compression format based on the + /// archive file's extension. + /// + /// The directory whose contents should be compressed. + /// + /// The archive file to create. Its extension determines the compression format (e.g. .zip, + /// .tar.gz, .tar.bz2). + /// + /// + /// An optional predicate used to filter which files are included in the archive. If null, all files are + /// included. + /// public static void CompressTo(this AbsolutePath directory, AbsolutePath archiveFile, Func filter = null) { if (archiveFile.HasExtension(".zip")) + { directory.ZipTo(archiveFile, filter); + } else if (archiveFile.HasExtension(".tar.gz", ".tgz")) + { directory.TarGZipTo(archiveFile, filter); + } else if (archiveFile.HasExtension(".tar.bz2", ".tbz2", ".tbz")) + { directory.TarBZip2To(archiveFile, filter); + } + else if (archiveFile.HasExtension(".tar.xz", ".txz")) + { + Assert.Fail( + $"Compressing a .tar.xz archive currently not supported. Archive file: '{Path.GetFileName(archiveFile)}'"); + } else + { Assert.Fail($"Unknown archive extension for archive '{Path.GetFileName(archiveFile)}'"); + } } + /// + /// Uncompresses into , choosing the decompression format based on + /// the archive file's extension. + /// + /// + /// The archive file to extract. Its extension determines the decompression format (e.g. .zip, + /// .tar.gz, .tar.bz2, .tar.xz). + /// + /// The directory into which the archive's contents are extracted. public static void UncompressTo(this AbsolutePath archiveFile, AbsolutePath directory) { if (archiveFile.HasExtension(".zip")) + { archiveFile.UnZipTo(directory); + } else if (archiveFile.HasExtension(".tar.gz", ".tgz")) + { archiveFile.UnTarGZipTo(directory); + } else if (archiveFile.HasExtension(".tar.bz2", ".tbz2", ".tbz")) + { archiveFile.UnTarBZip2To(directory); + } + else if (archiveFile.HasExtension(".tar.xz", ".txz")) + { + archiveFile.UnTarXzTo(directory); + } else + { Assert.Fail($"Unknown archive extension for archive '{Path.GetFileName(archiveFile)}'"); + } } + /// + /// Compresses into a ZIP archive at . + /// + /// The directory whose contents should be added to the ZIP archive. + /// The ZIP archive file to create. + /// + /// An optional predicate used to filter which files are included in the archive. If null, all files are + /// included. + /// + /// The compression level to use for the archive entries. + /// The used to open the archive file. public static void ZipTo( this AbsolutePath directory, AbsolutePath archiveFile, @@ -47,50 +108,71 @@ public static void ZipTo( archiveFile.Parent.CreateDirectory(); filter ??= _ => true; - var files = directory.GetFiles(depth: int.MaxValue).Where(filter).ToList(); + List files = directory.GetFiles(depth: int.MaxValue).Where(filter).ToList(); - using var fileStream = File.Open(archiveFile, fileMode, FileAccess.ReadWrite); - using var zipArchive = new ZipArchive(fileStream, ZipArchiveMode.Create); + using FileStream fileStream = File.Open(archiveFile, fileMode, FileAccess.ReadWrite); + using ZipArchive zipArchive = new(fileStream, ZipArchiveMode.Create); void AddFile(AbsolutePath file) { - var relativePath = directory.GetRelativePathTo(file); - var entryName = ZipEntry.CleanName(relativePath); + RelativePath relativePath = directory.GetRelativePathTo(file); + string entryName = ZipEntry.CleanName(relativePath); zipArchive.CreateEntryFromFile(file, entryName, compressionLevel); } files.ForEach(AddFile); } + /// + /// Extracts the contents of a ZIP archive at into . + /// Destination directory is created, and conflicting files are overwritten. + /// + /// The ZIP archive file to extract. + /// The directory into which the archive's contents are extracted. public static void UnZipTo(this AbsolutePath archiveFile, AbsolutePath directory) { - using var fileStream = File.OpenRead(archiveFile); - using var zipFile = new ICSharpCode.SharpZipLib.Zip.ZipFile(fileStream); + using FileStream fileStream = File.OpenRead(archiveFile); + using ZipFile zipFile = new(fileStream); - var entries = zipFile.Cast().Where(x => !x.IsDirectory); + IEnumerable entries = zipFile.Cast().Where(x => !x.IsDirectory); void HandleEntry(ZipEntry entry) { - var file = directory / entry.Name; + AbsolutePath file = directory / entry.Name; Directory.CreateDirectory(file.Parent.NotNull()); - using var entryStream = zipFile.GetInputStream(entry); - using var outputStream = File.Open(file, FileMode.Create); + using Stream entryStream = zipFile.GetInputStream(entry); + using FileStream outputStream = File.Open(file, FileMode.Create); entryStream.CopyTo(outputStream); } entries.ForEach(HandleEntry); } + /// + /// Compresses the given into a gzip-compressed tar archive at . + /// + /// The base directory used to compute the relative entry names of the archived files. + /// The tar.gz archive file to create. + /// The files to add to the archive. + /// The used to open the archive file. public static void TarGZipTo( this AbsolutePath baseDirectory, AbsolutePath archiveFile, IEnumerable files, - FileMode fileMode = FileMode.CreateNew) - { + FileMode fileMode = FileMode.CreateNew) => CompressTar(baseDirectory, archiveFile, files.ToList(), fileMode, x => new GZipOutputStream(x)); - } + /// + /// Compresses into a gzip-compressed tar archive at . + /// + /// The directory whose contents should be added to the archive. + /// The tar.gz archive file to create. + /// + /// An optional predicate used to filter which files are included in the archive. If null, all files are + /// included. + /// + /// The used to open the archive file. public static void TarGZipTo( this AbsolutePath directory, AbsolutePath archiveFile, @@ -98,19 +180,34 @@ public static void TarGZipTo( FileMode fileMode = FileMode.CreateNew) { filter ??= _ => true; - var files = directory.GetFiles(depth: int.MaxValue).Where(filter); + IEnumerable files = directory.GetFiles(depth: int.MaxValue).Where(filter); directory.TarGZipTo(archiveFile, files, fileMode); } + /// + /// Compresses the given into a bzip2-compressed tar archive at . + /// + /// The base directory used to compute the relative entry names of the archived files. + /// The tar.bz2 archive file to create. + /// The files to add to the archive. + /// The used to open the archive file. public static void TarBZip2To( this AbsolutePath directory, AbsolutePath archiveFile, IEnumerable files, - FileMode fileMode = FileMode.CreateNew) - { + FileMode fileMode = FileMode.CreateNew) => CompressTar(directory, archiveFile, files.ToList(), fileMode, x => new BZip2OutputStream(x)); - } + /// + /// Compresses into a bzip2-compressed tar archive at . + /// + /// The directory whose contents should be added to the archive. + /// The tar.bz2 archive file to create. + /// + /// An optional predicate used to filter which files are included in the archive. If null, all files are + /// included. + /// + /// The used to open the archive file. public static void TarBZip2To( this AbsolutePath directory, AbsolutePath archiveFile, @@ -118,20 +215,63 @@ public static void TarBZip2To( FileMode fileMode = FileMode.CreateNew) { filter ??= _ => true; - var files = directory.GetFiles(depth: int.MaxValue).Where(filter); + IEnumerable files = directory.GetFiles(depth: int.MaxValue).Where(filter); directory.TarBZip2To(archiveFile, files, fileMode); } - public static void UnTarGZipTo(this AbsolutePath archiveFile, AbsolutePath directory) - { + /// + /// Extracts the contents of a gzip-compressed tar archive at into . + /// Destination directory is created, and conflicting files are overwritten. + /// + /// The tar.gz archive file to extract. + /// The directory into which the archive's contents are extracted. + public static void UnTarGZipTo(this AbsolutePath archiveFile, AbsolutePath directory) => UncompressTar(archiveFile, directory, x => new GZipInputStream(x)); - } - public static void UnTarBZip2To(this AbsolutePath archiveFile, AbsolutePath directory) - { + /// + /// Extracts the contents of a bzip2-compressed tar archive at into . + /// Destination directory is created, and conflicting files are overwritten. + /// + /// The tar.bz2 archive file to extract. + /// The directory into which the archive's contents are extracted. + public static void UnTarBZip2To(this AbsolutePath archiveFile, AbsolutePath directory) => UncompressTar(archiveFile, directory, x => new BZip2InputStream(x)); + + /// + /// Extracts the contents of an xz-compressed tar archive at into . + /// Destination directory is created, and conflicting files are skipped. + /// + /// The tar.xz archive file to extract. + /// The directory into which the archive's contents are extracted. + public static void UnTarXzTo(this AbsolutePath archive, AbsolutePath directory) + { + using Stream stream = File.OpenRead(archive); + using IReader reader = ReaderFactory.OpenReader(stream); + + while (reader.MoveToNextEntry()) + { + if (reader.Entry.IsDirectory) + { + continue; + } + + reader.WriteEntryToDirectory(directory, new ExtractionOptions + { + ExtractFullPath = true, + Overwrite = true + }); + } } + /// + /// Compresses the given into a tar archive at , wrapping the underlying + /// file stream with the compression stream produced by . + /// + /// The base directory used to compute the relative entry names of the archived files. + /// The archive file to create. + /// The files to add to the archive. + /// The used to open the archive file. + /// A factory that wraps the raw archive file stream with the desired compression stream. private static void CompressTar( AbsolutePath baseDirectory, AbsolutePath archiveFile, @@ -141,26 +281,33 @@ private static void CompressTar( { archiveFile.Parent.CreateDirectory(); - using var fileStream = File.Open(archiveFile, fileMode, FileAccess.ReadWrite); - using var outputStream = outputStreamFactory(fileStream); - using var tarArchive = TarArchive.CreateOutputTarArchive(outputStream); + using FileStream fileStream = File.Open(archiveFile, fileMode, FileAccess.ReadWrite); + using Stream outputStream = outputStreamFactory(fileStream); + using TarArchive tarArchive = TarArchive.CreateOutputTarArchive(outputStream); void AddFile(AbsolutePath file) { - var entry = TarEntry.CreateEntryFromFile(file); + TarEntry entry = TarEntry.CreateEntryFromFile(file); entry.Name = baseDirectory.GetUnixRelativePathTo(file); - tarArchive.WriteEntry(entry, recurse: false); + tarArchive.WriteEntry(entry, false); } files.ForEach(AddFile); } + /// + /// Extracts the contents of a tar archive at into , wrapping the + /// underlying file stream with the decompression stream produced by . + /// + /// The archive file to extract. + /// The directory into which the archive's contents are extracted. + /// A factory that wraps the raw archive file stream with the desired decompression stream. private static void UncompressTar(AbsolutePath archiveFile, AbsolutePath directory, Func inputStreamFactory) { - using var fileStream = File.OpenRead(archiveFile); - using var inputStream = inputStreamFactory(fileStream); - using var tarArchive = TarArchive.CreateInputTarArchive(inputStream, nameEncoding: null); + using FileStream fileStream = File.OpenRead(archiveFile); + using Stream inputStream = inputStreamFactory(fileStream); + using TarArchive tarArchive = TarArchive.CreateInputTarArchive(inputStream, null); directory.CreateDirectory(); diff --git a/src/Fallout.Utilities.IO.Compression/Fallout.Utilities.IO.Compression.csproj b/src/Fallout.Utilities.IO.Compression/Fallout.Utilities.IO.Compression.csproj index 456306f23..7b6c8420a 100644 --- a/src/Fallout.Utilities.IO.Compression/Fallout.Utilities.IO.Compression.csproj +++ b/src/Fallout.Utilities.IO.Compression/Fallout.Utilities.IO.Compression.csproj @@ -10,6 +10,7 @@ +