mcpField
H
HybridS3

HybridS3

本地服务
github.com/psyb0t文件与存储51 周前更新

关于 HybridS3

HybridS3 是面向服务和 AI Agent 的轻量级可自托管对象存储服务。它整合兼容 S3 的接口、HTTP 和 MCP,使用配置文件定义存储桶,并支持对象自动过期。

主要功能

  1. 01兼容 boto3 的 S3 接口
  2. 02通过 HTTP 操作对象
  3. 03由 YAML 配置存储桶
  4. 04对象 TTL 自动过期
  5. 05支持预签名 URL

适用场景

  1. 01供服务存储和读取对象
  2. 02为 AI Agent 提供结构化存储工具
  3. 03自动清理临时存储桶内容

MCP 相关

HybridS3 在 `/mcp/` 通过 Streamable HTTP 提供 MCP 服务。MCP 客户端可调用带类型定义的工具操作存储桶,并使用可选的连接认证和单工具访问密钥。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"psyb0t-hybrids3": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"docker.io/psyb0t/hybrids3:v0.3.8"
]
}
}
}
启动命令docker run -i --rm docker.io/psyb0t/hybrids3:v0.3.8

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
ocidocker.io/psyb0t/hybrids3:v0.3.8

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

HybridS3

CI version license Docker Pulls

轻量级对象存储,支持 S3(兼容 boto3/AWS SDK)、纯 HTTP 和 MCP。使用 SQLite 存储元数据,磁盘上存储普通文件。


目录


为何选择

**大多数可自托管的兼容 S3 存储都面向大规模部署。**分布式纠删码、IAM 策略、WORM 合规、完整的 Web 控制台——如果你在运行云服务,这些都很实用;但如果你只是想找个地方存放文件,供各种服务和 AI Agent 读写,那就太复杂了。

**AWS Sig V4 在反向代理路径前缀后无法正常工作。**大多数实现都会使用完整的原始上游路径验证签名。将它们放在 nginx 的 /storage/ 后面时,nginx 会剥离前缀,服务端看到的是 /bucket/key 而不是 /storage/bucket/key,签名验证就会失败。HybridS3 提供了 path_prefix 配置选项——将其设为 /storage,所有路由都会移至该前缀下。无需剥离路径,也无需特殊代理标头。boto3 签名的路径与服务端看到的路径一致。

**三个接口,一个服务。**boto3 开箱即用。纯 HTTP 配合 curl 即可使用。AI Agent 通过 MCP 连接,并获取结构化的工具定义。无需为不同客户端运行独立服务。

**存储桶是配置,而非状态。**没有创建或删除存储桶的 API。存储桶定义在 YAML 配置文件中。你始终清楚具体有哪些存储桶,配置受版本控制,也不会有意外存储桶不断累积垃圾数据。

**内置 TTL 过期机制。**在存储桶上设置 ttl: 24h,对象就会在最后一次写入后自动过期。无需生命周期策略、cron 作业或独立进程。

**易于阅读和修改。**体量小,一个下午就能看懂。


快速开始

# get the example config
wget -O config.yaml https://raw.githubusercontent.com/psyb0t/docker-hybrids3/master/config.example.yaml

# edit it — set your keys and define your buckets
vi config.yaml

# run
docker run -d --name hybrids3 \
    -p 8080:8080 \
    -v ./config.yaml:/config/config.yaml:ro \
    -v hybrids3-data:/data \
    psyb0t/hybrids3

# verify
curl http://localhost:8080/health

运行

容器需要:

  • 配置文件位于 /config/config.yaml
  • 数据目录位于 /data
  • 端口为 8080

以 UID 1000 运行。

docker run:

docker run -d --name hybrids3 \
    -p 8080:8080 \
    -v ./config.yaml:/config/config.yaml:ro \
    -v hybrids3-data:/data \
    psyb0t/hybrids3

docker-compose:

