> ## Documentation Index
> Fetch the complete documentation index at: https://bun.ll1025.cn/llms.txt
> Use this file to discover all available pages before exploring further.

# 归档

> 使用 Bun 快速的原生实现创建和提取 tar 归档文件

`Bun.Archive` 是 Bun 用于 tar 归档的原生 API。它可以从内存数据创建归档、将归档提取到磁盘，以及在不提取的情况下读取归档内容。

## 快速入门

**从文件创建归档：**

```ts theme={null}
const archive = new Bun.Archive({
  "hello.txt": "Hello, World!",
  "data.json": JSON.stringify({ foo: "bar" }),
  "nested/file.txt": "嵌套内容",
});

// 写入磁盘
await Bun.write("bundle.tar", archive);
```

**提取归档：**

```ts theme={null}
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);
const entryCount = await archive.extract("./output");
console.log(`提取了 ${entryCount} 个条目`);
```

**不提取读取归档内容：**

```ts theme={null}
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()}`);
}
```

## 创建归档

使用 `new Bun.Archive()` 从对象创建归档，其中键是文件路径，值是文件内容。默认情况下，归档是未压缩的：

```ts theme={null}
// 创建未压缩的 tar 归档（默认）
const archive = new Bun.Archive({
  "README.md": "# 我的项目",
  "src/index.ts": "console.log('Hello');",
  "package.json": JSON.stringify({ name: "my-project" }),
});
```

文件内容可以是：

* **字符串** - 文本内容
* **Blob** - 二进制数据
* **ArrayBufferView**（如 `Uint8Array`）- 原始字节
* **ArrayBuffer** - 原始二进制数据

```ts theme={null}
const data = "binary data";
const arrayBuffer = new ArrayBuffer(8);

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

### 将归档写入磁盘

使用 `Bun.write()` 将归档写入磁盘：

```ts theme={null}
// 写入未压缩的 tar（默认）
const archive = new Bun.Archive({
  "file1.txt": "content1",
  "file2.txt": "content2",
});
await Bun.write("output.tar", archive);

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

### 获取归档字节

以字节或 Blob 形式获取归档数据：

```ts theme={null}
const archive = new Bun.Archive({ "hello.txt": "Hello, World!" });

// 作为 Uint8Array
const bytes = await archive.bytes();

// 作为 Blob
const blob = await archive.blob();

// 带 gzip 压缩（在构造时设置）
const gzipped = new Bun.Archive({ "hello.txt": "Hello, World!" }, { compress: "gzip" });
const gzippedBytes = await gzipped.bytes();
const gzippedBlob = await gzipped.blob();
```

## 提取归档

### 从现有归档数据

从现有的 tar/tar.gz 数据创建归档：

```ts theme={null}
// 从文件
const tarball = await Bun.file("package.tar.gz").bytes();
const archiveFromFile = new Bun.Archive(tarball);
```

```ts theme={null}
// 从 fetch 响应
const response = await fetch("https://example.com/archive.tar.gz");
const archiveFromFetch = new Bun.Archive(await response.blob());
```

### 提取到磁盘

使用 `.extract()` 将所有文件写入目录：

```ts theme={null}
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);
const count = await archive.extract("./extracted");
console.log(`提取了 ${count} 个条目`);
```

`extract()` 会创建目标目录（如果不存在），并覆盖现有文件。返回的计数包括文件、目录和符号链接（在 POSIX 系统上）。

**注意**：在 Windows 上，Bun 在提取时始终跳过符号链接，无论权限级别如何。在 Linux 和 macOS 上，符号链接正常提取。

**安全说明**：`Bun.Archive` 在提取过程中验证路径。它会拒绝绝对路径（POSIX `/`、Windows 驱动器号如 `C:\` 或 `C:/`，以及 UNC 路径如 `\\server\share`）和不安全的符号链接目标。路径遍历组件（`..`）会被规范化移除，以防止目录逃逸攻击：`dir/sub/../file` 变为 `dir/file`。

### 过滤提取的文件

使用 glob 模式仅提取特定文件。模式会与使用正斜杠（`/`）规范化的归档条目路径进行匹配。正向模式指定要包含的内容，负向模式（以 `!` 为前缀）指定要排除的内容。当只提供负向模式时，所有不匹配它们的条目都会被包含：

```ts theme={null}
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);

