# 手順解説
NuxtでCloudStorageに画像ファイルをアップロードする機能を作ります。
サーバにファイルを送り、サーバがそのファイルをcloudStorageにファイルをアップロードするという手間を省く為
クライアント側(vue側)からCloudStorageに直接アップロードする様にします。
cloudStorageに直接アップロードする為にはsignedUrlという期限付きのアップロード専用
urlをcloudStorageに作ってもらい、こちらのurlを使ってイメージファイルのアップロードを行います。
# Cloud Storageの設定
# サービスアカウントの作成
- こちらを参考にサービスアカウントを作成します。
- 作成後jsonファイルをダウンロードします。
# 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を指定していますが、特定のドメインからアクセスさせたい場合は配列に追加します。
- 以下の様なjsonファイルを作成します。
-
設定を反映させる
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
session部分はユーザーアクセス制限をかけています。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, ); });
必要に応じて制限をかけるコードを各々書き換えてください。
# クライアント側のコード
-
サーバー側から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を取得します。
- 画像をファイル選択するとcloudStorageにアップロードされたのち、画像のURLを取得します。