2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Next.js で Azure Storage に Blob をアップロードする

Last updated at Posted at 2022-02-13

やりたいこと

Next.js + typescript で、Image や Movie を Azure Storage にアップロードしたい。

BLOB サービスは階層構造ではなく、フラットなストレージ構造に基づきますが、 BLOB 名に文字または文字列の区切り記号を指定することで仮想階層を作成できます。
via. コンテナー、BLOB、メタデータの名前付けと参照

とあるので、ディレクトリを掘らずに、フラットに置くことにするが、その際に、名前がかぶらないように、こちらで名前を付けてアップロードしたい。

アップロードしたら、DB に名前を登録する。

ひとまず、1ファイルだけボタンからアップロードすることにする。

なお、最終的なコードは、こちら

準備

Azure に関係ないところから作っていく。
構成は、

  • Next.js のトップページにアップロードするフォームを置く。
  • フォームは、react-hook-form で操作。
  • DB は PostgreSQL を使い、ORM は Prisma を使用。
  • API ルートに取得と登録のAPIを作成。axios で呼び出す。

インストール

yarn create next-app azure-storage --typescript
cd azure-storage
yarn add @prisma/client react-hook-form axios
yarn add @types/axios prisma --dev

PostgreSQL の準備

docker-compose.yml
version: "3.8"

services:
  db:
    image: "postgres:12"
    ports:
      - "5432:5432"
    volumes:
      - ./pgdata:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASS}
      - POSTGRES_DB=${DB_NAME}
.env
DB_NAME=sample
DB_USER=johndoe
DB_PASS=randompassword

Prisma の設定

npx prisma init
.env
+ DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model Item {
  id        Int       @id @default(autoincrement())
  name      String
  createdAt DateTime  @default(now())

  @@map(name: "items")
}

シンプルなテーブルとした。

npx prisma generate
npx prisma migrate dev

コードの準備

form の file から名前を抜き取って DB に登録。と取得するだけのコードを準備する。ズラズラ書くのは面倒なので、コードは以下に。

kurab/next-azure-storage:baseForm

いじったところ

├── pages
│   ├── api
│   │   ├── items.ts     // item 取得
│   │   └── register.ts  // item 登録
│   └── index.tsx        // form とリスト表示
├── hooks
│   └── useItem.ts       // api を呼び出す hook
└── types
    └── ItemType.ts      // item の Type

これで、

docker-compose up -d
yarn dev

または、

docker-compose up -d
yarn build
yarn start

で動く。

Azure の手順

  1. Azure Storage の設定(説明略)
  2. 必要な Module のインストール
  3. 認証
  4. アップロード

クイックスタート: Node.js の JavaScript v12 SDK を使用して BLOB を管理する

このあたりを読みながら。(なんか、MS の別の記事を最初参考にして、コードはほとんどそのままだが、その記事が見つけられない…)

必要な Module のインストール

yarn add @azure/storage-blob uuid
yarn add @types/uuid --dev

Azure Storage にアップロードするために必要なのは、@azure/storage-blob だけ。今回はフラットに保存するので、ユニークなファイル名とするために uuid を利用した。

この Module を使うと、fd629c13-dcc5-4503-a0be-934e96b99b27 こんなようなユニークな ID を生成してくれる。らしい。

認証

認証というほどの大げさなものでもないが、今回は sasToken を利用する。sasToken は、Azure Portal で作る(Shared Access Signature)。有効期限がデフォルトだと1日と短いので、注意。sasToken の運用に関しては、本記事の趣旨とは関係ないので、よしなに。ここでは、有効な sasToken があるとして、進める。

.env
NEXT_PUBLIC_STORAGESASTOKEN='sv=.....'
NEXT_PUBLIC_STORAGERESOURCENAME='xxxxx'

NEXT_PUBLIC_STORAGESASTOKEN に Azure Portal で生成した sasToken を入れる。最初の ? はいらないので取り除く。
NEXT_PUBLIC_STORAGERESOURCENAME には、ストレージアカウントを入れる

うーん、NEXT_PUBLIC_ で良いのだろうか。

これらを使って、BlobServiceClient を初期化する。

import { BlobServiceClient } from '@azure/storage-blob';

const sasToken = process.env.NEXT_PUBLIC_STORAGESASTOKEN;
const storageAccountName = process.env.NEXT_PUBLIC_STORAGERESOURCENAME;

const blobService = new BlobServiceClient(
  `https://${storageAccountName}.blob.core.windows.net/?${sasToken}`
);

こんな感じ。

アップロード

ファイルをアップロードして、DB に登録したい。本来トランザクションを考える必要があるが、コードがぐちゃぐちゃするので、今回はどっちも失敗しないものとする。

先程の、github のコードの useItem.tx にアップロードするメソッドを追加し、index.tsx からそのメソッドと DB に追加するメソッドを呼び出す。

ファイル名は、アップロードされたファイル名ではなく、uuid によって作られた新しいものとしたいので、index.tsx で生成し、それぞれに渡すことにする。

index.tsx
...
import { v4 as uuidv4 } from 'uuid';
...
  const onClickSave = (formData: any) => {
    if (formData.files[0]) {
      const newFileName =
        uuidv4() + '.' + formData.files[0].name.split('.').pop();
      uploadFileToBlob(formData.files[0], newFileName);
      registerItem(newFileName);
    }
  };
...

いまさらだけど、onClickSave じゃなくて、onSubmitSave の方が良かった…
やっていることは単純で、uuid で作った文字列に、元々のファイル名から拡張子を抜き取ってくっつけている。で、それぞれに渡している。

useItem.ts
...
import { BlobServiceClient, ContainerClient } from '@azure/storage-blob';

const containerName = 'sample-container';
const sasToken = process.env.NEXT_PUBLIC_STORAGESASTOKEN;
const storageAccountName = process.env.NEXT_PUBLIC_STORAGERESOURCENAME;
...
  const uploadFileToBlob = useCallback(
    async (file: File | null, newFileName: string) => {
      setLoading(true);
      if (!file) {
        setMessage('No FILE');
      } else {
        const blobService = new BlobServiceClient(
          `https://${storageAccountName}.blob.core.windows.net/?${sasToken}`
        );

        const containerClient: ContainerClient =
          blobService.getContainerClient(containerName);
        await containerClient.createIfNotExists({
          access: 'container',
        });

        const blobClient = containerClient.getBlockBlobClient(newFileName);
        const options = { blobHTTPHeaders: { blobContentType: file.type } };

        await blobClient.uploadData(file, options);
        setMessage('uploaded');
      }
      setLoading(false);
    },
    []
  );
...

コンテナが必ずあるよって場合は、

await containerClient.createIfNotExists({
  access: 'container',
});

これはなくても良い。container が既にあると、

PUT https://.... 409 (The specified container already exists.) 

と毎度言われる。

const blobClient = containerClient.getBlockBlobClient(newFileName);

ここで、今回格納するファイル名用の blobClient を作っている。ファイル名をそのまま使いたい場合は、file.name と入れておけば良い。

アップロード後の挙動は、お好きにどうぞ。私は今回は、state に放り込んでおいた(github 上は)。

完成

kurab/next-azure-storage

AWS を使うことが圧倒的に多いので、Azure と言われると、くっ…となるけど、Azure はめちゃくちゃドキュメントが豊富なので、お目当てのものが見つかれば、そんなに困らない。

github や VSCode とも相性が良いし、Azure、便利である。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?