Skip to content

Commit

Permalink
ZipExtensionWrapper class added with unit tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
defnegoncu committed Jan 29, 2025
1 parent 1d9b7b9 commit 5e178be
Show file tree
Hide file tree
Showing 3 changed files with 776 additions and 44 deletions.
36 changes: 36 additions & 0 deletions DataAccess/Extensions/ZipExtensionWrapper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.IO.Abstractions;
using System.IO.Compression;

namespace DataAccess.Extensions;
/// <inheritdoc cref="ZipExtension"/>
public class ZipExtensionWrapper
{
/// <summary>
/// Gets the file system used by this wrapper.
/// </summary>
public IFileSystem FileSystem { get; private set; }


public ZipExtensionWrapper(IFileSystem fileSystem)
{
FileSystem = fileSystem;
}

/// <inheritdoc cref="ZipExtension.GetZipArchive"/>
public ZipArchive GetZipArchive(string archivePath)
{
return ZipExtensions.GetZipArchive(FileSystem, archivePath);
}

/// <inheritdoc cref="ZipExtension.ExtractToDirectory"/>
public void ExtractToDirectory(ZipArchive archive, string destination)
{
ZipExtensions.ExtractToDirectory(archive, FileSystem, destination);
}

/// <inheritdoc cref="ZipExtension.CreateFromDirectoryAsync"/>
public async Task CreateFromDirectoryAsync(string sourcePath, string destinationPath)
{
await ZipExtensions.CreateFromDirectoryAsync(FileSystem, sourcePath, destinationPath);
}
}
140 changes: 96 additions & 44 deletions DataAccess/Extensions/ZipExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,74 +6,126 @@ namespace DataAccess.Extensions;
public static class ZipExtensions
{
/// <summary>
/// Creates a <see cref="ZipArchive"/> object for a given path on the filesystem <paramref name="fs"/>.
/// Opens a zip archive in read mode.
/// </summary>
/// <param name="fs">The filesystem on which the file resides.</param>
/// <param name="archivePath">The path to the zip archive on the filesystem.</param>
/// <returns></returns>
public static ZipArchive GetZipArchive(IFileSystem fs, string archivePath)
/// <param name="fileSystemReference">The file system abstraction used to access the file.</param>
/// <param name="archivePath">The path to the zip archive file.</param>
/// <returns>A <see cref="ZipArchive"/> instance representing the archive.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="fileSystemReference"/> or <paramref name="archivePath"/> is null.</exception>
/// <exception cref="FileNotFoundException">Thrown if the specified file does not exist.</exception>
/// <exception cref="UnauthorizedAccessException">Thrown if the caller does not have the required permissions.</exception>
/// <exception cref="InvalidDataException">Thrown if the file is not a valid zip archive.</exception>
public static ZipArchive GetZipArchive(IFileSystem fileSystemReference, string archivePath)
{
var zipStream = fs.File.OpenRead(archivePath);
var zipStream = fileSystemReference.File.OpenRead(archivePath);
return new ZipArchive(zipStream, ZipArchiveMode.Read);
}

private static ZipArchive GetWritableZipArchive(IFileSystem fs, string archivePath)
private static ZipArchive GetWritableZipArchive(IFileSystem fileSystemReference, string archivePath)
{
var zipStream = fs.File.OpenWrite(archivePath);
var zipStream = fileSystemReference.File.OpenWrite(archivePath);
return new ZipArchive(zipStream, ZipArchiveMode.Create);
}

/// <summary>
/// Extracts the contents of a <see cref="ZipArchive"/> to a given directory on the filesystem <paramref name="fs"/>.
/// Extracts all entries from a zip archive to the specified directory.
/// </summary>
/// <param name="archive">The zip archive to extract.</param>
/// <param name="fs">The filesystem to extract the archive onto.</param>
/// <param name="destination">The destination path on the filesystem.</param>
public static void ExtractToDirectory(this ZipArchive archive, IFileSystem fs, string destination)
/// <param name="fileSystemReference">The file system abstraction used to access files and directories.</param>
/// <param name="destination">The directory to extract the archive's contents to.</param>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="destination"/> is null.</exception>
/// <exception cref="UnauthorizedAccessException">Thrown if the caller does not have write access to the destination directory.</exception>
/// <exception cref="IOException">Thrown if an I/O error occurs during extraction.</exception>
public static void ExtractToDirectory(this ZipArchive archive, IFileSystem fileSystemReference, string destination)
{
if (!fs.Directory.Exists(destination))
{
fs.Directory.CreateDirectory(destination);
}
EnsureDestinationDirectoryExists(fileSystemReference, destination);

foreach (var entry in archive.Entries)
{
var entryFullName = Path.DirectorySeparatorChar switch
{
//adjust paths for unpacking on unix when packed on windows
'/' => entry.FullName.Replace("\\", "/"),
//adjust paths for unpacking on windows when packed on unix
'\\' => entry.FullName.Replace("/", "\\"),
_ => entry.FullName
};
var path = Path.Combine(destination, entryFullName);
var directoryName = Path.GetDirectoryName(path);
if (directoryName != null) fs.Directory.CreateDirectory(directoryName);
if (fs.File.Exists(path)) fs.File.Delete(path);
var extractedEntryPath = NormalizeExtractedEntryPathForWindowsAndUnix(entry);
var fullPath = Path.Combine(destination, extractedEntryPath);
CreateEntryDirectory(fileSystemReference, fullPath);

if (fileSystemReference.File.Exists(fullPath))
fileSystemReference.File.Delete(fullPath);

using var destStream = fs.File.Create(path);
using var destinationStream = fileSystemReference.File.Create(fullPath);
using var sourceStream = entry.Open();
sourceStream.CopyTo(destStream);
sourceStream.CopyTo(destinationStream);
}
}

/// <summary>
/// Creates a zip archive from a given directory on the filesystem <paramref name="fs"/>.
/// Creates a zip archive from the contents of a directory asynchronously.
/// </summary>
/// <param name="fs">The filesystem to operate on.</param>
/// <param name="source">The folder that should be packed into the zip archive.</param>
/// <param name="destination">The file path the zip archive should be written to.</param>
public static async Task CreateFromDirectoryAsync(IFileSystem fs, string source, string destination)
/// <param name="fileSystemReference">The file system abstraction used to access files and directories.</param>
/// <param name="source">The source directory to compress.</param>
/// <param name="destination">The path of the resulting zip archive.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> or <paramref name="destination"/> is null.</exception>
/// <exception cref="DirectoryNotFoundException">Thrown if the source directory does not exist.</exception>
/// <exception cref="UnauthorizedAccessException">Thrown if the caller does not have the required permissions.</exception>
/// <exception cref="IOException">Thrown if an I/O error occurs during compression.</exception>
public static async Task CreateFromDirectoryAsync(IFileSystem fileSystemReference, string source, string destination)
{
using var archive = GetWritableZipArchive(fs, destination);
var files = fs.Directory.GetFiles(source, "*", SearchOption.AllDirectories);
ValidatePath(source);
ValidatePath(destination);
using var archive = GetWritableZipArchive(fileSystemReference, destination);

var files = GetAllFilesExceptSymbolicLinksToPreventLoops(fileSystemReference, source);
foreach (var file in files)
{
var relativePath = Path.GetRelativePath(source, file);
var entry = archive.CreateEntry(relativePath);
await using var entryStream = entry.Open();
await using var fileStream = fs.File.OpenRead(file);
await fileStream.CopyToAsync(entryStream);
await AddFileToArchiveAsync(fileSystemReference, archive, relativePath, file);
}
}
private static void ValidatePath(string path)
{
if (path == null)
throw new ArgumentNullException(nameof(path), "Path cannot be null.");
if (path.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
{
throw new ArgumentException($"The path is invalid: {path}");
}
}
private static async Task AddFileToArchiveAsync(IFileSystem fileSystemReference, ZipArchive archive,
string relativePath, string file)
{
var entry = archive.CreateEntry(relativePath);
await using var entryStream = entry.Open();
await using var fileStream = fileSystemReference.File.OpenRead(file);
await fileStream.CopyToAsync(entryStream);
}

private static string[] GetAllFilesExceptSymbolicLinksToPreventLoops(IFileSystem fileSystemReference, string source)
{
return fileSystemReference.Directory.GetFiles(source, "*", SearchOption.AllDirectories)
.Where(file => !fileSystemReference.File.GetAttributes(file).HasFlag(FileAttributes.ReparsePoint))
.ToArray();
}


private static void CreateEntryDirectory(IFileSystem fileSystemReference, string fullPath)
{
var directoryName = Path.GetDirectoryName(fullPath);
if (directoryName != null) fileSystemReference.Directory.CreateDirectory(directoryName);
}

private static string NormalizeExtractedEntryPathForWindowsAndUnix(ZipArchiveEntry entry)
{
return Path.DirectorySeparatorChar switch
{
//adjust paths for unpacking on unix when packed on windows
'/' => entry.FullName.Replace("\\", "/"),
//adjust paths for unpacking on windows when packed on unix
'\\' => entry.FullName.Replace("/", "\\"),
_ => entry.FullName
};
}

private static void EnsureDestinationDirectoryExists(IFileSystem fileSystemReference, string destination)
{
if (!fileSystemReference.Directory.Exists(destination))
{
fileSystemReference.Directory.CreateDirectory(destination);
}
}
}
Loading

0 comments on commit 5e178be

Please sign in to comment.