🚀

Next.js ServerActionsでファイルアップロード・ダウンロード

2024/06/25に公開

Next.jsのServerActionsでファイルのアップトードとダウンロードの実装例です。

この記事ではローカルにファイルを保存する実装にしています。実際にはAWS S3やGoogle Cloud Storage、Cloudinaryなどのクラウドストレージにアップロードすると思いますが、ファイルを保存する部分だけ該当サービスのSDKを使ったコードに差し替えるだけでできます。

ファイルアップロード

ファイルアップロードは通常のフォームとServerActionsの組み合わせでできます。まずはフォームコンポーネントを作成します。<form>action 属性にアップロード処理の関数を指定します。

UploadForm.tsx
"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 にファイルを保存しています。

uploadFile
"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" ヘッダーを返してダウンロードさせるだけです。

download/route.ts
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