// 仅提取 TypeScript 文件
const tsCount = await archive.extract("./extracted", { glob: "**/*.ts" });

// 从多个目录提取文件
const multiCount = await archive.extract("./extracted", {
  glob: ["src/**", "lib/**"],
});
```

当混合使用正向和负向模式时，条目必须匹配至少一个正向模式且不匹配任何负向模式：

```ts theme={null}
// 提取除了 node_modules 之外的所有内容
const distCount = await archive.extract("./extracted", {
  glob: ["**", "!node_modules/**"],
});

// 提取源文件但排除测试
const srcCount = await archive.extract("./extracted", {
  glob: ["src/**", "!**/*.test.ts", "!**/__tests__/**"],
});
```

## 读取归档内容

### 获取所有文件

使用 `.files()` 以 `File` 对象的 `Map` 形式获取归档内容，而无需提取到磁盘。与处理所有条目类型的 `extract()` 不同，`files()` 只返回常规文件（不包括目录）：

```ts theme={null}
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} 字节`);
  console.log(await file.text());
}
```

每个 `File` 对象包含：

* `name` - 归档内的文件路径（始终使用正斜杠 `/` 作为分隔符）
* `size` - 文件大小（字节）
* `lastModified` - 修改时间戳
* 标准 `Blob` 方法，如 `text()`、`arrayBuffer()` 和 `stream()`

**注意**：`files()` 会将文件内容加载到内存中。对于大型归档，请使用 `extract()` 直接写入磁盘。

### 错误处理

归档操作可能因数据损坏、I/O 错误或无效路径而失败。使用 try/catch 处理这些情况：

```ts theme={null}
try {
  const tarball = await Bun.file("package.tar.gz").bytes();
  const archive = new Bun.Archive(tarball);
  const count = await archive.extract("./output");
  console.log(`提取了 ${count} 个条目`);
} catch (e: unknown) {
  if (e instanceof Error) {
    const error = e as Error & { code?: string };
    if (error.code === "EACCES") {
      console.error("权限被拒绝");
    } else if (error.code === "ENOSPC") {
      console.error("磁盘空间不足");
    } else {
      console.error("归档错误:", error.message);
    }
  } else {
    console.error("归档错误:", String(e));
  }
}
```

常见错误场景：

* **损坏/截断的归档** - `new Archive()` 会加载归档数据；错误可能会延迟到读取/提取操作时
* **权限被拒绝** - 如果目标目录不可写，`extract()` 会抛出异常
* **磁盘空间不足** - 如果没有足够空间，`extract()` 会抛出异常
* **无效路径** - 对于格式错误的文件路径，操作会抛出异常

对于不可信归档的额外安全性，你可以在提取前枚举和验证路径：

```ts theme={null}
const archive = new Bun.Archive(untrustedData);
const files = await archive.files();

// 可选：用于额外检查的自定义验证
for (const [path] of files) {
  // 示例：拒绝隐藏文件
  if (path.startsWith(".") || path.includes("/.")) {
    throw new Error(`隐藏文件被拒绝: ${path}`);
  }
  // 示例：白名单特定目录
  if (!path.startsWith("src/") && !path.startsWith("lib/")) {
    throw new Error(`意外的路径: ${path}`);
  }
}

// 提取到受控的目标目录
await archive.extract("./safe-output");
```

当使用 glob 模式调用时，如果没有匹配的文件，`files()` 会返回空的 `Map`：

```ts theme={null}
const matches = await archive.files("*.nonexistent");
if (matches.size === 0) {
  console.log("未找到匹配的文件");
}
```

### 使用 Glob 模式过滤

传递 glob 模式以过滤返回哪些文件：

```ts theme={null}
// 仅获取 TypeScript 文件
const tsFiles = await archive.files("**/*.ts");

// 获取 src 目录中的文件
const srcFiles = await archive.files("src/*");

// 获取所有 JSON 文件（递归）
const jsonFiles = await archive.files("**/*.json");

// 使用模式数组获取多种文件类型
const codeFiles = await archive.files(["**/*.ts", "**/*.js"]);
```

支持的 glob 模式（[Bun.Glob](/docs/api/glob) 语法的子集）：

