> ## Documentation Index
> Fetch the complete documentation index at: https://bun-1dd33a4e-farm-b16f2c14-archive-zip-streaming-append.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Archive

> Create and extract tar and zip archives with Bun's fast native implementation

`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:**

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const archive = new Bun.Archive({
  "hello.txt": "Hello, World!",
  "data.json": JSON.stringify({ foo: "bar" }),
  "nested/file.txt": "Nested content",
});

// Write to disk
await Bun.write("bundle.tar", archive);
```

**Build a zip one entry at a time, streaming from disk:**

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const archive = new Bun.Archive(undefined, { format: "zip" });
await archive.append("app.js", Bun.file("./dist/app.js"));
await archive.append("video.mp4", Bun.file("./video.mp4"));

await Bun.Archive.write("bundle.zip", archive);
```

**Extract an archive:**

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);
const entryCount = await archive.extract("./output");
console.log(`Extracted ${entryCount} entries`);
```

**Read archive contents without extracting:**

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);
const files = await archive.files();

for (const [path, file] of files) {
  console.log(`${path}: ${await file.text()}`);
}
```

## 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:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// Creates an uncompressed tar archive (default)
const archive = new Bun.Archive({
  "README.md": "# My Project",
  "src/index.ts": "console.log('Hello');",
  "package.json": JSON.stringify({ name: "my-project" }),
});
```

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()`](#appending-entries) instead.

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const data = "binary data";
const arrayBuffer = new ArrayBuffer(8);

const archive = new Bun.Archive({
  "text.txt": "Plain text",
  "blob.bin": new Blob([data]),
  "bytes.bin": new Uint8Array([1, 2, 3, 4]),
  "buffer.bin": arrayBuffer,
});
```

### Writing Archives to Disk

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

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// Write uncompressed tar (default)
const archive = new Bun.Archive({
  "file1.txt": "content1",
  "file2.txt": "content2",
});
await Bun.write("output.tar", archive);

// Write gzipped tar
const compressed = new Bun.Archive({ "src/index.ts": "console.log('Hello');" }, { compress: "gzip" });
await Bun.write("output.tar.gz", compressed);
```

### Getting Archive Bytes

Get the archive data as bytes or a Blob:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const archive = new Bun.Archive({ "hello.txt": "Hello, World!" });

// As Uint8Array
const bytes = await archive.bytes();

// As Blob
const blob = await archive.blob();

// With gzip compression (set at construction)
const gzipped = new Bun.Archive({ "hello.txt": "Hello, World!" }, { compress: "gzip" });
const gzippedBytes = await gzipped.bytes();
const gzippedBlob = await gzipped.blob();
```

## Extracting Archives

### From Existing Archive Data

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

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// From a file
const tarball = await Bun.file("package.tar.gz").bytes();
const archiveFromFile = new Bun.Archive(tarball);
```

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// From a fetch response
const response = await fetch("https://example.com/archive.tar.gz");
const archiveFromFetch = new Bun.Archive(await response.blob());
```

### Extracting to Disk

Use `.extract()` to write all files to a directory:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);
const count = await archive.extract("./extracted");
console.log(`Extracted ${count} entries`);
```

`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:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);

// Extract only TypeScript files
const tsCount = await archive.extract("./extracted", { glob: "**/*.ts" });

// Extract files from multiple directories
const multiCount = await archive.extract("./extracted", {
  glob: ["src/**", "lib/**"],
});
```

When mixing positive and negative patterns, entries must match at least one positive pattern and no negative pattern:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// Extract everything except node_modules
const distCount = await archive.extract("./extracted", {
  glob: ["**", "!node_modules/**"],
});

// Extract source files but exclude tests
const srcCount = await archive.extract("./extracted", {
  glob: ["src/**", "!**/*.test.ts", "!**/__tests__/**"],
});
```

## 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):

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);
const files = await archive.files();

for (const [path, file] of files) {
  console.log(`${path}: ${file.size} bytes`);
  console.log(await file.text());
}
```

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:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
try {
  const tarball = await Bun.file("package.tar.gz").bytes();
  const archive = new Bun.Archive(tarball);
  const count = await archive.extract("./output");
  console.log(`Extracted ${count} entries`);
} catch (e: unknown) {
  if (e instanceof Error) {
    const error = e as Error & { code?: string };
    if (error.code === "EACCES") {
      console.error("Permission denied");
    } else if (error.code === "ENOSPC") {
      console.error("Disk full");
    } else {
      console.error("Archive error:", error.message);
    }
  } else {
    console.error("Archive error:", String(e));
  }
}
```

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:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const archive = new Bun.Archive(untrustedData);
const files = await archive.files();

