mcpField
H
HybridS3

HybridS3

ローカル
github.com/psyb0tファイル・ストレージ51 週間前に更新

HybridS3 について

HybridS3は、サービスやAIエージェント向けの軽量なセルフホスト可能オブジェクトストレージです。S3互換、HTTP、MCPの各インターフェースを備え、設定ファイルでバケットを定義し、オブジェクトの自動期限切れにも対応します。

主な機能

  1. 01boto3互換のS3 API
  2. 02HTTPによるオブジェクト操作
  3. 03YAMLでのバケット設定
  4. 04オブジェクトのTTL自動期限切れ
  5. 05署名付きURLの生成

活用シーン

  1. 01サービスでのオブジェクト保存と取得
  2. 02AIエージェントへの構造化ストレージツールの提供
  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 準拠、充実したウェブコンソールなどは、クラウドを運用する場合には便利ですが、さまざまなサービスや AI エージェントが読み書きできるファイル置き場が欲しいだけなら、過剰な機能です。

AWS Sig V4 はリバースプロキシのパスプレフィックスの背後では機能しません。 ほとんどの実装は、元の上流パス全体を使って署名を検証します。nginx の /storage/ 配下に配置すると、nginx がプレフィックスを取り除くため、サーバーが受け取るパスは /storage/bucket/key ではなく /bucket/key となり、署名の検証に失敗します。HybridS3 には path_prefix 設定オプションがあります。/storage に設定すると、すべてのルートがそのプレフィックス配下に移動します。パスの削除も、特別なプロキシヘッダーも不要です。boto3 が署名したパスと、サーバーが受け取るパスが一致します。

3 つのインターフェースを 1 つのサービスで提供します。 boto3 はそのまま使えます。プレーン HTTP も curl で利用できます。AI エージェントは 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

キーとアクセス

キーペアモデル

各バケットには、設定で定義された2つのキーがあります。

設定フィールド役割秘密にする必要があるか
key秘密鍵です。Bearer リクエストの認証と、S3 署名の生成に使用します。送信されることはなく、HMAC の計算または検証にローカルでのみ使用されます。はい
public_key公開識別子です。S3 認証の aws_access_key_id として使用され、署名付き URL の Credential= フィールドに表示されます。これ単体では何の権限も付与しません。いいえ — 共有しても安全です

この分離によって、署名付き URL を安全に利用できます。サーバーが検証対象のキーを特定できるよう、署名付き URL には Credential= フィールドに識別子を埋め込む必要があります。その識別子が public_key です。秘密ではないため、URL に含まれていても問題ありません。秘密鍵はサーバー上で URL に署名するために使用され、URL に現れることはありません。

マスターキー

master_key は、個々のバケットキーがなくても、すべてのバケットに対してあらゆる操作を行える、バケットをまたいで使用できる認証情報です。次の2つの場合に必要です。

  • 1つのクライアントから複数のバケットをまたいで操作する必要がある場合
  • 設定されているすべてのバケットを一覧表示する必要がある場合 — バケットキーで list_buckets を実行すると、自身のバケットのみが表示されます

master_public_key は、S3 認証で master_key と組み合わせて使用する、秘密ではない識別子です(aws_access_key_id として使用されます)。

マスターキーをクライアント向けのコードに埋め込まないでください。その場合はバケットごとのキーを使用してください。アクセスを1つのバケットのみに制限できます。

バケットの公開範囲
設定GET / HEAD / LISTPUTDELETE / 署名付き URL
public: true認証不要バケットキー、マスターキー、または有効な署名付き PUTバケットキーまたはマスターキー
public: falseバケットキー、マスターキー、または有効な署名付き GETバケットキー、マスターキー、または有効な署名付き PUTバケットキーまたはマスターキー

HTTP API

認証

HTTP リクエストは、Authorization ヘッダーの Bearer トークンを使用して認証します。バケットの秘密鍵 key、またはバケットをまたいだ操作にはマスターキーを指定します。

