Bun

S3 物件儲存

生產伺服器通常讀取、上傳和寫入檔案至相容 S3 的物件儲存服務,而不是本機檔案系統。 過去,這意味著您在開發中使用的本機檔案系統 API 無法在生產環境中使用。 當您使用 Bun 時,情況就不同了。

Bun 的 S3 API 速度很快

Bun's S3 API is fast
左:Bun v1.1.44。 右:Node.js v23.6.0

Bun 提供快速的原生綁定,用於與相容 S3 的物件儲存服務互動。 Bun 的 S3 API 設計簡潔,感覺與 fetch 的 ResponseBlob API 相似(如同 Bun 的本機檔案系統 API)。

import { s3, write, S3Client } from "bun";

// Bun.s3 reads environment variables for credentials
// file() returns a lazy reference to a file on S3
const metadata = s3.file("123.json");

// Download from S3 as JSON
const data = await metadata.json();

// Upload to S3
await write(metadata, JSON.stringify({ name: "John", age: 30 }));

// Presign a URL (synchronous - no network request needed)
const url = metadata.presign({
  acl: "public-read",
  expiresIn: 60 * 60 * 24, // 1 day
});

// Delete the file
await metadata.delete();

S3 是 事實上的標準 網路檔案系統。 Bun 的 S3 API 適用於相容 S3 的儲存服務,例如

  • AWS S3
  • Cloudflare R2
  • DigitalOcean Spaces
  • MinIO
  • Backblaze B2
  • ...以及任何其他相容 S3 的儲存服務

基本用法

有幾種方式可以與 Bun 的 S3 API 互動。

Bun.S3Client & Bun.s3

Bun.s3 等同於 new Bun.S3Client(),依賴環境變數取得憑證。

若要明確設定憑證,請將其傳遞至 Bun.S3Client 建構函式。

import { S3Client } from "bun";

const client = new S3Client({
  accessKeyId: "your-access-key",
  secretAccessKey: "your-secret-key",
  bucket: "my-bucket",
  // sessionToken: "..."
  // acl: "public-read",
  // endpoint: "https://s3.us-east-1.amazonaws.com",
  // endpoint: "https://<account-id>.r2.cloudflarestorage.com", // Cloudflare R2
  // endpoint: "https://<region>.digitaloceanspaces.com", // DigitalOcean Spaces
  // endpoint: "https://127.0.0.1:9000", // MinIO
});

// Bun.s3 is a global singleton that is equivalent to `new Bun.S3Client()`

使用 S3 檔案

S3Client 中的 file 方法會傳回 S3 上檔案的延遲參考

// A lazy reference to a file on S3
const s3file: S3File = client.file("123.json");

如同 Bun.file(path)S3Clientfile 方法是同步的。 在您呼叫依賴網路請求的方法之前,它不會發出任何網路請求。

從 S3 讀取檔案

如果您使用過 fetch API,您會熟悉 ResponseBlob API。 S3File 擴展了 Blob。 適用於 Blob 的相同方法也適用於 S3File

// Read an S3File as text
const text = await s3file.text();

// Read an S3File as JSON
const json = await s3file.json();

// Read an S3File as an ArrayBuffer
const buffer = await s3file.arrayBuffer();

// Get only the first 1024 bytes
const partial = await s3file.slice(0, 1024).text();

// Stream the file
const stream = s3file.stream();
for await (const chunk of stream) {
  console.log(chunk);
}

記憶體最佳化

text()json()bytes()arrayBuffer() 等方法會盡可能避免在記憶體中複製字串或位元組。

如果文字碰巧是 ASCII,Bun 會直接將字串傳輸到 JavaScriptCore(引擎),而無需轉碼且無需在記憶體中複製字串。 當您使用 .bytes().arrayBuffer() 時,它也會避免在記憶體中複製位元組。

這些輔助方法不僅簡化了 API,也使其速度更快。

寫入 & 上傳檔案至 S3

寫入 S3 同樣簡單。

// Write a string (replacing the file)
await s3file.write("Hello World!");