// Optional: Custom validation for additional checks
for (const [path] of files) {
  // Example: Reject hidden files
  if (path.startsWith(".") || path.includes("/.")) {
    throw new Error(`Hidden file rejected: ${path}`);
  }
  // Example: Whitelist specific directories
  if (!path.startsWith("src/") && !path.startsWith("lib/")) {
    throw new Error(`Unexpected path: ${path}`);
  }
}

// Extract to a controlled destination
await archive.extract("./safe-output");
```

When called with a glob pattern, `files()` returns an empty `Map` if no files match:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const matches = await archive.files("*.nonexistent");
if (matches.size === 0) {
  console.log("No matching files found");
}
```

### Filtering with Glob Patterns

Pass a glob pattern to filter which files are returned:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// Get only TypeScript files
const tsFiles = await archive.files("**/*.ts");

// Get files in src directory
const srcFiles = await archive.files("src/*");

// Get all JSON files (recursive)
const jsonFiles = await archive.files("**/*.json");

// Get multiple file types with array of patterns
const codeFiles = await archive.files(["**/*.ts", "**/*.js"]);
```

Supported glob patterns (subset of [Bun.Glob](/docs/api/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](/docs/api/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.

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// tar (default)
const tar = new Bun.Archive({ "hello.txt": "Hello, World!" });

// zip
const zip = new Bun.Archive({ "hello.txt": "Hello, World!" }, { format: "zip" });
await Bun.write("bundle.zip", zip);

// Reading detects the format from the bytes
const archive = new Bun.Archive(await Bun.file("bundle.zip").bytes());
console.log(await archive.files());
```

## 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.

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
// Default: uncompressed tar
const archive = new Bun.Archive({ "hello.txt": "Hello, World!" });

// Reading: automatically detects gzip
const gzippedTarball = await Bun.file("archive.tar.gz").bytes();
const readArchive = new Bun.Archive(gzippedTarball);

// Enable gzip compression
const compressed = new Bun.Archive({ "hello.txt": "Hello, World!" }, { compress: "gzip" });

// Gzip with custom level (1-12)
const maxCompression = new Bun.Archive({ "hello.txt": "Hello, World!" }, { compress: "gzip", level: 12 });

// zip: deflate by default, at level 1-9
const fastZip = new Bun.Archive({ "hello.txt": "Hello, World!" }, { format: "zip", level: 1 });

// zip: no compression
const storedZip = new Bun.Archive({ "hello.txt": "Hello, World!" }, { format: "zip", compress: "store" });
```

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:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const archive = new Bun.Archive(undefined, { format: "zip" });

await archive.append("README.md", "# Hello");
await archive.append("dist/app.js", Bun.file("./dist/app.js"));
await archive.append("video.mp4", Bun.file("./video.mp4")); // streamed, never buffered

await Bun.Archive.write("bundle.zip", archive);
```

Appends run in call order, so `Promise.all()` over a list of files produces a deterministic archive:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const archive = new Bun.Archive();
await Promise.all(paths.map(path => archive.append(path, Bun.file(path))));
```

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.

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const archive = new Bun.Archive(undefined, { format: "zip" });

const writer = Bun.file("everything.zip").writer();
const writing = (async () => {
  for await (const chunk of archive.stream()) writer.write(chunk);
  await writer.end();
})();

for await (const path of new Bun.Glob("**/*").scan(".")) {
  await archive.append(path, Bun.file(path)); // parks when the writer falls behind
}
archive.end();
await writing;
```

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:

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
Bun.serve({
  fetch() {
    const archive = new Bun.Archive(undefined, { format: "zip" });
    queueMicrotask(async () => {
      for (const path of paths) await archive.append(path, Bun.file(path));
      archive.end();
    });
    return new Response(archive.stream(), {
      headers: { "content-type": "application/zip" },
    });
  },
});
```

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

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
import { Glob } from "bun";

// Collect source files
const files: Record<string, string> = {};
const glob = new Glob("src/**/*.ts");

for await (const path of glob.scan(".")) {
  // Normalize path separators to forward slashes for cross-platform compatibility
  const archivePath = path.replaceAll("\\", "/");
  files[archivePath] = await Bun.file(path).text();
}

// Add package.json
files["package.json"] = await Bun.file("package.json").text();

// Create compressed archive and write to disk
const archive = new Bun.Archive(files, { compress: "gzip" });
await Bun.write("bundle.tar.gz", archive);
```

