概要
Microsoft Learnの静的 Web アプリから Azure Blob Storage に画像をアップロードするをStatic Web Apps で行おうとしたが、 私がFunctions に Typescript を使っていたため、utils.jsがコピペで動かず詰んだ。
JavaScript を使用してイメージを Azure Storage Blob にアップロードする のサンプルコードはTypescriptであり、こちらを使ったら動作させることができたので備忘録として残す。
blobの設定
Microsoft Learnの手順ではデフォルトの設定でと言っている部分だが、論理的な削除は不要な使い方をする予定なので、チェックを外しておく。(ファイルを消しても論理削除期間中は課金されるのが嫌なので)
書き換え
バックエンド
utils.jsを使っていた関数を対応する関数に書き換えた。
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
」から、必要な値を抜き出して設定ファイルに転記する。
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage":"",
"Azure_Storage_AccountName":"アカウント名",
"Azure_Storage_AccountKey": "アカウントキー",
"FUNCTIONS_WORKER_RUNTIME": "node",
"AzureWebJobsFeatureFlags": "EnableWorkerIndexing"
},
"Host": {
"CORS": "*"
}
}
余談:オリジンの末尾に/を入れたことにより403
手順では、「許可されたオリジン」は「*」を指定したが、色気をだして指定してみたところ、末尾に/
を入れてしまって、この後アップロードを行う時に失敗することとなった。。
フロントエンド
App.tsxの処理をhooksに移して書き換えている。awaitを使ったほうが好きなので、handlerもasyncを使った非同期関数に修正。また、今回はアップロードだけに絞るため、一覧を取得する処理は削除した。
/* 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,
};
};
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;
VITE_API_SERVER=http://localhost:7071
ローカル実行
cd api && npm run start
cd app && npm run dev
- 無事ファイルをアップロードできた
- 日本語を含むファイルもアップロードできたが、「スクリーンショット 2023-10-04 001528.png」はアップロードできなかった。空白文字などが問題?いったん備忘としてこれ以上の調査はここでは行わない。
デプロイ後の設定
環境変数の設定を忘れないこと(忘れた)
フォルダを切ってファイルを配置
現状のコードだと、コンテナ―直下に配置される。
例えば、upload
フォルダに入れるようにするには下記のようにフォルダ名/ファイル名
とすればよい。
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',
},
});