Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="SharpCompress" Version="0.50.1" />
Comment thread
ITaluone marked this conversation as resolved.
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
<PackageVersion Include="System.Security.Cryptography.Xml" Version="10.0.10" />
Expand Down Expand Up @@ -90,4 +91,4 @@
<PackageVersion Update="Microsoft.Build.Utilities.Core" Version="17.11.48" />
<PackageVersion Update="System.Security.Cryptography.Xml" Version="8.0.4" />
</ItemGroup>
</Project>
</Project>
213 changes: 180 additions & 33 deletions src/Fallout.Utilities.IO.Compression/CompressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
ITaluone marked this conversation as resolved.

public static class CompressionExtensions
{
/// <summary>
/// Compresses <paramref name="directory" /> into <paramref name="archiveFile" />, choosing the compression format based on the
/// archive file's extension.
/// </summary>
/// <param name="directory">The directory whose contents should be compressed.</param>
/// <param name="archiveFile">
/// The archive file to create. Its extension determines the compression format (e.g. <c>.zip</c>,
/// <c>.tar.gz</c>, <c>.tar.bz2</c>).
/// </param>
/// <param name="filter">
/// An optional predicate used to filter which files are included in the archive. If <c>null</c>, all files are
/// included.
/// </param>
public static void CompressTo(this AbsolutePath directory, AbsolutePath archiveFile, Func<AbsolutePath, bool> 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)}'");
}
}

/// <summary>
/// Uncompresses <paramref name="archiveFile" /> into <paramref name="directory" />, choosing the decompression format based on
/// the archive file's extension.
/// </summary>
/// <param name="archiveFile">
/// The archive file to extract. Its extension determines the decompression format (e.g. <c>.zip</c>,
/// <c>.tar.gz</c>, <c>.tar.bz2</c>, <c>.tar.xz</c>).
/// </param>
/// <param name="directory">The directory into which the archive's contents are extracted.</param>
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)}'");
}
}

/// <summary>
/// Compresses <paramref name="directory" /> into a ZIP archive at <paramref name="archiveFile" />.
/// </summary>
/// <param name="directory">The directory whose contents should be added to the ZIP archive.</param>
/// <param name="archiveFile">The ZIP archive file to create.</param>
/// <param name="filter">
/// An optional predicate used to filter which files are included in the archive. If <c>null</c>, all files are
/// included.
/// </param>
/// <param name="compressionLevel">The compression level to use for the archive entries.</param>
/// <param name="fileMode">The <see cref="FileMode" /> used to open the archive file.</param>
public static void ZipTo(
this AbsolutePath directory,
AbsolutePath archiveFile,
Expand All @@ -47,91 +108,170 @@ public static void ZipTo(
archiveFile.Parent.CreateDirectory();

filter ??= _ => true;
var files = directory.GetFiles(depth: int.MaxValue).Where(filter).ToList();
List<AbsolutePath> 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);
}

/// <summary>
/// Extracts the contents of a ZIP archive at <paramref name="archiveFile" /> into <paramref name="directory" />.
/// Destination directory is created, and conflicting files are overwritten.
/// </summary>
/// <param name="archiveFile">The ZIP archive file to extract.</param>
/// <param name="directory">The directory into which the archive's contents are extracted.</param>
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<ZipEntry>().Where(x => !x.IsDirectory);
IEnumerable<ZipEntry> entries = zipFile.Cast<ZipEntry>().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);
}

/// <summary>
/// Compresses the given <paramref name="files" /> into a gzip-compressed tar archive at <paramref name="archiveFile" />.
/// </summary>
/// <param name="baseDirectory">The base directory used to compute the relative entry names of the archived files.</param>
/// <param name="archiveFile">The tar.gz archive file to create.</param>
/// <param name="files">The files to add to the archive.</param>
/// <param name="fileMode">The <see cref="FileMode" /> used to open the archive file.</param>
public static void TarGZipTo(
this AbsolutePath baseDirectory,
AbsolutePath archiveFile,
IEnumerable<AbsolutePath> files,
FileMode fileMode = FileMode.CreateNew)
{
FileMode fileMode = FileMode.CreateNew) =>
CompressTar(baseDirectory, archiveFile, files.ToList(), fileMode, x => new GZipOutputStream(x));
}

/// <summary>
/// Compresses <paramref name="directory" /> into a gzip-compressed tar archive at <paramref name="archiveFile" />.
/// </summary>
/// <param name="directory">The directory whose contents should be added to the archive.</param>
/// <param name="archiveFile">The tar.gz archive file to create.</param>
/// <param name="filter">
/// An optional predicate used to filter which files are included in the archive. If <c>null</c>, all files are
/// included.
/// </param>
/// <param name="fileMode">The <see cref="FileMode" /> used to open the archive file.</param>
public static void TarGZipTo(
this AbsolutePath directory,
AbsolutePath archiveFile,
Func<AbsolutePath, bool> filter = null,
FileMode fileMode = FileMode.CreateNew)
{
filter ??= _ => true;
var files = directory.GetFiles(depth: int.MaxValue).Where(filter);
IEnumerable<AbsolutePath> files = directory.GetFiles(depth: int.MaxValue).Where(filter);
directory.TarGZipTo(archiveFile, files, fileMode);
}