### Extract and Process npm Package

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
const response = await fetch("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz");
const archive = new Bun.Archive(await response.blob());

// Get package.json
const files = await archive.files("package/package.json");
const packageJson = files.get("package/package.json");

if (packageJson) {
  const pkg = JSON.parse(await packageJson.text());
  console.log(`Package: ${pkg.name}@${pkg.version}`);
}
```

### Create Archive from Directory

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
import { readdir } from "node:fs/promises";
import { join } from "node:path";

async function archiveDirectory(dir: string, compress = false): Promise<Bun.Archive> {
  const files: Record<string, Blob> = {};

  async function walk(currentDir: string, prefix: string = "") {
    const entries = await readdir(currentDir, { withFileTypes: true });

    for (const entry of entries) {
      const fullPath = join(currentDir, entry.name);
      const archivePath = prefix ? `${prefix}/${entry.name}` : entry.name;

      if (entry.isDirectory()) {
        await walk(fullPath, archivePath);
      } else {
        files[archivePath] = Bun.file(fullPath);
      }
    }
  }

  await walk(dir);
  return new Bun.Archive(files, compress ? { compress: "gzip" } : undefined);
}

const archive = await archiveDirectory("./my-project", true);
await Bun.write("my-project.tar.gz", archive);
```

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

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
import { Glob } from "bun";

const archive = new Bun.Archive(undefined, { format: "zip" });
for await (const path of new Glob("**/*").scan("./my-project")) {
  await archive.append(path, Bun.file(`./my-project/${path}`));
}
await Bun.Archive.write("my-project.zip", archive);
```

## Reference

> **Note**: The following type signatures are simplified. See [`packages/bun-types/bun.d.ts`](https://github.com/oven-sh/bun/blob/main/packages/bun-types/bun.d.ts) for the full type definitions.

```ts theme={"theme":{"light":"github-light","dark":"dracula"}}
type ArchiveInput =
  | Record<string, string | Blob | Bun.ArrayBufferView | ArrayBufferLike>
  | Blob
  | Bun.ArrayBufferView
  | ArrayBufferLike;

type ArchiveOptions = {
  /** Container format to write. Reading auto-detects tar, tar.gz, and zip. */
  format?: "tar" | "zip";
  /** "gzip" for tar; "deflate" (default) or "store" for zip. */
  compress?: "gzip" | "deflate" | "store";
  /** Compression level: 1-12 for gzip, 1-9 for deflate (default 6). */
  level?: number;
};

interface ArchiveExtractOptions {
  /** Glob pattern(s) to filter extraction. Supports negative patterns with "!" prefix. */
  glob?: string | readonly string[];
}

class Archive {
  /**
   * Create an Archive from input data
   * @param data - Files to archive (as object), existing archive data (as bytes/blob),
   *               or nothing, for an empty archive to append() to
   * @param options - Format and compression options. Uncompressed tar by default.
   */
  constructor(data?: ArchiveInput, options?: ArchiveOptions);

  /**
   * Stream one more entry into an archive that is still being built.
   * Only valid before the archive's bytes are first read.
   */
  append(path: string, data: string | Blob | Bun.ArrayBufferView | ArrayBufferLike): Promise<void>;

  /**
   * The archive's bytes as they are produced. Draining it chunk by chunk is what
   * lets the next append() make progress, so the archive need not fit in memory.
   */
  stream(): ReadableStream<Uint8Array>;

  /** Finish the archive, closing its stream() if it has one. */
  end(): void;

  /**
   * Extract archive to a directory
   * @returns Number of entries extracted (files, directories, and symlinks)
   */
  extract(path: string, options?: ArchiveExtractOptions): Promise<number>;

  /**
   * Get archive as a Blob (uses compression setting from constructor)
   */
  blob(): Promise<Blob>;

  /**
   * Get archive as a Uint8Array (uses compression setting from constructor)
   */
  bytes(): Promise<Uint8Array<ArrayBuffer>>;

  /**
   * Get archive contents as File objects (regular files only, no directories)
   */
  files(glob?: string | readonly string[]): Promise<Map<string, File>>;
}
```
