🪐

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

4 min read

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

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です。

予めサービスアカウント(今回はexample_streaming@appspot.gserviceaccount.com)に対してiam.serviceAccounts.signBlobを付与しておく必要があります。

加えて、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

ログインするとコメントできます