LoginSignup
0
0

静的 Web アプリから Azure Blob Storage に画像をアップロードするメモ

Last updated at Posted at 2024-04-23

概要

Microsoft Learnの静的 Web アプリから Azure Blob Storage に画像をアップロードするStatic Web Apps で行おうとしたが、 私がFunctions に Typescript を使っていたため、utils.jsがコピペで動かず詰んだ。

JavaScript を使用してイメージを Azure Storage Blob にアップロードするサンプルコードはTypescriptであり、こちらを使ったら動作させることができたので備忘録として残す。

blobの設定

Microsoft Learnの手順ではデフォルトの設定でと言っている部分だが、論理的な削除は不要な使い方をする予定なので、チェックを外しておく。(ファイルを消しても論理削除期間中は課金されるのが嫌なので)

image.png

書き換え

バックエンド

utils.jsを使っていた関数対応する関数に書き換えた。

api/src/functions/credentiols.ts
import {
  HttpRequest,
  HttpResponseInit,
  InvocationContext,
  app,
} from '@azure/functions';
import { generateSASUrl } from '../lib/azure-storage.js';

export async function getGenerateSasToken(
  request: HttpRequest,
  context: InvocationContext,
): Promise<HttpResponseInit> {
  context.log(`Http function processed request for url "${request.url}"`);

  try {
    if (
      !process.env?.Azure_Storage_AccountName ||
      !process.env?.Azure_Storage_AccountKey
    ) {
      return {
        status: 405,
        jsonBody: 'Missing required app configuration',
      };
    }

    const containerName = request.query.get('container') || 'images';
    const fileName = request.query.get('file') || 'nonamefile';
    const permissions = request.query.get('permission') || 'w';
    const timerange = parseInt(request.query.get('timerange') || '10'); // 10 minutes

    context.log(`containerName: ${containerName}`);
    context.log(`fileName: ${fileName}`);
    context.log(`permissions: ${permissions}`);
    context.log(`timerange: ${timerange}`);

    const url = await generateSASUrl(
      process.env?.Azure_Storage_AccountName,
      process.env?.Azure_Storage_AccountKey,
      containerName,
      fileName,
      permissions,
    );

    return {
      jsonBody: {
        url,
      },
    };
  } catch (error) {
    return {
      status: 500,
      jsonBody: error,
    };
  }
}

app.http('credentials', {
  methods: ['GET', 'POST'],
  authLevel: 'anonymous',
  handler: getGenerateSasToken,
});

ストレージアカウントの接続文字列の取得でコピーした文字列「DefaultEndpointsProtocol=https;AccountName=アカウント名;AccountKey=アカウントキー;EndpointSuffix=core.windows.net」から、必要な値を抜き出して設定ファイルに転記する。

api/local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage":"",
    "Azure_Storage_AccountName":"アカウント名",
    "Azure_Storage_AccountKey": "アカウントキー",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
  },
  "Host": {
    "CORS": "*"
  }
}
余談:オリジンの末尾に/を入れたことにより403

手順では、「許可されたオリジン」は「*」を指定したが、色気をだして指定してみたところ、末尾に/を入れてしまって、この後アップロードを行う時に失敗することとなった。。

image.png

フロントエンド

App.tsxの処理をhooksに移して書き換えている。awaitを使ったほうが好きなので、handlerもasyncを使った非同期関数に修正。また、今回はアップロードだけに絞るため、一覧を取得する処理は削除した。

app/src/hooks/useImageUploaderPageHooks.ts
/* eslint-disable turbo/no-undeclared-env-vars */
import { BlockBlobClient } from '@azure/storage-blob';
import { convertFileToArrayBuffer } from '@yakumi-app/domain/storageAccount/convert-file-to-arraybuffer';
import { ChangeEvent, useState } from 'react';

