概述
本文是笔者的系列博文 《Bun技术评估》 中的第七篇。
本文主要探讨的内容,是探讨在bun的体系中,如何集成和处理网络化的对象存储系统,具体而言就是对Amazon S3或者其兼容系统的操作的方式和内容。
这部分内容,其实对于现代的Web应用是非常关键和重要的,因为现在的Web应用,很多情况下都需要和各种各样的文件处理打交道。原有的实现方式,基本上是以文件系统+HTTP服务的模式来提供的。这个模式存在很多问题,比如上传文件,需要使用MultiPart文件模式操作实现服务,无法提供多文件选择和上传进度的监控,体验不佳。在技术上,可能还需要对临时文件系统进行复制和迁移,或者需要使用NFS这种系统在服务器后端来共享文件,还可能需要Web文件服务,访问控制等等,缺点就是系统和环节众多,实现流程复杂,性能损耗较大等等。
所以,现在的解决方式,都比较倾向于选择Amazon提出的网络化的文件存储系统和解决方案,即S3系统。
S3和MINIO
S3的全称是Simple Storage Service(简单存储服务),它早期是一个由Amazon Web Services(AWS)提供的对象存储和访问服务,这里的对象是指任意形式的数据文件,包括但不限于网站静态资源、图片、音视频、日志文件、备份数据等等。
这里借用一张图来总结一下S3的特点和优势(当然它主要是从云计算系统的角度出发的描述):

