⛰️

Next.jsからgcsに画像を登録したり呼び出したりしたい!!!!

2023/04/10に公開

まずはGoogleCloudPlattformに登録するにょ〜

まあ、この辺参照しながら頑張ってみてください。
https://cloud.google.com/free?utm_source=google&utm_medium=cpc&utm_campaign=japac-JP-all-ja-dr-BKWS-all-all-trial-EXA-dr-1605216&utm_content=text-ad-none-none-DEV_c-CRE_521780177400-ADGP_[DNU]Hybrid | BKWS - EXA | Txt ~ GCP ~ General_google-KWID_43700063983700146-aud-1596662389094%3Akwd-1195304733301&userloc_1009296-network_g&utm_term=KW_gcp 登録&gclid=CjwKCAjw586hBhBrEiwAQYEnHYALkkep401h3YULjJa9dWkDvyApSSj87mT9wYaOs9soJE5bRevY0RoCD80QAvD_BwE&gclsrc=aw.ds&hl=ja

サービスアカウントを作るにょ〜

登録できたら、プロジェクトを作成します。
作成が終わったら、サービスアカウントを作成していきます。

この画面で認証情報をクリックします。

画面上部の認証情報の作成→サービスアカウントの作成をクリックします。

この辺は任意でつけられます。

そしたらこのサービスアカウントに対してGCSにアクセス可能なロールを与えてあげます。
まあ、難しいことは考えずにストレージ管理者でいいでしょう!!!!知らんけど!!!!

あとは適当にすっ飛ばしてサービスアカウント作成完了です🙌

認証情報を管理するにょ〜

作成できたサービスアカウントをクリックして画面上部のキーを選択します。

JSONを選択します。
鍵がダウンロードされるので、それをリポジトリの一番上におきます。(正確には一番上っていうかDockerfileやら.envがあるあたり)

ここまでできた自分を褒めよう!!!!讃えよう!!!!

ここまできたら富士山で言うところの8号目を突破したと言っても過言じゃないです(過言)
自分に大きな拍手!!!!!🎉

バケットを作るにょ〜

上記画像のGCSをクリック

こんな画面が来るので、バケットを作成


保存場所はまあ、どこでもいいんだけどなんとなくデータと近くにいたいから東京で!
ロケーションタイプはお財布と相談しながら決めるにょ〜
(個人開発程度ならResionでいいかな)


これもautoclassでいいかな。あの天下のGoogleさんだしよしなにやってくれるだろ。知らんけど!

この機能により、ユーザ自身でライフサイクルを設定しなくてもオブジェクトのアクセスパターンに応じて Google 側で自動でストレージ クラスを移行することが可能となります。
Autoclass はバケット単位で有効化します。Autocalss に新規で書き込まれたオブジェクトは、Standard Storage に格納され、下記のルールに従い、オブジェクトのストレージ クラスを変更します。
オブジェクトのデータにアクセスすると、そのオブジェクトは Standard Storage に移行される
30 日間アクセスされなかったオブジェクトは、Nearline Storage に移行される
90 日間アクセスされなかったオブジェクトは、Coldline Storage に移行される
365 日間アクセスされなかったオブジェクトは、Archive Storage に移行される
Archive Storage に格納されているオブジェクトは、アクセスされるまでストレージ クラスの移行は行われない


ここら辺は、まあよくわからん!なので適当にスルー


ここまできたら作成ボタンをクリック!!!!!

バケットの完成🎉

コードを書いていくにょ〜

lib/image.ts
import {v4 as uuidv4} from 'uuid'

export const uploadImg = async (file:File) => {
  const fileName = uuidv4()
  const res = await fetch(`/api/uploadImage?file=${fileName}`) // ①
  const { url, fields } = await res.json();
  const body = new FormData();
  Object.entries({ ...fields, file }).forEach(([key, value]) => {
    body.append(key, value as string | Blob );
  });
  const upload = await fetch(url, {method:"POST", body})

  if (!upload.ok) {
    console.log('upload failed')
    return ''
  }
  return fileName
}

jsとかtsに慣れてる人は説明しなくてもわかるわ!!!って感じだと思うけど、①のURLの中にいる?fileってやつはquery parameterとか言ったりするにょ〜

よくわからんぞ🤔って人はこの辺参考にしてみてね
https://be-marke.jp/articles/knowhow-queryparameter#:~:text=クエリパラメータとは、サーバー,してきたか判別できます。