services:
  hybrids3:
    image: psyb0t/hybrids3
    ports:
      - "8080:8080"
    volumes:
      - ./config.yaml:/config/config.yaml:ro
      - hybrids3-data:/data
    restart: unless-stopped

volumes:
  hybrids3-data:

配置

# Cross-bucket god key. Works on every bucket for every operation.
# Only credential that can list all buckets. Keep secret.
master_key: "change-me-to-something-secret"

# Non-secret identifier paired with master_key in S3 auth (aws_access_key_id).
# Safe to share — it grants nothing without the master_key.
master_public_key: "master"

# How often the background loop runs to delete expired objects and orphan files.
# Accepts human-readable durations: 30s, 5m, 1h, or raw seconds: 60
cleanup_interval: 1m

# How long a request may wait to acquire a lock on an object before giving up with 503.
# Accepts seconds as a float. Applies per object key, globally across all buckets.
lock_acquire_timeout: 30

# How long a request may hold a lock before it is forcibly released and the request gets 503.
# Protects against slow or stalled uploads blocking writes on the same key indefinitely.
lock_hold_timeout: 300

# Maximum number of requests that may queue for the same object key at once.
# Once the queue is full, new requests are rejected immediately with 503.
lock_max_waiters: 100

# Serve all routes under this path prefix.
# Set this when running behind a reverse proxy at a subpath (e.g. /storage).
# Accepts "/storage" or "/storage/" — both are normalized.
# Leave empty or omit to serve at the root.
# path_prefix: /storage

# TTL formats:   30s  5m  1h  1h30m12s  1d  2d12h  0 (never expire)
# Size formats:  500B  50KB  10MB  1GB  0 (no limit)

buckets:
  uploads:
    # true  — anyone can read (GET/HEAD/LIST) without authentication
    # false — authentication required for all operations
    public: true

    # Private key for this bucket.
    # Used as the Bearer token value and as aws_secret_access_key in S3 auth.
    # Never transmitted — only used to sign or verify request signatures.
    # Keep secret.
    key: "uploads-secret"

    # Public identifier for this bucket in S3 auth (aws_access_key_id).
    # Also appears in presigned URL Credential= fields.
    # Safe to share — it identifies the bucket but grants no access.
    # Defaults to the bucket name if not set.
    public_key: "uploads-id"

    # Objects expire this long after their last write. Overwriting resets the clock.
    # 0 means objects never expire.
    ttl: 24h

    # Maximum upload size. Requests over this limit are rejected with 413.
    # 0 means no limit.
    max_file_size: 50MB

  permanent:
    public: false
    key: "perm-secret"
    public_key: "permanent-id"
    ttl: 0
    max_file_size: 100MB

密钥与访问

密钥对模型

每个存储桶在配置中都有两个密钥:

配置字段作用是否需要保密?
key私钥。用于验证 Bearer 请求和签署 S3 签名。不会传输——只在本地用于计算或验证 HMAC。是
public_key公共标识符。在 S3 身份验证中用作 aws_access_key_id,并显示在预签名 URL 的 Credential= 字段中。单独使用它不会授予任何权限。否——可安全分享

这种分离使预签名 URL 能够安全地工作。预签名 URL 必须在 Credential= 字段中嵌入一个标识符,以便服务知道要使用哪个密钥进行验证——该标识符就是 public_key。由于它不是机密信息,将其放在 URL 中没有问题。私钥会在服务端对 URL 签名,且绝不会出现在 URL 中。

主密钥

master_key 是一个跨存储桶凭据,无需单独的存储桶密钥,即可对每个存储桶执行所有操作。以下两种情况需要使用它:

  • 你需要通过单个客户端操作多个存储桶
  • 你需要列出所有已配置的存储桶——存储桶密钥在 list_buckets 中只能看到各自的存储桶

master_public_key 是与 S3 身份验证中的 master_key 配对的非机密标识符(用作 aws_access_key_id)。

不要将主密钥嵌入面向客户端的代码中。请改用各存储桶的密钥——它们只能访问一个存储桶。

