この記事では、Express.jsを使用してAWS SDKを利用し、S3バケットにファイルをアップロードする方法と、署名付きURL(Presigned URL)を生成する方法を紹介する。

全体像

実装の流れ

  • パッケージインストール
  • 環境変数設定
  • S3に接続するためのクラスを作成する
  • MediaFileモデルの実装
  • MediaFileを操作するためのコントローラーを実装
  • ルーティングを定義

実装

パッケージインストール

以下のパッケージをインストールする。

npm install @aws-sdk/client-s3 @aws-sdk/s3-request-presigner multer uuid
  • @aws-sdk/client-s3:
    Amazon S3 (Simple Storage Service) を操作するためのクライアントライブラリ
  • @aws-sdk/s3-request-presigner:
    Amazon S3のリクエストに対して事前署名URL (Presigned URL) を生成するためのライブラリ
  • multer:
    Node.jsとExpressのためのミドルウェアで、フォームデータ(特にファイルのアップロード)の処理を行う。ファイルのアップロード、アップロードされたファイルの一時保存などをサポートする。

環境変数を設定

バケット名、アクセスキー、シークレットアクセスキー、リージョンを環境変数で登録する。

# AWS S3 設定情報
AWS_S3_BUCKET_NAME=xxxxxxx
AWS_S3_ACCESS_KEY_ID=xxxxxxxxxx
AWS_S3_SECRET_ACCESS_KEY=xxxxxxxxxx
AWS_REGION=ap-northeast-1

S3に接続するためのクラスを作成する

以下のコードでS3クライアントの設定とファイルのアップロード、および署名付きURLを生成するクラスを作成する。

src/config/aws/s3.ts

import {
  S3Client,
  PutObjectCommand,
  GetObjectCommand,
} from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import dotenv from "dotenv";
import { v4 as uuidv4 } from "uuid";

dotenv.config();

// Presigned URLの有効期限: 1時間有効で設定
const SIGNED_URL_EXPIRES_IN = 3600;

class AWSS3Config {
  private static instance: AWSS3Config;
  private s3Client: S3Client;

  private constructor() {
    // S3クライアントの初期化
    this.s3Client = new S3Client({
      region: process.env.AWS_REGION,
      credentials: {
        accessKeyId: process.env.AWS_S3_ACCESS_KEY_ID!,
        secretAccessKey: process.env.AWS_S3_SECRET_ACCESS_KEY!,
      },
    });
  }

  public static getInstance(): AWSS3Config {
    if (!AWSS3Config.instance) {
      AWSS3Config.instance = new AWSS3Config();
    }
    return AWSS3Config.instance;
  }

  /**
   * S3にファイルをアップロードする
   * @param file ファイル
   * @param directory ディレクトリパス(例: 'post/{postId}')
   * @returns アップロードされたファイルのURL
   */
  public async uploadToS3(file: Express.Multer.File, directory: string) {
    const params = {
      Bucket: process.env.AWS_S3_BUCKET_NAME,
      Key: `${directory}/${uuidv4()}-${file.originalname}`, // ディレクトリパスを含むユニークなファイル名を生成
      Body: file.buffer,
      ContentType: file.mimetype,
    };

    try {
      const command = new PutObjectCommand(params);
      await this.s3Client.send(command);
      return `https://${process.env.AWS_S3_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${params.Key}`;
    } catch (error) {
      console.error("Error uploading to s3: ", error);
      throw error;
    }
  }

  /**
   * Presigned URLを生成する関数(GET操作用)
   * @param key S3オブジェクトのキー(ファイル名)
   * @returns Presigned URL
   */
  public async generatePresignedUrlForViewing(key: string): Promise<string> {
    const command = new GetObjectCommand({
      Bucket: process.env.AWS_S3_BUCKET_NAME,
      Key: key,
    });

    try {
      const url = await getSignedUrl(this.s3Client, command, {
        expiresIn: SIGNED_URL_EXPIRES_IN,
      });
      return url;
    } catch (error) {
      console.error("Error generating presigned URL: ", error);
      throw error;
    }
  }
}

export default AWSS3Config.getInstance();

MediaFileモデルの実装

