歸檔
通過 Bun 的快速本地實現建立和解壓 tar 歸檔檔案
Bun.Archive 是 Bun 用於處理 tar 歸檔檔案的原生 API。它可以根據記憶體中的資料建立歸檔檔案,將歸檔檔案解壓到磁碟,並在不解壓的情況下讀取歸檔內容。
快速開始
從檔案建立歸檔:
const archive = new Bun.Archive({
"hello.txt": "Hello, World!",
"data.json": JSON.stringify({ foo: "bar" }),
"nested/file.txt": "巢狀內容",
});
// 寫入磁碟
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": "# 我的專案",
"src/index.ts": "console.log('Hello');",
"package.json": JSON.stringify({ name: "my-project" }),
});
檔案內容可以是:
- 字串 - 文本內容
- Blob - 二進位制資料
- ArrayBufferView(例如
Uint8Array)- 原始位元組 - ArrayBuffer - 原始二進位制資料
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() 將歸檔寫入磁碟:
// 寫入未壓縮的 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:
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} 個條目`);
extract() 會在目標目錄不存在時建立該目錄,並覆蓋現有檔案。傳回的計數包括檔案、目錄和符號連結(在 POSIX 系統上)。
注意:在 Windows 上,無論許可權級別如何,Bun 都會在解壓時跳過符號連結。在 Linux 和 macOS 上,Bun 會正常解壓符號連結。
安全注意事項:Bun.Archive 會在解壓期間驗證路徑。它會拒絕絕對路徑(POSIX /、Windows 驅動器磁碟機代號,如 C:\ 或 C:/,以及 UNC 路徑,如 \\server\share)和不安全的符號連結目標。它會規範化並移除路徑遍歷元件(..),以防止目錄逃逸攻擊:dir/sub/../file 會變為 dir/file。
過濾解壓的檔案
使用 glob 模式僅解壓特定檔案。Bun 會根據歸檔條目路徑匹配模式,並將路徑規範化為使用正斜槓(/)。正模式用於指定要包含的內容,負模式(以 ! 為字首)用於指定要排除的內容。如果只傳入負模式,Bun 會包含所有不匹配這些模式的條目:
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 Bun.Archive()會載入歸檔資料;Bun 可能會將錯誤延遲到讀取或解壓操作時才報告 - 許可權被拒絕 - 如果目標目錄不可寫,
extract()會丟擲錯誤 - 磁碟空間不足 - 如果空間不足,
extract()會丟擲錯誤 - 無效路徑 - 檔案路徑格式錯誤時,操作會丟擲錯誤
對於不受信任的歸檔,可以在解壓前列舉並驗證路徑,以提高安全性:
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 語法(包括轉義和高階模式),請參閱 Bun.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 });
options 引數接受:
- 不傳或
undefined- 預設未壓縮 tar { compress: "gzip" }- 啟用 gzip 壓縮,預設等級 6{ compress: "gzip", level: number }- gzip 壓縮,自定義等級 1-12(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>>;
}