/// <summary>
/// Compresses the given <paramref name="files" /> into a bzip2-compressed tar archive at <paramref name="archiveFile" />.
/// </summary>
/// <param name="directory">The base directory used to compute the relative entry names of the archived files.</param>
/// <param name="archiveFile">The tar.bz2 archive file to create.</param>
/// <param name="files">The files to add to the archive.</param>
/// <param name="fileMode">The <see cref="FileMode" /> used to open the archive file.</param>
public static void TarBZip2To(
this AbsolutePath directory,
AbsolutePath archiveFile,
IEnumerable<AbsolutePath> files,
FileMode fileMode = FileMode.CreateNew)
{
FileMode fileMode = FileMode.CreateNew) =>
CompressTar(directory, archiveFile, files.ToList(), fileMode, x => new BZip2OutputStream(x));
}

/// <summary>
/// Compresses <paramref name="directory" /> into a bzip2-compressed tar archive at <paramref name="archiveFile" />.
/// </summary>
/// <param name="directory">The directory whose contents should be added to the archive.</param>
/// <param name="archiveFile">The tar.bz2 archive file to create.</param>
/// <param name="filter">
/// An optional predicate used to filter which files are included in the archive. If <c>null</c>, all files are
/// included.
/// </param>
/// <param name="fileMode">The <see cref="FileMode" /> used to open the archive file.</param>
public static void TarBZip2To(
this AbsolutePath directory,
AbsolutePath archiveFile,
Func<AbsolutePath, bool> filter = null,
FileMode fileMode = FileMode.CreateNew)
{
filter ??= _ => true;
var files = directory.GetFiles(depth: int.MaxValue).Where(filter);
IEnumerable<AbsolutePath> files = directory.GetFiles(depth: int.MaxValue).Where(filter);
directory.TarBZip2To(archiveFile, files, fileMode);
}

public static void UnTarGZipTo(this AbsolutePath archiveFile, AbsolutePath directory)
{
/// <summary>
/// Extracts the contents of a gzip-compressed tar archive at <paramref name="archiveFile" /> into <paramref name="directory" />.
/// Destination directory is created, and conflicting files are overwritten.
/// </summary>
/// <param name="archiveFile">The tar.gz archive file to extract.</param>
/// <param name="directory">The directory into which the archive's contents are extracted.</param>
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)
{
/// <summary>
/// Extracts the contents of a bzip2-compressed tar archive at <paramref name="archiveFile" /> into <paramref name="directory" />.
/// Destination directory is created, and conflicting files are overwritten.
/// </summary>
/// <param name="archiveFile">The tar.bz2 archive file to extract.</param>
/// <param name="directory">The directory into which the archive's contents are extracted.</param>
public static void UnTarBZip2To(this AbsolutePath archiveFile, AbsolutePath directory) =>
UncompressTar(archiveFile, directory, x => new BZip2InputStream(x));

/// <summary>
/// Extracts the contents of an xz-compressed tar archive at <paramref name="archive" /> into <paramref name="directory" />.
/// Destination directory is created, and conflicting files are skipped.
/// </summary>
/// <param name="archive">The tar.xz archive file to extract.</param>
/// <param name="directory">The directory into which the archive's contents are extracted.</param>
public static void UnTarXzTo(this AbsolutePath archive, AbsolutePath directory)
Comment thread
ITaluone marked this conversation as resolved.
{
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
});
}
}

/// <summary>
/// Compresses the given <paramref name="files" /> into a tar archive at <paramref name="archiveFile" />, wrapping the underlying
/// file stream with the compression stream produced by <paramref name="outputStreamFactory" />.
/// </summary>
/// <param name="baseDirectory">The base directory used to compute the relative entry names of the archived files.</param>
/// <param name="archiveFile">The archive file to create.</param>
/// <param name="files">The files to add to the archive.</param>
/// <param name="fileMode">The <see cref="FileMode" /> used to open the archive file.</param>
/// <param name="outputStreamFactory">A factory that wraps the raw archive file stream with the desired compression stream.</param>
private static void CompressTar(
AbsolutePath baseDirectory,
AbsolutePath archiveFile,
Expand All @@ -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);
}

/// <summary>
/// Extracts the contents of a tar archive at <paramref name="archiveFile" /> into <paramref name="directory" />, wrapping the
/// underlying file stream with the decompression stream produced by <paramref name="inputStreamFactory" />.
/// </summary>
/// <param name="archiveFile">The archive file to extract.</param>
/// <param name="directory">The directory into which the archive's contents are extracted.</param>
/// <param name="inputStreamFactory">A factory that wraps the raw archive file stream with the desired decompression stream.</param>
private static void UncompressTar(AbsolutePath archiveFile, AbsolutePath directory, Func<Stream, Stream> 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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

<ItemGroup>
<PackageReference Include="SharpZipLib" />
<PackageReference Include="SharpCompress" />
Comment thread
ITaluone marked this conversation as resolved.
</ItemGroup>

</Project>
Loading