【MS Learn】静的 Web アプリから Azure Blob Storage に画像をアップロードする を .NET でやってみた
はじめに
SAS の発行をアプリケーションでどうやってやるんだーと思ったので、このLearn をやってみました。
説明はご本家様を参考にしてください。
環境
Windows 11 WSL on Ubuntu 22.04
.NET 8
Azure Functions Core Tools
ソースコード
Learn
ユニット3: 演習 - BLOB ストレージ アカウントを設定する
Azure ストレージ アカウントを作成する
手順に従ってストレージアカウントを作成します。
新しいストレージ コンテナーを作成する
手順に従ってコンテナを作成します。
CORS を設定する
フロントエンドから画像をストレージに直接アップロードできるように、Blob Storage に CORS の設定を追加します。
手順に従って設定を追加します。
CORS は設定のサブメニューになっていました。
ユニット4: サーバレス バックエンドの説明
SAS トークンとURLが生成できたら、クライアント側に以下のようなjsonを返却します。
{
"sasurl": "https://uploadimages.blob.core.windows.net?sv=2020-06-12&se=2021-04-26T19%3A32%3A43Z&sr=c&sp=c&sig=<SecretSignature>"
}
ユニット5: 演習 - サーバーレス バックエンドを構築する
Azure Functions を .NET core 8 で作成します。
SAS 作成はこのあたりを参考にし、サービス SAS を作成します。
Azure Function プロジェクトを作成する
-
任意のフォルダに api フォルダを作成し、そこに Azure Functions プロジェクトを作成します。『Ctrl + Shift + P』でコマンドパレットを開き、『Azure Functions: Create New Project...』を選択します。ディレクトリは作成した api フォルダを選択します。
-
言語は 『C#』を選択し、.NET runtime は『.NET 8.0』を選択します。
-
最初に作る関数は『HttpTrigger』を選択し、関数名は『Credentials』という名前にします。関数の認証レベルは『Anonymous』を選択します。
-
Azure Portal で作成したストレージアカウントを表示し、『セキュリティとネットワーク』の下にある『アクセスキー』を選択します。key1 の接続文字列を表示させ、コピーします。
-
Visual Studio Code に戻り、プロジェクトの local.settings.json の AzureWebJobsStorage キーに接続文字列を追加し、ファイルを保存します。
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "<Blob Storage の接続文字列>",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
}
}
- SAS トークンの生成のために、Azure SDK を依存関係に追加します。
https://learn.microsoft.com/ja-jp/dotnet/api/overview/azure/storage?view=azure-dotnet
dotnet add package Microsoft.Extensions.Azure # DI するのに必要
dotnet add package Azure.Storage.Blobs --version 12.20.0
BLOB のサービス SAS を作成する
BLOB のサービス SAS を作成する にあるように、サービス SAS を作成するクラスを作成します。
using Azure.Storage.Blobs;
using Azure.Storage.Sas;
public class GenerateSASToken
{
public Uri CreateServiceSASBlob(
BlobContainerClient blobContainerClient,
string storedPolicyName = null)
{
// Check if BlobContainerClient object has been authorized with Shared Key
if (blobContainerClient.CanGenerateSasUri)
{
// Create a SAS token that's valid for one day
BlobSasBuilder sasBuilder = new BlobSasBuilder()
{
BlobContainerName = "image",
Resource = "c" // container に対して発行するので『c』。ファイル単位の場合は『b』を指定する。
};
if (storedPolicyName == null)
{
sasBuilder.ExpiresOn = DateTimeOffset.UtcNow.AddHours(2);
sasBuilder.SetPermissions(BlobContainerSasPermissions.Write);
}
else
{
sasBuilder.Identifier = storedPolicyName;
}
Uri sasURI = blobContainerClient.GenerateSasUri(sasBuilder);
return sasURI;
}
else
{
// Client object is not authorized via Shared Key
return null;
}
}
}
api のエンドポイントを実装
HttpTrigger の credentials エンドポイントの口だけ作成されているので、先ほど作成した GenerateSASToken クラスを呼び出します。
Program.cs で使用するBlobContainerClientをDIします。
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Azure;
using Azure.Storage.Blobs;
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
services.AddAzureClients(builder =>
{
builder.AddBlobServiceClient("<使用するBlob Storage の接続文字列>");
});
// Register BlobContainerClient
services.AddSingleton(x =>
{
var blobServiceClient = x.GetRequiredService<BlobServiceClient>();
var containerClient = blobServiceClient.GetBlobContainerClient("image");
return containerClient;
});
services.AddSingleton<GenerateSASToken>();
})
.Build();
host.Run();
using Azure.Storage.Blobs;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Logging;
namespace BlobStorageSAS.Functions;
public class Credentials
{
private readonly ILogger<Credentials> _logger;
private readonly BlobContainerClient _blobContainerClient;
private readonly GenerateSASToken _generateSASToken;
public Credentials(ILogger<Credentials> logger, BlobContainerClient blobContainerClient, GenerateSASToken generateSASToken)
{
_logger = logger;
_blobContainerClient = blobContainerClient;
_generateSASToken = generateSASToken;
}
[Function("credentials")]
public IActionResult Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequest req)
{
_logger.LogInformation("C# HTTP trigger function processed a request.");
var sasuri = _generateSASToken.CreateServiceSASBlob(_blobContainerClient, null);
return new OkObjectResult(new { sasuri = sasuri });
}
}
functions を起動して、sasuri が返ってくれば成功です。
ユニット7: 演習 - 画像アップロードのフロントエンドを作成する
フロントエンドのソースは app フォルダの下に作っていきます。
フロントエンドの HTML を作成する
- 次のHTMLを app/index.html に記述していきます。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Azure Blob Storage Image Upload</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.0/css/bulma.min.css">
</head>
<body>
<section class="section">
<div class="container">
<h1 class="title">Loading SASKey from the API: </h1>
<pre id="name">...</pre>
<br>
<label for="image">Choose a profile picture:</label>
<input type="file" id="image" name="image" accept="image/png, image/jpeg">
</div>
</section>
<script src="./dist/main.js" type="text/javascript"></script>
<script>
(async function () {
const response = await fetch("/api/credentials");
const {sasuri} = await response.json();
document.querySelector('#name').textContent = `SAS URI: ${sasuri}`;
function uploadFile() {
const file = document.getElementById('image').files[0];
blobUpload(file, sasuri, 'image');
};
const fileInput = document.getElementById('image');
fileInput.addEventListener("change", uploadFile);
}())
</script>
</body>
</html>
- app/src/index.js を作成して、以下を記述していきます。
const { BlockBlobClient, AnonymousCredential } = require("@azure/storage-blob");
blobUpload = function(file, sasuri, container) {
var blobName = buildBlobName(file);
var splitedSASUri = splitSasuri(sasuri);
var login = `${splitedSASUri[0]}/${container}/${blobName}?${splitedSASUri[1]}`;
var blockBlobClient = new BlockBlobClient(login, new AnonymousCredential());
blockBlobClient.uploadBrowserData(file);
}
function buildBlobName(file) {
var filename = file.name.substring(0, file.name.lastIndexOf('.'));
var ext = file.name.substring(file.name.lastIndexOf('.'));
return filename + '_' + Math.random().toString(16).slice(2) + ext;
}
function splitSasuri(sasuri) {
// ? で分ける
var url = sasuri.substring(0, sasuri.lastIndexOf('?'));
var sasKey = sasuri.substring(sasuri.lastIndexOf('?'));
return [url, sasKey];
}
Azure Blob Storage SDK と Webpack を使用する
- コンソールでblobstorage-sdk と webpack をインストールします。
$ cd app/
$ npm install @azure/storage-blob
$ npm install webpack --save-dev
$ npm install webpack-cli --save-dev
- package.json ファイルを編集し、buildタスクを追加します。
{
"devDependencies": {
"webpack": "^5.91.0",
"webpack-cli": "^5.1.4"
},
"scripts": {
"start": "func start",
"build": "webpack --mode=development"
},
"dependencies": {
"@azure/storage-blob": "^12.18.0"
}
}
- webpack の実行をします。
$ npm run build
> build
> webpack --mode=development
asset main.js 1.58 MiB [emitted] (name: main)
orphan modules 73.8 KiB [orphan] 49 modules
runtime modules 1.13 KiB 5 modules
modules by path ./node_modules/@azure/ 1.11 MiB 143 modules
modules by path ./node_modules/@opentelemetry/api/build/esm/ 50.4 KiB 23 modules
modules by path ./node_modules/uuid/dist/esm-browser/*.js 3.24 KiB
./node_modules/uuid/dist/esm-browser/v4.js 544 bytes [built] [code generated]
./node_modules/uuid/dist/esm-browser/rng.js 1.02 KiB [built] [code generated]
./node_modules/uuid/dist/esm-browser/stringify.js 1.43 KiB [built] [code generated]
./node_modules/uuid/dist/esm-browser/validate.js 141 bytes [built] [code generated]
./node_modules/uuid/dist/esm-browser/regex.js 133 bytes [built] [code generated]
./src/index.js 868 bytes [built] [code generated]
./node_modules/tslib/tslib.es6.mjs 15.9 KiB [built] [code generated]
os (ignored) 15 bytes [built] [code generated]
./node_modules/events/events.js 14.5 KiB [built] [code generated]
webpack 5.91.0 compiled successfully in 859 ms
プロジェクトをローカルで実行する
F5 で Functions を起動します。
Live Server を使用してプロジェクトをテストする
Live Server 拡張機能 を使います。
-
.vscode/settings.json
にAPIの呼び出しをバックエンドのFunctionsに流すように追記します。
"liveServer.settings.proxy": {
"enable": true,
"baseUri": "/api",
"proxyUri": "http://127.0.0.1:7071/api"
}
-
ファイル ツリーで、
index.html
ファイルを右クリックし、コンテキスト メニューから Open with Live Server を選択します。 -
http://localhost:5500/app/
にアクセスすると以下のようなページがブラウザで表示されます。
-
ファイルをアップロードします。
-
ファイルがBlob Storage にアップロードされているか、確認します。
終わりに
SASの発行ができるようになりました。
コンテナベースでSASを発行するか、それともファイルベースでSASを発行するかは要件次第ですね。
ちなみに、ファイルの場合は、アップロードされる前のファイル名を決めて発行すればよいみたいです。
その際は、ファイル名をSASから取得する必要があるかな?(サーバ側とクライアント側で同じ命名規則で作れるなら、SASを解析する必要はないですが、大した手間ではない気も)
Discussion