存储桶可见性
设置GET / HEAD / LISTPUTDELETE / 预签名
public: true无需身份验证存储桶密钥、主密钥或有效的预签名 PUT存储桶密钥或主密钥
public: false存储桶密钥、主密钥或有效的预签名 GET存储桶密钥、主密钥或有效的预签名 PUT存储桶密钥或主密钥

HTTP API

身份验证

HTTP 请求通过 Authorization 标头中的 Bearer 令牌进行身份验证。传入存储桶的私钥 key,或用于跨存储桶操作的主密钥。

Authorization: Bearer <private_key>
  • 公共存储桶读取(GET、HEAD、LIST)——无需身份验证。
  • 所有写入操作(PUT、DELETE)和对私有存储桶的读取操作——需要存储桶密钥或主密钥。
  • 列出存储桶(GET /)——主密钥可列出所有存储桶;存储桶密钥只能列出自己的存储桶。
  • 预签名端点(POST /presign/...)——需要存储桶密钥或主密钥。
# write to a bucket
curl -X PUT http://localhost:8080/uploads/file.txt \
  -H "Authorization: Bearer uploads-secret" \
  -d "hello"

# read from a public bucket — no auth needed
curl http://localhost:8080/uploads/file.txt

# read from a private bucket
curl http://localhost:8080/permanent/doc.pdf \
  -H "Authorization: Bearer perm-secret"

# list all buckets — master key sees all
curl http://localhost:8080/ \
  -H "Authorization: Bearer your-master-key"

# list buckets — bucket key sees only its own bucket
curl http://localhost:8080/ \
  -H "Authorization: Bearer uploads-secret"

端点
方法路径认证描述
GET/health无返回 {"status":"ok"}
GET/主密钥或存储桶密钥列出存储桶——主密钥列出全部,存储桶密钥仅列出其对应的存储桶
HEAD/{bucket}读取检查存储桶是否存在——返回 200 或 404
PUT/{bucket}写入S3 兼容空操作:若存储桶存在于配置中则返回 200,否则返回 404
GET/{bucket}读取列出存储桶中的对象
PUT/{bucket}/{key}写入上传对象
GET/{bucket}/{key}读取下载对象
HEAD/{bucket}/{key}读取对象元数据——不包含响应正文
DELETE/{bucket}/{key}写入删除对象——即使对象不存在也返回 204
POST/presign/{bucket}/{key}写入生成预签名 URL(GET 或 PUT,参见 method 查询参数)
POST/mcp/按工具MCP Streamable HTTP 端点

PUT /{bucket} 仅用于兼容 S3 客户端。boto3 会在执行任何操作前发送 create_bucket 调用,该调用会映射到此端点。HybridS3 会将其视为空操作——不会创建或修改任何存储桶。

列出对象时接受 prefix 和 max-keys 查询参数:

curl "http://localhost:8080/uploads?prefix=images/&max-keys=50" \
  -H "Authorization: Bearer uploads-secret"

上传会返回 ETag 标头(文件内容的 MD5)。GET 和 HEAD 也会返回 ETag、Last-Modified 和 Content-Length。


对象键

键支持使用 / 表示嵌套路径。写入时会自动创建父目录,删除时会在目录为空后将其清理。

curl -X PUT http://localhost:8080/uploads/reports/2024/january.pdf \
  -H "Authorization: Bearer uploads-secret" \
  --data-binary @january.pdf

响应格式

包含 AWS Sig V4 Authorization 标头的请求会收到兼容 S3 的 XML 响应。其他所有请求都会收到 JSON 响应。错误响应包含 "error" 字段和 "request_id" 字段。

每个响应都包含 X-Request-Id,用于关联日志,并包含 X-Content-Type-Options: nosniff。


S3 接口

认证

