【Next.js】Vercel Blobでファイルアップロードを実装する②|FormDataを使ってAPI・コンポーネント作成

Vervel Blob作成方法② Programming

前回は、【Next.js】Vercel Blobでファイルアップロードを実装する①|Storage作成・環境変数設定で、@vercel/blobのインストールや環境変数の設定まで行いました。今回は、実際にファイルを登録する処理を実装していきます。

今回の処理では、ファイル本体をVercel Blobに保存し、ファイルに関する情報をPrisma/PostgreSQLに保存します。

大まかな流れは以下のようになります。

(クライアント側)
ファイルを選択
↓
FormDataにファイルとプロジェクトIDを追加
↓
APIに送信
↓
(サーバー側)
ログインユーザー・プロジェクトを確認
↓
Vercel Blobにファイルを保存
↓
ファイル情報をPrisma/PostgreSQLに保存
↓
登録完了

ちなみに、ポストの順番はAPI作成→ コンポーネント順で説明します。

APIを作成する

まず、ファイルを受け取るAPIを作成します。(ファイル名は任意です)

app
└── api
    └── documents
        └── route.ts

Vercel Blobへのアップロードにはputを使用します。

import { authOptions } from "@/lib/auth";
import prisma from "@/lib/prisma";
import { getServerSession } from "next-auth";
import { NextResponse } from "next/server";
import { put } from "@vercel/blob"; ⭐️⭐️⭐️

export async function POST(request: Request) {
    // ...
}

ログインユーザーを確認する

まず、ファイルをアップロードしようとしているユーザーがログインしているか確認します。

const session = await getServerSession(authOptions);

if (!session?.user?.email) {
    return NextResponse.json(
        { message: "Unauthorized" },
        { status: 401 }
    );
}

ログインしていない場合は、ここで処理を終了します。

その後、セッションのメールアドレスからユーザーを取得します。

const user = await prisma.user.findUnique({
    where: {
        email: session.user.email,
    },
});

if (!user) {
    return NextResponse.json(
        { message: "User not found" },
        { status: 404 }
    );
}

FormDataでデータを取得する

次に、コンポーネントから送信されたデータを取得します。

ファイルをアップロードする場合、JSONではなくFormDataを使用します。

const formData = await request.formData();

const projectID = formData.get("projectId") as string;
const file = formData.get("file") as File;

今回送信するデータは、この二つです。

projectId → ファイルを登録するプロジェクト
file → アップロードするファイル

通常の文字列などのデータだけであればJSONで送信できますが、今回はファイルそのものを送信するため、FormDataを使用します。また、formData.get("file")で取得したファイルはFileオブジェクトとして扱うことができます。

念のため、ファイルが正しく送信されているか確認します。

if (!(file instanceof File)) {
    return NextResponse.json(
        { message: "Invalid file" },
        { status: 400 }
    );
}

プロジェクトを確認する

次に、ファイルを登録するプロジェクトが存在するか確認します。こちらの作業は自分が作っているアプリケーションの内容によって異なります。

const project = await prisma.project.findUnique({
    where: {
        id: projectID,
    },
});

if (!project) {
    return NextResponse.json(
        { message: "Project not found" },
        { status: 404 }
    );
}

さらに、現在ログインしているユーザーが、そのプロジェクトの所有者かどうかも確認します。

if (project.userId !== user.id) {
    return NextResponse.json(
        { message: "Unauthorized" },
        { status: 401 }
    );
}

これは、他のユーザーのprojectIdを指定してファイルを登録できないようにするためです。

Vercel Blobにファイルを保存する

ユーザーとプロジェクトの確認が終わったら、Vercel Blobにファイルを保存します。

今回は、プロジェクトごとにファイルを管理しやすいようにパスを作成します。

const path = `projects/${projectID}/${file.name}`;

そして、put()を使ってファイルをアップロードします。

const blob = await put(path, file, {
    access: "private",
});

これで、ファイル本体はVercel Blobに保存されます。

ファイル情報をデータベースに保存する

ファイル本体をBlobに保存したら、次にファイルに関する情報をデータベースに保存します。

