アップロード〜公開パイプライン
全体の流れ
- クライアントが
POST /api/worksでdraft状態のworks行を作成し、work_idを受け取る。 - クライアントが
Upload-Metadata: work_id=<uuid>を付けて/filesへTUSアップロードを開始する。 - tusdの
PreUploadCreateCallback(tus-handler)がFirebaseで認証し、当該workがdraftで本人所有かつ未アップロードであることを検証したうえで、uploads行を作成しworks.upload_id↔uploads.work_idを相互リンクする。すべて単一トランザクション内で行う。 - クライアントはアップロード中でも
PATCH /api/works/:idでtitle/description/tagsを更新できる(status published/deletedでは拒否)。 - アップロード完了時、tusdが
CompleteUploadsを発火。backend/cmd/app/main.goのgoroutineがuploads.Handler.CompleteTUSUpload(backend/internal/uploads/service.goのcompleteTUSUpload)を呼び、uploadをtranscodingにしつつjobs.TranscodeAudioArgsのRiverジョブを同一トランザクション内で積む。コミット後にRiverキューへ通知することで、DBとキューの不整合を避けている。 backend/internal/transcode/worker.goのWorkerがジョブを取得し、DBトランザクションの外側で(ffmpeg実行やS3 IOは時間がかかるため)Garageから元ファイルをダウンロードし、ffmpegでOpus 128kbpsにエンコードし、ffprobeで再生時間を取得し、変換後ファイルを永続ストレージ(開発中はGarageの別バケット、本番はCloudflare R2)へアップロードする(transcodeAudio,backend/internal/transcode/audio.go)。その後トランザクション内でfinishUpload(backend/internal/transcode/service.go)がconverted_key・duration_seconds・converted_sizeを記録し、親のworkがpublish_pendingであればpublishedに更新する。POST /api/works/:id/publishは、アップロードが既にdoneなら即座に公開し、そうでなければpublish_pendingにして、変換完了時にworker側で公開を完了させる(状態機械はbackend/internal/works/service.goのpublishOrPend)。
Pre-Create フック
backend/internal/uploads/tus_handler.goのpreUploadCreateCallback(トランスポート)が、backend/internal/uploads/service.goのcreatePendingUpload(ドメインロジック)に委譲する。バリデーション内容:
work_idが存在し、認証済みユーザー本人が所有していることworks.status = draftであることworks.upload_idが未設定であること(二重アップロード防止)
検証成功時、uploads行の作成とworks.upload_idの更新を1つのトランザクションで行う。
状態遷移
2つのenumがパイプライン全体を駆動する(定義はdatabase参照)。
upload_status:uploading → transcoding → done | failedwork_status:draft → publish_pending | published、他にdeleted
未確定事項(設計書より)
- タグ名の正規化(大文字小文字・全角半角の統一)
- 投稿削除時に実ファイルを物理削除するか
- アップロード中のファイル再選択時の挙動
- Riverのリトライ/タイムアウトポリシー
- アップロード中の頻繁な
PATCH(自動保存)に対するレート制限