* `*` - 匹配除 `/` 以外的任意字符
* `**` - 匹配包括 `/` 在内的任意字符
* `?` - 匹配单个字符
* `[abc]` - 匹配字符集合
* `{a,b}` - 匹配备选方案
* `!pattern` - 排除匹配模式的文件（否定）。当只提供否定模式时，所有不匹配它们的文件都会被包含。

完整的 glob 语法（包括转义和高级模式），请参见 [Bun.Glob](/docs/api/glob)。

## 压缩

默认情况下，Bun.Archive 创建未压缩的 tar 归档。使用 `{ compress: "gzip" }` 启用 gzip 压缩：

```ts theme={null}
// 默认：未压缩的 tar
const archive = new Bun.Archive({ "hello.txt": "Hello, World!" });

// 读取：自动检测 gzip
const gzippedTarball = await Bun.file("archive.tar.gz").bytes();
const readArchive = new Bun.Archive(gzippedTarball);

// 启用 gzip 压缩
const compressed = new Bun.Archive({ "hello.txt": "Hello, World!" }, { compress: "gzip" });

// 自定义压缩级别 (1-12) 的 gzip
const maxCompression = new Bun.Archive({ "hello.txt": "Hello, World!" }, { compress: "gzip", level: 12 });
```

`options` 参数接受：

* 无选项或 `undefined` - 未压缩的 tar（默认）
* `{ compress: "gzip" }` - 在级别 6 启用 gzip 压缩
* `{ compress: "gzip", level: number }` - 使用自定义级别 1-12 的 gzip（1 = 最快，12 = 最小）

## 示例

### 打包项目文件

```ts theme={null}
import { Glob } from "bun";

// 收集源文件
const files: Record<string, string> = {};
const glob = new Glob("src/**/*.ts");

for await (const path of glob.scan(".")) {
  // 将路径分隔符规范化为正斜杠，以实现跨平台兼容性
  const archivePath = path.replaceAll("\\", "/");
  files[archivePath] = await Bun.file(path).text();
}

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

// 创建压缩归档并写入磁盘
const archive = new Bun.Archive(files, { compress: "gzip" });
await Bun.write("bundle.tar.gz", archive);
```

### 提取并处理 npm 包

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

// 获取 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(`包: ${pkg.name}@${pkg.version}`);
}
```

### 从目录创建归档

```ts theme={null}
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);
```

## 参考

> **注意**：以下类型签名已简化。完整的类型定义请参见 [`packages/bun-types/bun.d.ts`](https://github.com/oven-sh/bun/blob/main/packages/bun-types/bun.d.ts)。

```ts theme={null}
type ArchiveInput =
  | Record<string, string | Blob | Bun.ArrayBufferView | ArrayBufferLike>
  | Blob
  | Bun.ArrayBufferView
  | ArrayBufferLike;

type ArchiveOptions = {
  /** 压缩算法。目前仅支持 "gzip"。 */
  compress?: "gzip";
  /** 压缩级别 1-12（启用 gzip 时默认为 6）。 */
  level?: number;
};

interface ArchiveExtractOptions {
  /** 用于过滤提取的 Glob 模式。支持以 "!" 为前缀的否定模式。 */
  glob?: string | readonly string[];
}

class Archive {
  /**
   * 从输入数据创建 Archive
   * @param data - 要归档的文件（作为对象）或现有的归档数据（作为字节/blob）
   * @param options - 压缩选项。默认为未压缩。
   *                  传递 { compress: "gzip" } 以启用压缩。
   */
  constructor(data: ArchiveInput, options?: ArchiveOptions);

  /**
   * 将归档提取到目录
   * @returns 提取的条目数（文件、目录和符号链接）
   */
  extract(path: string, options?: ArchiveExtractOptions): Promise<number>;

  /**
   * 获取 Blob 格式的归档（使用构造函数中的压缩设置）
   */
  blob(): Promise<Blob>;

  /**
   * 获取 Uint8Array 格式的归档（使用构造函数中的压缩设置）
   */
  bytes(): Promise<Uint8Array<ArrayBuffer>>;

  /**
   * 获取归档内容为 File 对象（仅常规文件，不含目录）
   */
  files(glob?: string | readonly string[]): Promise<Map<string, File>>;
}
```