Authorization: Bearer <private_key>
  • 公開バケットの読み取り(GET、HEAD、LIST)— 認証は不要です。
  • すべての書き込み(PUT、DELETE)および非公開バケットの読み取り — バケットキーまたはマスターキーが必要です。
  • バケットの一覧表示(GET /)— マスターキーではすべてのバケットを一覧表示でき、バケットキーでは自身のバケットのみを一覧表示できます。
  • 署名付き URL のエンドポイント(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 エージェントは MCP 互換の任意のクライアントから接続し、型付きの入力と出力を備えた構造化されたツール定義を受け取ります。

接続
{
  "mcpServers": {
    "hybrids3": {
      "type": "streamable-http",
      "url": "http://localhost:8080/mcp/"
    }
  }
}
認証
エンドポイントレベルの認証

/mcp/ エンドポイントでは、ツールを呼び出す前に接続を認証するためのトークンを任意で指定できます。すべてのアクセス権を持つマスターキーを使用するか、そのバケットの範囲に接続を制限するバケットキーを使用してください。トークンはマスターキーおよびすべてのバケットキーと照合されます。

次の2つの方法を使用できます。

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)を返し、プレーンテキストのフォールバックも提供します。


エージェントとの連携

スキルは、peen など、.agents/skills/ を読み込むあらゆるエージェントで動作し、以下のクライアントにはネイティブにインストールできます。

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

Claude Code は HybridS3 の URL と、エンドポイントで接続レベルの認証が必要な場合はバケットキーまたはマスターキーを尋ねます。キーは OS のキーチェーンに保存されます。

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

マーケットプレイスからインストールすると、スキルは $hybrids3:hybrids3 として呼び出されます。また、.agents/skills/ を含む任意のリポジトリでは、Codex がスキルを自動的に検出するため、インストールは不要です。その場合、スキルは $hybrids3 として呼び出されます。

OpenClaw

スキルはリリースのたびに ClawHub に公開されます:

openclaw skills install @psyb0t/hybrids3

ローカルの stdio で通信する MCP クライアントには、@psyb0t/hybrids3 プラグインを使って、サービスの /mcp/ エンドポイントに接続します:

openclaw plugins install clawhub:@psyb0t/hybrids3

次に、HYBRIDS3_URL を設定します(エンドポイントで接続レベルの認証が必要な場合は HYBRIDS3_KEY も設定します)。


Presigned URL

署名付き URL を使うと、リンクを知っている人なら誰でも、Authorization ヘッダーを送信せずに、限られた時間内に特定のオブジェクトを読み取ったり書き込んだりできます。/presign/{bucket}/{key} エンドポイントでは、method クエリパラメーターを使って次の 2 つの方法を指定できます:

  • method=GET(デフォルト)— URL の受信者はオブジェクトをダウンロードできます。
  • method=PUT — URL の受信者はオブジェクトをアップロード(上書き)できます。

署名付き URL は HTTP メソッドに紐付けられています。GET URL を PUT に使用することはできず、その逆も同様です。署名には正規リクエストのメソッドが含まれます。

有効期限の範囲:1 秒~604800 秒(7 日間)。デフォルト:3600。期限切れまたは改ざんされた URL は 403 を返します。

Presigned 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}

パブリックバケット — GET に認証が不要なため、署名も有効期限もない通常の URL を返します。

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}
Presigned 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 で実行できる操作は 1 つだけ(GET または PUT)で、対象となるキーも 1 つだけです。

/presign/ HTTP エンドポイントと presign_url MCP ツールは、どちらも同じロジックに従います。アップロード用 URL を取得するには、MCP ツールに method="PUT" を渡します。


MIME Type Detection

アップロードのたびにコンテンツタイプが自動検出されます。Content-Type ヘッダーは不要です。検出には libmagic を使い、ファイルコンテンツの先頭 8 KB を調べます。libmagic が汎用タイプを返した場合は、ファイル名の拡張子を代わりに使用します。検出されたタイプはメタデータに保存され、GET および HEAD レスポンスの Content-Type で返されます。

自動検出を上書きするには、アップロードリクエストで Content-Type を明示的に設定します。


TTL and Expiry

バケットに ttl を設定すると、最後に書き込まれてからその期間が経過したオブジェクトは自動的に期限切れになります。オブジェクトを上書きすると、有効期限の計時がリセットされます。ttl: 0 を設定すると、オブジェクトは期限切れになりません。

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

バックグラウンドのクリーンアップループが cleanup_interval 秒ごとに実行され、次の 2 つの処理を行います:

  1. TTL による期限切れ処理 — expires_at を過ぎたすべてのオブジェクトを見つけ、ディスクとメタデータから削除します。
  2. 孤立ファイルのクリーンアップ — 各バケットのディレクトリをディスク上でスキャンし、対応するメタデータレコードがないファイルを削除します。これにより、中断されたアップロードで残されたファイルを処理します。

