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:Creating Archives
Usenew Bun.Archive() to create an archive from an object where keys are file paths and values are file contents. By default, archives are uncompressed:
- 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
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
UseBun.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:
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):
File object includes:
name- The file path within the archive (always uses forward slashes/as separators)size- File size in byteslastModified- Modification timestamp- Standard
Blobmethods such astext(),arrayBuffer(), andstream()
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:- 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
files() returns an empty Map if no files match:
Filtering with Glob Patterns
Pass a glob pattern to filter which files are returned:*- 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.
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.
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:
Promise.all() over a list of files produces a deterministic archive:
- Only archives you build can be appended to: one created from an object, or from no data at all. Appending to an
Archivethat 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
0644and 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.
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:
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 anyReadableStreamwhose producer never closes it. For any other archiveend()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(), andextract()throw. - If the consumer cancels, a parked
append()rejects rather than hanging, and a failedappend()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; useformat: "zip", whose compression is per-entry.
Examples
Bundle Project Files
Extract and Process npm Package
Create Archive from 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.