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

## やりたいこと

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

> BLOB サービスは階層構造ではなく、フラットなストレージ構造に基づきますが、 BLOB 名に文字または文字列の区切り記号を指定することで仮想階層を作成できます。 
via. [コンテナー、BLOB、メタデータの名前付けと参照](https://docs.microsoft.com/ja-jp/rest/api/storageservices/naming-and-referencing-containers--blobs--and-metadata)

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

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

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

なお、最終的なコードは、[こちら](https://github.com/kurab/next-azure-storage)。

## 準備

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

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

### インストール

```bash
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 の準備

```yml: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}
```

```text:.env
DB_NAME=sample
DB_USER=johndoe
DB_PASS=randompassword
```

### Prisma の設定

```bash
npx prisma init
```

```diff:.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")
}
```
シンプルなテーブルとした。

```bash
npx prisma generate
npx prisma migrate dev
```

### コードの準備

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

[kurab/next-azure-storage:baseForm](https://github.com/kurab/next-azure-storage/tree/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 の設定（説明略）
1. 必要な Module のインストール
1. 認証
1. アップロード

[クイックスタート: Node.js の JavaScript v12 SDK を使用して BLOB を管理する](https://docs.microsoft.com/ja-jp/azure/storage/blobs/storage-quickstart-blobs-nodejs)

このあたりを読みながら。（なんか、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``` があるとして、進める。

```text:.env
NEXT_PUBLIC_STORAGESASTOKEN='sv=.....'
NEXT_PUBLIC_STORAGERESOURCENAME='xxxxx'
```

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

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

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

```typescript
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``` で生成し、それぞれに渡すことにする。


```typescript: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 で作った文字列に、元々のファイル名から拡張子を抜き取ってくっつけている。で、それぞれに渡している。

```typescript: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);
    },
    []
  );
...
```

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

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

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

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

と毎度言われる。

```typescript
const blobClient = containerClient.getBlockBlobClient(newFileName);
```

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

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



## 完成

[kurab/next-azure-storage](https://github.com/kurab/next-azure-storage)

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

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