お問い合わせ
Nuxt3 @google-cloud/storageを使って cloud storageに画像をアップロードする
投稿日:2023-08-30
@prompta
(株)ブロックセブンスソフトウェア
Nuxt3
GoogleCloudStorage
typeScript
@google-cloud/storage

# 手順解説

NuxtでCloudStorageに画像ファイルをアップロードする機能を作ります。
サーバにファイルを送り、サーバがそのファイルをcloudStorageにファイルをアップロードするという手間を省く為
クライアント側(vue側)からCloudStorageに直接アップロードする様にします。

cloudStorageに直接アップロードする為にはsignedUrlという期限付きのアップロード専用
urlをcloudStorageに作ってもらい、こちらのurlを使ってイメージファイルのアップロードを行います。

# Cloud Storageの設定

# サービスアカウントの作成

# cloud storageの設定

  • バケットを作る

  • publicアクセスを可能にする(privateのままにする場合はこちらは実行しない)

    • allUserに読み込み権限を付与し、公開アクセスを可能にします。
    • この設定によりアップロードした画像がインターネット上に公開され誰もがアクセス可能となります。
  • cors設定を有効にする。originの配列にdomainを追加する

    • 以下の様なjsonファイルを作成します。cors.json
    [
      {
        "origin": ["http://localhost:3000"], //許可したいドメインが複数ある場合は配列に追加
        "method": ["PUT", "POST"], // PUT POSTのみ許可
        "responseHeader": ["Content-Type"],
        "maxAgeSeconds": 3600
      }
    ]
    • クライアント側から署名URLを使ったアクセスをする際にアクセス元がわからないと拒否されてしまうので
    • originの設定で許可してもらいます。localhostを指定していますが、特定のドメインからアクセスさせたい場合は配列に追加します。
  • 設定を反映させる

    gcloud storage buckets update gs://<作ったバケット名> --cors-file=<先ほど作ったjsonファイルの保存先>
    • こちらの内容とほぼ一緒です。
    • 設定が正しく反映できたか確認するために以下のコマンドを実行します。
      gcloud storage buckets describe gs://<作ったバケット名> --format="default(cors_config)"
      以下の内容が出力されます。
      - maxAgeSeconds: 3600
        method:
        - PUT
        - POST
        origin:
        - http://localhost:3000
        responseHeader:
        - Content-Type
  • 作成したバケットにサービスアカウントユーザのアクセス権限(書き込み権限)をつける

    • 先ほど作ったサービスアカウントユーザーにStorage オブジェクト作成者権限を付与します。
    • CloudStorageの管理画面、バケットを選択しアクセス権を付与を押して、新しいプリンシパルにサービスアカウントのメールアドレスをロールにcloud Storage => Storageオブジェクト作成者を指定します。

# Nuxtの設定

  • モジュールをインストールする
    npm i @google-cloud/storage
  • runtimeConfigにcloud storage用の環境変数追加
    export default defineNuxtConfig({
      .
      .
      .
      runtimeConfig: {
        GCLOUD_KEY_FILE: process.env.GCLOUD_KEY_FILE,
        GCLOUD_PRJ_ID: process.env.GCLOUD_PRJ_ID,
        GCLOUD_BUKET: process.env.GCLOUD_BUKET,
      }
      .
      .
      .
    })
  • 環境変数に追加
    .env
    GCLOUD_KEY_FILE='./service-account.json'
    GCLOUD_PRJ_ID=your-project-id
    GCLOUD_BUKET=yor-bucket-name
    GCLOUD_KEY_FILEにはサービスアカウントを作った時にダウンロードしたjsonのパスを指定します。
    GCLOUD_PRJ_IDにはCloudConsoleのプロジェクト選択時に表示されるIDを指定します(nameの値ではないので注意)
    GCLOUD_BUKETにはCloudStorageに作ったバケット名を指定します。

# サーバ側のコード

  • ファイルをアップロードする為のsignedUrlを取得するコードを追加
    ./composables/image/image.service.ts
    import { Storage } from "@google-cloud/storage";
      
      export const useImageService = () => {
        const config = useRuntimeConfig();
        function getCloudStorage() {
          const storage = new Storage({
            projectId: config.GCLOUD_PRJ_ID,
            keyFilename: config.GCLOUD_KEY_FILE,
            scopes: [
              "https://www.googleapis.com/auth/devstorage.read_write",
              "https://www.googleapis.com/auth/pubsub",
            ],
          });
      
          return storage;
        }
      
        async function makeSignedUrl(fileName: string, contentType: string) {
          const data = await getCloudStorage()
            .bucket(config.GCLOUD_BUKET)
            .file(fileName)
            .getSignedUrl({
              version: "v4",
              action: "write",
              expires: Date.now() + 15 * 60 * 1000, // 15 minutes
              contentType,
            });
          return data.shift();
        }
        return { makeSignedUrl };
      };
  • apiファイルを追加する。
    ./server/api/file/index.get.ts
    import { useImageService } from "~/composables/image/image.service";
    import { getServerSession } from "#auth";
    export default defineEventHandler(async (e) => {
      const session = await getServerSession(e);
      if (!session) {
        throw createError({
          statusCode: 401,
          statusMessage: "not authorized",
        });
      }
      const query = getQuery<{ fileName: string; contentType: string }>(e);
      return await useImageService().makeSignedUrl(
        query.fileName,
        query.contentType,
      );
    });
    session部分はユーザーアクセス制限をかけています。
    必要に応じて制限をかけるコードを各々書き換えてください。

# クライアント側のコード

  • サーバー側からsignedUrlを取得してCloudStrageにアップロードするスクリプトを追加
    ./composables/image/image.request.ts

    export const useImageRequest = () => {
      const api = "/api/file";
      async function signedUrlRequest(file: File) {
        const { data } = await useAsyncData<string>("get-image-upload-url", () =>
          $fetch(`${api}/?fileName=${file.name}&contentType=${file.type}`),
        );
        if (data && data.value) {
          return await fetch(data.value, {
            method: "PUT",
            headers: { "Content-Type": file.type },
            body: file,
          });
        }
      }
      return { getSignedUrlRequest };
    };

    サーバ側でcloudStorageにファイルをアップロードするためのURLを生成した後、このURLを使って
    クライアント側からcloudStorageに直接アップロードします。
    こうすることでサーバに対してファイルを受信する負荷をなくすことが出来ます。

  • vueを作ってファイルをアップロードする

    <template>
      <div>
        <h1>ファイルアップロード</h1>
        <div>
          <input type="file" name="file" @change="onChange" />
        </div>
        <div v-for="image, index in imageUrlList" :key="`imageIndex-${index}`">
          <img v-if="image" :src="image" />
        </div>
      </div>
    </template>
    <script lang='ts' setup>
    const imageUrlList = ref<(string | undefined)[]>([])
    const onChange = async (event:Event) => {
    
      const target = event?.target as HTMLInputElement;
      const files = target.files;
      const res = await Promise.all(
        [...files!].map(async (file) => {
          return await useImageRequest().signedUrlRequest(file);
        }),
      );
      imageUrlList.value = res.map((value) => value?.url?.replace(/\?.*/, ""))
    };
    </script>
    • 画像をファイル選択するとcloudStorageにアップロードされたのち、画像のURLを取得します。