🕌

【MS Learn】静的 Web アプリから Azure Blob Storage に画像をアップロードする を .NET でやってみた

2024/05/20に公開

はじめに

SAS の発行をアプリケーションでどうやってやるんだーと思ったので、このLearn をやってみました。
https://learn.microsoft.com/ja-jp/training/modules/blob-storage-image-upload-static-web-apps/
node.js でやるみたい
説明はご本家様を参考にしてください。

環境

Windows 11 WSL on Ubuntu 22.04
.NET 8
Azure Functions Core Tools

ソースコード

https://github.com/yukiko-bass/azure-blob-storage-sas-dotnet

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 を作成します。
https://learn.microsoft.com/ja-jp/azure/storage/blobs/sas-service-create-dotnet

Azure Function プロジェクトを作成する

  1. 任意のフォルダに api フォルダを作成し、そこに Azure Functions プロジェクトを作成します。『Ctrl + Shift + P』でコマンドパレットを開き、『Azure Functions: Create New Project...』を選択します。ディレクトリは作成した api フォルダを選択します。

  2. 言語は 『C#』を選択し、.NET runtime は『.NET 8.0』を選択します。

  3. 最初に作る関数は『HttpTrigger』を選択し、関数名は『Credentials』という名前にします。関数の認証レベルは『Anonymous』を選択します。

  4. Azure Portal で作成したストレージアカウントを表示し、『セキュリティとネットワーク』の下にある『アクセスキー』を選択します。key1 の接続文字列を表示させ、コピーします。

  5. Visual Studio Code に戻り、プロジェクトの local.settings.json の AzureWebJobsStorage キーに接続文字列を追加し、ファイルを保存します。

local.settings.json
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "<Blob Storage の接続文字列>",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
  }
}
  1. 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 を作成するクラスを作成します。

GenerateSASToken.cs
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します。

Program.cs
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();
Credentials.cs
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 を作成する

  1. 次のHTMLを app/index.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>
  1. app/src/index.js を作成して、以下を記述していきます。
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 を使用する

  1. コンソールでblobstorage-sdk と webpack をインストールします。
$ cd app/
$ npm install @azure/storage-blob
$ npm install webpack --save-dev
$ npm install webpack-cli --save-dev
  1. package.json ファイルを編集し、buildタスクを追加します。
package.json
{
  "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"
  }
}
  1. 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 拡張機能 を使います。

  1. .vscode/settings.json にAPIの呼び出しをバックエンドのFunctionsに流すように追記します。
.vscode/settings.json
"liveServer.settings.proxy": {
        "enable": true,
        "baseUri": "/api",
        "proxyUri": "http://127.0.0.1:7071/api"
}
  1. ファイル ツリーで、index.html ファイルを右クリックし、コンテキスト メニューから Open with Live Server を選択します。

  2. http://localhost:5500/app/ にアクセスすると以下のようなページがブラウザで表示されます。

  3. ファイルをアップロードします。

  4. ファイルがBlob Storage にアップロードされているか、確認します。

終わりに

SASの発行ができるようになりました。
コンテナベースでSASを発行するか、それともファイルベースでSASを発行するかは要件次第ですね。
ちなみに、ファイルの場合は、アップロードされる前のファイル名を決めて発行すればよいみたいです。
その際は、ファイル名をSASから取得する必要があるかな?(サーバ側とクライアント側で同じ命名規則で作れるなら、SASを解析する必要はないですが、大した手間ではない気も)

Discussion