// Write a Buffer (replacing the file)
await s3file.write(Buffer.from("Hello World!"));

// Write a Response (replacing the file)
await s3file.write(new Response("Hello World!"));

// Write with content type
await s3file.write(JSON.stringify({ name: "John", age: 30 }), {
  type: "application/json",
});

// Write using a writer (streaming)
const writer = s3file.writer({ type: "application/json" });
writer.write("Hello");
writer.write(" World!");
await writer.end();

// Write using Bun.write
await Bun.write(s3file, "Hello World!");

處理大型檔案 (串流)

Bun 會自動處理大型檔案的多部分上傳,並提供串流功能。 適用於本機檔案的相同 API 也適用於 S3 檔案。

// Write a large file
const bigFile = Buffer.alloc(10 * 1024 * 1024); // 10MB
const writer = s3file.writer({
  // Automatically retry on network errors up to 3 times
  retry: 3,

  // Queue up to 10 requests at a time
  queueSize: 10,

  // Upload in 5 MB chunks
  partSize: 5 * 1024 * 1024,
});
for (let i = 0; i < 10; i++) {
  await writer.write(bigFile);
}
await writer.end();

預先簽署 URL

當您的生產服務需要讓使用者上傳檔案到您的伺服器時,使用者直接上傳到 S3 通常比您的伺服器充當中間人更可靠。

為了方便起見,您可以為 S3 檔案預先簽署 URL。 這會產生一個帶有簽章的 URL,允許使用者安全地將該特定檔案上傳到 S3,而無需洩露您的憑證或授予他們不必要的存取您儲存桶的權限。

預設行為是產生一個 GET URL,在 24 小時後過期。 Bun 會嘗試從檔案副檔名推斷內容類型。 如果無法推斷,則預設為 application/octet-stream

import { s3 } from "bun";

// Generate a presigned URL that expires in 24 hours (default)
const download = s3.presign("my-file.txt"); // GET, text/plain, expires in 24 hours

const upload = s3.presign("my-file", {
  expiresIn: 3600, // 1 hour
  method: "PUT",
  type: "application/json", // No extension for inferring, so we can specify the content type to be JSON
});

// You can call .presign() if on a file reference, but avoid doing so
// unless you already have a reference (to avoid memory usage).
const myFile = s3.file("my-file.txt");
const presignedFile = myFile.presign({
  expiresIn: 3600, // 1 hour
});

設定 ACL

若要設定預先簽署 URL 的 ACL(存取控制列表),請傳遞 acl 選項

const url = s3file.presign({
  acl: "public-read",
  expiresIn: 3600,
});

您可以傳遞以下任何 ACL

ACL說明
"public-read"物件可由公眾讀取。
"private"物件只能由儲存桶擁有者讀取。
"public-read-write"物件可由公眾讀取和寫入。
"authenticated-read"物件可由儲存桶擁有者和已驗證的使用者讀取。
"aws-exec-read"物件可由發出請求的 AWS 帳戶讀取。
"bucket-owner-read"物件可由儲存桶擁有者讀取。
"bucket-owner-full-control"物件可由儲存桶擁有者讀取和寫入。
"log-delivery-write"物件可由用於日誌傳遞的 AWS 服務寫入。

過期 URL

若要為預先簽署 URL 設定過期時間,請傳遞 expiresIn 選項。

const url = s3file.presign({
  // Seconds
  expiresIn: 3600, // 1 hour

  // access control list
  acl: "public-read",

  // HTTP method
  method: "PUT",
});

method

若要為預先簽署 URL 設定 HTTP 方法,請傳遞 method 選項。

const url = s3file.presign({
  method: "PUT",
  // method: "DELETE",
  // method: "GET",
  // method: "HEAD",
  // method: "POST",
  // method: "PUT",
});

new Response(S3File)

若要快速將使用者重新導向至 S3 檔案的預先簽署 URL,請將 S3File 實例作為主體傳遞給 Response 物件。

const response = new Response(s3file);
console.log(response);