S3 客户端使用 AWS Signature V4 进行认证。客户端使用存储桶的 public_key 作为 aws_access_key_id,并使用存储桶的私有 key 作为 aws_secret_access_key 对每个请求进行签名。生成的 Authorization 标头会在 Credential= 字段中以明文包含访问密钥 ID,并在 Signature= 字段中包含计算得出的 HMAC。私钥不会传输——它仅在本地用于计算签名。

Authorization: AWS4-HMAC-SHA256 Credential=uploads-id/20240101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=<hmac>

HybridS3 从 Credential= 中读取访问密钥 ID,找到具有该 public_key 的存储桶,然后使用该存储桶的私钥重新计算预期签名,并将其与标头中的签名进行比较。仅有访问密钥 ID 不会授予任何权限——签名必须匹配。

import boto3
from botocore.config import Config

# per-bucket client — access is limited to the "uploads" bucket
s3 = boto3.client(
    "s3",
    endpoint_url="http://localhost:8080",
    aws_access_key_id="uploads-id",         # public_key from config
    aws_secret_access_key="uploads-secret",  # key from config
    region_name="us-east-1",
    config=Config(signature_version="s3v4"),
)

# master key client — access to all buckets, can list them
s3_admin = boto3.client(
    "s3",
    endpoint_url="http://localhost:8080",
    aws_access_key_id="master",              # master_public_key from config
    aws_secret_access_key="your-master-key", # master_key from config
    region_name="us-east-1",
    config=Config(signature_version="s3v4"),
)

操作
s3.put_object(Bucket="uploads", Key="file.txt", Body=b"hello")
s3.get_object(Bucket="uploads", Key="file.txt")
s3.head_object(Bucket="uploads", Key="file.txt")
s3.delete_object(Bucket="uploads", Key="file.txt")
s3.list_objects_v2(Bucket="uploads", Prefix="images/")
s3_admin.list_buckets()  # master key: returns all buckets
s3.list_buckets()        # bucket key: returns only the "uploads" bucket

# generate a presigned URL for a private bucket object
url = s3.generate_presigned_url(
    "get_object",
    Params={"Bucket": "permanent", "Key": "doc.pdf"},
    ExpiresIn=3600,
)

# presigned URL someone else can use to upload a single key
put_url = s3.generate_presigned_url(
    "put_object",
    Params={"Bucket": "uploads", "Key": "inbox/report.pdf"},
    ExpiresIn=600,
)

MCP 接口

MCP 服务使用 Streamable HTTP 传输协议运行于 /mcp/。AI Agent 可通过任何兼容 MCP 的客户端连接,并获得包含类型化输入和输出的结构化工具定义。

连接
{
  "mcpServers": {
    "hybrids3": {
      "type": "streamable-http",
      "url": "http://localhost:8080/mcp/"
    }
  }
}
认证
端点级身份验证

/mcp/ 端点接受可选令牌,用于在调用任何工具之前验证连接。使用主密钥可获得完整访问权限;使用存储桶密钥可将连接限制在该存储桶的范围内。令牌会与主密钥和所有存储桶密钥进行验证。

支持以下两种方式:

Authorization 标头 — 适用于支持自定义标头的客户端(例如 Claude Code):

Authorization: Bearer <master_key_or_bucket_key>

查询参数 — 适用于无法设置自定义标头的客户端(例如 ChatGPT):

http://localhost:8080/mcp/?auth=<master_key_or_bucket_key>

如果提供的令牌与任何已知密钥都不匹配,请求将被拒绝,并返回 401。如果完全未提供令牌,则请求会通过,随后应用各工具的身份验证。

各工具的身份验证

每个操作存储桶的工具都接受一个 auth_key 参数,即存储桶的私有 key 或主密钥。此验证独立于端点级身份验证,并控制每次单独的工具调用可以执行的操作。

# public bucket read — no auth_key
download_object(bucket="uploads", key="file.txt")

# public bucket write — auth_key required
upload_object(bucket="uploads", key="file.txt", content="hello", auth_key="uploads-secret")

