8
3

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.

SvelteKit + Azure Blob Storage でファイルアップロード

Last updated at Posted at 2023-01-31

はじめに

Webアプリケーション開発で厄介なものの1つにファイルアップロードがあります。

なぜ厄介かというと、アップロード可能なファイルサイズの上限を数GBまで上げてしまうと、Webサーバーの負担が大きくなるからです。ファイルアップロードにリソース(ネットワーク帯域、CPU時間、メモリ、等)を割かれてしまい、肝心の画面表示系の処理が重くなってしまいます。

この課題を解決するにはWebサーバーを経由せずにファイルをアップロードする方法があります。

その方法の一つとして、Web ブラウザーから直接 Azure Blob Storage にファイルをアップロードする方法があります。ここでは SvelteKit で実装する方法を紹介します。

概念

Azure Blob Storage からファイルアップロード用の一時トークン(SAS:Shared Access Signatures)を生成してもらい、そのトークンと一緒にファイルをアップロードすることでセキュアなファイルアップロードを実現できます。

事前準備

アプリケーションの基盤

を参考に GitHub Codespaces 上で SvelteKit のスケルトンを作成してください。

Azure Blob Storage 用のライブラリを導入

ターミナル
npm i @azure/storage-blob

Azure Storage 接続文字列の取得

Azure Portal の Storage account の Access Key から 取得できます。
image.png

取得した文字列を .env ファイルに記載します。

.env
AZURE_STORAGE_CONNECTION_STRING=[接続文字列]

Azure Blob Storage にコンテナを追加する

fileupload という名前のコンテナを追加します。

image.png

Azure Blob Storage の CORS 設定

以下の様に設定して Save ボタンをクリックして保存します。
image.png

実装

+page.server.ts
src/routes/+page.server.ts
import type { Actions } from './$types';
import { BlobSASPermissions, BlobServiceClient } from '@azure/storage-blob';
import crypto from 'crypto';
import { env } from '$env/dynamic/private';

export const actions = {
	/**
	 * SAS URL を取得するアクション
	 */
	getSasUrl: async ({ request }) => {
		try {
			const fileName = (await request.formData()).get('fileName') as string;
			const sasUrl = await getSasUrl("dummyuser_id", "fileupload", fileName);
			return createData({ sasUrl: sasUrl });
		} catch (error: any) {
			console.error(error);
			return createData({ sasMessage: 'error:' + error.message });
		}
	}
} satisfies Actions;

type Args = {
	message?: string;
	sasUrl?: string;
	sasMessage?: string;
};

const createData = (args: Args) => {
	return {
		message: args.message,
		sasUrl: args.sasUrl,
		sasMessage: args.sasMessage
	};
};

async function getSasUrl(userId: string, containerName: string, fileName: string) {
	const containerClient = getContainerClient(containerName);

	// 特定のパスを指定
	const blockBlobClient = containerClient.getBlockBlobClient(
		userId + '/' + crypto.randomUUID() + '_' + fileName
	);

	return blockBlobClient.generateSasUrl({

		// 書き込みを許可
		permissions: BlobSASPermissions.from({
			write: true
		}),

		// 有効期間は1時間
		expiresOn: new Date(new Date().setHours(new Date().getHours() + 1))
	});
}

function getContainerClient(containerName: string) {
	const blobServiceClient = getBlobServiceClient();
	const containerClient = blobServiceClient.getContainerClient(containerName);
	return containerClient;
}

function getBlobServiceClient() {
	const AZURE_STORAGE_CONNECTION_STRING = env.AZURE_STORAGE_CONNECTION_STRING;
	if (!AZURE_STORAGE_CONNECTION_STRING) {
		throw Error('Azure Storage Connection string not found');
	}
	const blobServiceClient = BlobServiceClient.fromConnectionString(AZURE_STORAGE_CONNECTION_STRING);
	return blobServiceClient;
}
+page.svelte
src/routes/+page.svelte
<script lang="ts">
	import type { ActionData } from './$types';
	import { enhance } from '$app/forms';
	import { BlockBlobClient } from '@azure/storage-blob';
	let fileInput: HTMLInputElement;
	let fileName: HTMLInputElement;
	let success = false;

	const uploadFile = async (sasUrl) => {
		if (!fileInput) {
			return;
		}
		const files = fileInput.files;
		if (!files || files.length === 0) {
			return;
		}
		const blockBlobClient = new BlockBlobClient(sasUrl);
		const promises = [];
		promises.push(blockBlobClient.uploadData(files[0]));
		await Promise.all(promises);
	};
</script>

<h1>Upload to Blob Storage by SAS</h1>
<input
	type="file"
	name="file"
	bind:this={fileInput}
	on:change={() => {
		const files = fileInput.files;
		if (!files || files.length === 0) return;
		fileName.value = files[0].name;
	}}
/>
<form
	method="POST"
	action="?/getSasUrl"
	use:enhance={() => {
		return async ({ result, update }) => {
			success = false;
			const sasUrl = result.data.sasUrl;
			// SAS URL を取得できたらファイルをアップロードする
			uploadFile(sasUrl).then(() => {
				success = true;
			});
		};
	}}
>
	<input type="hidden" name="fileName" bind:this={fileName} />
	<button type="submit" disabled={!fileName?.value}>upload to blob storage</button>
</form>
{#if success}
	<div>success!</div>
{/if}

動作確認

ターミナル
npm run dev

表示されたURLにアクセスしてください。

たまにポートフォワードがうまくいかず 502 エラーが発生します。
この場合、コマンド末尾に -- --host 127.0.0.1 を追加してみてください。

こんな画面が表示されます。
image.png

ファイルを選択して upload to blob storage ボタンをクリックすると、ファイルアップロードが実行され、以下の様に success が表示されます。
image.png

Azure Blob Storage にファイルがアップロードされているか確認します。
image.png
アップロードされてますね!

まとめ

BlockBlobClientuploadData(..) 以外のメソッドもあるので、いろんなケースに対応できると思います。ファイルアップロードの進捗表示もできそうです。

これでファイルアップロードに悩まされることはなくなりそうです!:smiley:

8
3
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
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?