export const useImageUploaderPageHooks = () => {
  const containerName = `images`;
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [sasTokenUrl, setSasTokenUrl] = useState<string>('');
  const [uploadStatus, setUploadStatus] = useState<string>('');

  const handleFileSelection = (event: ChangeEvent<HTMLInputElement>) => {
    const { target } = event;

    if (!(target instanceof HTMLInputElement)) return;
    if (
      target?.files === null ||
      target?.files?.length === 0 ||
      target?.files[0] === null
    )
      return;

    setSelectedFile(target?.files[0]);

    // reset
    setSasTokenUrl('');
    setUploadStatus('');
  };

  const handleFileSasToken = async () => {
    const permission = 'w'; //write
    const timerange = 5; //minutes

    if (!selectedFile) return;
    try {
      const requestUrl = `${import.meta.env.VITE_API_SERVER ?? ''}/api/credentials?file=${encodeURIComponent(
        selectedFile.name,
      )}&permission=${permission}&container=${containerName}&timerange=${timerange}`;
      const response = await fetch(requestUrl, {
        method: 'POST',
        mode: 'cors',
        headers: {
          'Content-Type': 'application/json',
        },
      });
      const data = await response.json(); // JSON のレスポンスをネイティブの JavaScript オブジェクトに解釈
      const { url } = data;
      setSasTokenUrl(url);
    } catch (error) {
      console.warn(error);
      if (error instanceof Error) {
        const { message, stack } = error;
        setSasTokenUrl(`Error getting sas token: ${message} ${stack || ''}`);
      } else {
        setUploadStatus(error as string);
      }
    }
  };

  const handleFileUpload = async () => {
    if (sasTokenUrl === '') return;
    try {
      const fileArrayBuffer = await convertFileToArrayBuffer(
        selectedFile as File,
      );

      if (
        fileArrayBuffer === null ||
        fileArrayBuffer.byteLength < 1 ||
        fileArrayBuffer.byteLength > 256000
      )
        return;

      const blockBlobClient = new BlockBlobClient(sasTokenUrl);
      await blockBlobClient.uploadData(fileArrayBuffer);

      setUploadStatus('Successfully finished upload');

    } catch (error) {
      console.warn(error);
      if (error instanceof Error) {
        const { message, stack } = error;

        setUploadStatus(
          `Failed to finish upload with error : ${message} ${stack || ''}`,
        );
      } else {
        setUploadStatus(error as string);
      }
    }
  };

  return {
    handleFileSelection,
    handleFileUpload,
    handleFileSasToken,
    uploadStatus,
  };
};
app/pages/ImageUploaderPage.tsx
import { useImageUploaderPageHooks } from '@yakumi-app/hooks/useImageUploaderPageHooks';

function ImageUploaderPage() {
  const vm = useImageUploaderPageHooks();

  return (
    <div style={{ margin: '1rem' }}>
      <h1>画像アップロード</h1>
      <input
        type="file"
        id="image"
        name="image"
        accept="image/png, image/jpeg"
        onChange={vm.handleFileSelection}
      ></input>
      <button onClick={vm.handleFileSasToken}>SASトークン取得</button>
      <button onClick={vm.handleFileUpload}>アップロード</button>
      {vm.uploadStatus}
    </div>
  );
}

export default ImageUploaderPage;
app/.env.local
VITE_API_SERVER=http://localhost:7071

ローカル実行

cd api && npm run start
cd app && npm run dev
  • 無事ファイルをアップロードできた
  • 日本語を含むファイルもアップロードできたが、「スクリーンショット 2023-10-04 001528.png」はアップロードできなかった。空白文字などが問題?いったん備忘としてこれ以上の調査はここでは行わない。

デプロイ後の設定

環境変数の設定を忘れないこと(忘れた)

image.png

フォルダを切ってファイルを配置

現状のコードだと、コンテナ―直下に配置される。
例えば、uploadフォルダに入れるようにするには下記のようにフォルダ名/ファイル名とすればよい。

app/src/hooks/useImageUploaderPageHooks.ts
  const handleFileSasToken = async () => {
    const permission = 'w'; //write
    const timerange = 5; //minutes
    if (!selectedFile) return;

+   const fileName = `upload/${selectedFile.name}`;

    try {
      const requestUrl = `${import.meta.env.VITE_API_SERVER ?? ''}/api/credentials?file=${encodeURIComponent(
-        selectedFile.name,
+        fileName,        
      )}&permission=${permission}&container=${containerName}&timerange=${timerange}`;
      const response = await fetch(requestUrl, {
        method: 'POST',
        mode: 'cors',
        headers: {
          'Content-Type': 'application/json',
        },
      });
0
0
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
0
0