Skip to content

アップロード〜公開パイプライン

全体の流れ

  1. クライアントが POST /api/works でdraft状態のworks行を作成し、work_idを受け取る。
  2. クライアントが Upload-Metadata: work_id=<uuid> を付けて /files へTUSアップロードを開始する。
  3. tusdの PreUploadCreateCallbacktus-handler)がFirebaseで認証し、当該workがdraftで本人所有かつ未アップロードであることを検証したうえで、uploads行を作成しworks.upload_iduploads.work_idを相互リンクする。すべて単一トランザクション内で行う。
  4. クライアントはアップロード中でも PATCH /api/works/:id でtitle/description/tagsを更新できる(status published/deletedでは拒否)。
  5. アップロード完了時、tusdが CompleteUploads を発火。backend/cmd/app/main.goのgoroutineがuploads.Handler.CompleteTUSUploadbackend/internal/uploads/service.gocompleteTUSUpload)を呼び、uploadをtranscodingにしつつjobs.TranscodeAudioArgsのRiverジョブを同一トランザクション内で積む。コミット後にRiverキューへ通知することで、DBとキューの不整合を避けている。
  6. backend/internal/transcode/worker.goWorkerがジョブを取得し、DBトランザクションの外側で(ffmpeg実行やS3 IOは時間がかかるため)Garageから元ファイルをダウンロードし、ffmpegでOpus 128kbpsにエンコードし、ffprobeで再生時間を取得し、変換後ファイルを永続ストレージ(開発中はGarageの別バケット、本番はCloudflare R2)へアップロードする(transcodeAudio, backend/internal/transcode/audio.go)。その後トランザクション内でfinishUploadbackend/internal/transcode/service.go)がconverted_keyduration_secondsconverted_sizeを記録し、親のworkがpublish_pendingであればpublishedに更新する。
  7. POST /api/works/:id/publishは、アップロードが既にdoneなら即座に公開し、そうでなければpublish_pendingにして、変換完了時にworker側で公開を完了させる(状態機械はbackend/internal/works/service.gopublishOrPend)。

Pre-Create フック

backend/internal/uploads/tus_handler.gopreUploadCreateCallback(トランスポート)が、backend/internal/uploads/service.gocreatePendingUpload(ドメインロジック)に委譲する。バリデーション内容:

  • work_idが存在し、認証済みユーザー本人が所有していること
  • works.status = draftであること
  • works.upload_idが未設定であること(二重アップロード防止)

検証成功時、uploads行の作成とworks.upload_idの更新を1つのトランザクションで行う。

状態遷移

2つのenumがパイプライン全体を駆動する(定義はdatabase参照)。

  • upload_status: uploading → transcoding → done | failed
  • work_status: draft → publish_pending | published、他にdeleted

未確定事項(設計書より)

  • タグ名の正規化(大文字小文字・全角半角の統一)
  • 投稿削除時に実ファイルを物理削除するか
  • アップロード中のファイル再選択時の挙動
  • Riverのリトライ/タイムアウトポリシー
  • アップロード中の頻繁なPATCH(自動保存)に対するレート制限