這會自動將使用者重新導向至 S3 檔案的預先簽署 URL,為您節省將檔案下載到伺服器並將其發送回使用者的記憶體、時間和頻寬成本。

Response (0 KB) {
  ok: false,
  url: "",
  status: 302,
  statusText: "",
  headers: Headers {
    "location": "https://<account-id>.r2.cloudflarestorage.com/...",
  },
  redirected: true,
  bodyUsed: false
}

支援相容 S3 的服務

Bun 的 S3 實作適用於任何相容 S3 的儲存服務。 只要指定適當的端點即可

將 Bun 的 S3Client 用於 AWS S3

AWS S3 是預設值。 您也可以傳遞 region 選項而不是 endpoint 選項給 AWS S3。

import { S3Client } from "bun";

// AWS S3
const s3 = new S3Client({
  accessKeyId: "access-key",
  secretAccessKey: "secret-key",
  bucket: "my-bucket",
  // endpoint: "https://s3.us-east-1.amazonaws.com",
  // region: "us-east-1",
});

將 Bun 的 S3Client 用於 Google Cloud Storage

若要將 Bun 的 S3 用戶端與 Google Cloud Storage 搭配使用,請在 S3Client 建構函式中將 endpoint 設定為 "https://storage.googleapis.com"

import { S3Client } from "bun";

// Google Cloud Storage
const gcs = new S3Client({
  accessKeyId: "access-key",
  secretAccessKey: "secret-key",
  bucket: "my-bucket",
  endpoint: "https://storage.googleapis.com",
});

將 Bun 的 S3Client 用於 Cloudflare R2

若要將 Bun 的 S3 用戶端與 Cloudflare R2 搭配使用,請在 S3Client 建構函式中將 endpoint 設定為 R2 端點。 R2 端點包含您的帳戶 ID。

import { S3Client } from "bun";

// CloudFlare R2
const r2 = new S3Client({
  accessKeyId: "access-key",
  secretAccessKey: "secret-key",
  bucket: "my-bucket",
  endpoint: "https://<account-id>.r2.cloudflarestorage.com",
});

將 Bun 的 S3Client 用於 DigitalOcean Spaces

若要將 Bun 的 S3 用戶端與 DigitalOcean Spaces 搭配使用,請在 S3Client 建構函式中將 endpoint 設定為 DigitalOcean Spaces 端點。

import { S3Client } from "bun";

const spaces = new S3Client({
  accessKeyId: "access-key",
  secretAccessKey: "secret-key",
  bucket: "my-bucket",
  // region: "nyc3",
  endpoint: "https://<region>.digitaloceanspaces.com",
});

將 Bun 的 S3Client 用於 MinIO

若要將 Bun 的 S3 用戶端與 MinIO 搭配使用,請在 S3Client 建構函式中將 endpoint 設定為 MinIO 正在執行的 URL。

import { S3Client } from "bun";

const minio = new S3Client({
  accessKeyId: "access-key",
  secretAccessKey: "secret-key",
  bucket: "my-bucket",

  // Make sure to use the correct endpoint URL
  // It might not be localhost in production!
  endpoint: "https://127.0.0.1:9000",
});

將 Bun 的 S3Client 用於 supabase

若要將 Bun 的 S3 用戶端與 supabase 搭配使用,請在 S3Client 建構函式中將 endpoint 設定為 supabase 端點。 supabase 端點包含您的帳戶 ID 和 /storage/v1/s3 路徑。 請務必在 supabase 儀表板中的 https://supabase.com/dashboard/project/<account-id>/settings/storage 上設定啟用透過 S3 協定連線,並設定在同一區段中告知的區域。

import { S3Client } from "bun";

const supabase = new S3Client({
  accessKeyId: "access-key",
  secretAccessKey: "secret-key",
  bucket: "my-bucket",
  region: "us-west-1",
  endpoint: "https://<account-id>.supabase.co/storage/v1/s3/storage",
});

將 Bun 的 S3Client 用於 S3 虛擬託管樣式端點

