mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
feat(archive): change API to constructor-based with S3 support (#25940)
## Summary
- Change Archive API from `Bun.Archive.from(data)` to `new
Bun.Archive(data, options?)`
- Change compression options from `{ gzip: true }` to `{ compress:
"gzip", level?: number }`
- Default to no compression when no options provided
- Use `{ compress: "gzip" }` to enable gzip compression (level 6 by
default)
- Add Archive support for S3 and local file writes via `Bun.write()`
## New API
```typescript
// Create archive - defaults to uncompressed tar
const archive = new Bun.Archive({
"hello.txt": "Hello, World!",
"data.json": JSON.stringify({ foo: "bar" }),
});
// Enable gzip compression
const compressed = new Bun.Archive(files, { compress: "gzip" });
// Gzip with custom level (1-12)
const maxCompression = new Bun.Archive(files, { compress: "gzip", level: 12 });
// Write to local file
await Bun.write("archive.tar", archive); // uncompressed by default
await Bun.write("archive.tar.gz", compressed); // gzipped
// Write to S3
await client.write("archive.tar.gz", compressed); // S3Client.write()
await Bun.write("s3://bucket/archive.tar.gz", compressed); // S3 URL
await s3File.write(compressed); // s3File.write()
// Get bytes/blob (uses compression setting from constructor)
const bytes = await archive.bytes();
const blob = await archive.blob();
```
## TypeScript Types
```typescript
type ArchiveCompression = "gzip";
type ArchiveOptions = {
compress?: "gzip";
level?: number; // 1-12, default 6 when gzip enabled
};
```
## Test plan
- [x] 98 archive tests pass
- [x] S3 integration tests updated to new API
- [x] TypeScript types updated
- [x] Documentation updated with new examples
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
@@ -10,21 +10,21 @@ Bun provides a fast, native implementation for working with tar archives through
|
||||
**Create an archive from files:**
|
||||
|
||||
```ts
|
||||
const archive = Bun.Archive.from({
|
||||
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.Archive.write("bundle.tar", archive);
|
||||
await Bun.write("bundle.tar", archive);
|
||||
```
|
||||
|
||||
**Extract an archive:**
|
||||
|
||||
```ts
|
||||
const tarball = await Bun.file("package.tar.gz").bytes();
|
||||
const archive = Bun.Archive.from(tarball);
|
||||
const archive = new Bun.Archive(tarball);
|
||||
const entryCount = await archive.extract("./output");
|
||||
console.log(`Extracted ${entryCount} entries`);
|
||||
```
|
||||
@@ -33,7 +33,7 @@ console.log(`Extracted ${entryCount} entries`);
|
||||
|
||||
```ts
|
||||
const tarball = await Bun.file("package.tar.gz").bytes();
|
||||
const archive = Bun.Archive.from(tarball);
|
||||
const archive = new Bun.Archive(tarball);
|
||||
const files = await archive.files();
|
||||
|
||||
for (const [path, file] of files) {
|
||||
@@ -43,10 +43,11 @@ for (const [path, file] of files) {
|
||||
|
||||
## Creating Archives
|
||||
|
||||
Use `Bun.Archive.from()` to create an archive from an object where keys are file paths and values are file contents:
|
||||
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
|
||||
const archive = Bun.Archive.from({
|
||||
// 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" }),
|
||||
@@ -64,7 +65,7 @@ File contents can be:
|
||||
const data = "binary data";
|
||||
const arrayBuffer = new ArrayBuffer(8);
|
||||
|
||||
const archive = Bun.Archive.from({
|
||||
const archive = new Bun.Archive({
|
||||
"text.txt": "Plain text",
|
||||
"blob.bin": new Blob([data]),
|
||||
"bytes.bin": new Uint8Array([1, 2, 3, 4]),
|
||||
@@ -74,18 +75,19 @@ const archive = Bun.Archive.from({
|
||||
|
||||
### Writing Archives to Disk
|
||||
|
||||
Use `Bun.Archive.write()` to create and write an archive in one operation:
|
||||
Use `Bun.write()` to write an archive to disk:
|
||||
|
||||
```ts
|
||||
// Write uncompressed tar
|
||||
await Bun.Archive.write("output.tar", {
|
||||
// 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 files = { "src/index.ts": "console.log('Hello');" };
|
||||
await Bun.Archive.write("output.tar.gz", files, "gzip");
|
||||
const compressed = new Bun.Archive({ "src/index.ts": "console.log('Hello');" }, { compress: "gzip" });
|
||||
await Bun.write("output.tar.gz", compressed);
|
||||
```
|
||||
|
||||
### Getting Archive Bytes
|
||||
@@ -93,8 +95,7 @@ await Bun.Archive.write("output.tar.gz", files, "gzip");
|
||||
Get the archive data as bytes or a Blob:
|
||||
|
||||
```ts
|
||||
const files = { "hello.txt": "Hello, World!" };
|
||||
const archive = Bun.Archive.from(files);
|
||||
const archive = new Bun.Archive({ "hello.txt": "Hello, World!" });
|
||||
|
||||
// As Uint8Array
|
||||
const bytes = await archive.bytes();
|
||||
@@ -102,9 +103,10 @@ const bytes = await archive.bytes();
|
||||
// As Blob
|
||||
const blob = await archive.blob();
|
||||
|
||||
// With gzip compression
|
||||
const gzippedBytes = await archive.bytes("gzip");
|
||||
const gzippedBlob = await archive.blob("gzip");
|
||||
// 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
|
||||
@@ -116,13 +118,13 @@ Create an archive from existing tar/tar.gz data:
|
||||
```ts
|
||||
// From a file
|
||||
const tarball = await Bun.file("package.tar.gz").bytes();
|
||||
const archiveFromFile = Bun.Archive.from(tarball);
|
||||
const archiveFromFile = new Bun.Archive(tarball);
|
||||
```
|
||||
|
||||
```ts
|
||||
// From a fetch response
|
||||
const response = await fetch("https://example.com/archive.tar.gz");
|
||||
const archiveFromFetch = Bun.Archive.from(await response.blob());
|
||||
const archiveFromFetch = new Bun.Archive(await response.blob());
|
||||
```
|
||||
|
||||
### Extracting to Disk
|
||||
@@ -131,7 +133,7 @@ Use `.extract()` to write all files to a directory:
|
||||
|
||||
```ts
|
||||
const tarball = await Bun.file("package.tar.gz").bytes();
|
||||
const archive = Bun.Archive.from(tarball);
|
||||
const archive = new Bun.Archive(tarball);
|
||||
const count = await archive.extract("./extracted");
|
||||
console.log(`Extracted ${count} entries`);
|
||||
```
|
||||
@@ -148,7 +150,7 @@ Use glob patterns to extract only specific files. Patterns are matched against a
|
||||
|
||||
```ts
|
||||
const tarball = await Bun.file("package.tar.gz").bytes();
|
||||
const archive = Bun.Archive.from(tarball);
|
||||
const archive = new Bun.Archive(tarball);
|
||||
|
||||
// Extract only TypeScript files
|
||||
const tsCount = await archive.extract("./extracted", { glob: "**/*.ts" });
|
||||
@@ -181,7 +183,7 @@ Use `.files()` to get archive contents as a `Map` of `File` objects without extr
|
||||
|
||||
```ts
|
||||
const tarball = await Bun.file("package.tar.gz").bytes();
|
||||
const archive = Bun.Archive.from(tarball);
|
||||
const archive = new Bun.Archive(tarball);
|
||||
const files = await archive.files();
|
||||
|
||||
for (const [path, file] of files) {
|
||||
@@ -206,7 +208,7 @@ Archive operations can fail due to corrupted data, I/O errors, or invalid paths.
|
||||
```ts
|
||||
try {
|
||||
const tarball = await Bun.file("package.tar.gz").bytes();
|
||||
const archive = Bun.Archive.from(tarball);
|
||||
const archive = new Bun.Archive(tarball);
|
||||
const count = await archive.extract("./output");
|
||||
console.log(`Extracted ${count} entries`);
|
||||
} catch (e: unknown) {
|
||||
@@ -227,7 +229,7 @@ try {
|
||||
|
||||
Common error scenarios:
|
||||
|
||||
- **Corrupted/truncated archives** - `Archive.from()` loads the archive data; errors may be deferred until read/extract operations
|
||||
- **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
|
||||
@@ -239,7 +241,7 @@ The count returned by `extract()` includes all successfully written entries (fil
|
||||
For additional security with untrusted archives, you can enumerate and validate paths before extraction:
|
||||
|
||||
```ts
|
||||
const archive = Bun.Archive.from(untrustedData);
|
||||
const archive = new Bun.Archive(untrustedData);
|
||||
const files = await archive.files();
|
||||
|
||||
// Optional: Custom validation for additional checks
|
||||
@@ -298,26 +300,28 @@ See [Bun.Glob](/docs/api/glob) for the full glob syntax including escaping and a
|
||||
|
||||
## Compression
|
||||
|
||||
Bun.Archive supports gzip compression for both reading and writing:
|
||||
Bun.Archive creates uncompressed tar archives by default. Use `{ compress: "gzip" }` to enable gzip compression:
|
||||
|
||||
```ts
|
||||
// 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 archive = Bun.Archive.from(gzippedTarball);
|
||||
const readArchive = new Bun.Archive(gzippedTarball);
|
||||
|
||||
// Writing: specify compression
|
||||
const files = { "hello.txt": "Hello, World!" };
|
||||
await Bun.Archive.write("output.tar.gz", files, "gzip");
|
||||
// Enable gzip compression
|
||||
const compressed = new Bun.Archive({ "hello.txt": "Hello, World!" }, { compress: "gzip" });
|
||||
|
||||
// Getting bytes: specify compression
|
||||
const gzippedBytes = await archive.bytes("gzip");
|
||||
// Gzip with custom level (1-12)
|
||||
const maxCompression = new Bun.Archive({ "hello.txt": "Hello, World!" }, { compress: "gzip", level: 12 });
|
||||
```
|
||||
|
||||
The compression argument accepts:
|
||||
The options accept:
|
||||
|
||||
- `"gzip"` - Enable gzip compression
|
||||
- `true` - Same as `"gzip"`
|
||||
- `false` or `undefined` - No compression
|
||||
- No options or `undefined` - Uncompressed tar (default)
|
||||
- `{ compress: "gzip" }` - Enable gzip compression at level 6
|
||||
- `{ compress: "gzip", level: number }` - Gzip with custom level 1-12 (1 = fastest, 12 = smallest)
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -339,15 +343,16 @@ for await (const path of glob.scan(".")) {
|
||||
// Add package.json
|
||||
files["package.json"] = await Bun.file("package.json").text();
|
||||
|
||||
// Create compressed archive
|
||||
await Bun.Archive.write("bundle.tar.gz", files, "gzip");
|
||||
// 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
|
||||
const response = await fetch("https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz");
|
||||
const archive = Bun.Archive.from(await response.blob());
|
||||
const archive = new Bun.Archive(await response.blob());
|
||||
|
||||
// Get package.json
|
||||
const files = await archive.files("package/package.json");
|
||||
@@ -365,7 +370,7 @@ if (packageJson) {
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
async function archiveDirectory(dir: string): Promise<Bun.Archive> {
|
||||
async function archiveDirectory(dir: string, compress = false): Promise<Bun.Archive> {
|
||||
const files: Record<string, Blob> = {};
|
||||
|
||||
async function walk(currentDir: string, prefix: string = "") {
|
||||
@@ -384,11 +389,11 @@ async function archiveDirectory(dir: string): Promise<Bun.Archive> {
|
||||
}
|
||||
|
||||
await walk(dir);
|
||||
return Bun.Archive.from(files);
|
||||
return new Bun.Archive(files, compress ? { compress: "gzip" } : undefined);
|
||||
}
|
||||
|
||||
const archive = await archiveDirectory("./my-project");
|
||||
await Bun.Archive.write("my-project.tar.gz", archive, "gzip");
|
||||
const archive = await archiveDirectory("./my-project", true);
|
||||
await Bun.write("my-project.tar.gz", archive);
|
||||
```
|
||||
|
||||
## Reference
|
||||
@@ -396,14 +401,19 @@ await Bun.Archive.write("my-project.tar.gz", archive, "gzip");
|
||||
> **Note**: The following type signatures are simplified for documentation purposes. 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
|
||||
type ArchiveCompression = "gzip" | boolean;
|
||||
|
||||
type ArchiveInput =
|
||||
| Record<string, string | Blob | Bun.ArrayBufferView | ArrayBufferLike>
|
||||
| Blob
|
||||
| Bun.ArrayBufferView
|
||||
| ArrayBufferLike;
|
||||
|
||||
type ArchiveOptions = {
|
||||
/** Compression algorithm. Currently only "gzip" is supported. */
|
||||
compress?: "gzip";
|
||||
/** Compression level 1-12 (default 6 when gzip is enabled). */
|
||||
level?: number;
|
||||
};
|
||||
|
||||
interface ArchiveExtractOptions {
|
||||
/** Glob pattern(s) to filter extraction. Supports negative patterns with "!" prefix. */
|
||||
glob?: string | readonly string[];
|
||||
@@ -412,13 +422,11 @@ interface ArchiveExtractOptions {
|
||||
class Archive {
|
||||
/**
|
||||
* Create an Archive from input data
|
||||
* @param data - Files to archive (as object) or existing archive data (as bytes/blob)
|
||||
* @param options - Compression options. Uncompressed by default.
|
||||
* Pass { compress: "gzip" } to enable compression.
|
||||
*/
|
||||
static from(data: ArchiveInput): Archive;
|
||||
|
||||
/**
|
||||
* Write an archive directly to disk
|
||||
*/
|
||||
static write(path: string, data: ArchiveInput | Archive, compress?: ArchiveCompression): Promise<void>;
|
||||
constructor(data: ArchiveInput, options?: ArchiveOptions);
|
||||
|
||||
/**
|
||||
* Extract archive to a directory
|
||||
@@ -427,14 +435,14 @@ class Archive {
|
||||
extract(path: string, options?: ArchiveExtractOptions): Promise<number>;
|
||||
|
||||
/**
|
||||
* Get archive as a Blob
|
||||
* Get archive as a Blob (uses compression setting from constructor)
|
||||
*/
|
||||
blob(compress?: ArchiveCompression): Promise<Blob>;
|
||||
blob(): Promise<Blob>;
|
||||
|
||||
/**
|
||||
* Get archive as a Uint8Array
|
||||
* Get archive as a Uint8Array (uses compression setting from constructor)
|
||||
*/
|
||||
bytes(compress?: ArchiveCompression): Promise<Uint8Array<ArrayBuffer>>;
|
||||
bytes(): Promise<Uint8Array<ArrayBuffer>>;
|
||||
|
||||
/**
|
||||
* Get archive contents as File objects (regular files only, no directories)
|
||||
|
||||
Reference in New Issue
Block a user