Runtime: Add better ZipFile extraction APIs

Created on 15 Jun 2018  路  8Comments  路  Source: dotnet/runtime

Proposal

The System.IO.Compression.ZipFile assembly is designed as a filesystem wrapper around ZipArchive to enable users to easily compress/decompress archives on disk. There are some common code patterns that currently require several lines of potentially complex code that can be greatly simplified if we expand the ZipFile assembly to include helpers for those scenarios. This has the benefit of reducing line count for the developer as well as reducing the chance of the pattern being incorrectly followed.

Patterns

Filter zip to only extract entries that match some pattern

This is the most common usage of ExtractToFile. Often times when you have a zip, you only want to decompress the entries that match some sort of pattern e.g. you have a zip of a number of different file types and you want all of the txt files. To do that currently, you need several nested blocks:
```c#
using (ZipArchive archive = ZipFile.OpenRead(zipPath))
{
foreach (ZipArchiveEntry entry in archive.Entries)
{
if (entry.FullName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase))
{
entry.ExtractToFile(Path.Combine(extractPath, entry.FullName));
}
}
}

I am proposing that we simplify that to:
```c#
ZipFile.ExtractToDirectory(zipPath, extractPath, (entry) => entry.FullName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase));

ExtractToFile with a path nested under some root extraction folder

Our implementation of ExtractToFile takes a path to extract the file to, requiring the developer to determine for themselves where that path should be in relation to their root archive extraction location. It would be far better for both design and usability if we absorbed that logic into the function itself, so that entry.ExtractToRelativeDirectory(somedirectory); would combine the path of the entry with somedirectory and extract it to that location.

API Option 1

```c#
namespace System.IO.Compression
{
public static partial class ZipFile
{
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Predicate filter) { }
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, System.Text.Encoding entryNameEncoding, Predicate filter) { }
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, bool overwriteFiles, Predicate filter) { }
public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, System.Text.Encoding entryNameEncoding, bool overwriteFiles, Predicate filter) { }
}
public static partial class ZipFileExtensions
{
public static void ExtractToDirectory(this System.IO.Compression.ZipArchive source, string destinationDirectoryName, Predicate filter) { }
public static void ExtractToDirectory(this System.IO.Compression.ZipArchive source, string destinationDirectoryName, bool overwriteFiles, Predicate filter) { }
public static void ExtractRelativeToDirectory(this System.IO.Compression.ZipArchiveEntry source, string destinationDirectoryName) { }
public static void ExtractRelativeToDirectory(this System.IO.Compression.ZipArchiveEntry source, string destinationDirectoryName, bool overwrite) { }
}
}

# API Option 2
```c#
public class ZipFileOptions
{
    bool Overwrite = false;
    bool PreserveUnixPermissions = true;
    Encoding Encoding = null;
    CompressionLevel CompressionLevel = CompressionLevel.Optimal;
    Predicate<ZipArchiveEntry> EntryFilter = null;
}

public static partial class ZipFile
{
    public static void CreateFromDirectory(string sourceDirectoryName, string destinationArchiveFileName, ZipFileOptions options) {}
    public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, ZipFileOptions options) {}
}
public static partial class ZipFileExtensions
{
    public static void ExtractToDirectory(this System.IO.Compression.ZipArchive source, string destinationDirectoryName, ZipFileOptions options) { }
    public static void ExtractRelativeToDirectory(this System.IO.Compression.ZipArchiveEntry source, string destinationDirectoryName, ZipFileOptions options) { }
}

cc: @viktorhofer, @stephentoub, @jeremykuhne @leculver @terrajobst

api-suggestion area-System.IO.Compression

Most helpful comment

I would still recommend using Predicate<ZipArchiveEntry> filter over Predicate<string> filter. I know there's not a lot actually in ZipArchiveEntry, but I still think it would give the user more flexibility to filter based on Length, ExternalAttributes, LastWriteTime, etc. The example given still works with minimal modification:

c# ZipFile.ExtractToDirectory(zipPath, extractPath, (zaentry) => zaentry.FullName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase));

Though I'm 100% sure the vast majority of cases will be filtering on FullName anyway, I don't see any reason to overly restrict what we hand out for the Predicate.

All 8 comments

I would still recommend using Predicate<ZipArchiveEntry> filter over Predicate<string> filter. I know there's not a lot actually in ZipArchiveEntry, but I still think it would give the user more flexibility to filter based on Length, ExternalAttributes, LastWriteTime, etc. The example given still works with minimal modification:

c# ZipFile.ExtractToDirectory(zipPath, extractPath, (zaentry) => zaentry.FullName.EndsWith(".txt", StringComparison.OrdinalIgnoreCase));

Though I'm 100% sure the vast majority of cases will be filtering on FullName anyway, I don't see any reason to overly restrict what we hand out for the Predicate.

Good points! I'll update the proposal.

Video

The scenario makes sense.

The idea with an option type looks good in principles, but we'd probably want one for creation and one for extraction because not all options for both sides -- which seems overkill.

@weshaggard had the idea of introducing the new method using default parameters so that we get away with fewer overloads.

@ianhays, could you give that a shot?

Reassigned to @JeremyKuhne while @ianhays is out of office.

@joshfree I assume this should not be milestone 2.2, correct?

I assume this should not be milestone 2.2, correct?

@danmosemsft correct

@weshaggard had the idea of introducing the new method using default parameters so that we get away with fewer overloads. @ianhays, could you give that a shot?

Sure. I was under the impression that default params were generally frowned upon for public API for some reason so I went with the more verbose options. Here is the first option reworked with defaults:

API Option 3 - Default Params

c# namespace System.IO.Compression { public static partial class ZipFile { public static void ExtractToDirectory(string sourceArchiveFileName, string destinationDirectoryName, Predicate<ZipArchiveEntry> filter, System.Text.Encoding entryNameEncoding = null, bool overwriteFiles = false) { } } public static partial class ZipFileExtensions { public static void ExtractToDirectory(this System.IO.Compression.ZipArchive source, string destinationDirectoryName, Predicate<ZipArchiveEntry> filter, bool overwriteFiles = false) { } public static void ExtractRelativeToDirectory(this System.IO.Compression.ZipArchiveEntry source, string destinationDirectoryName, bool overwrite = false) { } } }

Triage:
This API ask is for better control of zips with respect to the filesystem, we should re-evaluate this in light of the new filesystem enumeration APIs and see if there is an opportunity to mirror those improvements.

@JeremyKuhne we need to pick your brain on this.

Was this page helpful?
0 / 5 - 0 ratings