次に、MediaFileモデルを定義する。このモデルは、アップロードされたファイルのデータを保存する。

src/model/MediaFile.ts

import mongoose, { Document, Schema } from "mongoose";

export interface IMediaFile extends Document {
  url: string;
  title?: string;
  description?: string;
  alt_text?: string;
  file_type: string; // 画像、動画、PDFなど
  uploaded_by: string; // Cognito Sub
  created_at: Date;
  updated_at: Date;
}

const mediaFileSchema = new Schema(
  {
    url: { type: String, required: true },
    title: { type: String },
    description: { type: String },
    alt_text: { type: String },
    file_type: { type: String, required: true }, // 画像、動画、PDFなど
    uploaded_by: { type: String, required: true }, // Cognito Sub
  },
  { timestamps: { createdAt: "created_at", updatedAt: "updated_at" } }
);

const MediaFile = mongoose.model<IMediaFile>("MediaFile", mediaFileSchema);

export default MediaFile;

MediaFileを操作するためのコントローラーを実装

次に、ファイルのアップロードや署名付きURLの生成を行うコントローラーを実装する。

src/controllers/MediaFileController.ts

import { NextFunction, Request, Response } from "express";
import MediaFile from "../model/MediaFile";
import AWSS3Config from "../config/aws/s3";

// メディアファイルの一覧取得
export const getFiles = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const results = await MediaFile.find();
    return res.status(200).json({ status: "success", data: results });
  } catch (error) {
    next(error);
  }
};

/**
 * 画像をS3にアップロードして、その情報をImageモデルに保存する
 */
export const uploadFile = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const file = req.file;
    if (!file) {
      return res.status(400).json({ message: "No file uploaded" });
    }

    const directory = req.body.directory || "others";
    // アップロードしてURLを受け取る
    const s3Url = await AWSS3Config.uploadToS3(file, directory);

    // アップロード失敗の場合はエラーにする
    if (!s3Url) {
      throw new Error("S3 upload failed");
    }

    const { title, description, alt_text, uploaded_by, file_type } = req.body;
    // MediaFileモデルに保存
    const newMediaFile = new MediaFile({
      url: s3Url,
      title,
      description,
      alt_text,
      file_type,
      uploaded_by,
    });
    await newMediaFile.save();

    res.status(201).json({ status: "success", data: newMediaFile });
  } catch (error) {
    next(error);
  }
};

/**
 * Presigned URLを生成するコントローラー関数
 */
export const generatePresignedUrlForViewing = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const { url } = req.body;

  // URL未指定の場合は400エラーを返す
  if (!url) {
    return res
      .status(400)
      .json({ status: "error", message: "URL is required" });
  }

  try {
    // URLからバケット名とキーを抽出
    const bucketName = process.env.AWS_S3_BUCKET_NAME;
    const key = url.split(
      `${bucketName}.s3.${process.env.AWS_REGION}.amazonaws.com/`
    )[1];

    // keyが抽出できない場合は400エラーを返す
    if (!key) {
      return res.status(400).json({ status: "error", message: "Invalid URL" });
    }

    // Presigned URLを生成
    const presignedUrl = await AWSS3Config.generatePresignedUrlForViewing(key);
    res.status(200).json({ status: "success", data: presignedUrl });
  } catch (error) {
    next(error);
  }
};

ルーティングを定義

最後に、ルーティングを定義する。ここでは、ファイルのアップロードや署名付きURLの生成を行うエンドポイントを定義。

src/routes/mediaFile.ts

import express from "express";
import {
  generatePresignedUrlForViewing,
  uploadFile,
  getFiles,
} from "../controllers/MediaFileController";
import { checkAuth } from "../middleware/authMiddleware";
import multer from "multer";

const router = express.Router();
const upload = multer();

router.get("/list", getFiles);

router.post("/upload", checkAuth, upload.single("file"), uploadFile);

router.post(
  "/generate-presigned-url",
  checkAuth,
  generatePresignedUrlForViewing
);

export default router;

これで、AWS SDKを使用してS3へのファイルアップロードと署名付きURLの取得ができるようになる。

カテゴリー: awsNode.js