const document = await prisma.sourceDocument.create({
    data: {
        projectId: projectID,
        fileName: file.name,
        fileType: file.type,
        filePath: blob.url,
    },
});

ここでは、ファイル本体ではなく、

  • どのプロジェクトのファイルなのか
  • ファイル名
  • ファイルタイプ
  • Vercel Blobに保存されている場所

などの情報を保存しています。

つまり、今回の構成は以下のようになります。

ファイル本体
  ↓
Vercel Blob

ーーーーーーーーーーーー

ファイル情報
  ↓
Prisma / PostgreSQL

このように、ファイル本体とファイル情報を分けて管理します。

最後に、APIの結果を返します。

return NextResponse.json({
    message: "File uploaded successfully",
    document: document,
});

これでAPI側の処理は完了です。


アップロード用コンポーネントを作成する

次に、実際にファイルを選択してAPIに送信するコンポーネントを作成します。

今回は、ファイルとプロジェクトを選択できる簡単なコンポーネントを作成します。

"use client";

import { Project } from "@prisma/client";
import { useEffect, useState } from "react";

export default function Upload() {
    const [selectedFile, setSelectedFile] = useState<File | null>(null);
    const [projectId, setProjectId] = useState<string>("");
    const [projects, setProjects] = useState<Project[]>([]);

    // ...
}

selectedFileには選択したファイルを、projectIdにはファイルを登録するプロジェクトのIDを保存します。

ファイルを選択する

const handleFileChange = (
    event: React.ChangeEvent<HTMLInputElement>
) => {
    if (event.target.files && event.target.files.length > 0) {
        setSelectedFile(event.target.files[0]);
    }
};

input type="file"から選択したファイルをFileオブジェクトとして取得し、selectedFileに保存します。

プロジェクトを選択する

const handleProjectChange = (
    event: React.ChangeEvent<HTMLSelectElement>
) => {
    setProjectId(event.target.value);
};

選択したプロジェクトのIDをprojectIdに保存します。

FormDataを作成してAPIに送信する

ファイルとプロジェクトIDを取得できたら、FormDataを作成します。

const formData = new FormData();

formData.append("file", selectedFile);
formData.append("projectId", projectId);

ここで追加している名前は、API側で取得するときの名前と一致させます。

コンポーネント
formData.append("file", selectedFile)
formData.append("projectId", projectId)
        ↓
API
formData.get("file")
formData.get("projectId")

そして、作成したFormDataをAPIに送信します。

const response = await fetch("/api/documents", {
    method: "POST",
    body: formData,
});

ここではContent-Typeを自分で指定していません。

FormDataをbodyに指定すると、ブラウザがmultipart/form-dataとして必要な情報を設定してくれます。

そのため、以下のようにContent-Typeを手動で設定する必要はありません。

// 設定しない
headers: {
    "Content-Type": "multipart/form-data",
}

これで、選択したファイルとプロジェクトIDをAPIに送信できます。

実際の処理

今回のファイル登録処理をまとめると、以下のようになります。

① ファイルを選択
        ↓
② プロジェクトを選択
        ↓
③ FormDataを作成
        ↓
④ APIにPOST
        ↓
⑤ ログインユーザーを確認
        ↓
⑥ プロジェクトと所有者を確認
        ↓
⑦ Vercel Blobにファイルを保存
        ↓
⑧ BlobのURLなどをDBに保存
        ↓
⑨ ファイル登録完了

これで、Vercel Blobを使った基本的なファイル登録処理が実装できました。

ファイル本体はVercel Blobで管理し、データベースではファイル名やファイルタイプ、保存先などの情報を管理しています。

今回のTransFlowでは、この後にアップロードしたWordファイルからテキストを取得し、翻訳する単位に分割してデータベースに保存する処理を追加しています。

ファイル登録画像

分かりづらいかもしれませんが、プロジェクトを選んで、Docファイルをアップロードします。

結果は以下のように、「。,!」などの記号を基準に翻訳する単位が分割されます。

ファイル登録結果

こちらが原本です。

ファイル原本

タイトルとURLをコピーしました