執行環境套件管理器打包器測試執行器程式碼檢查格式化指南參考部落格意見回饋

歸檔

通過 Bun 的快速本地實現建立和解壓 tar 歸檔檔案

Bun.Archive 是 Bun 用於處理 tar 歸檔檔案的原生 API。它可以根據記憶體中的資料建立歸檔檔案,將歸檔檔案解壓到磁碟,並在不解壓的情況下讀取歸檔內容。

快速開始

從檔案建立歸檔:

TypeScript
const archive = new Bun.Archive({
  "hello.txt": "Hello, World!",
  "data.json": JSON.stringify({ foo: "bar" }),
  "nested/file.txt": "巢狀內容",
});

// 寫入磁碟
await Bun.write("bundle.tar", archive);

解壓歸檔:

TypeScript
const tarball = await Bun.file("package.tar.gz").bytes();
const archive = new Bun.Archive(tarball);
const entryCount = await archive.extract("./output");
console.log(`解壓了 ${entryCount} 個條目`);

讀取歸檔內容而不解壓:

TypeScript
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() 從一個物件建立歸檔,該物件的鍵是檔案路徑,值是檔案內容。預設情況下,歸檔是不壓縮的:

TypeScript
// 建立未壓縮的 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 - 原始二進位制資料
TypeScript
const 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() 將歸檔寫入磁碟:

TypeScript
// 寫入未壓縮的 tar(預設)
const archive = new Bun.Archive({
  "file1.txt": "內容1",
  "file2.txt": "內容2",
});
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:

TypeScript
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 資料建立歸檔:

TypeScript
// 從檔案
const tarball = await Bun.file("package.tar.gz").bytes();
const archiveFromFile = new Bun.Archive(tarball);
TypeScript
// 從 fetch 響應
const response = await fetch("https://example.com/archive.tar.gz");
const archiveFromFetch = new Bun.Archive(await response.blob());

解壓到磁碟

使用 .extract() 將所有檔案寫入目錄:

TypeScript
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 會正常解壓符號連結。

安全注意事項:Bun.Archive 會在解壓期間驗證路徑。它會拒絕絕對路徑(POSIX /、Windows 驅動器磁碟機代號,如 C:\ 或 C:/,以及 UNC 路徑,如 \\server\share)和不安全的符號連結目標。它會規範化並移除路徑遍歷元件(..),以防止目錄逃逸攻擊:dir/sub/../file 會變為 dir/file。

過濾解壓的檔案

使用 glob 模式僅解壓特定檔案。Bun 會根據歸檔條目路徑匹配模式,並將路徑規範化為使用正斜槓(/)。正模式用於指定要包含的內容,負模式(以 ! 為字首)用於指定要排除的內容。如果只傳入負模式,Bun 會包含所有不匹配這些模式的條目:

TypeScript
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/**"],
});

混合使用正模式和負模式時,條目必須至少匹配一個正模式,且不能匹配任何負模式:

TypeScript
// 解壓所有檔案,除了 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() 只傳回普通檔案(不包含目錄):

TypeScript
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 處理這些情況:

TypeScript
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 Bun.Archive() 會載入歸檔資料;Bun 可能會將錯誤延遲到讀取或解壓操作時才報告
  • 許可權被拒絕 - 如果目標目錄不可寫,extract() 會丟擲錯誤
  • 磁碟空間不足 - 如果空間不足,extract() 會丟擲錯誤
  • 無效路徑 - 檔案路徑格式錯誤時,操作會丟擲錯誤

對於不受信任的歸檔,可以在解壓前列舉並驗證路徑,以提高安全性:

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

TypeScript
const matches = await archive.files("*.nonexistent");
if (matches.size === 0) {
  console.log("未找到匹配檔案");
}

使用 Glob 模式過濾

傳入 glob 模式來篩選要傳回的檔案:

TypeScript
// 只獲取 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 語法(包括轉義和高階模式),請參閱 Bun.Glob。

壓縮

Bun.Archive 預設建立未壓縮的 tar 歸檔。可通過 { compress: "gzip" } 啟用 gzip 壓縮:

TypeScript
// 預設:未壓縮 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 });

options 引數接受:

  • 不傳或 undefined - 預設未壓縮 tar
  • { compress: "gzip" } - 啟用 gzip 壓縮,預設等級 6
  • { compress: "gzip", level: number } - gzip 壓縮,自定義等級 1-12(1 為最快,12 為最小)。

範例

打包專案檔案

TypeScript
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 包

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

從目錄建立歸檔

TypeScript
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。

TypeScript
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>>;
}