Skip to main content
Bun 通过 Bun.Archive 提供了一个快速的原生实现来处理 tar 归档。它支持从内存数据创建归档,将归档解压到磁盘,以及在不解压的情况下读取归档内容。

快速开始

从文件创建归档:
const archive = new Bun.Archive({
  "hello.txt": "Hello, World!",
  "data.json": JSON.stringify({ foo: "bar" }),
  "nested/file.txt": "Nested content",
});

// 写入磁盘
await Bun.write("bundle.tar", archive);
解压归档:
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);
const entryCount = await archive.extract("./output");
console.log(`解压了 ${entryCount} 个条目`);
在不解压的情况下读取归档内容:
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() 从对象创建归档,其中键是文件路径,值是文件内容。默认情况下,归档是未压缩的:
// 创建未压缩的 tar 归档(默认)
const archive = new Bun.Archive({
  "README.md": "# My Project",
  "src/index.ts": "console.log('Hello');",
  "package.json": JSON.stringify({ name: "my-project" }),
});
文件内容可以是:
  • 字符串 - 文本内容
  • Blobs - 二进制数据
  • ArrayBufferViews (例如 Uint8Array) - 原始字节
  • ArrayBuffers - 原始二进制数据
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,
});

将归档写入磁盘

使用 Bun.write() 将归档写入磁盘:
// 写入未压缩的 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:
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 数据创建归档:
// 从文件
const tarball = await Bun.file("package.tar.gz").bytes();
const archiveFromFile = new Bun.Archive(tarball);
// 从 fetch 响应
const response = await fetch("https://example.com/archive.tar.gz");
const archiveFromFetch = new Bun.Archive(await response.blob());

解压到磁盘

使用 .extract() 将所有文件写入目录:
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);
const count = await archive.extract("./extracted");
console.log(`解压了 ${count} 个条目`);
如果目标目录不存在,则会自动创建。现有文件会被覆盖。返回的计数包括文件、目录和符号链接(在 POSIX 系统上)。 注意: 在 Windows 上,归档中的符号链接在解压期间总是被跳过。Bun 不尝试创建它们,无论权限级别如何。在 Linux 和 macOS 上,符号链接会正常解压。 安全说明: Bun.Archive 在解压期间验证路径,拒绝绝对路径(POSIX 的 /,Windows 驱动器字母如 C:\C:/,以及 UNC 路径如 \\server\share)。路径遍历组件(..)会被标准化(例如,dir/sub/../file 变成 dir/file),以防止目录遍历攻击。

过滤解压文件

使用 glob 模式仅解压特定文件。模式与归档条目路径匹配,并标准化为使用正斜杠(/)。正模式指定要包含的内容,负模式(以 ! 为前缀)指定要排除的内容。负模式在正模式之后应用,因此仅使用负模式将不会匹配任何内容(您必须首先包含像 ** 这样的正模式):
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/**"],
});
使用负模式(以 ! 为前缀)排除文件。当混合使用正模式和负模式时,条目必须匹配至少一个正模式且不匹配任何负模式:
// 解压除 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() 仅返回常规文件(没有目录):
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 来处理这些情况:
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() 会抛出异常
  • 无效路径 - 对于格式错误的文件路径,操作会抛出异常
extract() 返回的计数包括所有成功写入的条目(文件、目录和 POSIX 系统上的符号链接)。 安全说明: Bun.Archive 在解压期间自动验证路径。绝对路径(POSIX 的 /,Windows 驱动器字母,UNC 路径)和不安全的符号链接目标会被拒绝。路径遍历组件(..)会被标准化以防止目录遍历。 对于不受信任的归档,您可以枚举并在解压前验证路径:
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
const matches = await archive.files("*.nonexistent");
if (matches.size === 0) {
  console.log("未找到匹配的文件");
}

使用 Glob 模式过滤

传递 glob 模式来过滤返回的文件:
// 仅获取 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 语法的子集):
  • * - 匹配除 / 以外的任何字符
  • ** - 匹配包括 / 在内的任何字符
  • ? - 匹配单个字符
  • [abc] - 匹配字符集
  • {a,b} - 匹配替代项
  • !pattern - 排除匹配的文件(否定)。必须与正模式结合使用;仅使用负模式不会匹配任何内容。
请参阅 Bun.Glob 了解完整的 glob 语法,包括转义和高级模式。

压缩

Bun.Archive 默认创建未压缩的 tar 归档。使用 { compress: "gzip" } 启用 gzip 压缩:
// 默认:未压缩的 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" });

// 使用自定义级别进行 Gzip 压缩(1-12)
const maxCompression = new Bun.Archive({ "hello.txt": "Hello, World!" }, { compress: "gzip", level: 12 });
选项接受:
  • 无选项或 undefined - 未压缩的 tar(默认)
  • { compress: "gzip" } - 启用 gzip 压缩,级别为 6
  • { compress: "gzip", level: number } - 使用自定义级别 1-12 的 Gzip(1 = 最快,12 = 最小)

示例

打包项目文件

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 包

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}`);
}

从目录创建归档

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
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 {
  /**
   * 从输入数据创建归档
   * @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>>;
}