①のAPIはここで用意する

pages/api/uploadImage.ts
import { Storage } from "@google-cloud/storage";

export default async function handler(req: any, res: any) {
  const storage = new Storage({
    projectId: process.env.PROJECT_ID,
    keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
  });
  const bucketName = process.env.BUCKET_NAME ?? ''
  const bucket = storage.bucket(bucketName)
  const file = bucket.file(req.query.file)
  const options = {
    expires: Date.now() + 1 * 60 * 1000,
    fields: { "x-goog-meta-test": "data" }
  }
  const [response] = await file.generateSignedPostPolicyV4(options)
  res.status(200).json(response)
}

envは.env.localというファイルを用意してそこに書こう!

.env.local(sample)
PROJECT_ID="hogehoge"
GOOGLE_APPLICATION_CREDENTIALS="hugahuga"
BUCKET_NAME="hshs"

ここで注意することは
@google-cloud/storage
はNode.jsのライブラリなのでフロントからは叩けないと言うこと。
自分はこれで試行錯誤しましたが、、、無念、、、

やっと叩くぞ〜!<upload編>

ここからはフロントエンドから叩く方法をご紹介

pages/index.tsx
export const HomePage: NextPage = () => {
  const [file, setFile] = useState<File | null>(null)
  // 以下でfileの変更を検知してデータを取得
  const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
    const selectedFile = event.target.files && event.target.files[0]
    if (!selectedFile) return
    setFile(selectedFile)
}
---
  const handleSubmit = async(file: File) => {
    const imageIDinGcs = await uploadImg(file)
    if (!imageIDinGcs) return alert('画像のアップロードに失敗しました。')
  // ここから先はまあ、データベースに登録するなり好きにやってください
  }
}

おめでとう!!これでuploadが完了したはず!
consoleを見るなり、バケットを見にいくなりしてください

やっと叩くぞ〜!<get編>

ほんほん、なるほど。と。
URLを登録するのではなく、名前(idと言った方がしっくり来るかも?)を登録するのかと。
これじゃあ、Imageとして読めないじゃないかと。
思った

鋭いですねえええ

はい。その通りです。なので、gcsで検索をかける処理を書かなければなりません。
そりゃPostがあればGetもあって然るべきですよね。

/pages/api/getImage.ts
export default async function handler(req: any, res: any) {

  const storage = new Storage({
    projectId: process.env.PROJECT_ID,
    keyFilename: process.env.GOOGLE_APPLICATION_CREDENTIALS,
  })

  const bucketName = process.env.BUCKET_NAME ?? ''
  const fileName = req.query.file ?? ''

  if (fileName === '') {
    res.status(400).json('file name is empty')
    return
  }

  const bucket = storage.bucket(bucketName)
  const file = bucket.file(fileName)

  // 有効期間を指定してSigned URLを生成する
  const options = {
    version: 'v4' as const,
    action: "read" as const,
    expires: Date.now() + 5 * 60 * 1000, // 5分間有効
  }

  const [url] = await file.getSignedUrl(options)
  return res.status(200).json({url})
}

とまあこんな感じ。
handlerの型が気になる人は適当にhttpからRequestとかResponseとか引っ張ってくればいいかと

フロントから叩く時はこんな感じ

/pages/users/[id].tsx
const [image, setImage] = React.useState<string | null>(null)
  useEffect(() => {
    if (user) {
      fetchData(String(user.image))
    }
  }, [user])
  const fetchData = async (fileName: string) => {
    const res = await fetch(`/api/getProfImage?file=${fileName}`)
    const JSONRes = await res.json()
    setImage(JSONRes.url)
  }

こんな感じにするといい感じに表示されるにょ🎉
useEffectを使用する理由は、userが取得できていない段階で叩いてしまうとエラーになるので、取得してから発火するため

最後に

ここまでお疲れ様でした〜〜〜!!
クラウド系覚えること多い(特にロールがめんどい)けど、頑張って一つずつ使えるようになってクラウドマスター目指していきましょ〜〜〜!!!

参考文献

https://qiita.com/toki_dev/items/09c4665525e44beae4e5

https://sterfield.co.jp/programmer/署名付きurlでcloud-storage-に画像ファイルを直接アップロー/

https://zenn.dev/google_cloud_jp/articles/cc882427a2bfeb

著者

https://twitter.com/ryuji_vlog

Discussion