Next.js ServerActionsでファイルアップロード・ダウンロード
Next.jsのServerActionsでファイルのアップトードとダウンロードの実装例です。
この記事ではローカルにファイルを保存する実装にしています。実際にはAWS S3やGoogle Cloud Storage、Cloudinaryなどのクラウドストレージにアップロードすると思いますが、ファイルを保存する部分だけ該当サービスのSDKを使ったコードに差し替えるだけでできます。
ファイルアップロード
ファイルアップロードは通常のフォームとServerActionsの組み合わせでできます。まずはフォームコンポーネントを作成します。<form>
の action
属性にアップロード処理の関数を指定します。
"use client";
import { uploadFile } from "@/app/file/_commands/uploadFile";
export function UploadForm() {
return (
<form action={uploadFile}>
<input type="file" name="file" />
<button type="submit">アップロード</button>
</form>
);
}
つづいてアップロードの処理を書いていきます。今回はトップディレクトリの ./uploads
にファイルを保存しています。
"use server";
import { promises as fs } from "node:fs";
import { resolve } from "node:path";
import { revalidatePath } from "next/cache";
export async function uploadFile(formData: FormData) {
const file = formData.get("file") as File;
if (file && file.size > 0) {
const data = await file.arrayBuffer();
const buffer = Buffer.from(data);
const filePath = resolve(
process.cwd(),
"./uploads",
`${crypto.randomUUID()}.${file.name.split(".").pop()}`,
);
await fs.writeFile(filePath, buffer);
}
revalidatePath("/file");
}
revalidatePath
は、ファイルアップロード後に画面の更新をするために指定していますが、画面の仕様によっては redirect
だったりするので適宜変更してください。
ファイルのダウンロード
ファイルのダウンロードはServerActionsは使わず、RouteHandlersを利用します。要は、ファイル専用のURLを作ってあげて、そこに遷移させるだけです。
download?file=foo.png
といった形でURLパラメータでファイル名を受け取り、ファイルの存在確認をした後に "Content-Disposition"
ヘッダーを返してダウンロードさせるだけです。
import { promises as fs } from "node:fs";
import { resolve } from "node:path";
import { type NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const fileName = searchParams.get("file");
if (!fileName) {
return new NextResponse("File not found", { status: 404 });
}
const dirPath = resolve(process.cwd(), "./uploads");
const files = await fs.readdir(dirPath);
if (!files.includes(fileName)) {
return new NextResponse("File not found", { status: 404 });
}
const filePath = resolve(dirPath, fileName);
const fileContent = await fs.readFile(filePath);
const headers = new Headers({
"Content-Disposition": `attachment; filename=${fileName}`,
});
return new NextResponse(fileContent, { headers });
}
ダウンロードする際には next/link
の <Link>
ではなく、通常のリンク <a>
を利用してください。
// ❌ NG
<Link href="download?file=foo.png">Download</Link>
// ✅ OK
<a href="download?file=foo.png">Download</a>
理由は、<Link>
だと router.push
でページ遷移が変更されてしまいダウンロード後の画面では revalidatePath
などが効かなくなります(おそらく繊維状では別のページに移動してしまっているので描画反映の対象外になっている)。ファイルをダウンロードするだけなので通常のリンクで問題ありません。
また、上記のコード例をGitHubリポジトリにpushしているので、完全版を見たい方は参考にしてみてください。
Discussion