0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C#のAPIでフロントエンドからS3に画像をアップロードしたい!

0
Posted at

はじめに

今回は、フロントエンドから Amazon S3 へ直接画像をアップロードする方法についてまとめます。

筆者は、画像情報を管理するためのDBとして独自に構築したデータベースを使用しています。しかし、画像データそのものをDBに保存すると容量が大きくなり、アプリケーションのパフォーマンス低下を招くという課題がありました。

一般的に、画像や動画などの大容量バイナリデータは、DBではなくオブジェクトストレージに保存することが推奨されています。

そこで今回は、比較的扱いに慣れている Amazon S3 をストレージとして採用しました。
本記事では、署名付きURL(Pre-signed URL)を利用して、フロントエンドから直接 S3 に画像をアップロードする方法を紹介します。

言語

フロントエンド: React (Vite)
バックエンド: C#
ストレージ: Amazon S3

開発

動作の流れ

  1. フロントエンドで画像を保存したいタイミングで、バックエンドに署名付きURLをリクエストする
  2. バックエンドで S3 の署名付きURL(PUT)を発行する
  3. フロントエンドから、その URL を使用して S3 に直接画像をアップロードする

署名付きURLとは?

署名付きURL(Pre-signed URL) は、バケットポリシーを変更することなく、特定の S3 オブジェクトに対して期限付きのアクセス権を付与できる仕組みです。

この仕組みを利用することで、AWS の認証情報(アクセスキーなど)をフロントエンドに持たせることなく、安全に S3 へアップロードが可能になります。

今回の構成では、AWS アカウントを持たない一般ユーザーでも、フロントエンド経由で画像をアップロードできるようにするため署名付きURLを使用しています。

使用するメソッド

C#を使用したバックエンドの実装では、AWS SDK for .NETというライブラリのAWSSDK.S3というパッケージを使用します。

このパッケージにあるGetPreSignedURLメゾットが署名付きURLの作成を行います。
このメゾットにバケット名やファイル名、シークレットキーを渡すと、サーバとの通信を行わずに署名付きURLを発行します。

実際のコード

バックエンド

namespace Your_namespace.Controllers
{
    using Amazon;
    using Amazon.S3;
    using Amazon.S3.Model;
    using Microsoft.AspNetCore.Mvc;

    /// <summary>S3署名付きURLを発行するAPI</summary>
    [ApiController]
    [Route("api/upload")]
    public class UploadController : ControllerBase
    {
        /// <summary>
        /// S3 へのアップロード用署名付きURL(PUT)を発行する。
        /// </summary>
        /// <param name="fileName">アップロード先オブジェクトキー(ファイル名)</param>
        /// <param name="contentType">アップロードするファイルのContent-Type</param>
        /// <returns>署名付きURLを含むJSON</returns>
        [HttpGet("generate-presigned-url")]
        public IActionResult GeneratePresignedUrl(
            [FromQuery] string fileName,
            [FromQuery] string contentType)
        {
            if (string.IsNullOrWhiteSpace(fileName))
            {
                return this.BadRequest(new { message = "fileName is required." });
            }

            if (string.IsNullOrWhiteSpace(contentType))
            {
                return this.BadRequest(new { message = "contentType is required." });
            }

            //// 環境変数に入れておく
            var accessKey = Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID");
            var secretKey = Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY");
            var region = Environment.GetEnvironmentVariable("AWS_REGION") ?? "ap-northeast-1";
            var bucketName = Environment.GetEnvironmentVariable("S3_BUCKET_NAME") ?? "your-bucket-name";

            if (string.IsNullOrWhiteSpace(accessKey) || string.IsNullOrWhiteSpace(secretKey))
            {
                return this.StatusCode(500, new { message = "AWS credentials are not configured." });
            }

            var credentials = new Amazon.Runtime.BasicAWSCredentials(accessKey, secretKey);
            var regionEndpoint = RegionEndpoint.GetBySystemName(region);

            using var s3Client = new AmazonS3Client(credentials, regionEndpoint);

            var request = new GetPreSignedUrlRequest
            {
                BucketName = bucketName,
                Key = fileName,
                Verb = HttpVerb.PUT,
                ContentType = contentType,
                //// URLの有効期限(秒)
                Expires = DateTime.UtcNow.AddSeconds(60),
            };

            var url = s3Client.GetPreSignedURL(request);

            return this.Ok(new { url });
        }
    }
}

別途AWSSDK.S3をインストールします。

dotnet add package AWSSDK.S3

フロントエンド

  // S3署名付きURLを取得してファイルをアップロードし、S3フルパスを返す
  const uploadToS3 = async (file: File): Promise<string> => {
    const uniqueKey = `uploads/${Date.now()}-${file.name}`;

    const apiUrl = import.meta.env.VITE_API_URL;

    // 署名付きURLの取得
    const query = `fileName=${encodeURIComponent(
      uniqueKey
    )}&contentType=${encodeURIComponent(file.type)}`;
    const response = await fetch(
      `${apiUrl}/api/upload/generate-presigned-url?${query}`,
      {
        method: "GET",
        headers: {
          "Content-Type": "application/json",
        },
      }
    );

    if (!response.ok) {
      throw new Error("署名付きURLの取得に失敗しました");
    }

    const presigned = await response.json(); // { url: "..." } が入る

    // 署名付きURLを使用した画像アップロード
    const uploadRes = await fetch(presigned.url, {
      method: "PUT",
      headers: { "Content-Type": file.type },
      body: file,
    });

    if (!uploadRes.ok) {
      throw new Error("画像のアップロードに失敗しました。再度お試しください。");
    }

    // バケットの公開エンドポイント + オブジェクトキー
    return `${S3_BASE_URL}/${uniqueKey}`;
  };

uploadToS3 を画像アップロードしたいタイミングで呼び出すことで、S3にアップロードできます。
また、返却されたパスを使用することで画像を表示できるため、このパスをDBに保存しておけば、後から画像を取得・表示することも可能です。

AWSの設定

フロントエンドから直接S3にアクセスするには、バケットのアクセス許可設定を編集する必要があります。

バケットポリシー
※ 本設定ではオブジェクトが公開状態になるため、機密情報を含むファイルには使用しないでください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::バケット名/*"
        }
    ]
}

CORS

[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "PUT",
            "GET",
            "HEAD"
        ],
        "AllowedOrigins": [
            "http://フロントエンドのURL",
            "http://複数記載する場合"
        ],
        "ExposeHeaders": [
            "ETag"
        ]
    }
]

最後に

以上、署名付きURLを利用した S3 への直接アップロードの実装方法を紹介しました。
サーバーを経由せずにファイルをアップロードできるため、負荷軽減やパフォーマンス向上が期待できます。

参考

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?