Skip to main content
Bun.Archive is Bun’s native API for tar and zip archives. It creates archives from in-memory data, streams entries into an archive one at a time, extracts archives to disk, and reads archive contents without extraction.

Quickstart

Create an archive from files:
Build a zip one entry at a time, streaming from disk:
Extract an archive:
Read archive contents without extracting:

Creating Archives

Use new Bun.Archive() to create an archive from an object where keys are file paths and values are file contents. By default, archives are uncompressed:
File contents can be:
  • Strings - Text content
  • Blobs - Binary data
  • Bun.file() - Read from disk synchronously, in chunks, while the constructor runs
  • ArrayBufferViews (such as Uint8Array) - Raw bytes
  • ArrayBuffers - Raw binary data
A Bun.file() entry is never buffered whole, but it does block the constructor. To add large files without blocking, use append() instead.

Writing Archives to Disk

Use Bun.write() to write an archive to disk:

Getting Archive Bytes

Get the archive data as bytes or a Blob:

Extracting Archives

From Existing Archive Data

Create an archive from existing tar/tar.gz data:

Extracting to Disk

Use .extract() to write all files to a directory:
extract() creates the target directory if it doesn’t exist and overwrites existing files. The returned count includes files, directories, and symlinks (on POSIX systems). Note: On Windows, Bun always skips symbolic links during extraction, regardless of privilege level. On Linux and macOS, symlinks are extracted normally. Security note: Bun.Archive validates paths during extraction. It rejects absolute paths (POSIX /, Windows drive letters like C:\ or C:/, and UNC paths like \\server\share) and unsafe symlink targets. Path traversal components (..) are normalized away to prevent directory escape attacks: dir/sub/../file becomes dir/file.

Filtering Extracted Files

Use glob patterns to extract only specific files. Patterns are matched against archive entry paths normalized to use forward slashes (/). Positive patterns specify what to include, and negative patterns (prefixed with !) specify what to exclude. When only negative patterns are provided, all entries that don’t match them are included:
When mixing positive and negative patterns, entries must match at least one positive pattern and no negative pattern:

Reading Archive Contents

Get All Files

Use .files() to get archive contents as a Map of File objects without extracting to disk. Unlike extract(), which processes all entry types, files() returns only regular files (no directories):
Each File object includes:
  • name - The file path within the archive (always uses forward slashes / as separators)
  • size - File size in bytes
  • lastModified - Modification timestamp
  • Standard Blob methods such as text(), arrayBuffer(), and stream()
Note: files() loads file contents into memory. For large archives, use extract() to write directly to disk instead.

Error Handling

Archive operations can fail due to corrupted data, I/O errors, or invalid paths. Use try/catch to handle these cases:
Common error scenarios:
  • Corrupted/truncated archives - new Archive() loads the archive data; errors may be deferred until read/extract operations
  • Permission denied - extract() throws if the target directory is not writable
  • Disk full - extract() throws if there’s insufficient space
  • Invalid paths - Operations throw for malformed file paths
For additional security with untrusted archives, you can enumerate and validate paths before extraction:
When called with a glob pattern, files() returns an empty Map if no files match:

Filtering with Glob Patterns

Pass a glob pattern to filter which files are returned:
Supported glob patterns (subset of Bun.Glob syntax):
  • * - Match any characters except /
  • ** - Match any characters including /
  • ? - Match single character
  • [abc] - Match character set
  • {a,b} - Match alternatives
  • !pattern - Exclude files matching pattern (negation). When only negative patterns are provided, all files not matching them are included.
See Bun.Glob for the full glob syntax including escaping and advanced patterns.

Formats

Bun.Archive writes tar (the default) and zip. Reading auto-detects tar, tar.gz, and zip, so you never pass a format when you are reading an existing archive.

Compression

tar archives are uncompressed unless you pass { compress: "gzip" }, which gzips the finished archive. zip archives deflate each entry by default; pass { compress: "store" } to turn that off.
When Bun.Archive.write() is handed an existing Archive, its bytes are already packed, so format has no effect there; only compress still applies, and omitting it inherits the archive’s own setting. The options argument accepts:
  • { format: "tar" | "zip" } - Container format (default "tar")
  • { compress: "gzip" } - Gzip the finished tar archive (tar only)
  • { compress: "deflate" | "store" } - Per-entry zip compression (zip only; "deflate" is the default)
  • { level: number } - 1-12 for gzip, 1-9 for deflate (default 6)

Appending Entries

append() adds one more entry to an archive you are building, without holding the entry’s contents in memory first. Pass a Bun.file() and it is read from disk a chunk at a time, so the file’s bytes are never buffered whole:
Appends run in call order, so Promise.all() over a list of files produces a deterministic archive:
Rules:
  • Only archives you build can be appended to: one created from an object, or from no data at all. Appending to an Archive that wraps existing archive data throws.
  • An archive is closed the first time its bytes are read (bytes(), blob(), files(), extract(), Bun.write()). append() after that throws.
  • Reading an archive while an append() is still pending throws. Await your appends first.
  • If an append() rejects, the entry was left half-written: the archive is truncated and can no longer be used.
  • Entries are written with mode 0644 and the current time as their modification time.

Streaming Large Archives

stream() hands you the archive’s bytes as a ReadableStream while it is still being built. Reading the stream is what lets the next append() make progress: once the stream’s queue fills up, an in-flight append() parks until the consumer has caught up. A consumer that drains the stream chunk by chunk therefore never has to hold the whole archive in memory.
Drain the stream incrementally, as above, for the memory bound to hold. A consumer that asks for the whole thing at once, like new Response(archive.stream()).bytes(), buffers the entire archive by definition. Streaming a zip straight to an HTTP client, so the client sees bytes before the archive is finished:
A slow client does not currently throttle the append()s on this path: the server hands each chunk straight to the socket, which buffers whatever it cannot write yet. The response is still produced incrementally, but peak memory is bounded by the archive, not by the high-water mark. Rules:
  • Call stream() before appending anything, and only once.
  • Finish with end() to close the stream. Abandoning a streaming archive without it leaves the consumer waiting, exactly like any ReadableStream whose producer never closes it. For any other archive end() just seals it, which the first read of its bytes does anyway.
  • A streamed archive’s bytes belong to the consumer, so bytes(), blob(), files(), and extract() throw.
  • If the consumer cancels, a parked append() rejects rather than hanging, and a failed append() fails the consumer’s read rather than leaving it waiting.
  • compress: "gzip" is a post-filter over the finished tar, so it cannot be streamed. stream() throws on a gzip archive; use format: "zip", whose compression is per-entry.

Examples

Bundle Project Files

Extract and Process npm Package

Create Archive from Directory

For a large directory, append() keeps the constructor from blocking:

Reference

Note: The following type signatures are simplified. See packages/bun-types/bun.d.ts for the full type definitions.