Next.js App RouterでS3ファイルをアップロード・取得・削除する
前提
この記事では、以下の技術を使ったS3ファイルの操作方法を記述していきます。
- Next.js App Router (v14.2.10)
- Amazon S3
- AWS SDK for JavaScript v3
- AWS IAMユーザー(SDK認証用)
なお、この記事ではローカルで挙動確認することを目的としているため、簡易的にIAMユーザーを用いています。AWS上で恒久的なシステムとして公開する場合は、セキュリティ強化のため各サービスのIAMロールやCognitoの利用をご検討ください。
事前準備
事前準備として、S3バケットを作成し、IAMユーザにS3アクションを許可するように設定します。
手順の内容はすべてAWSコンソール上で操作します。
S3
まずはS3バケットを作成します。
バケット作成
-
バケットを作成
を押下 -
バケット名
に任意の値を入力 - 他はデフォルトのまま、ページ最下部の
バケットを作成
を押下
IAM
続いて、IAMユーザーにS3操作を実行できるポリシーを関連付けます。
ポリシー作成
- 左メニューで
ポリシー
を押下 -
ポリシーの作成
を押下 - 以下のとおりに設定
- サービス:
S3
- アクション許可:
GetObject
,PutObject
,DeleteObject
- リソース > object
-
ARNを設定
を押下 - Resource bucket name:作成したS3のバケット名
- Resource object name:
*
-
ARNを追加
を押下
-
- サービス:
-
次へ
を押下 -
ポリシー名
に任意の値を入力 -
ポリシーの作成
を押下
ここではResource object name
を*
と設定しているため、作成したS3バケットに存在するすべてのオブジェクトに対して許可を与えている設定となります。
対象のオブジェクトを制限したい場合は、制限に応じた内容でポリシーを編集します。
ユーザー作成
- 左メニューで
ユーザー
を押下 -
ユーザーの作成
を押下 -
ユーザー名
に任意の値を入力 -
次へ
を押下 - 以下のとおりに設定
- 許可のオプション:
ポリシーを直接アタッチする
- 許可ポリシー:作成したポリシー名
- 許可のオプション:
-
次へ
を押下 -
ユーザーの作成
を押下
アクセスキー発行
IAMユーザのアクセスキーが未発行の場合のみ対応します。
- 左メニューで
ユーザー
を押下 - 対象のユーザー名を押下
-
セキュリティ認証情報
タブ内のアクセスキーを作成
を押下 - 以下のとおりに設定
- ユースケース:
ローカルコード
- 上記のレコメンデーションを理解し、アクセスキーを作成します。:ON
- ユースケース:
-
次へ
を押下 -
アクセスキーを作成
を押下 - 画面上に
アクセスキー
とシークレットアクセスキー
が表示されるので、コピーして控えておく
Next.js
プロジェクトの作成については省略します。公式ドキュメントをご参照ください。
環境変数設定
機密情報秘匿のため、AWS関連の値を環境変数として設定します。
リポジトリ直下の.env
に、以下の記述を追加します。
AWS_REGION={AWSリージョン}
S3_BUCKET_NAME={S3バケット名}
IAM_ACCESS_KEY={IAMユーザのアクセスキー}
IAM_SECRET_ACCESS_KEY={IAMユーザのシークレットアクセスキー}
SDKライブラリ追加
以下のコマンドを実行して、AWS SDKライブラリを追加します。
npm install @aws-sdk/client-s3
または
yarn add @aws-sdk/client-s3
これで、IAMユーザーによる認証でS3操作を実行する準備が整いました。
以降は、Next.js App RouterにてS3操作を実装していきます。
ファイルをアップロードする
サーバアクション
"use server";
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.IAM_ACCESS_KEY!,
secretAccessKey: process.env.IAM_SECRET_ACCESS_KEY!,
},
});
export async function uploadFile(base64: string, fileName: string) {
try {
const command = new PutObjectCommand({
Bucket: process.env.S3_BUCKET_NAME!,
Key: fileName,
Body: Buffer.from(base64.split(",")[1], "base64")
});
await s3Client.send(command).then(() => {
console.log("File uploaded successfully");
}).catch((error) => {
console.error("Error uploading file:", error);
throw error;
});
return;
} catch (error) {
console.error("Error uploading file:", error);
throw error;
}
}
まず最初にAWSリージョンとIAMユーザの認証情報を指定し、S3Client
を生成しています。
サーバアクションはシリアライズ可能な値のみを送受できるため、アップロードするファイルはbase64の文字列として受け取ります。S3へのコマンド送信のため、さらにBuffer
に変換しています。
ファイルはS3バケット直下に、引数として渡されたファイル名でアップロードされます。
コンポーネント
"use client";
import { uploadFile } from "./action";
function convertFileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result as string);
reader.onerror = (error) => reject(error);
});
}
async function handleUpload(event: React.ChangeEvent<HTMLInputElement>) {
try {
const file = event.target.files?.[0];
if (file) {
const base64 = await convertFileToBase64(file);
await uploadFile(base64, file.name);
}
} catch (error) {
console.error("Error uploading file:", error);
}
}
export default function UploadInput() {
return <input type="file" onChange={handleUpload} />;
}
フィールドにてファイルを選択した時点で、アップロード処理が動きます。
File
をbase64形式のstring
に変換したうえで、サーバアクションに送信します。
実行後にサーバでFile uploaded successfully
のログが出たら、アップロード成功です。
ファイルを取得する
サーバアクション
"use server";
import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3";
import { Readable } from "stream";
const s3Client = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.IAM_ACCESS_KEY!,
secretAccessKey: process.env.IAM_SECRET_ACCESS_KEY!,
},
});
export async function getFile(): Promise<Buffer> {
try {
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET_NAME,
Key: "test.txt",
});
const response = await s3Client.send(command);
const stream = response.Body as Readable;
const chunks: Buffer[] = [];
return new Promise((resolve, reject) => {
stream.on("data", (chunk) => {
chunks.push(chunk);
});
stream.on("end", () => {
const buffer = Buffer.concat(chunks);
resolve(buffer);
});
stream.on("error", (error) => {
reject(`An error occurred: ${error.message}`);
});
});
} catch (error) {
console.error("Error getting file:", error);
throw error;
}
}
アップロードと同じようにKey
へのパスを指定してS3コマンドを実行しますが、今回はtest.txt
で固定としています。
S3からのレスポンスをReadable
に型変換し、すべてのチャンクを結合してからBuffer
としてクライアントに返却します。
コンポーネント
"use client";
import { getFile } from "./action";
async function handleGetButton() {
const buffer = Buffer.from(await getFile());
const fileContent = buffer.toString("utf-8");
const blob = new Blob([fileContent], {
type: "text/plain",
});
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "test.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
}
export default function GetFileButton() {
return <button onClick={handleGetButton}>ファイル取得</button>;
}
ボタンが押下されると、イベントハンドラからサーバアクションを実行します。
今回はUTF-8への文字コード変換後、Blob
を作成して自動でダウンロードさせています。
テキストファイル以外を取得する場合
例としてtest.png
の変換処理のうち、test.txt
と変更がある箇所のみ記載します。
サーバアクションでは、S3バケットのファイルパス(Key
)を変更します。
const command = new GetObjectCommand({
Bucket: process.env.S3_BUCKET_NAME,
Key: "test.png",
});
コンポーネントでは、MIMEタイプとダウンロードファイル名を変更します。
また、文字コード変換処理は削除します。
const buffer = Buffer.from(await getFile());
const blob = new Blob([new Uint8Array(buffer)], {
type: "image/png",
});
// ...中略...
a.download = "test.png";
ファイルを削除する
サーバアクション
"use server";
import { DeleteObjectCommand, S3Client } from "@aws-sdk/client-s3";
const s3Client = new S3Client({
region: process.env.AWS_REGION!,
credentials: {
accessKeyId: process.env.IAM_ACCESS_KEY!,
secretAccessKey: process.env.IAM_SECRET_ACCESS_KEY!,
},
});
export async function deleteFile() {
try {
const command = new DeleteObjectCommand({
Bucket: process.env.S3_BUCKET_NAME!,
Key: "test.txt",
});
const response = await s3Client.send(command);
console.log("File deleted successfully");
return response;
} catch (error) {
console.error("Error deleting file:", error);
throw error;
}
}
こちらもKey
はtest.txt
で固定としています。
コンポーネント
"use client";
import { deleteFile } from "./action";
async function handleDelete () {
await deleteFile();
};
export default function DeleteButton() {
return <button onClick={handleDelete}>ファイル削除</button>;
}
ボタンが押下されると、イベントハンドラからサーバアクションを実行します。
実行後にサーバでFile deleted successfully
のログが出たら、削除成功です。
NCDC株式会社( ncdc.co.jp/ )のエンジニアチームです。 募集中のエンジニアのポジションや、採用している技術スタックの紹介などはこちら( github.com/ncdcdev/recruitment )をご覧ください! ※エンジニア以外も記事を投稿することがあります
Discussion