s3vfs (cnb.cool/svn/s3vfs) 是一个独立的 Go 模块,将分层文件树映射为兼容 Amazon S3 协议的 HTTP 服务。它从 OpenList 的 server/s3 思路中提炼而来,通过稳定的 Tree 接口将底层存储抽象与 S3 协议层解耦,并提供可直接复用的 localfs 本地文件系统适配器。
ListBucket、HeadObject、GetObject、PutObject、DeleteObject、DeleteMulti、CopyObject 等核心操作CreateBucket、DeleteBucket,以及 bucket 存在性快速判断GetBucketVersioning、PutBucketVersioning、ListObjectVersions、delete marker 及版本回溯能力rangefs 可把支持 HTTP Range 的远端对象映射成进程内可 Read / Seek / Close 的文件句柄,适合无法使用 FUSE 的容器场景prefix、delimiter、marker、max-keys 列表参数localfs 可按 prefix 命中已排序索引,并对进程内写入即时更新、对外部文件变化定期自动追平If-Match、If-None-Match 等 Put ConditionsThisIsAnEmptyFolderInTheS3Bucket 占位对象以保持空目录在 S3 视角下的可见性Tree 仅定义支撑 S3 协议映射所需的最小方法集合(Buckets、Stat、List、Open、Put、Delete、MkdirAll),避免适配器实现负担过重RangeTree、BucketManager、BucketVersioner 等)按需追加,不污染主接口localfs 通过可选的 PrefixLister 直接按 prefix 命中对象索引,避免 ListBucket 退化为全桶递归扫描protocolBackend 适配器将无 context.Context 的 gofakes3.Backend 接口桥接至带上下文的 Tree 接口,使上层逻辑可正常使用 context 进行超时和取消控制johannesboyne/gofakes3 移除了内置 auth,本模块在 Server 外层自研 V4 签名中间件,维持动态 AK/SK 能力的同时保持对协议层的最小侵入go get cnb.cool/svn/s3vfs@latest
要求: Go 1.24 或更高版本。
以下示例展示如何将本地目录暴露为 S3 兼容的 HTTP 服务:
package main
import (
"log"
"net/http"
"cnb.cool/svn/s3vfs"
"cnb.cool/svn/s3vfs/localfs"
)
func main() {
// 创建静态 bucket 映射
store, err := localfs.Static(
localfs.Dir("public", "./data/public"),
localfs.Dir("backup", "./data/backup"),
)
if err != nil {
log.Fatal(err)
}
// 创建 S3 服务器并启用认证
server, err := s3vfs.New(
store,
s3vfs.WithCredentials("your-access-key", "your-secret-key"),
)
if err != nil {
log.Fatal(err)
}
// 启动 HTTP 服务(默认监听端口 9000)
log.Fatal(http.ListenAndServe(":9000", server.Handler()))
}
启动后即可使用 AWS SDK 或任何 S3 兼容客户端访问:
Endpoint: http://localhost:9000 AccessKey: your-access-key SecretKey: your-secret-key Region: us-east-1
说明:
localfs.Static(...)只注册静态 bucket 到本地目录的映射,不会自动创建./data/public或./data/backup。这与 S3 中“声明 bucket”不等于“执行 CreateBucket” 的语义一致。若需要在启动时确保目录已落盘,请预先创建目录,或显式调用store.CreateBucket(...)。
若开启了 WithCredentials(...) 或 WithCredentialMap(...),所有请求都必须使用 AWS Signature V4 做签名;直接用浏览器或未签名的curl去访问时,会被认证中间件拒绝。
若需要通过 Nginx 暴露 s3vfs,推荐给 S3 API 分配独立域名并代理到根路径。以下为最小可用配置:
server { listen 443 ssl http2; server_name s3.example.com; ssl_certificate /etc/nginx/certs/s3.example.com.crt; ssl_certificate_key /etc/nginx/certs/s3.example.com.key; client_max_body_size 0; merge_slashes off; location / { proxy_http_version 1.1; proxy_pass http://127.0.0.1:9000; # 透传签名相关头(SigV4 签名依赖这些字段,任何改动都会导致 403) proxy_pass_request_headers on; proxy_set_header Host $http_host; proxy_set_header Authorization $http_authorization; proxy_set_header X-Amz-Date $http_x_amz_date; proxy_set_header X-Amz-Content-Sha256 $http_x_amz_content_sha256; proxy_set_header X-Amz-Security-Token $http_x_amz_security_token; proxy_set_header Amz-Sdk-Invocation-Id $http_amz_sdk_invocation_id; proxy_set_header Amz-Sdk-Request $http_amz_sdk_request; proxy_set_header Accept-Encoding $http_accept_encoding; proxy_set_header Range $http_range; proxy_set_header Connection ""; proxy_request_buffering off; proxy_buffering off; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } }
关键要求:
https://s3.example.com/,不要挂在 /s3/ 等子路径下。rewrite、try_files、301/302 跳转或路径标准化,否则 SigV4 签名立即失效。Host——proxy_set_header Host $http_host 不可省略,否则签名与后端收到的 Host 不一致。Authorization、X-Amz-Date、X-Amz-Content-Sha256、Amz-Sdk-*、Accept-Encoding 等头不被删除、压缩或改写。使用 localfs.Static 将预定义的本地目录映射为固定名称的 bucket:
store, err := localfs.Static(
localfs.Dir("assets", "/srv/assets"), // bucket "assets" -> /srv/assets
localfs.Dir("archive", "/srv/archive"), // bucket "archive" -> /srv/archive
localfs.Dir("docs", "/opt/documents"), // bucket "docs" -> /opt/documents
)
注意:
- 静态模式下仅能创建预定义列表中的 bucket,不支持运行时动态添加新 bucket。
localfs.Static(...)不会自动执行mkdir -p;若映射目录尚不存在,请预先创建,或在启动时对预定义 bucket 显式调用store.CreateBucket(...)。
使用 localfs.Namespace 将一个根目录作为动态 bucket 命名空间,根目录下的每个子目录自动成为一个 bucket:
store, err := localfs.Namespace("./data")
// ./data/photos -> bucket "photos"
// ./data/videos -> bucket "videos"
// 运行时可通过 S3 API 动态创建新 bucket(即新建子目录)
提供三种方式配置 AWS Signature V4 认证:
// 方式一:单组静态 AK/SK
s3vfs.WithCredentials("access-key-1", "secret-key-1")
// 方式二:多组静态 AK/SK
s3vfs.WithCredentialMap(map[string]string{
"access-key-1": "secret-key-1",
"access-key-2": "secret-key-2",
})
// 方式三:注入可运行时修改的 CredentialStore
store := s3vfs.NewCredentialStore(map[string]string{
"access-key": "secret-key",
})
s3vfs.WithCredentialStore(store)
若不设置任何认证选项,服务将以匿名模式运行,所有请求无需签名即可访问。
Server 提供完整的运行时凭证管理 API,可在不重启服务的情况下增删改 AK/SK:
server, _ := s3vfs.New(store, s3vfs.WithCredentials("initial-ak", "initial-sk"))
// 新增或覆盖一组凭证
_ = server.SetCredential("new-ak", "new-sk")
// 移除指定凭证
server.DeleteCredential("old-ak")
// 整体替换所有凭证
_ = server.ReplaceCredentials(map[string]string{
"ak-1": "sk-1",
"ak-2": "sk-2",
})
// 查询当前生效的凭证
sk, ok := server.Credential("ak-1") // 查询单组
all := server.Credentials() // 查询全部快照
store := server.CredentialStore() // 获取底层 CredentialStore
使用 log/slog 标准库进行结构化日志输出:
logger := slog.Default()
// 或自定义 logger:
// logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
server, err := s3vfs.New(
store,
s3vfs.WithCredentials("ak", "sk"),
s3vfs.WithLogger(logger),
)
s3vfs.Server 实现了 http.Handler 接口,可灵活集成到现有的 HTTP 路由体系中:
mux := http.NewServeMux()
// 方式一:挂载到特定路径前缀
mux.Handle("/s3/", http.StripPrefix("/s3/", server.Handler()))
// 方式二:直接作为 Handler 使用(Server 自身也实现了 ServeHTTP)
mux.Handle("/s3/", server)
// 方式三:独立端口
go func() {
log.Fatal(server.ListenAndServe(":9000"))
}()
// 其他路由继续使用主 mux
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello from main app"))
})
log.Fatal(http.ListenAndServe(":8080", mux))
完整的 API 文档请参阅 pkg.go.dev/cnb.cool/svn/s3vfs。
若需要在应用内部把远端对象当作“近似本地文件”读取,而不是对外提供 S3 服务,可直接使用同仓子包 rangefs。该子包提供基于 HTTP Range 的虚拟文件句柄、共享块缓存、小范围 Seek 连接复用与指数退避能力,详细设计见 rangefs/README.md。
| 类型 | 定义位置 | 说明 |
|---|---|---|
Server | server.go | S3 HTTP 服务实例,封装协议引擎与认证中间件 |
Tree | types.go | 核心存储抽象接口,定义文件树到 S3 映射的最小方法集 |
Bucket | types.go | 描述一个 S3 bucket 的元数据 |
Entry | types.go | 描述 bucket 内的对象或目录条目 |
ObjectReader | types.go | 对象读取结果,包含元信息与读取流 |
PutObjectInput | types.go | 对象写入请求的输入描述 |
ObjectMetadata | types.go | 持久化的 S3 对象元数据 |
CredentialStore | credentials.go | 线程安全的动态 AK/SK 存储 |
Tree 是 s3vfs 的核心抽象,定义了将任意后端存储暴露为 S3 服务必须实现的最低方法集:
type Tree interface {
Buckets(ctx context.Context) ([]Bucket, error)
Stat(ctx context.Context, bucket, key string) (Entry, error)
List(ctx context.Context, bucket, dir string) ([]Entry, error)
Open(ctx context.Context, bucket, key string) (*ObjectReader, error)
Put(ctx context.Context, bucket, key string, input PutObjectInput) error
Delete(ctx context.Context, bucket, key string) error
MkdirAll(ctx context.Context, bucket, dir string) error
}
以下接口均为可选实现,s3vfs 会通过类型断言检测并使用相应能力:
| 接口 | 定义位置 | 方法 | 说明 |
|---|---|---|---|
RangeTree | types.go | OpenRange | 高效范围读取,避免全量加载 |
BucketChecker | types.go | BucketExists | 快速判断 bucket 是否存在 |
BucketManager | types.go | CreateBucket, DeleteBucket | Bucket 生命周期管理 |
ObjectCopier | types.go | Copy | 底层原生复制,避免 read-write 回环 |
MetadataLoader | types.go | LoadObjectMetadata | 加载持久化的对象元数据 |
PrefixLister | types.go | ListPrefix | 按 prefix 高效列对象,避免全桶递归扫描 |
BucketVersioner | versioning.go | Versioning, SetVersioning | Bucket 版本控制配置管理 |
VersionedObjectWriter | versioning.go | PutVersioned | 带版本语义的对象写入 |
VersionedObjectRemover | versioning.go | DeleteVersioned | 带版本语义的对象删除(创建 delete marker) |
VersionedObjectReader | versioning.go | StatVersion, OpenVersion | 按版本 ID 读取对象 |
VersionedRangeReader | versioning.go | OpenVersionRange | 按版本高效范围读取 |
VersionedObjectDeleter | versioning.go | DeleteVersion | 删除指定版本 |
VersionLister | versioning.go | ListVersions | 列出对象的所有版本历史 |
MultipartObjectStore | multipart.go | BeginMultipartUpload, PutMultipartPart, CompleteMultipartUpload, AbortMultipartUpload, ListMultipartUploads, ListMultipartParts | 完整的 multipart upload 能力 |
| 方法 | 签名 | 说明 |
|---|---|---|
New | func New(tree Tree, opts ...Option) (*Server, error) | 基于 Tree 和选项创建 S3 Server |
MustNew | func MustNew(tree Tree, opts ...Option) *Server | 创建失败时 panic,适合已固化配置的启动路径 |
NewHandler | func NewHandler(tree Tree, opts ...Option) (http.Handler, error) | New(...).Handler() 的便捷封装 |
| 方法 | 签名 | 说明 |
|---|---|---|
Handler | func (s *Server) Handler() http.Handler | 返回可挂载的 http.Handler |
ServeHTTP | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) | 实现 http.Handler 接口 |
ListenAndServe | func (s *Server) ListenAndServe(addr string) error | 直接启动 HTTP 服务 |
| 方法 | 签名 | 说明 |
|---|---|---|
Buckets | func (s *Server) Buckets(ctx context.Context) ([]Bucket, error) | 列出当前可见的 bucket |
| 方法 | 签名 | 说明 |
|---|---|---|
SetCredential | func (s *Server) SetCredential(accessKey, secretKey string) error | 新增或覆盖一组 AK/SK |
DeleteCredential | func (s *Server) DeleteCredential(accessKey string) | 移除指定 AK/SK |
ReplaceCredentials | func (s *Server) ReplaceCredentials(all map[string]string) error | 整体替换全部 AK/SK |
Credential | func (s *Server) Credential(accessKey string) (string, bool) | 查询单组 AK/SK |
Credentials | func (s *Server) Credentials() map[string]string | 查询全部 AK/SK 快照 |
CredentialStore | func (s *Server) CredentialStore() *CredentialStore | 获取底层凭证仓库引用 |
通过函数式选项模式配置 Server 行为:
| 函数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
WithCredentials | Option | 无(匿名模式) | 设置单组静态 AK/SK |
WithCredentialMap | Option | 无 | 设置多组静态 AK/SK |
WithCredentialStore | Option | 空 store | 注入外部 CredentialStore 实例 |
WithLogger | Option | nil(静默) | 设置结构化日志输出器(*slog.Logger) |
WithIntegrityCheck | Option | true | 是否校验上传请求中的 Content-MD5 |
WithRequestIDGenerator | Option | 随机生成 | 自定义 gofakes3 请求 ID 生成器 |
CredentialStore 提供线程安全的动态 AK/SK 存储:
// 构造
store, err := s3vfs.NewCredentialStore(map[string]string{
"ak-1": "sk-1",
})
// 操作
store.Set("ak-2", "sk-2") // 写入或覆盖
store.Delete("ak-1") // 删除
all := store.List() // 快照
count := store.Len() // 数量
sk, ok := store.Get("ak-2") // 查询
store.Replace(newMap) // 整体替换
s3vfs 使用哨兵错误值(sentinel errors)进行精确错误分类:
| 错误变量 | 说明 | 判断辅助函数 |
|---|---|---|
ErrNotFound | 对象或目录不存在 | IsNotFound(err) |
ErrVersionNotFound | 对象版本不存在 | IsVersionNotFound(err) |
ErrMultipartUploadNotFound | Multipart upload 不存在 | IsMultipartUploadNotFound(err) |
ErrBucketNotFound | Bucket 不存在 | IsBucketNotFound(err) |
ErrBucketAlreadyExists | Bucket 已存在 | — |
ErrBucketNotEmpty | Bucket 非空不可删除 | — |
ErrNotSupported | 底层适配器未实现该操作 | — |
ErrInvalidBucket | Bucket 名称非法 | — |
ErrInvalidKey | 对象 key 非法 | — |
ErrInvalidPart | Multipart part 非法或与服务端不一致 | — |
ErrInvalidPartOrder | Complete multipart 时 part 顺序非法 | — |
所有错误均支持 errors.Is() 进行精确匹配。
localfs 是 s3vfs 内置的本地文件系统适配器,完整实现了 Tree 接口及所有可选扩展接口。
| 函数 | 签名 | 说明 |
|---|---|---|
Dir | func Dir(name, root string) Bucket | 创建一条 bucket 到本地目录的映射定义 |
Static | func Static(buckets ...Bucket) (*Store, error) | 基于固定目录映射创建 Store |
MustStatic | func MustStatic(buckets ...Bucket) *Store | 失败时 panic 的 Static 变体 |
Namespace | func Namespace(root string) (*Store, error) | 基于根目录创建动态 namespace Store |
MustNamespace | func MustNamespace(root string) *Store | 失败时 panic 的 Namespace 变体 |
localfs 默认启用轻量级 list 索引,首次 LIST 时惰性建索引,随后:
Put / Delete / MkdirAll 会即时增量更新索引LIST 且超过刷新周期后自动重扫追平2s,也可按业务密度调整store, _ := localfs.Static(localfs.Dir("archive", "/srv/archive"))
store.ConfigureListIndex(localfs.ListIndexConfig{
RefreshInterval: 2 * time.Second,
})
Tree、RangeTree、BucketChecker、BucketManager、ObjectCopier、MetadataLoaderBucketVersioner、VersionedObjectWriter、VersionedObjectRemover、VersionedObjectReader、VersionedRangeReader、VersionedObjectDeleter、VersionListerMultipartObjectStore.s3vfs 隐藏目录内.. 路径逃逸攻击| 类别 | 操作 | 说明 |
|---|---|---|
| Bucket | GET / (ListBuckets) | 列出所有 bucket |
PUT /{bucket} (CreateBucket) | 创建 bucket | |
DELETE /{bucket} (DeleteBucket) | 删除空 bucket | |
HEAD /{bucket} (BucketExists) | 判断 bucket 是否存在 | |
| 对象 | HEAD /{bucket}/{key} (HeadObject) | 获取对象元信息 |
GET /{bucket}/{key} (GetObject) | 下载对象(支持 Range) | |
PUT /{bucket}/{key} (PutObject) | 上传对象(支持 Conditions) | |
DELETE /{bucket}/{key} (DeleteObject) | 删除对象 | |
POST /?delete (DeleteMulti) | 批量删除对象 | |
PUT /{bucket}/{key} (CopyObject) | 复制对象(通过 x-amz-copy-source) | |
| 列表 | GET /{bucket}?list-type=2 (ListBucketV2) | 列出对象(支持 prefix/delimiter/max-keys/marker 分页) |
| 版本控制 | GET /{bucket}?versioning | 获取版本控制配置 |
PUT /{bucket}?versioning | 设置版本控制配置 | |
GET /{bucket}?versions | 列出对象所有版本 | |
GET /{bucket}/{key}?versionId= | 按 versionId 获取/头信息/删除 | |
| Multipart | POST /{bucket}/{key}?uploads | 发起 multipart upload |
PUT /...?partNumber=&uploadId= | 上传 part | |
POST /...?uploadId= (Complete) | 完成 multipart upload | |
DELETE /...?uploadId= (Abort) | 中止 multipart upload | |
GET /{bucket}?uploads | 列出进行中的 multipart uploads | |
GET /{bucket}/{key}?uploadId= | 列出指定 upload 的 parts |
s3vfs 完整实现 S3 版本控制语义:
Enabled(启用)、Suspended(暂停)、None(从未启用)PutObject 生成新的 VersionIDDeleteObject 在启用状态下创建 delete marker 而非真正删除数据versionId 获取历史版本的元信息与内容DeleteObject?versionId= 删除指定版本(含 delete marker)ListObjectVersions 返回完整的版本历史,包含 delete marker基于 johannesboyne/gofakes3 的原生 MultipartBackend 扩展点实现:
MaxMultipartPartNumber 定义).s3vfs 目录,支持服务重启后恢复未完成的 uploadCompletedPart.PartNumber 顺序组装,避免二次全量拷贝截至 2026-04-05 的实测数据(对比 rclone/gofakes3 主线方案):
| 基准测试 | vs rclone | 内存分配 (B/op) 变化 |
|---|---|---|
SignedHeadExistingObject | 基本持平 | — |
MultipartTwoPartUpload | 慢 57.15% | 下降 93.08% |
MultipartConcurrentUploads-4 | 慢 71.66% | 下降 73.84% |
MultipartLargeObjectUpload-4 | 快 ~18.50% (p=0.114, 不宣称统计显著) | 下降 98.31% |
结论: 本方案在所有 multipart 场景下内存占用显著更低,大对象吞吐具有优势;小对象并发场景吞吐弱于 rclone 但换来更低的资源消耗与 restart-safe 语义保证。
同日新增 localfs 轻量级 list 索引后,NVR 风格目录树(archive/YYYY/MM/DD/cam-XX/HH/clip.ts)的 LIST 延时变化如下:
| 场景 | 旧实现 | 新实现 | 改善 |
|---|---|---|---|
10k month_browse | 96.15ms | 10.85ms | -88.72% |
10k day_browse | 93.89ms | 0.54ms | -99.42% |
10k camera_day_list | 93.57ms | 0.28ms | -99.70% |
20k month_browse | 168.23ms | 21.50ms | -87.22% |
20k day_browse | 166.81ms | 0.87ms | -99.48% |
20k camera_day_list | 164.79ms | 0.36ms | -99.78% |
20k day_browse x8 平均 | 368.02ms | 1.74ms | -99.53% |
20k day_browse x8 p95 | 405.85ms | 2.55ms | -99.37% |
结论: 旧实现的 LIST 成本主要由“全桶递归扫描”决定;引入 PrefixLister + localfs 轻量级索引后,NVR 归档这类深目录树已基本回到“与命中 prefix 成比例”的延时区间,更适合 1-2 万 文件量级的浏览型场景。
# 运行全部测试
go test ./...
# 运行指定包的测试
go test ./localfs/...
go test -v -run TestServer ./...
# 性能基准测试
go test -run '^$' -bench 'BenchmarkMultipart(TwoPartUpload|LargeObjectUpload|ConcurrentUploads)$' -benchmem ./...
s3vfs/ ├── server.go # Server 核心结构与构造逻辑 ├── server_auth.go # AWS SigV4 认证中间件 ├── server_compat.go # 目录兼容中间件 ├── backend.go # 核心 Backend 实现(Tree → gofakes3 桥接) ├── backend_protocol.go # Protocol adapter(context 注入与 multipart 路由) ├── backend_versioning.go # 版本控制相关 Backend 方法 ├── types.go # Tree 接口定义与核心数据类型 ├── options.go # Option 函数式选项与默认配置 ├── credentials.go # 动态凭证存储 ├── auth_v4.go # AWS Signature V4 签名验证算法 ├── errors.go # 哨兵错误值与判断辅助函数 ├── list.go # 对象遍历与分页逻辑 ├── multipart.go # Multipart 接口定义与类型 ├── versioning.go # 版本控制接口定义与类型 ├── logger.go # 结构化日志适配 ├── localfs/ # 本地文件系统适配器 │ ├── store.go # Store 实现(Tree 全接口) │ ├── objects.go # 对象读写操作 │ ├── list_index.go # 轻量级 list 索引与自动刷新 │ ├── metadata.go # 元数据持久化 │ ├── versioning.go # 版本控制状态管理 │ ├── versioning_state.go # 版本状态持久化 │ ├── versioning_files.go # 版本文件 I/O │ ├── versioning_commit.go # 版本提交逻辑 │ ├── multipart.go # multipart 主逻辑 │ ├── multipart_state.go # multipart 状态持久化 │ ├── multipart_files.go # multipart 文件 I/O │ └── multipart_runtime.go # multipart 运行时缓存 ├── examples/ │ └── basic/main.go # 最简使用示例 ├── server_test.go # Server 集成测试 ├── server_list_index_test.go # list 索引回归测试 ├── server_benchmark_test.go # 性能基准测试 ├── auth_test.go # 签名验证单元测试 └── localfs/localfs_test.go # localfs 适配器测试
欢迎提交 Issue 和 Pull Request!
git checkout -b feature/my-featurego test ./...errors.Is() 进行哨兵错误匹配本项目基于 MIT 许可证开源。详见 LICENSE 文件。