當使用 S3 虛擬託管樣式端點時,您需要將 virtualHostedStyle 選項設定為 true,如果未提供端點,Bun 將使用區域和儲存桶來推斷 AWS S3 的端點,如果未提供區域,則將使用 us-east-1。 如果您提供端點,則無需提供儲存桶名稱。

import { S3Client } from "bun";

// AWS S3 endpoint inferred from region and bucket
const s3 = new S3Client({
  accessKeyId: "access-key",
  secretAccessKey: "secret-key",
  bucket: "my-bucket",
  virtualHostedStyle: true,
  // endpoint: "https://my-bucket.s3.us-east-1.amazonaws.com",
  // region: "us-east-1",
});

// AWS S3
const s3WithEndpoint = new S3Client({
  accessKeyId: "access-key",
  secretAccessKey: "secret-key",
  endpoint: "https://<bucket-name>.s3.<region>.amazonaws.com",
  virtualHostedStyle: true,
});

// Cloudflare R2
const r2WithEndpoint = new S3Client({
  accessKeyId: "access-key",
  secretAccessKey: "secret-key",
  endpoint: "https://<bucket-name>.<account-id>.r2.cloudflarestorage.com",
  virtualHostedStyle: true,
});

憑證

憑證是使用 S3 最困難的部分之一,我們已盡力使其盡可能容易。 預設情況下,Bun 會讀取以下環境變數以取得憑證。

選項名稱環境變數
accessKeyIdS3_ACCESS_KEY_ID
secretAccessKeyS3_SECRET_ACCESS_KEY
regionS3_REGION
endpointS3_ENDPOINT
bucketS3_BUCKET
sessionTokenS3_SESSION_TOKEN

如果未設定 S3_* 環境變數,Bun 也會檢查上述每個選項的 AWS_* 環境變數。

選項名稱備用環境變數
accessKeyIdAWS_ACCESS_KEY_ID
secretAccessKeyAWS_SECRET_ACCESS_KEY
regionAWS_REGION
endpointAWS_ENDPOINT
bucketAWS_BUCKET
sessionTokenAWS_SESSION_TOKEN

這些環境變數是從 .env 檔案 或初始化時的程序環境讀取的(process.env 不用於此)。

這些預設值會被您傳遞給 s3.file(credentials)new Bun.S3Client(credentials) 或任何接受憑證的方法的選項覆寫。 因此,舉例來說,如果您對不同的儲存桶使用相同的憑證,您可以將憑證設定一次在您的 .env 檔案中,然後將 bucket: "my-bucket" 傳遞給 s3.file() 函數,而無需再次指定所有憑證。

S3Client 物件

當您不使用環境變數或使用多個儲存桶時,您可以建立 S3Client 物件以明確設定憑證。

import { S3Client } from "bun";

const client = new S3Client({
  accessKeyId: "your-access-key",
  secretAccessKey: "your-secret-key",
  bucket: "my-bucket",
  // sessionToken: "..."
  endpoint: "https://s3.us-east-1.amazonaws.com",
  // endpoint: "https://<account-id>.r2.cloudflarestorage.com", // Cloudflare R2
  // endpoint: "https://127.0.0.1:9000", // MinIO
});

// Write using a Response
await file.write(new Response("Hello World!"));

// Presign a URL
const url = file.presign({
  expiresIn: 60 * 60 * 24, // 1 day
  acl: "public-read",
});

// Delete the file
await file.delete();

S3Client.prototype.write

若要上傳或寫入檔案至 S3,請在 S3Client 實例上呼叫 write

const client = new Bun.S3Client({
  accessKeyId: "your-access-key",
  secretAccessKey: "your-secret-key",
  endpoint: "https://s3.us-east-1.amazonaws.com",
  bucket: "my-bucket",
});
await client.write("my-file.txt", "Hello World!");
await client.write("my-file.txt", new Response("Hello World!"));

// equivalent to
// await client.file("my-file.txt").write("Hello World!");

S3Client.prototype.delete

若要從 S3 刪除檔案,請在 S3Client 實例上呼叫 delete