# private bucket — auth_key required for all operations
download_object(bucket="permanent", key="doc.pdf", auth_key="perm-secret")
upload_object(bucket="permanent", key="doc.pdf", content="...", auth_key="perm-secret")

# master key lists all buckets; bucket key lists only that bucket
list_buckets(auth_key="your-master-key")
list_buckets(auth_key="uploads-secret")  # returns only the uploads bucket
工具
工具是否需要身份验证描述
upload_object存储桶密钥或主密钥上传文本或 Base64 编码的二进制数据。如果未指定内容类型,则自动检测。
download_object存储桶密钥或主密钥(仅限私有存储桶)下载对象内容。返回文本或 Base64 编码的二进制数据。超过 50 MB 的对象无法通过 MCP 下载 — 请直接使用 HTTP API。
delete_object存储桶密钥或主密钥删除对象。
list_objects存储桶密钥或主密钥(仅限私有存储桶)列出对象,可选前缀筛选。默认返回 100 个结果,最多 1000 个。
list_buckets主密钥或存储桶密钥主密钥可列出所有存储桶。存储桶密钥只能列出对应的存储桶。
object_info存储桶密钥或主密钥(仅限私有存储桶)获取对象元数据(大小、内容类型、ETag、过期时间),而不下载内容。
presign_url存储桶密钥或主密钥生成可分享的 URL。传入 method="GET"(默认值)或 method="PUT"。对公共存储桶执行 GET 会返回普通 URL;其他情况则会返回带签名的限时 URL。

所有工具都会为支持结构化输出的客户端返回结构化输出(structuredContent),并提供纯文本回退。


Agent 集成

技能可用于任何会读取 .agents/skills/ 的 Agent(例如 peen),并且可以原生安装到以下客户端中。

Claude Code
claude plugin marketplace add psyb0t/agents
claude plugin install hybrids3@psyb0t

Claude Code 会提示输入 HybridS3 URL;如果端点要求连接级身份验证,还会提示输入存储桶密钥或主密钥 — 密钥会存储在你的操作系统钥匙串中。

Codex
codex plugin marketplace add psyb0t/agents
codex plugin add hybrids3@psyb0t

通过 marketplace 安装后,该技能会以 $hybrids3:hybrids3 的形式调用。Codex 也会自动发现该技能,无需安装;只要仓库中包含 .agents/skills/,就会以 $hybrids3 的形式直接调用。

OpenClaw

每次发布时,都会将该技能发布到 ClawHub:

openclaw skills install @psyb0t/hybrids3

对于使用本地 stdio 通信的 MCP 客户端,@psyb0t/hybrids3 插件可桥接到服务的 /mcp/ 端点:

openclaw plugins install clawhub:@psyb0t/hybrids3

然后设置 HYBRIDS3_URL(如果你的端点要求连接级身份验证,还需设置 HYBRIDS3_KEY)。


预签名 URL

预签名 URL 允许任何持有链接的人在有限时间内读取或写入特定对象,无需发送 Authorization 标头。/presign/{bucket}/{key} 端点通过 method 查询参数支持两种方法:

  • method=GET(默认)——接收者可以下载对象。
  • method=PUT——接收者可以上传(覆盖)对象。

预签名 URL 与其 HTTP 方法绑定。GET URL 不能用于 PUT,反之亦然——签名会将方法包含在其规范请求中。

有效期范围:1 秒至 604800 秒(7 天)。默认值:3600。过期或遭篡改的 URL 会返回 403。

预签名 GET

私有存储桶——生成 AWS Sig V4 预签名 URL。服务器使用存储桶的私钥对 URL 签名,私钥不会出现在 URL 中。URL 的 Credential= 字段中包含 public_key,X-Amz-Signature 中包含 HMAC。

# generate via HTTP endpoint
curl -X POST "http://localhost:8080/presign/permanent/doc.pdf?expires=3600" \
  -H "Authorization: Bearer perm-secret"

