やりたいこと
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 の準備
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}
DB_NAME=sample
DB_USER=johndoe
DB_PASS=randompassword
Prisma の設定
npx prisma init
+ DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
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 の手順
- Azure Storage の設定(説明略)
- 必要な Module のインストール
- 認証
- アップロード
クイックスタート: 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
があるとして、進める。
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
で生成し、それぞれに渡すことにする。
...
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 で作った文字列に、元々のファイル名から拡張子を抜き取ってくっつけている。で、それぞれに渡している。
...
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 上は)。
完成
AWS を使うことが圧倒的に多いので、Azure と言われると、くっ…となるけど、Azure はめちゃくちゃドキュメントが豊富なので、お目当てのものが見つかれば、そんなに困らない。
github や VSCode とも相性が良いし、Azure、便利である。