const client = new Bun.S3Client({
  accessKeyId: "your-access-key",
  secretAccessKey: "your-secret-key",
  bucket: "my-bucket",
});

await client.delete("my-file.txt");
// equivalent to
// await client.file("my-file.txt").delete();

S3Client.prototype.exists

若要檢查檔案是否存在於 S3 中,請在 S3Client 實例上呼叫 exists

const client = new Bun.S3Client({
  accessKeyId: "your-access-key",
  secretAccessKey: "your-secret-key",
  bucket: "my-bucket",
});

const exists = await client.exists("my-file.txt");
// equivalent to
// const exists = await client.file("my-file.txt").exists();

S3File

S3File 實例是透過呼叫 S3Client 實例方法或 s3.file() 函數建立的。如同 Bun.file()S3File 實例是延遲載入的。它們不一定在建立時就指向實際存在的東西。這就是為什麼所有不涉及網路請求的方法都是完全同步的。

interface S3File extends Blob {
  slice(start: number, end?: number): S3File;
  exists(): Promise<boolean>;
  unlink(): Promise<void>;
  presign(options: S3Options): string;
  text(): Promise<string>;
  json(): Promise<any>;
  bytes(): Promise<Uint8Array>;
  arrayBuffer(): Promise<ArrayBuffer>;
  stream(options: S3Options): ReadableStream;
  write(
    data:
      | string
      | Uint8Array
      | ArrayBuffer
      | Blob
      | ReadableStream
      | Response
      | Request,
    options?: BlobPropertyBag,
  ): Promise<number>;

  exists(options?: S3Options): Promise<boolean>;
  unlink(options?: S3Options): Promise<void>;
  delete(options?: S3Options): Promise<void>;
  presign(options?: S3Options): string;

  stat(options?: S3Options): Promise<S3Stat>;
  /**
   * Size is not synchronously available because it requires a network request.
   *
   * @deprecated Use `stat()` instead.
   */
  size: NaN;

  // ... more omitted for brevity
}

如同 Bun.file()S3File 擴展了 Blob,因此所有在 Blob 上可用的方法也都在 S3File 上可用。從本機檔案讀取資料的相同 API 也可用於從 S3 讀取資料。

方法輸出
await s3File.text()字串
await s3File.bytes()Uint8Array
await s3File.json()JSON
await s3File.stream()ReadableStream
await s3File.arrayBuffer()ArrayBuffer

這表示將 S3File 實例與 fetch()Response 和其他接受 Blob 實例的 Web API 一起使用,就能正常運作。

使用 slice 進行部分讀取

若要讀取檔案的部分範圍,您可以使用 slice 方法。

const partial = s3file.slice(0, 1024);

// Read the partial range as a Uint8Array
const bytes = await partial.bytes();

// Read the partial range as a string
const text = await partial.text();

在內部,這是透過使用 HTTP Range 標頭來僅請求您想要的位元組來運作。此 slice 方法與 Blob.prototype.slice 相同。

從 S3 刪除檔案

若要從 S3 刪除檔案,您可以使用 delete 方法。

await s3file.delete();
// await s3File.unlink();

deleteunlink 相同。

錯誤代碼

當 Bun 的 S3 API 拋出錯誤時,它將會有一個 code 屬性,該屬性符合以下值之一

  • ERR_S3_MISSING_CREDENTIALS
  • ERR_S3_INVALID_METHOD
  • ERR_S3_INVALID_PATH
  • ERR_S3_INVALID_ENDPOINT
  • ERR_S3_INVALID_SIGNATURE
  • ERR_S3_INVALID_SESSION_TOKEN

當 S3 物件儲存服務傳回錯誤時(也就是說,不是 Bun 的錯誤),它將會是一個 S3Error 實例(一個名稱為 "S3Error"Error 實例)。

S3Client 靜態方法

S3Client 類別提供了幾個與 S3 互動的靜態方法。

S3Client.presign (靜態)

若要為 S3 檔案產生預先簽署的 URL,您可以使用 S3Client.presign 靜態方法。

