Drive 機能仕様書
ステータス: Draft — 仕様更新中 作成日: 2026-05-26 最終更新: 2026-05-26(アクセス制御追加)
1. 概要
タスク管理 SaaS に Misskey ライクなドライブ機能を追加する。 ドライブはテナント単位のファイル管理スペースであり、アップロードしたファイルをフォルダで整理し、タスクへの添付や共有 URL 発行に利用できる。
ストレージバックエンドは S3 互換(AWS S3 / MinIO) と ローカルディスク の 2 種類をサポートし、環境変数で切り替える。
2. スコープ
Phase 1(MVP)— 本仕様書の対象
Phase 2 以降(本仕様外)
- ファイルをタスクへ添付
- 画像サムネイル生成
- ファイル全文検索
- フォルダ共有の
editor権限(アップロード・削除) - クォータ超過テナントの管理者向け監査 UI
3. データモデル
3.1 drive_folders テーブル
// entities/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: DateTimeUtc,
}
プロジェクトフォルダの自動作成: プロジェクト作成時に同名フォルダを
drive_foldersに自動作成し、project_idを紐付ける。プロジェクト削除時は CASCADE で削除される。
3.2 drive_files テーブル
// entities/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: DateTimeUtc,
}
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 テーブルへの追加カラム
既存テーブルに以下のカラムを追加する。
// entities/tenants.rs に追加
pub drive_quota_bytes: Option<i64>, // NULL = システムデフォルト(DRIVE_DEFAULT_QUOTA_MB 参照)
4. アクセス制御
4.1 アクセスルール
4.2 アクセス判定ロジック
// アクセス可否チェックの擬似コード
fn can_access_file(file: &DriveFile, caller: &Caller) -> bool {
// 一般ファイルは誰でもアクセス可
if file.project_id.is_none() {
return true;
}
// 認証済みユーザーの場合
if let Caller::User(user) = caller {
// プロジェクトメンバー OR テナントオーナー
if is_project_member(user.id, file.project_id)
|| is_tenant_owner(user.id, file.tenant_id)
{
return true;
}
// フォルダ共有(ユーザー指定)でアクセス権あり
if let Some(folder_id) = file.folder_id {
if has_user_share(user.id, folder_id) {
return true;
}
}
}
// 公開リンクトークンの場合
if let Caller::ShareToken(token) = caller {
if let Some(folder_id) = file.folder_id {
if has_token_share(token, folder_id) {
return true;
}
}
}
false
}
has_user_share / has_token_share はファイルの folder_id からフォルダ階層を祖先方向へ辿り、いずれかのフォルダに有効な共有レコードがあれば true を返す。フォルダ深度は通常浅い(3〜5 段)ため、再帰クエリで許容範囲。
4.3 権限レベル
Phase 1 では
viewerのみ実装。editorは Phase 2 以降。
4.4 URL 戦略
全ファイルの URL は /v1/drive/files/{id}/content に統一する。
S3 バックエンドであっても S3 の直 URL はクライアントに渡さない。バックエンドが S3 からストリーム取得してレスポンスする。これにより S3 エンドポイント変更時も DB の URL が陳腐化しない(storage_key さえ正しければよい)。
4.5 プロジェクトフォルダのライフサイクル
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 = 無制限
match tenant.drive_quota_bytes {
Some(q) => Some(q), // テナント個別設定を優先
None => match config.default_quota_bytes {
0 => None, // デフォルト 0 = 無制限
q => Some(q), // デフォルト値を適用
},
}
}
テナントオーナーがクォータを設定する際のバリデーション:
DRIVE_SYSTEM_MAX_QUOTA_MB > 0の場合:quota_bytes ≤ system_maxでなければ400 Bad RequestDRIVE_SYSTEM_MAX_QUOTA_MB = 0(天井なし)の場合: 制限なく設定可能
5.2 使用量の計算
使用量はアップロード時に drive_files テーブルを集計して算出する(キャッシュなし、常に正確な値)。
SELECT COALESCE(SUM(size), 0) FROM drive_files WHERE tenant_id = $1
アップロード前に「現在の使用量 + 新ファイルのサイズ ≤ 有効クォータ」を検証し、超過する場合は 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 は変更しない。
起動時に全テナントをスキャンし、超過テナントを警告ログに出力する:
WARN drive_quota: tenant {tenant_id} ({display_id}) quota {quota_bytes} exceeds system_max {system_max_bytes}
将来の Phase 2 で管理者向け監査エンドポイント(例: GET /v1/admin/drive/quota-violations)を追加し、超過テナント一覧を UI で確認できるようにする。アップロード時のクォータ検証にはテナントの drive_quota_bytes(=現状値)を使うため、上限引き下げが既存テナントのアップロードをブロックすることはない。
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>;
async fn public_url(&self, key: &str) -> String;
}
ストリーミング設計の理由:
Bytesを受け取ると最大UPLOAD_MAX_SIZE_MB分をメモリに全展開してから S3/ローカルへ渡す。100MB ファイルを複数同時アップロードすると GByte 単位のメモリを消費しうる。BoxStreamを使うことで axum の multipart ストリームをそのままバックエンドへ流し、メモリ使用量をチャンク単位に抑える。S3 の場合はCreateMultipartUploadと組み合わせ、ローカルの場合はtokio::io::copyでファイルに書き込む。
6.2 S3 バックエンド
- クレート:
aws-sdk-s3 - S3 互換エンドポイントに対応(MinIO / Cloudflare R2 / Backblaze B2)
- バケットのパブリックアクセスポリシーを前提に公開 URL を生成
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_PUBLIC_BASE_URL=https://cdn.example.com # CDN 前段を置く場合(省略時はエンドポイント+バケット名)
S3_FORCE_PATH_STYLE=true # MinIO 等で必要。false がデフォルト
S3_FORCE_PATH_STYLE: AWS S3 は仮想ホスト形式(bucket.s3.amazonaws.com)が標準だが、MinIO などのセルフホスト互換ではhttp://endpoint/bucket/key形式(パス形式)が必要。trueに設定するとaws-sdk-s3のforce_path_styleオプションが有効になる。
アップロードフロー(S3):
- クライアント →
POST /v1/tenants/{id}/drive/files(multipart) - バックエンドが multipart ストリームを受信
aws-sdk-s3でPutObject(< 5MB)またはCreateMultipartUpload(≥ 5MB)をストリーミングで実行- 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): 認証不要 - プロジェクトファイル(
project_idあり): 以下いずれかが必要- セッション or PAT 認証(プロジェクトメンバーまたはテナントオーナー)
- 有効な
share_token(?token=クエリパラメータ) - いずれも満たさない場合 →
403 Forbidden
Content-Typeをmime_typeから設定Content-Disposition: inline(画像はブラウザで表示)- ストレージバックエンドの
get_stream()でストリーミング配信(メモリに全展開しない)
7. PAT スコープ
7.1 既存スコープとの関係
現在定義されているスコープ:
admin:tenant を持つ PAT はドライブ操作を含むすべての操作が可能(既存の ScopeList::has_scope が AdminTenant を最上位として扱う)。
7.2 Drive 用新スコープ
7.3 エンドポイント別必要スコープ一覧
7.4 実装上の注意
entities/scopes.rs の Scope enum に以下を追加する:
#[serde(rename = "read:drive")]
ReadDrive,
#[serde(rename = "write:drive")]
WriteDrive,
write:drive は read:drive を暗黙的に包含する(write を持つなら read も可能)。
has_scope の実装でこの包含関係を反映する:
pub fn has_scope(&self, scope: Scope) -> bool {
self.0.contains(&scope)
|| self.0.contains(&Scope::AdminTenant)
|| (scope == Scope::ReadDrive && self.0.contains(&Scope::WriteDrive))
}
8. API 設計
全エンドポイントはセッション認証 or PAT 認証が必須(既存の auth ミドルウェアを流用)。
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"
}
],
"total": 42
}
POST /v1/tenants/{tenant_id}/drive/files
リクエスト: multipart/form-data
レスポンス: 作成された DriveFile オブジェクト (201 Created)
制限:
- 最大ファイルサイズ: 環境変数
UPLOAD_MAX_SIZE_MBで設定(デフォルト 100MB) - 許可 MIME タイプ: 全種類
8.2 フォルダ API
フォルダ削除時の挙動:
- フォルダ内ファイルが存在する場合:
409 Conflictを返し削除しない(強制削除は Phase 2 以降)
8.3 フォルダ共有 API
POST /v1/tenants/{tenant_id}/drive/folders/{id}/shares
リクエスト(ユーザー指定共有):
リクエスト(公開リンク共有):
レスポンス(公開リンク共有の場合):
share_urlは返さない。フロントエンドがwindow.location.origin + "/drive/share/" + share_tokenで組み立てるshare_tokenは URL-safe な 32 文字ランダム文字列- フォルダ作成者またはテナントオーナーのみ共有操作可
- Phase 1 で
permission: "editor"を指定した場合 →422 Unprocessable Entity(editorは Phase 2 以降)
GET /v1/drive/share/{token}
- 認証不要
- フォルダメタデータ(名前、作成者名、ファイル数)を返す
- 有効期限切れの場合は
410 Gone
9. フロントエンド UI 設計
9.1 ページ構成
/tenants/{tenant_id}/drive # ドライブトップ(ルートフォルダ)
/tenants/{tenant_id}/drive/folders/{folder_id} # フォルダ内
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 に追加する環境変数:
# ストレージバックエンド(必須)
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_PUBLIC_BASE_URL= # CDN を使う場合(省略時はエンドポイント+バケット名)
S3_FORCE_PATH_STYLE=false # MinIO 等では true に設定
# ローカル用(STORAGE_BACKEND=local の場合)
LOCAL_UPLOAD_DIR=./uploads
12. 実装計画
ブランチ名
feat/drive
タスク分解
実装順序: バックエンド完全完了後にフロントエンドへ移行する。