# response
{"url": "http://localhost:8080/permanent/doc.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&...&X-Amz-Signature=...", "method": "GET", "expires": 3600}

公共存储桶——返回不带签名且没有有效期的普通 URL,因为公共存储桶上的 GET 操作本身无需身份验证。

curl -X POST "http://localhost:8080/presign/uploads/photo.jpg" \
  -H "Authorization: Bearer uploads-secret"

# response
{"url": "http://localhost:8080/uploads/photo.jpg", "method": "GET", "expires": null}
预签名 PUT

使用 method=PUT,即可将 URL 交给他人,让对方上传到指定键,而无需看到你的存储桶密钥。公共存储桶在这里也不能走捷径——允许匿名读取,但绝不允许匿名写入,因此即使是公共存储桶,预签名 PUT URL 也始终会经过签名。

# generate a presigned upload URL
curl -X POST "http://localhost:8080/presign/uploads/inbox/report.pdf?method=PUT&expires=600" \
  -H "Authorization: Bearer uploads-secret"

# response
{"url": "http://localhost:8080/uploads/inbox/report.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&...&X-Amz-Signature=...", "method": "PUT", "expires": 600}

# upload using only the URL — no Authorization header
curl -X PUT "<url>" --data-binary @report.pdf

上传过程中,服务器会强制执行存储桶的 max_file_size;无论请求通过何种方式进行身份验证,超大请求体都会被拒绝并返回 413。

使用 boto3:

url = s3.generate_presigned_url(
    "put_object",
    Params={"Bucket": "uploads", "Key": "inbox/report.pdf"},
    ExpiresIn=600,
)
# hand `url` to a client; they PUT their bytes to it

生成预签名 URL 需要存储桶的私钥或主密钥。生成的 URL 只授予对单个键执行单项操作(GET 或 PUT)的权限。

/presign/ HTTP 端点和 presign_url MCP 工具遵循相同逻辑——向 MCP 工具传入 method="PUT" 即可获取上传 URL。


MIME 类型检测

每次上传时都会自动检测内容类型。无需提供 Content-Type 标头。检测使用 libmagic 检查文件内容的前 8 KB;如果 libmagic 返回通用类型,则回退到根据文件名扩展名判断。检测到的类型会存储在元数据中,并在 GET 和 HEAD 响应的 Content-Type 中返回。

若要覆盖自动检测结果,请在上传请求中显式设置 Content-Type。


TTL 和过期时间

在存储桶上设置 ttl 后,对象会在最后一次写入后的指定时长后自动过期。覆盖对象会重置其过期计时。设置 ttl: 0 表示对象永不过期。

buckets:
  staging:
    ttl: 1h # objects expire 1 hour after last write

后台清理循环每隔 cleanup_interval 秒运行一次,并执行两项任务:

  1. TTL 过期清理——查找所有 expires_at 已过期的对象,并从磁盘和元数据中删除。
  2. 孤立文件清理——扫描磁盘上每个存储桶的目录,并删除没有对应元数据记录的文件。这用于清理因上传中断而遗留的文件。

每次删除时,都会向上移除空的父目录,直至存储桶根目录。


锁定

每个对象键都有自己的异步读写锁。多个读取者可以同时持有该锁。写入者获得独占访问权,并阻止该键上的所有并发读取者和写入者。锁会在操作开始前获取,并在响应完全发送后释放。

这可以防止并发访问时发生读取数据撕裂、写入内容部分可见和数据损坏。

以下三种情况会导致 503 Service Unavailable:

条件触发条件
过载同一键上排队的请求超过 lock_max_waiters
获取超时请求等待 lock_acquire_timeout 秒仍未获取锁
持有超时请求持有锁的时间超过 lock_hold_timeout 秒

持有超时可防止缓慢或停滞的上传无限期持有写锁。超时触发时,锁会被释放,请求会收到 503。

默认值(获取超时 30s、持有超时 300s、最多等待者 100)适用于大型文件上传。对于高吞吐量的小对象工作负载,可调小这些值,以便更快地减轻负载。