import { S3Client } from "bun";

const credentials = {
  accessKeyId: "your-access-key",
  secretAccessKey: "your-secret-key",
  bucket: "my-bucket",
  // endpoint: "https://s3.us-east-1.amazonaws.com",
  // endpoint: "https://<account-id>.r2.cloudflarestorage.com", // Cloudflare R2
};

const url = S3Client.presign("my-file.txt", {
  ...credentials,
  expiresIn: 3600,
});

這等同於呼叫 new S3Client(credentials).presign("my-file.txt", { expiresIn: 3600 })

S3Client.exists (靜態)

若要檢查 S3 檔案是否存在,您可以使用 S3Client.exists 靜態方法。

import { S3Client } from "bun";

const credentials = {
  accessKeyId: "your-access-key",
  secretAccessKey: "your-secret-key",
  bucket: "my-bucket",
  // endpoint: "https://s3.us-east-1.amazonaws.com",
};

const exists = await S3Client.exists("my-file.txt", credentials);

相同的方法也適用於 S3File 實例。

import { s3 } from "bun";

const s3file = s3.file("my-file.txt", {
  ...credentials,
});
const exists = await s3file.exists();

S3Client.stat (靜態)

若要取得 S3 檔案的大小、etag 和其他 metadata,您可以使用 S3Client.stat 靜態方法。

import { S3Client } from "bun";

const credentials = {
  accessKeyId: "your-access-key",
  secretAccessKey: "your-secret-key",
  bucket: "my-bucket",
  // endpoint: "https://s3.us-east-1.amazonaws.com",
};

const stat = await S3Client.stat("my-file.txt", credentials);
// {
//   etag: "\"7a30b741503c0b461cc14157e2df4ad8\"",
//   lastModified: 2025-01-07T00:19:10.000Z,
//   size: 1024,
//   type: "text/plain;charset=utf-8",
// }

S3Client.delete (靜態)

若要刪除 S3 檔案,您可以使用 S3Client.delete 靜態方法。

import { S3Client } from "bun";

const credentials = {
  accessKeyId: "your-access-key",
  secretAccessKey: "your-secret-key",
  bucket: "my-bucket",
  // endpoint: "https://s3.us-east-1.amazonaws.com",
};

await S3Client.delete("my-file.txt", credentials);
// equivalent to
// await new S3Client(credentials).delete("my-file.txt");

// S3Client.unlink is alias of S3Client.delete
await S3Client.unlink("my-file.txt", credentials);

s3:// 協定

為了更輕鬆地對本機檔案和 S3 檔案使用相同的程式碼,fetchBun.file() 中支援 s3:// 協定。

const response = await fetch("s3://my-bucket/my-file.txt");
const file = Bun.file("s3://my-bucket/my-file.txt");

您可以額外將 s3 選項傳遞給 fetchBun.file 函數。

const response = await fetch("s3://my-bucket/my-file.txt", {
  s3: {
    accessKeyId: "your-access-key",
    secretAccessKey: "your-secret-key",
    endpoint: "https://s3.us-east-1.amazonaws.com",
  },
  headers: {
    "range": "bytes=0-1023",
  },
});

UTF-8、UTF-16 和 BOM(位元組順序記號)

如同 ResponseBlobS3File 預設採用 UTF-8 編碼。

當在 S3File 上呼叫 text()json() 方法之一時

  • 當偵測到 UTF-16 位元組順序記號 (BOM) 時,它將被視為 UTF-16。JavaScriptCore 原生支援 UTF-16,因此它會跳過 UTF-8 轉碼過程(並移除 BOM)。這在大多數情況下是好的,但這確實意味著如果您的 UTF-16 字串中有無效的代理配對字元,它們將會傳遞到 JavaScriptCore(與原始碼相同)。
  • 當偵測到 UTF-8 BOM 時,它會在字串傳遞到 JavaScriptCore 之前被移除,並且無效的 UTF-8 碼位會被替換為 Unicode 替換字元 (\uFFFD)。
  • 不支援 UTF-32。