即:
- 按需付费:费用并非一个固定的软硬件投资,而是按照所需要的容量、流量和时间付费
- 可扩展性:用户可以方便扩展容量
- 可用性和持续性:Amazon保证服务的可用性和可持续性
- 安全性:Amazon设计的安全和访问控制体系,保证比网络文件系统更高的安全性
- 易用性:提供各种客户端和API,方便使用和集成
- 内置的优化:Amazon会优化数据的存储
笔者理解,和传统的基于文件系统的文件存储服务不同,S3就是一个兼容HTTP协议的互联网化的文件存储、访问和管理系统S3,并且提供了很多特别适合于网络应用的功能、特性和规范。虽然不是SMB、NFS这种标准化的技术系统,但由于其设计实现完善合理,使用成熟而广泛,已经成为互联网对象存储服务的事实标准。而且最重要的是,虽然从官方角度而言S3是Amazon提供的一种网络服务,但这一技术体系基本上是开放的,市场上已经有很多兼容的系统可用,甚至用户可用自己架构起私有的S3系统,比如笔者在本文中评估和使用的minio,就是这样一个系统。
MINIO是一个兼容S3的开源对象存储系统。这里兼容的意思是其操作方式,命令、客户端和API都是和S3一样的,对于开发者而言,基本上不需要修改原有的代码,仅修改配置信息,就可以使当前的支持S3的应用系统,支持MINIO系统,而这个系统可以是自己架构的,这一就为很多企业和政府的应用,提供了S3范式的文件存储和服务能力。
现在MINIO已经更名为AISTOR,但它的MINIO软件,还是以社区开源的方式,提供给用户进行自行部署和管理。MINIO的安装和部署非常简单,它是那种典型的"互联网"应用系统,可以提供单一可执行文件+参数启动的部署方式,如下面的命令,可以简单的启动一个单主机多磁盘模式的MINIO服务:
shell
// minio主文件夹
mkdir /opt/minio -p
// 数据文件夹,多磁盘挂载
mkdir /opt/minio/data1 && mount /dev/sdc /opt/minio/data1
mkdir /opt/minio/data2 && mount /dev/sdd /opt/minio/data2
mkdir /opt/minio/data3 && mount /dev/sde /opt/minio/data3
mkdir /opt/minio/data4 && mount /dev/sdf /opt/minio/data4
// 下载 minio
cd /opt/minio
wget https://dl.min.io/server/minio/release/linux-amd64/minio
chmod +x minio
// 启动服务
./minio --version
/opt/minio/minio server --address :9000 --console-address :9001 /opt/minio/data{1...4}
除了S3和Minio之外,其他Bun S3支持的兼容系统,还包括Google Cloud Storeage,Cloudflare R2,Deigital Ocean Space,Supabase等系统,配置和使用方式也是大同小异。
在了解的S3系统的基本情况之后,下面进入本文的主题,将S3系统和Bun应用进行集成。
基本形式
Bun对S3提供原生和内置的支持,可以直接使用。在bun s3模块中,进行数据和文件操作的基本模式可以参考下面的代码:
js
// import module
import { s3, write, S3Client } from "bun";
// 配置和客户端实例
const mioConfig = {
endpoint: "http://192.168.9.192:9000", // Make sure to use the correct endpoint URL
accessKeyId: "public",
secretAccessKey: "Public2025",
bucket: "public", // woring bucket
}
const minio = new S3Client(mioConfig);
// 写入内容
await minio.write("data.json", JSON.stringify({ hello: "world" }), {
// ...credentials,
type: "application/json",
});
// 下载内容
const s3file = minio.file("data.json");
const data = await s3file.json();
console.log("Data:", data);
简单说明如下:
- 使用前需要import模块相关方法,此处的核心是S3Client这个类,当然这些类都是内置的,无需外部安装
- 使用连接参数对S3Client进行初始化
- 基本参数包括accessKeyId(用户名)、secretAccessKey(密码)、bucket存储桶、endpoint端点地址等
- 如果要上传文件或者写入内容,可以使用client的write方法
- 访问或者下载文件,需要先实例化一个文件对象
- 然后执行对象的内容转换方法,就可以获取对象内容(有点像http模块的fetch方法)
文件对象模式
在基本形式中,是直接使用客户端实例来对数据进行操作的。但bun s3也提高了基于文件对象进行操作的模式,更加直观方便:
js
// file() returns a lazy reference to a file on S3
const metadata = minio.file("user/tom.json");
// Upload to S3
await write(metadata, JSON.stringify({ name: "Tom", age: 0 | Math.random()*50 }));
这个机制工作的原理是,先使用文件名,和file()方法,创建一个文件对象实例,然后就可以使用这个实例进行相关的操作了,比如写入信息,读取信息或者流等等。这个时候,其实文件可以是不先已经存在的,就是一个逻辑对象,minio服务器会自动处理。
内容读取
读取S3文件的内容,示例中使用的是json(),因为我们确定存储的是一个JSON对象(虽然是文件方式)。其他可用方法包括:
- json(), json对象
- text(),文本文件内容
- arrayBuffer(),文件buffer,支持任意实际内容和格式
- slice(),文件片段
- stream(),读取流
除了读取整个文件之外,还配合使用slice方法,读取部分内容,操作方式如下:
js
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();
最后,还有一种常用重要的信息读取方式,就是文件流,我们在后续章节专门讨论。
文件流
S3设计的一个主要目标,就是大量的文件处理,当然也就包括大规模并发的大型文件的上传和下载的应用,在这种情况下,将整个文件作为一个整体来处理是不现实的,必须要引入数据流的技术。官方提供的示例代码如下:
js
// 下载文件流
const stream = s3file.stream();
for await (const chunk of stream) {
console.log(chunk);
}
// 上传大型文件
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,
});
const bigFile = Buffer.alloc(10 * 1024 * 1024); // 10MB
for (let i = 0; i < 10; i++) {
await writer.write(bigFile);
}
await writer.end();
在这个示例中,对于文件的读取和写入,都可以使用文件流的操作方式。文件流的基本原理和概念,就是不一次性处理整个文件,而是将文件分割成为方便处理的小型片段(slice),然后一次处理一个片段,就不会遇到很大的性能和资源占用的问题了,当然,使用流处理大型文件,除了分片之外,还需要考虑分片处理的效率、有序性、完整性等问题,这也是任何一个流处理机制需要解决的问题。
示例代码中可以看到,Bun S3处理下载和上传方式略有不同。下载时,s3file对象本身就提供了stream方法,它本身就可以输出数据流,只需要在接收端处理数据片段即可。
上传文件稍微复杂一点。需要先创建一个写流对象,并且有一些选项可以配置重试次数,写流分片的大小,队列大小等。然后调用其写流的方法,来传输实际的文件内容(当然也可以对接一个读取流)。
Presign URL 预签名地址
数据的访问控制,是所有对象存储系统都需要考虑的问题,尤其是在互联网应用这种非常开放的应用场景当中。传统的基于文件系统的用户认证和文件访问控制机制,显然是不能适应这些应用场景的。而普通的HTTP资源访问控制方式(用户认证-响应)也无法很好的适应这种程序化的数据访问和传输场景。为此S3设计和提供了一种Presign(预签名)的访问控制方式,有效的简化和解决这个问题。
我们先来看一下这个操作是如何实现的:
js
// file() returns a lazy reference to a file on S3
const metadata = minio.file("user/tom.json");
// // Presign a URL (synchronous - no network request needed)
const url = metadata.presign({
acl: "public-read",
expiresIn: 60 * 60 * 24, // 1 day
});
console.log("URL:", url);
// 预签名URL内容
URL: http://192.168.9.192:9000/public/user/tom.json
?X-Amz-Acl=public-read
&X-Amz-Algorithm=AWS4-HMAC-SHA256
&X-Amz-Credential=public%2F20250630%2Fus-east-1%2Fs3%2Faws4_request
&X-Amz-Date=20250630T065109Z&X-Amz-Expires=86400
&X-Amz-SignedHeaders=host
&X-Amz-Signature=8427050878db389957a2f7b44d54d9f4c94111a870ba5dec87ff8696271ba275
S3处理的基本机制是,对于所有的文件对象,都可以抽象成为一个URL地址。然后对这个地址,基于服务器的配置进行一个签名,并且生成一个带有签名信息的新的URL地址。这些签名信息中具有访问控制信息、访问策略、签名时间和内容等等。S3客户端需要使用这个签名后的URL来进行操作,这时服务器可以对这个操作URL进行验证,由于这些信息是动态的,就保证了这一过程的安全性。这样就实现了一种服务器可验证的临时的对象访问的控制。
开发者可以通过控制签名的过程,来实现不同的访问控制策略。比如示例中可以配置签名过期的时间,和预置的访问策略(公共可读,或者任何可配置的ACL),简单而灵活。
系统内置的常见可用ACL包括:
- "public-read": 可开放读取
- "public-read-write": 可开放读写
- "private": 只有桶所有者可读
- "authenticated-read": 认证用户可读
- "bucket-owner-read": 桶所有者可读
- "bucket-owner-full-control": 桶所有者完全控制
- "aws-exec-read": aws账号可读
- "log-delivery-write": AWS服务用于日志记录
除了ACL之外,预签名还可以配置访问方式,包括PUT/DELETE/GET/HEAD/POST等等,达成更精细的控制。
文件管理
除了最常用的文件上传下载之外,Bun S3还提供了很多兼容的S3文件管理功能,包括:
- S3Client.list: 文件列表,可以使用prefix提供简单的查询功能
- S3Client.exists: 检查文件是否存在
- S3Client.size: 检查文件大小
- S3Client.stat: 获取文件信息
- S3Client.delete: 删除文件
错误处理
bun s3 API操作时,可能会抛出错误,这个错误会带有一个错误编码,常见的错误和代码包括:
- 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,会话错误
开发者应该熟悉这些错误信息,理解其发生的原因,并且能够快速的调整代码并且排除这些错误。
S3Client静态类
bun提供了一个静态类S3Client,和相关的静态方法,在一些情况下可以简化代码和操作:
js
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
};
// Write string
await S3Client.write("my-file.txt", "Hello World");
// Write JSON with type
await S3Client.write("data.json", JSON.stringify({ hello: "world" }), {
...credentials,
type: "application/json",
});
// Write from fetch
const res = await fetch("https://example.com/data");
await S3Client.write("data.bin", res, credentials);
// Write with ACL
await S3Client.write("public.html", html, {
...credentials,
acl: "public-read",
type: "text/html",
});
静态方法虽然操作方便,但笔者怀疑,由于无法复用客户端示例,每次都需要进行配置和验证,在大量操作情况下,性能应当不如客户端实例那么好,所以,可能更适合于一些临时性的操作。
S3协议访问
在系列文章前面的内容中,我们应该已经了解到,bun支持标准的Web客户端方法fetch来发起http请求。实际上, bun中也可以支持fetch方式来直接访问S3对象。方法是在URL中,使用"S3://"协议,如下面代码所示:
js
// fetch方法+s3配置
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",
},
});
const response = await fetch("s3://my-bucket/my-file.txt");
// bun.file 也支持s3协议
const file = Bun.file("s3://my-bucket/my-file.txt");
这样的协议支持和使用方式,显然让开发工作更加简单灵活。
性能
按照bun技术文档的说法,相比nodejs的s3 npm实现,bun s3可以提供高达5倍左右的性能。
小结
本文探讨了bun内置的S3对象存储和其兼容系统的操作支持,包括一般形式和流程,文件对象模式,数据上传下载,文件管理,预签名地址,错误处理等方面的内容。
笔者认为,对S3和其兼容系统的内置支持,模式和性能都比较成熟完善,是bun的一个非常重要和有特点的功能特性,大大的简化了Web应用程序对于文件的处理和操作。