日志记录

以结构化 JSON 格式输出到 stdout,每行一条:

{
  "ts": "21:05:33",
  "level": "INFO",
  "src": "routes:put_object:227",
  "rid": "4432cd8a4ac1",
  "msg": "PUT",
  "bucket": "uploads",
  "key": "photo.jpg",
  "size": 48231,
  "content_type": "image/jpeg"
}
字段说明
ts时间戳(HH:MM:SS)
levelINFO、WARNING 或 ERROR
srcmodule:function:line
rid请求 ID — 与响应头 X-Request-Id 相匹配
msg事件名称
其他具体上下文:bucket、key、size、content_type 等

请求期间生成的每条日志都会包含请求 ID,便于端到端追踪单个请求。异常日志会包含完整的回溯信息。


在反向代理后运行

在配置中设置 path_prefix,使其与代理位置相匹配。HybridS3 原生支持在该前缀下提供所有路由 — nginx 会原样转发路径,无需移除前缀。由于客户端签名的路径与服务端看到的路径一致,SigV4 签名可以正常工作。

# config.yaml
path_prefix: /storage
# ✅ CORRECT — no URI in proxy_pass, nginx forwards full path as-is
location /storage {
    proxy_pass http://hybrids3:8080;
    proxy_set_header Host $http_host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

# ❌ WRONG — trailing slash in proxy_pass is a URI, nginx strips /storage/
# location /storage/ {
#     proxy_pass http://hybrids3:8080/;   <-- this "/" is a URI, causes stripping
# }
#
# ❌ WRONG — $host strips the port, SigV4 signs Host: yourdom:4000
# but nginx sends Host: yourdom → signature mismatch
# proxy_set_header Host $host;   <-- use $http_host instead

关键在于:proxy_pass http://backend:8080;(末尾没有 /)会转发完整路径。proxy_pass http://backend:8080/;(末尾有 /)会让 nginx 替换 location 前缀 — 这会移除 /storage/,导致 SigV4 失效。

使用此配置:

# boto3
s3 = boto3.client("s3", endpoint_url="http://yourdomain/storage", ...)

# curl
curl http://yourdomain/storage/uploads/file.txt

# MCP
{"url": "http://yourdomain/storage/mcp/"}

设置 path_prefix 后,所有端点都会移至该前缀下,包括健康检查端点(/storage/health)。


安全

  • AWS Sig V4 验证 — 对请求头认证和预签名 URL 进行完整的签名验证;过期或遭篡改的预签名 URL 会返回 403
  • 恒定时间密钥比较 — 所有凭据检查均使用 hmac.compare_digest,以防止计时攻击
  • 私钥隔离 — 私有 key 永不会出现在任何响应中、不会嵌入预签名 URL,也不会被记录
  • 路径遍历防护 — 在访问磁盘前,使用 Path.relative_to 验证所有对象键路径;使用遍历键的上传请求会返回 400
  • 防止枚举存储桶 — GET / 需要有效密钥;主密钥可以列出所有存储桶,存储桶密钥只能列出自己的存储桶;未授权访问或访问不存在的存储桶时始终返回 404——调用方无法区分“存储桶不存在”和“无权访问”
  • 防止 SQLite 前缀注入 — 在 LIKE 查询中使用用户提供的 prefix 值之前,会先对其进行转义
  • 原子写入 — 对象先写入临时文件,再重命名到目标位置;读取方不会看到不完整的写入内容
  • 流式上传 — 请求正文直接流式写入磁盘,不会将整个正文缓存在内存中
  • 孤立文件清理 — 后台扫描会删除磁盘上没有元数据记录的文件
  • 每个密钥的读写锁 — 过载或超时的请求会收到 503,而不会无限期排队
  • MCP 错误信息屏蔽 — 不会向 MCP 工具调用方暴露内部错误详情
  • MCP 下载上限 — 超过 50 MB 的对象无法通过 MCP 下载;请直接使用 HTTP API
  • 非 root 容器 — 以 UID 1000 运行
  • X-Content-Type-Options: nosniff — 每个响应均包含此标头

测试

# build prod image + run unit tests in the dev container + run integration suite
make test

# run a single integration test by name
./test.sh test_s3_presigned_private

# list all available test names
./test.sh --help

# unit tests only, inside the dev container
make test-unit

所有测试输出都会自动通过 tee 写入 test.log。

集成测试涵盖:HTTP API、S3/boto3 兼容性、MCP 工具、Bearer 认证、AWS Sig V4 认证、预签名 URL(HTTP 端点和 S3 SDK,GET 和 PUT)、主密钥、跨存储桶密钥隔离、TTL 过期、孤立文件清理、MIME 检测、大小限制、路径遍历尝试、二进制文件、并发读写、单元测试和 HTTP 层面的读写锁行为、请求 ID 以及安全标头。


开发

所有开发工具都在沙盒化的开发容器中运行,以保持宿主机整洁并限制供应链风险的影响范围。宿主机只需安装 docker、make、git 和 shell——无需 Python 解释器、pip、uv,也无需在全局安装项目依赖。

# one-time: build the dev container (also rebuilds when uv.lock changes)
make dev-image

# drop into a shell with the full dev env on PATH
make shell

# lint + format + unit tests — all run in the dev container
make lint
make format
make test-unit
依赖管理

项目使用 uv,并采用哈希锁定的 uv.lock(已提交)和供应链时效门限(pyproject.toml 中的 [tool.uv] exclude-newer),拒绝安装在固定日期之后发布的任何软件包版本。每次变更依赖项时,都会在同一提交中将该日期更新为3 天前,然后重新锁定依赖。

目标功能是否更新时效门限?
make pkg-lock根据当前门限刷新 uv.lock否
make pkg-add PKG=name[==ver]添加软件包是
make pkg-remove PKG=name移除软件包是
make pkg-update PKG=name升级一个软件包是
make pkg-upgrade升级所有软件包是

切勿直接运行 uv——所有受支持的变更都有对应的 Make 目标,可确保门限保持固定。

生产镜像

make build 会生成生产镜像。Dockerfile 采用多阶段构建,通过 @sha256 摘要固定 Python 基础镜像和 uv,使用 uv sync --frozen --no-dev 安装依赖(经锁文件验证,不含开发依赖),并且只将生成的 venv 和应用源代码复制到运行时阶段。本项目中没有任何地方调用 pip。


架构

app/
  main.py          FastAPI app, lifespan management, request ID middleware, MCP mount
  config.py        YAML config loader — TTL durations, sizes, key pairs, lock settings
  routes.py        HTTP and S3 endpoint handlers, presign endpoint
  lock.py          Per-key async read-write lock with acquire/hold timeouts and overload cap
  mcp_server.py    MCP tool definitions — Streamable HTTP at /mcp/
  auth.py          Bearer token and AWS Sig V4 authentication (header and presigned)
  sigv4.py         AWS Signature V4 signing and verification
  db.py            SQLite metadata store — WAL mode, objects table with TTL tracking
  storage.py       File I/O, path validation, atomic writes, orphan detection
  mimetype.py      Content type detection — libmagic content sniff with extension fallback
  s3xml.py         S3 XML response builders for list, error, and location responses
  logger.py        JSON structured logging with per-request ID propagation via contextvars
  scheduler.py     Background loop for TTL expiry and orphan file cleanup

限制

  • 不支持分段上传(S3 的分块上传协议)
  • 不支持对象版本控制
  • 除存储桶级别的公开/私有设置外,不支持 ACL
  • 不支持通过 API 创建或删除存储桶——存储桶在配置中定义
  • 不提供 CORS 标头——如有需要,请在反向代理层添加
  • 不支持复制
  • 不支持静态数据加密

License

WTFPL

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。