Drive 機能仕様書
ステータス: バックエンド実装済み/フロントエンド未実装(既知の差分あり) 作成日: 2026-05-26 最終更新: 2026-09-20(公開共有・フォルダ境界の修正、PAT スコープ、残る差分を反映)
1. 概要
タスク管理 SaaS に Misskey ライクなドライブ機能を追加する。 ドライブはテナント単位のファイル管理スペースであり、アップロードしたファイルをフォルダで整理し、タスクへの添付や共有 URL 発行に利用できる。
ストレージバックエンドは S3 互換(AWS S3 / MinIO) と ローカルディスク の 2 種類をサポートし、環境変数で切り替える。
2. スコープ
現在の実装範囲
実装済みとした項目にも、仕様を完全には満たしていない箇所がある。詳細は
今後の拡張
- 画像サムネイル生成
- ファイル全文検索
- フォルダ共有の
editor権限(アップロード・削除) - クォータ超過テナントの管理者向け監査 UI
3. データモデル
3.1 drive_folders テーブル
// apps/backend/crates/entity/src/_generated/drive_folders.rs(主なフィールド)
pub struct Model {
pub id: Uuid,
pub name: String,
pub parent_id: Option<Uuid>, // 自己参照(ルートフォルダは None)
pub tenant_id: Uuid,
pub project_id: Option<Uuid>, // プロジェクト紐付き(設定時はプロジェクトフォルダ)
pub created_by: Uuid, // FK → users
pub created_at: DateTimeWithTimeZone,
}
プロジェクトフォルダの自動作成: プロジェクト作成時に同名フォルダを
drive_foldersに自動作成し、project_idを紐付ける。プロジェクト削除時は CASCADE で削除される。
3.2 drive_files テーブル
// apps/backend/crates/entity/src/_generated/drive_files.rs(主なフィールド)
pub struct Model {
pub id: Uuid,
pub name: String, // 表示名(元ファイル名)
pub size: i64, // バイト数
pub mime_type: String, // application/octet-stream 等
pub storage_type: StorageType, // enum: s3 | local
pub storage_key: String, // S3 key または ローカル相対パス
// url カラムなし — API レスポンス時に /v1/drive/files/{id}/content を生成
pub tenant_id: Uuid,
pub project_id: Option<Uuid>, // 非正規化。フォルダの project_id を引き継ぐ
pub uploader_id: Uuid, // FK → users
pub folder_id: Option<Uuid>, // FK → drive_folders
pub created_at: DateTimeWithTimeZone,
pub updated_at: DateTimeWithTimeZone,
}
CHECK 制約:
CHECK (project_id IS NULL OR folder_id IS NOT NULL)
project_id がセットされているときは必ず folder_id も非 NULL でなければならない。DB レベルと API ハンドラ(バリデーション層)の両方で強制する。
urlカラムなし: 全ファイルのアクセス URL は/v1/drive/files/{id}/contentに統一。DB にはstorage_keyのみ保持し、API レスポンス生成時に URL を組み立てる。S3 エンドポイント変更やローカルサーバー移転の影響を受けない。
project_idの非正規化: アクセス制御を毎回フォルダ階層を辿らず O(1) で判定するため、ファイルにもフォルダのproject_idを保持する。ファイルのフォルダ移動時はproject_idを再設定する。
3.3 drive_folder_shares テーブル
フォルダの共有設定を管理する。ユーザー指定共有と公開リンク共有の 2 種類をサポートする。
制約:
shared_with_user_idとshare_tokenはどちらか一方のみ設定される(CHECK 制約)。両方 NULL または両方 NOT NULL は不正。
共有の適用範囲: フォルダを共有すると、その配下のサブフォルダ・ファイルすべてにアクセス権が及ぶ(再帰的に継承)。
3.4 tenants テーブルへの追加カラム
既存テーブルに以下のカラムを追加する。
// apps/backend/crates/entity/src/_generated/tenants.rs
pub drive_quota_bytes: Option<i64>, // NULL = システムデフォルト(DRIVE_DEFAULT_QUOTA_MB 参照)
4. アクセス制御
4.1 ファイル本文のアクセスルール
①は「ファイルの tenant_id に所属していること」を先に確認したうえで、プロジェクトの公開規則で判定する。
ファイル ID だけで引ける配信経路があるため、プロジェクト所属だけを見るとテナント境界を越えられる。
判定の詳細は テナント / プロジェクト認可 を参照。
テナントに所属しないプロジェクト限定の客分(Guest)は、プロジェクトメンバーであるだけでは
Drive へアクセスできない。
③のフォルダ共有は所属とは独立した明示的な付与なので、テナントから外しても失効しない。 ただしテナント一般ファイルの本文取得ではユーザー指定共有を見ず、テナント所属か共有トークンだけで判定する。
4.2 アクセス判定ロジック
// アクセス可否チェックの擬似コード
fn can_access_file(file: &DriveFile, caller: &Caller) -> bool {
// 共有トークンはテナント所属と独立して判定する
if let Caller::ShareToken(token) = caller {
return file.folder_id
.is_some_and(|folder_id| has_token_share(token, folder_id));
}
// 認証済みユーザーの場合
if let Caller::User(user) = caller {
if !has_scope(user, "read:drive") {
return false;
}
// テナント一般ファイルはテナント所属者またはオーナーに許可(ユーザー指定共有は見ない)
if file.project_id.is_none() {
return can_access_tenant(user.id, file.tenant_id);
}
// テナントオーナー OR(テナントに所属 AND プロジェクトの公開規則を満たす)
if is_tenant_owner(user.id, file.tenant_id)
|| can_access_project(user.id, file.tenant_id, file.project_id)
{
return true;
}
// フォルダ共有(ユーザー指定)でアクセス権あり
if let Some(folder_id) = file.folder_id {
if has_user_share(user.id, folder_id) {
return true;
}
}
}
false
}
has_user_share / has_token_share はファイルの folder_id からフォルダ階層を祖先方向へ辿り、いずれかのフォルダに有効な共有レコードがあれば true を返す。現行実装は各階層を順に問い合わせる。
空でない token を提示した場合はトークン判定を優先し、無効・期限切れなら認証済みでも 403 を返す。
4.3 権限レベル
現在は
viewerのみ実装。editorは将来対応。
4.4 URL 戦略
全ファイルの URL は /v1/drive/files/{id}/content に統一する。
S3 バックエンドであっても S3 の直 URL はクライアントに渡さない。バックエンドが S3 からストリーム取得してレスポンスする。これにより S3 エンドポイント変更時も DB の URL が陳腐化しない(storage_key さえ正しければよい)。
4.5 プロジェクトフォルダのライフサイクル
作成・移動とフォルダ削除はテナント単位の Drive ロックで直列化し、ロック取得後に階層と
プロジェクト権限を確認する。ファイル移動でも移動元・移動先の権限が必要。
本文更新とファイル削除は対象ファイルの行ロック取得後に再認可する。
既存データの project_id 不整合は backfill マイグレーションで補正する(§14)。
5. クォータ管理
5.1 クォータの 3 層構造
DRIVE_SYSTEM_MAX_QUOTA_MB ← システム上限(ハードキャップ。設定時はこれを超えて設定不可)
↓ 上限として機能
tenants.drive_quota_bytes ← テナント個別設定(テナントオーナーが変更可)
↓ NULL 時のフォールバック
DRIVE_DEFAULT_QUOTA_MB ← システムデフォルト(テナント未設定時に適用)
有効クォータの決定ロジック:
fn effective_quota(tenant: &Tenant, config: &DriveConfig) -> Option<i64> {
// None = 無制限。システム上限があれば常に最後に適用する。
let requested = tenant.drive_quota_bytes
.unwrap_or(config.default_quota_bytes);
let requested = (requested != 0).then_some(requested);
let system_max = (config.system_max_quota_bytes != 0)
.then_some(config.system_max_quota_bytes);
match (requested, system_max) {
(Some(q), Some(max)) => Some(q.min(max)),
(None, Some(max)) => Some(max),
(Some(q), None) => Some(q),
(None, None) => None,
}
}
テナントオーナーがクォータを設定する際のバリデーション:
DRIVE_SYSTEM_MAX_QUOTA_MB > 0の場合:quota_bytes ≤ system_maxでなければ400 Bad Request- 負の
quota_bytesは400 Bad Request。0は無制限の指定だが、システム上限があれば適用される DRIVE_SYSTEM_MAX_QUOTA_MB = 0(天井なし)の場合: 非負の値を上限なく設定可能
5.2 使用量の計算
使用量はアップロード時に drive_files テーブルを集計して算出する(キャッシュなし、常に正確な値)。
SELECT COALESCE(SUM(size)::BIGINT, 0) FROM drive_files WHERE tenant_id = $1
アップロード開始前に既存の使用量が上限に達していないかを確認する。保存後、DB 登録前にテナント行をロックし、実測サイズで「現在の使用量 + 新ファイルのサイズ ≤ 有効クォータ」を再検証する。超過時は保存済みオブジェクトの削除を試み、413 Content Too Large を返す。有効クォータが None(無制限)の場合は容量の検証をスキップする。
5.3 クォータ取得 API
GET /v1/tenants/{tenant_id}/drive/usage
レスポンス:
{
"used_bytes": 524288000,
"quota_bytes": 10737418240,
"system_max_bytes": 53687091200,
"unlimited": false
}
5.4 クォータ設定 API
テナントオーナーがドライブ容量を変更できる。
PATCH /v1/tenants/{tenant_id}/drive/quota
リクエスト:
{ "quota_bytes": 10737418240 }
nullを渡すとシステムデフォルトにリセット- テナントオーナー権限が必要(既存の
ensure_tenant_ownerを流用) DRIVE_SYSTEM_MAX_QUOTA_MB > 0の場合、quota_bytes > system_maxなら400 Bad Request
5.5 システム上限引き下げ時の挙動
DRIVE_SYSTEM_MAX_QUOTA_MB を引き下げた場合、既存テナントの drive_quota_bytes 自体は変更しない。
起動時に個別設定値がシステム上限を超えるテナントを抽出し、tenant_id・quota_bytes・system_max_bytes とともに警告を出力する:
WARN tenant drive quota exceeds system_max — update tenant quota or raise DRIVE_SYSTEM_MAX_QUOTA_MB
将来の拡張で管理者向け監査エンドポイント(例: GET /v1/admin/drive/quota-violations)を追加し、超過テナント一覧を UI で確認できるようにする。実行時の有効クォータにはシステム上限が常に適用される。使用量が上限以上なら新規アップロードを拒否し、本文更新は差し替え後の合計が上限を超える場合に 413 Content Too Large を返す。本文を縮めても合計が上限を超えたままなら拒否する。
6. ストレージバックエンド
6.1 抽象インターフェース(Rust trait)
#[async_trait]
pub trait StorageBackend: Send + Sync {
/// ストリームを受け取る。全量バッファの有無はバックエンド実装に依存する。
async fn upload(
&self,
key: &str,
stream: BoxStream<'static, Result<Bytes, StorageError>>,
content_length: u64,
mime: &str,
) -> Result<(), StorageError>;
async fn delete(&self, key: &str) -> Result<(), StorageError>;
/// ストリーミングダウンロード(プロキシ配信用)。
async fn get_stream(
&self,
key: &str,
) -> Result<BoxStream<'static, Result<Bytes, StorageError>>, StorageError>;
}
ストリーミング設計の理由: 100MB ファイルを複数同時に全量バッファすると GByte 単位のメモリを消費しうる。
BoxStreamでチャンクを受け取り、ローカル実装はBufWriterへ順に書き込む。S3 実装は既知の長さが 5MiB 以上なら multipart upload を使うが、通常のアップロード API は長さ不明として0を渡すため全量バッファになる(§12)。
6.2 S3 バックエンド
- クレート:
object_store(AmazonS3Builder) - S3 互換エンドポイントに対応(MinIO / Cloudflare R2 / Backblaze B2)
- バケットは非公開でよい。ファイルはバックエンドからプロキシ配信する
STORAGE_BACKEND=s3
S3_ENDPOINT=https://s3.amazonaws.com # MinIO: http://localhost:9000
S3_BUCKET=my-task-drive
S3_REGION=ap-northeast-1
S3_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
S3_SECRET_ACCESS_KEY=wJalrXUtnFEMI...
S3_FORCE_PATH_STYLE=true # MinIO 等で必要。false がデフォルト
S3_FORCE_PATH_STYLE: AWS S3 は仮想ホスト形式(bucket.s3.amazonaws.com)が標準だが、MinIO などのセルフホスト互換ではhttp://endpoint/bucket/key形式(パス形式)が必要。trueに設定するとAmazonS3Builderの仮想ホスト形式を無効にする。
アップロードフロー(S3):
- クライアント →
POST /v1/tenants/{tenant_id}/drive/files(multipart) - バックエンドが multipart ストリームを受信
object_storeで単一 PUT または multipart upload を実行- DB に
drive_filesレコードを登録(storage_keyのみ保持、urlカラムなし) - レスポンスに
/v1/drive/files/{id}/contentをurlとして組み立てて返却
6.3 ローカルバックエンド
- 環境変数
LOCAL_UPLOAD_DIRで保存先ディレクトリを指定 - バックエンドが
GET /v1/drive/files/{id}/contentでファイルを配信 - 開発環境・セルフホスト向け
STORAGE_BACKEND=local
LOCAL_UPLOAD_DIR=/var/task/uploads
ファイル配信エンドポイント(全バックエンド共通):
GET /v1/drive/files/{id}/content
GET /v1/drive/files/{id}/content?token={share_token}
- テナント一般ファイル(
project_id = NULL): テナント所属者・オーナー、または有効なshare_tokenが必要 - プロジェクトファイル(
project_idあり): 以下いずれかが必要- セッション or PAT 認証(そのプロジェクトに入れる人またはテナントオーナー)
- セッション or PAT 認証と有効なユーザー指定フォルダ共有(テナント外の受信者も可)
- 有効な
share_token(?token=クエリパラメータ) - いずれも満たさない場合 →
403 Forbidden
Content-Typeをmime_typeから設定Content-Disposition: attachmentを常に設定し、ブラウザ上で同一オリジンのコンテンツとして実行させない- API 共通ミドルウェアが
X-Content-Type-Options: nosniffを全レスポンスに設定し、宣言したContent-Type以外への MIME sniffing を禁止する - ストレージバックエンドの
get_stream()でストリーミング配信(メモリに全展開しない)
7. PAT スコープ
7.1 既存スコープとの関係
現在定義されているスコープ:
admin:project は read:drive / write:drive を包含するが、admin:tenant を要求するクォータ設定は含まない。
admin:tenant は全スコープを包含する。いずれもスコープ要件を満たすだけであり、テナント所属・プロジェクト権限・オーナー限定の判定は別に行う。
7.2 Drive 用スコープ
7.3 エンドポイント別必要スコープ一覧
7.4 実装上の注意
apps/backend/crates/entity/src/scopes.rs の Scope enum に定義済み:
#[serde(rename = "read:drive")]
ReadDrive,
#[serde(rename = "write:drive")]
WriteDrive,
write:drive は read:drive を暗黙的に包含する(write を持つなら read も可能)。
包含関係は Scope::implies の網羅 match が一箇所で持ち、has_scope はそれを引くだけである
(規則の一覧は apps/backend/docs/personal-access-tokens-authz.md の「含意の規則」):
pub fn implies(self, other: Scope) -> bool {
if self == other {
return true;
}
match self {
Scope::AdminTenant => true,
Scope::AdminProject => other.layer() == ScopeLayer::Project,
Scope::WriteDrive => other == Scope::ReadDrive,
// …他のスコープも同様に、含意する先を明示する
}
}
8. API 設計
テナント配下の管理 API はセッション認証または PAT 認証が必須。GET /v1/drive/files/{id}/content
は認証任意だが、実際の取得にはテナント/プロジェクト権限または共有トークンが必要。
GET /v1/drive/share/{token} とその /files は共有トークン自体を認証情報として扱う。
8.1 ファイル API
GET /v1/tenants/{tenant_id}/drive/files
クエリパラメータ:
レスポンス:
{
"files": [
{
"id": "...",
"name": "screenshot.png",
"size": 204800,
"mime_type": "image/png",
"url": "/v1/drive/files/xxxxxxxx-.../content",
"folder_id": null,
"created_at": "2026-05-26T12:00:00Z",
"updated_at": "2026-05-26T12:00:00Z"
}
],
"total": 42
}
POST /v1/tenants/{tenant_id}/drive/files
リクエスト: multipart/form-data
現在のストリーミング実装では、name と folder_id は file パートより前に送る必要がある。
file を処理した時点でレスポンスを返すため、それ以降のパートは解釈されない。この順序制約は
OpenAPI だけでは表現できないため、クライアント実装でも明示的に順序を固定する。
レスポンス: 作成された DriveFile オブジェクト (201 Created)
制限:
- 最大ファイルサイズ: 環境変数
UPLOAD_MAX_SIZE_MBで設定(デフォルト 100MB) - 許可 MIME タイプ: 全種類
- 空ファイルまたは
fileパートなし:400 Bad Request
PATCH /v1/tenants/{tenant_id}/drive/files/{id}
name で名前を変更し、folder_id に UUID を渡すとそのフォルダへ移動する。
folder_id の省略は配置を維持し、明示的な null はドライブ直下へ移動する。
移動元・移動先の権限が必要で、移動に合わせて project_id も更新する。
PUT /v1/tenants/{tenant_id}/drive/files/{id}/content
{ "content": "更新後の本文" } を受け取り、更新後の DriveFile を返す(200 OK)。
text/*、+json / +xml、JSON・JavaScript・YAML 等の許可されたテキスト系 MIME が対象で、
対象外は 400 Bad Request。空文字列は許可する。UTF-8 のバイト数がファイルサイズ上限を
超える場合、または差し替え後のテナント使用量がクォータを超える場合は 413 を返す。
新しいストレージキーへ保存してから DB を更新し、成功後に旧キーの削除を試みる。
8.2 フォルダ API
作成時の parent_id で親フォルダを指定し、親の project_id を継承する。
更新時の parent_id は省略で変更なし、明示的な null でドライブ直下への移動を表す。
移動元・移動先の権限確認、配下の同期とルート保護は §4.5 に従う。
フォルダ削除時の挙動:
- 直下にファイルまたは子フォルダが存在する場合:
409 Conflictを返し削除しない(強制削除は将来対応) - プロジェクトルートは空でも
409 Conflict。権限のないプロジェクト配下の操作は先に403で拒否する
8.3 フォルダ共有 API
POST /v1/tenants/{tenant_id}/drive/folders/{folder_id}/shares
リクエスト(ユーザー指定共有):
リクエスト(公開リンク共有):
レスポンス(公開リンク共有の場合):
share_urlは返さない。フロントエンドがwindow.location.origin + "/drive/share/" + share_tokenで組み立てるshare_tokenは URL-safe な 32 文字ランダム文字列- フォルダ作成者またはテナントオーナーのみ共有操作可
- 現在
permission: "editor"を指定した場合 →422 Unprocessable Entity(editorは将来対応)
GET /v1/drive/share/{token}
- 認証不要
- フォルダメタデータ(名前、作成者名、直下のファイル数)を返す
- 不明なトークンは
404 Not Found - 有効期限切れの場合は
410 Gone
GET /v1/drive/share/{token}/files
同じトークン検証を行い、共有フォルダ直下の DriveFile の配列を返す。子フォルダは列挙しない。
返却される url にトークンは付かないため、共有リンクから本文を取得するクライアントは
?token={share_token} を付ける。
8.4 タスク添付 API
Drive ファイルは既存タスクへ添付できる。添付は中間レコードの作成・削除であり、解除しても Drive ファイル本体は削除しない。
添付できるのは同じテナントの一般ファイル、または対象タスクと同じプロジェクトのファイル。
別テナント・別プロジェクトのファイルは 403 Forbidden とする。同じファイルの二重添付は
409 Conflict とする。添付解除は添付を作成したユーザーまたはテナントオーナーに限る。
9. フロントエンド UI 設計
9.1 ページ構成
/{tenant}/drive # ドライブトップ(ルートフォルダ)
/{tenant}/drive/{folder_id} # フォルダ内
現在はページ・コンポーネントとも未実装。上記は
docs/frontend/url-spec.mdに合わせた予定 URL。
9.2 レイアウト
┌─────────────────────────────────────────────────────┐
│ Breadcrumb: ドライブ > フォルダA > サブフォルダB │
├────────────────┬────────────────────────────────────┤
│ │ ┌──────────────────────────────┐ │
│ [+ 新しい │ │ 🔍 ファイル検索 │ │
│ フォルダ] │ └──────────────────────────────┘ │
│ │ │
│ ▼ ドライブ │ [▲ アップロード] [リスト/グリッド] │
│ フォルダA │ │
│ フォルダB │ 📁 フォルダA 📁 フォルダB │
│ │ 📄 report.pdf 🖼 image.png │
└────────────────┴────────────────────────────────────┘
9.3 コンポーネント構成
9.4 主要インタラクション
- アップロード: ボタンクリック or エリアへドラッグ&ドロップ → プログレスバー表示
- フォルダ作成: サイドバーの「+ 新しいフォルダ」ボタン → インライン入力
- ファイル詳細: ファイルカードをクリック → 右サイドシートで詳細表示・URL コピー
- 削除: 右クリックメニュー or 詳細パネルの削除ボタン → 確認ダイアログ
10. セキュリティ
11. 設定まとめ
apps/backend/.env に追加する環境変数:
# ストレージバックエンド(未指定時は local)
STORAGE_BACKEND=local # "local" または "s3"
# アップロード・クォータ設定(共通)
UPLOAD_MAX_SIZE_MB=100 # 1ファイルあたりの上限 MB(デフォルト 100)
DRIVE_SYSTEM_MAX_QUOTA_MB=51200 # テナントが設定できる容量の上限 MB(デフォルト 50GB)。0 = 天井なし
DRIVE_DEFAULT_QUOTA_MB=10240 # テナントデフォルト容量 MB(デフォルト 10GB)。0 = 無制限
# S3 用(STORAGE_BACKEND=s3 の場合)
S3_ENDPOINT=https://s3.amazonaws.com
S3_BUCKET=
S3_REGION=ap-northeast-1
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_FORCE_PATH_STYLE=false # MinIO 等では true に設定
# ローカル用(STORAGE_BACKEND=local の場合)
LOCAL_UPLOAD_DIR=./uploads
12. 現在の実装との差分・既知の問題
2026-09-20 時点で、次の差分を確認している。ここに記載した項目は期待仕様ではなく、 修正対象として追跡するための現状説明である。
公開共有ルートの二重 prefix と、フォルダの project_id 継承・移動先認可・プロジェクトルート保護は修正済み。
既存データの backfill、階層変更の直列化、ファイル更新・削除時のロック後の再認可も実装済み。
関連する回帰テストは以下にある(パスは apps/backend/ からの相対)。
13. 今後の実装順序
バックエンドの基本 API と階層変更の境界保護は揃っているため、残るストレージ・共有の差分を直し、 その契約をテストで固定してからフロントエンドへ進む。