削除のたびに、空になった親ディレクトリはバケットのルートまで削除されます。


ロック

各オブジェクトキーには、それぞれ独自の非同期読み書きロックが適用されます。複数の読み取り処理は同時にロックを保持できます。書き込み処理は排他的にアクセスし、そのキーに対するすべての同時読み取り処理と書き込み処理をブロックします。ロックは操作の開始前に取得され、レスポンスの送信が完了した後に解放されます。

これにより、同時アクセス時の読み取りの不整合、書き込み途中のデータの可視化、データ破損を防ぎます。

次の3つの条件では 503 Service Unavailable が返されます。

条件発生条件
過負荷同じキーに対して、キューに入ったリクエストが lock_max_waiters を超えた場合
取得タイムアウトリクエストがロックを取得できないまま lock_acquire_timeout 秒待機した場合
保持タイムアウトリクエストがロックを lock_hold_timeout 秒を超えて保持した場合

保持タイムアウトは、遅いアップロードや停止したアップロードが書き込みロックを無期限に保持するのを防ぎます。タイムアウトが発生すると、ロックが解放され、リクエストには503が返されます。

デフォルト値(取得30秒、保持300秒、最大待機数100)は、大容量ファイルのアップロードに適しています。小さなオブジェクトを高スループットで処理する場合は、負荷をより早く切り離せるよう、これらの値を小さくしてください。


ロギング

構造化されたJSONログが標準出力に1行ずつ出力されます。

{
  "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はロケーションのプレフィックスを置き換えるため、/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 プレフィックスインジェクションの防止 — ユーザー指定の prefix 値は、LIKE クエリで使用する前にエスケープします
  • アトミック書き込み — オブジェクトは一時ファイルに書き込んでから配置先へリネームするため、読み取り側が不完全な書き込みを参照することはありません
  • ストリーミングアップロード — リクエストボディは全体をメモリにバッファリングせず、ディスクへ直接ストリーミングします
  • 孤立ファイルのクリーンアップ — メタデータレコードのないディスク上のファイルは、バックグラウンドの一括処理で削除されます
  • キーごとの RW ロック — 過負荷またはタイムアウトしたリクエストには、無期限にキューイングする代わりに 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

すべてのテスト出力は自動的に test.log にも出力されます。

統合テストでは、HTTP API、S3/boto3 互換性、MCP ツール、Bearer 認証、AWS Sig V4 認証、署名付き URL(HTTP エンドポイントと S3 SDK、GET と PUT)、マスターキー、バケット間のキー分離、TTL の有効期限、孤立ファイルのクリーンアップ、MIME 検出、サイズ制限、パストラバーサルの試行、バイナリファイル、同時読み書き、ユニットテストと HTTP レベルでの RW ロックの動作、リクエスト ID、セキュリティヘッダーを網羅しています。


開発

すべての開発ツールはサンドボックス化された開発コンテナ内で動作するため、ホスト環境をクリーンに保ち、サプライチェーン侵害の影響範囲を抑えられます。ホストに必要なのは docker、make、git、シェルだけです。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.lock(コミット済み)と、サプライチェーンの公開日制限(pyproject.toml の [tool.uv] exclude-newer)を使用して uv を管理しています。この制限により、指定した固定日より後に公開されたパッケージバージョンはインストールできません。依存関係を変更するたびに、同じコミット内でこの日付を3日前に更新し、その後ロックファイルを再生成します。

ターゲット動作公開日制限を更新するか
make pkg-lock現在の制限のもとで uv.lock を更新しますいいえ
make pkg-add PKG=name[==ver]パッケージを追加しますはい
make pkg-remove PKG=nameパッケージを削除しますはい
make pkg-update PKG=name1 つのパッケージをアップグレードしますはい
make pkg-upgradeすべてのパッケージをアップグレードしますはい

uv を直接実行しないでください。制限を維持できるよう、サポートされている依存関係の変更にはすべて Make ターゲットが用意されています。

本番用イメージ

make build で本番用イメージを生成します。Dockerfile はマルチステージ構成で、Python ベースイメージと uv の両方を @sha256 ダイジェストで固定します。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

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。