🪐

ポリシードキュメントを使用したCloudStorageのファイルアップロード

2021/09/26に公開

こちらの記事に追記しても良かったのですが、この内容単体で出す方が内容も纏まると思ったので投稿しました。

CloudStorageに静的コンテンツをアップロードする場合、REST APIやCloudStorage用のライブラリを使用してアップロードすることがほとんどです。ですがどちらもアップロードするコンテンツの内容、命名をサービス提供者側が強制することは難しいです。

例えばですがfirestoreのドキュメントとCloudStorageのコンテンツを1対1で紐づけたい場合や、CloudFunctionsのCloudStorageイベントトリガーなどでアップロードされたファイル名に依存するような関数を実装する場合クライアント側でファイル名の変更ができてしまうのはあまり好ましくありません。

この問題を解決するためにCloudStorageが提供しているポリシードキュメントを使用したファイルアップロードを利用します。

サーバーサイドの設計

ポリシードキュメントで指定できる内容はこちらで確認することができます。今回は以下の要件を満たすようなポリシーを作成します。

  • アップロードできるサイズは1GBまで。
  • mimeTypeはvideo/mp4
  • ポリシーの有効期限は1時間
  • ファイルの格納場所は以下の通り
    • bucket: video_streaming
    • path: contents/video/(contentID).mp4

実装

環境はNode.jsのv14.16です。

加えて、file.generateSignedPostPolicyV4での署名ではconditionsにContent-Typeの制限を記載したからといってresponseに返却されるfieldsの中でContent-Typeに対する指定などは特に組み込まれないため別途fields引数に対して使用するfieldの値を渡す必要があります。今回はkey,success_action_redirect,Content-Type,Expiresを指定しています。

クライアントサイドで指定できるのであれば問題ありませんが、実装が少しでも異なるとinvalid argment error等が発生するのでポリシーを生成するタイミングで指定するのが無難だと思います。

ポリシードキュメントの生成機


import dayjs from "dayjs"
import utc from "dayjs/plugin/utc"
import { Storage } from "@google-cloud/storage"

dayjs.extend(utc)

export const generatePolicy = async (id: string): Promise<Policy> => {
    const key = `contents/video/${id}.mp4`
    const storage = new Storage()
    const file = storage.bucket("video_streaming").file(key)

    const today = dayjs.utc()
    const todayToFormatYYYYMMDD = today.format("YYYYMMDD")
    const todayToFormatGoog_date = today.format("YYYYMMDDTHHmmss[Z]")
    const algorithm = "GOOG4-RSA-SHA256"
    const account = "example_streaming@appspot.gserviceaccount.com"
    const credential = `${account}/${todayToFormatYYYYMMDD}/auto/storage/goog4_request`
    const expiration = today.add(1, "hour").toISOString()
    const [response] = await file.generateSignedPostPolicyV4({
      expires: today.add(1, "hour").toDate(),
      conditions: [
        ["eq", "$key", key],
        { bucket: "video_streaming" },
        {
          success_action_redirect: `https://streaming.example.com/${id}`,
        },
        ["content-length-range", 0, 1 * 1000 ** 3],
        ["eq", "$Content-Type", "video/mp4"],
        { "x-goog-algorithm": algorithm },
        { "x-goog-date": todayToFormatGoog_date },
        {
          "x-goog-credential": credential,
        },
        ["eq", "$Expires", expiration],
      ],
      fields: {
        key,
        success_action_redirect: `https://streaming.example.com/${id}`,
        "Content-Type": "video/mp4",
        Expires: expiration,
      },
    })
    return {
      url: response.url,
      fields: response.fields,
    }
  }
}

policyの書き方にはfull matchかprefixどちらかを使用することができ、今回はfull matchで厳格にアップロードに制限をかけていますが、["starts-with", "$Content-Type", "video/"]のようにprefixで制限をかけることもできます。

クライアントサイドの設計

@google-cloud/storagefile.generateSignedPostPolicyV4methodを使用したpolicyは別途署名やHexEncoding等をせずにそのままformの要素として利用することができます。
今回使用したプロジェクトはAppEngine上で動作するNext.jsで実装しており、SSR時にポリシーを生成しPropsで注入する、あるいはAPIにPolicyを生成する実装を用意し、useEffect, useCallback等でPolicyをfetchするといった実装が考えられます。

実装

React.jsを使用しています。

interface Policy {
  url: string
  fields: { [key: string]: string }
}
const Form: FC<{ policy: Policy }> = ({ policy }) => (
  <form action={policy.url} method="post" encType="multipart/form-data">
    {Object.keys(policy.fields).map((name, key) => (
      <input key={key} name={name} value={policy.fields[name]} type="hidden" />
    ))}
    <label>
      アップロードする動画を選択
      <input
        name="file"
        type="file"
        accept="video/mp4,.mp4"
      />
    </label>
  </form>
)

余談

参考文献の記事でも使用されていますが、今回紹介したポリシードキュメントの仕組みの他にBucket LockObject Lifecycleを組み合わせることで冪等性を担保しつつ安全にファイルをアップロードすることができます。

参考文献

https://cloud.google.com/blog/ja/products/gcp/uploading-images-directly-to-cloud-storage-by-using-signed-url

Discussion