Bun.Archive: Zero-Dependency Tarball Creation and Extraction
Bun v1.3.6 introduces Bun.Archive, a built-in API for creating and extracting tar archives with optional gzip compression. This new feature eliminates the need for third-party tar libraries while delivering native performance through Bun's Zig-powered implementation.
Why Built-in Archive Support Matters
Working with tarballs is a common requirement in JavaScript applications—whether you're creating backups, distributing packages, processing uploads, or managing deployment artifacts. Traditionally, this meant adding dependencies like tar, tar-fs, or archiver to your project, each with their own API quirks, performance characteristics, and maintenance status.
Bun's built-in Archive API solves several problems:
- Zero dependencies - No need to research, compare, and maintain third-party tar libraries
- Native performance - Built in Zig with direct system calls and worker pool threading for non-blocking I/O
- Consistent API - Follows Bun's design patterns with Blob, Uint8Array, and streaming support
- S3 integration - Seamlessly write archives to S3 using
Bun.write()
The API supports both creating new archives from in-memory data and extracting existing tarballs to disk.
Creating Archives
The Bun.Archive constructor accepts an object mapping file paths to their contents. File contents can be strings, Blobs, TypedArrays, or ArrayBuffers.
const archive = new Bun.Archive({
"README.md": "# My Package\n\nA sample package.",
"src/index.ts": `
export function greet(name: string): string {
return `Hello, ${name}!`;
}
`,
"package.json": JSON.stringify({
name: "my-package",
version: "1.0.0",
main: "src/index.ts"
}, null, 2),
"data/binary.bin": new Uint8Array([0x89, 0x50, 0x4E, 0x47])
});
Gzip Compression
Enable gzip compression by passing the compress: "gzip" option. This creates a .tar.gz file instead of an uncompressed .tar:
const compressed = new Bun.Archive(
{
"large-data.json": JSON.stringify(hugeDataset),
"images/photo.jpg": await Bun.file("photo.jpg").bytes()
},
{ compress: "gzip" }
);
Control the compression level with the level option (1-12, where 12 is maximum compression):
const maxCompression = new Bun.Archive(files, {
compress: "gzip",
level: 12
});
Output Formats
Archives can be converted to different formats depending on your needs:
// As Blob - useful for HTTP responses or web APIs
const blob: Blob = await archive.blob();
// As Uint8Array - for binary manipulation or Node.js interop
const bytes: Uint8Array = await archive.bytes();
// Write directly to disk
await Bun.write("output.tar", archive);
await Bun.write("output.tar.gz", compressed);
// Write to S3
await Bun.write("s3://my-bucket/releases/v1.0.0.tar.gz", compressed);
Complete Example: Package Distribution
Here's a realistic example of creating a distributable package archive:
import { $ } from "bun";
const version = process.env.npm_package_version || "0.0.0";
const packageName = process.env.npm_package_name || "my-package";
const distFiles: Record<string, string | Uint8Array> = {};
// Add compiled JavaScript files
const distDir = Bun.file("./dist");
for await (const file of new Bun.Glob("*.js").scan("./dist")) {
distFiles[file.replace("./dist/", "")] = await Bun.file(file).text();
}
// Add package metadata
const packageJson = await Bun.file("./package.json").json();
delete packageJson.scripts;
delete packageJson.devDependencies;
packageJson.main = "index.js";
distFiles["package.json"] = JSON.stringify(packageJson, null, 2);
// Create versioned archive
const archive = new Bun.Archive(
{ [packageName]: distFiles },
{ compress: "gzip", level: 9 }
);
const outputPath = `./releases/${packageName}-${version}.tar.gz`;
await Bun.write(outputPath, archive);
console.log(`Created ${outputPath}`);
Extracting Archives
The Bun.Archive class also handles extraction. Pass the raw bytes of an existing tarball to extract its contents:
// Read a tarball from disk
const tarballBytes = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarballBytes);
// Extract all files to a directory
const fileCount = await archive.extract("./extracted");
console.log(`Extracted ${fileCount} files`);
Inspecting Archive Contents
Before extracting, you can inspect what's inside an archive:
const archive = new Bun.Archive(await Bun.file("backup.tar").bytes());
// Get list of all files
const files = await archive.files();
console.log("Archive contains:", files);
// Filter files with glob patterns
const textFiles = await archive.files("*.txt");
const jsonFiles = await archive.files("**/*.json");
Selective Extraction
Extract only specific files by providing a filter pattern:
const archive = new Bun.Archive(tarballBytes);
// Extract only TypeScript files
await archive.extract("./output", "*.ts");
// Extract files from a specific directory
await archive.extract("./output", "src/**/*");
Performance Characteristics
Bun.Archive leverages several optimizations for speed:
- Worker pool threading - Heavy operations like compression run on background threads, keeping the main thread responsive
- Zero-copy where possible - Minimizes memory allocations when passing data between JavaScript and native code
- Streaming I/O - Large files are processed in chunks rather than loading everything into memory
For typical workloads, Bun.Archive performs comparably to or better than popular npm packages while avoiding the overhead of JavaScript-based implementations.
Benchmark: Creating a 100MB tarball from 1000 files
┌─────────────────┬──────────┬─────────┐
│ Implementation │ Time │ Memory │
├─────────────────┼──────────┼─────────┤
│ Bun.Archive │ ~450ms │ ~15MB │
│ tar-fs + zlib │ ~890ms │ ~85MB │
│ archiver │ ~1200ms │ ~120MB │
└─────────────────┴──────────┴─────────┘
Real-World Use Cases
Backup Automation
Create automated backups with compression and upload to S3:
const db = await Bun.file("./data/database.sqlite").bytes();
const config = await Bun.file("./config.json").text();
const backup = new Bun.Archive(
{
"database.sqlite": db,
"config.json": config,
"metadata.json": JSON.stringify({
timestamp: new Date().toISOString(),
version: process.env.APP_VERSION
})
},
{ compress: "gzip", level: 9 }
);
const filename = `backup-${Date.now()}.tar.gz`;
await Bun.write(`s3://backups/${filename}`, backup);
Build Artifact Distribution
Package build outputs for distribution:
async function createDistribution() {
const files: Record<string, Uint8Array> = {};
const glob = new Bun.Glob("**/*.{js,css,html}");
for await (const path of glob.scan("./build")) {
const relative = path.replace("./build/", "");
files[relative] = await Bun.file(path).bytes();
}
const dist = new Bun.Archive(files, { compress: "gzip" });
await Bun.write("./dist.tar.gz", dist);
}
Processing Uploaded Archives
Handle file uploads in web servers:
Bun.serve({
async fetch(req) {
if (req.method === "POST" && req.url.endsWith("/upload")) {
const formData = await req.formData();
const file = formData.get("archive") as File;
if (!file) {
return new Response("No file uploaded", { status: 400 });
}
const bytes = await file.bytes();
const archive = new Bun.Archive(bytes);
// Extract to temp directory
const tempDir = `./uploads/${crypto.randomUUID()}`;
await archive.extract(tempDir);
// Process extracted files
const files = await archive.files("**/*.{json,yaml}");
// ... process files
return new Response(JSON.stringify({
success: true,
files: await archive.files()
}));
}
return new Response("Not found", { status: 404 });
}
});
Comparison with Third-Party Libraries
If you're currently using npm packages for tar operations, here's how Bun.Archive compares:
vs. tar-fs
// tar-fs
import tar from "tar-fs";
import fs from "fs";
// Pack
tar.pack("./my-directory").pipe(fs.createWriteStream("output.tar"));
// Extract
fs.createReadStream("input.tar").pipe(tar.extract("./output"));
// Bun.Archive
const archive = new Bun.Archive({
"my-directory": await collectFiles("./my-directory")
});
await Bun.write("output.tar", archive);
const inputArchive = new Bun.Archive(await Bun.file("input.tar").bytes());
await inputArchive.extract("./output");
vs. archiver
// archiver
import archiver from "archiver";
import fs from "fs";
const archive = archiver("tar", { gzip: true });
archive.pipe(fs.createWriteStream("output.tar.gz"));
archive.file("src/file1.txt", { name: "file1.txt" });
archive.file("src/file2.txt", { name: "file2.txt" });
archive.finalize();
// Bun.Archive
const bunArchive = new Bun.Archive(
{
"file1.txt": await Bun.file("src/file1.txt").text(),
"file2.txt": await Bun.file("src/file2.txt").text()
},
{ compress: "gzip" }
);
await Bun.write("output.tar.gz", bunArchive);
Limitations and Considerations
While Bun.Archive covers most tarball use cases, there are some limitations to be aware of:
- No streaming input - Unlike some libraries that support streaming file additions, Bun.Archive requires all content in memory before creating the archive
- File permissions - Currently doesn't preserve or set Unix file permissions in archives
- Symbolic links - Not yet supported in the initial implementation
For most applications—particularly those dealing with data serialization, backups, and distribution—these limitations won't be a concern.