Skip to content

Repository files navigation

RemoteZip

Build status NuGet Status

Read entries from a remote zip over HTTP range requests, without downloading the whole archive.

A zip's table of contents (the central directory) lives at the end of the file, and every entry is compressed independently. So listing a remote archive costs one request for the file tail, and reading an entry costs one request sized to that entry — a few KB fetched from a multi-MB archive. Useful for any zip-shaped format: nupkg, vsix, jar, apk, docx, epub.

Async all the way down, so it works from Blazor WebAssembly — the original motivation was inspecting NuGet packages client-side in the browser against nuget.org's flat container, which supports both range requests and the CORS preflight for them.

See Milestones for release notes.

NuGet package

https://nuget.org/packages/RemoteZip/

Contents

Usage

public static async Task PrintRemoteZip(HttpClient http, string url)
{
    var zip = await RemoteZipArchive.Open(http, url);

    foreach (var entry in zip.Entries)
    {
        Console.WriteLine($"{entry.FullName} ({entry.Length} bytes)");
    }

    var readme = zip.Find("readme.md");
    if (readme != null)
    {
        Console.WriteLine(await zip.ReadText(readme));
    }
}

snippet source | anchor

Reading multiple entries

Entries that sit close together in the archive coalesce into a single request:

public static async Task<string?> ReadNuspec(HttpClient http, string packageUrl)
{
    var zip = await RemoteZipArchive.Open(http, packageUrl);

    // Entries close together in the archive are fetched in one request.
    var wanted = zip.Entries
        .Where(_ => _.FullName.EndsWith(".nuspec") || _.FullName.StartsWith("build/"))
        .ToList();
    var contents = await zip.Read(wanted);

    var nuspec = wanted.FirstOrDefault(_ => _.FullName.EndsWith(".nuspec"));
    return nuspec == null ? null : Encoding.UTF8.GetString(contents[nuspec]);
}

snippet source | anchor

How it works

Opening sends one suffix range request (Range: bytes=-131072) and parses the end-of-central-directory record from it. For typical archives the whole central directory is inside that tail, so enumeration costs exactly one request; an oversized central directory costs one more. Each Read then fetches local header + entry data in one range request, over-fetching by 512 bytes to cover local extra fields; a batched Read merges entries whose ranges are within 8 KB of each other.

Browsers add a wrinkle: CORS hides Content-Range unless the server explicitly exposes it (nuget.org does not), so the file's total length cannot be read from the response. RemoteZip instead derives every absolute offset from the end-of-central-directory record itself and validates the result against the central directory signature — no Content-Range, HEAD request, or Content-Length needed.

Zip64 archives are supported. Entries must be stored or deflated (the only methods that appear in practice); every read is crc-validated.

Fallback behavior

Servers that ignore range requests (plain 200 responses) degrade transparently: the archive is buffered in full — bounded by MaxBufferLength — and reads are served from memory. The same happens when the file is smaller than the tail request. DownloadedWholeFile reports which mode an archive ended up in. A server answering a valid range request with 200 mid-session (observed on nuget.org's CDN for cold edge caches) is also handled per request.

Options

Option Default Purpose
TailLength 128 KiB Size of the opening suffix request. The default always contains the end-of-central-directory record and, for typical archives, the whole central directory.
MaxBufferLength 1 GiB Cap on full-download fallback and on any single entry read.
ConfigureRequest Applied to every outgoing request.

Blazor WebAssembly

The browser HTTP cache can answer one range request with the cached body of another. Opt ranged requests out of the cache:

var options = new RemoteZipOptions
{
    ConfigureRequest = request => request.SetBrowserRequestCache(BrowserRequestCache.NoStore)
};

Server requirements

The server must support Range requests (206 Partial Content). For browser use it must also allow the Range header in its CORS policy (Access-Control-Allow-Headers: range). Exposing Content-Range is not required. nuget.org's flat container satisfies all of this, including from localhost origins.

Limitations

  • Encrypted entries and compression methods other than stored/deflate throw.
  • Archives with data prepended to the zip (self-extracting layouts) are rejected.
  • Entry names are decoded as UTF-8 regardless of the cp437 flag; non-ASCII names in ancient archives may decode differently than intended.

Icon

Original artwork created for this project.

About

No description, website, or topics provided.

Resources

Contributing

Stars

Watchers

Forks

Releases

Contributors

Languages