Closed8
Remixでファイルのアップロード機能を作る
やりたいこと
- 画像ファイルをアップロードさせる
- ストレージにはCloudinaryを使用する
- 複数ファイルを同時にアップロード可能にする
まずはドキュメント読む。下記が参考になりそう
unstable_parseMultipartFormData
アプリケーションでのファイルアップロードを処理できるAPI。
このAPIを使う上ではFile APIを理解する必要があるみたいだな。
使い方
const formData = await unstable_parseMultipartFormData(request, uploadHandler);
export const action = async ({
request,
}: ActionFunctionArgs) => {
const formData = await unstable_parseMultipartFormData(request, uploadHandler);
const avatarUrl = formData.get("avatar");
await updateUserAvatar(request, avatarUrl);
return redirect("/account");
}
export default function AvatarUploadRoute() {
return (
<Form method="post" encType="multipart/form-data">
<label htmlFor="avatar-input">Avatar</label>
<input id="avatar-input" type="file" name="avatar" />
<button>Upload</button>
</Form>
);
}
アップロードされたファイルを読み取るには、Blob APIから継承した.text()
や.arrayBuffer()
などを使う。
uploadHandler
- ファイルアップロード機能における最も重要な役割を持つ
- ファイルの保存(ディスクやメモリなど)や外部のファイルストレージに送信するプロキシとして機能する
uploadHandlerを作成するには?
Remixは以下2つのユーティリティを提供してるので、これを使う感じっぽい。
unstable_createFileUploadHandler
unstable_createMemoryUploadHandler
unstable_createFileUploadHandler
- Node.jsのアップロードハンドラー
- ファイル名を持つ部分をディスクに書き込み、メモリに残さない。
- ファイル名のない部分は解析されない。
使い方
export const action = async ({
request,
}: ActionFunctionArgs) => {
const uploadHandler = unstable_composeUploadHandlers(
unstable_createFileUploadHandler({
maxPartSize: 5_000_000, // 最大アップロードサイズ(byte)
file: ({ filename }) => filename, // ディレクトリ内のファイル
}),
unstable_createMemoryUploadHandler()
);
const formData = await unstable_parseMultipartFormData(
request,
uploadHandler
);
const file = formData.get("avatar");
// file is a "NodeOnDiskFile" which implements the "File" API
// ... etc
};
各オプションについて
-
avoidFileConflicts
- ディスク上にすでに存在する場合は、ファイル名の最後にタイムスタンプを付加することで、ファイルの衝突を避ける。
-
directory
- アップロードを書き込むディレクトリ
-
file
-
object
を受け取り、string
を返す。 - ディレクトリ内のファイル名。相対パスも可能で、存在しない場合はディレクトリ構造が作成される。
-
-
maxPartSize
- 許容される最大アップロードサイズ(バイト)。このサイズを超えた場合は MaxPartSizeExceededError がスローされます。
-
fileter
- fileName, name, contentType, 名に基づいて、、ブール値またはPromiseを返す。
- アップロードされたファイルが保存されないようにするための関数。falseを返すとファイルは無視されます。
こちらも参考になりそうなのであとで読む
useFetcherを使った実装をしている
Exampleにいいリポジトリあった!
- Remixビルトインの uploadHandler と Form をマルチパートデータで使って、組み込みのローカルアップローダでファイルをアップロードし、カスタムアップローダで画像ファイルを cloudinary にアップロードして表示する簡単な例があったぞ。
Cloudinaryのドキュメント
下記ページがおそらく参考になりそう
サンプルを参考に実装してみる
下記を参考にしたCloudinary
にアップロードする用の関数
cloudinary.server.ts
import type { UploadApiResponse, UploadStream } from "cloudinary";
import { writeAsyncIterableToWritable } from "@remix-run/node";
import cloudinary from "cloudinary";
cloudinary.v2.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.API_KEY,
api_secret: process.env.API_SECRET,
});
console.log("configs", cloudinary.v2.config());
export async function uploadImageToCloudinary(data: AsyncIterable<Uint8Array>) {
const uploadPromise = new Promise<UploadApiResponse>(
async (resolve, reject) => {
const uploadStream: UploadStream = cloudinary.v2.uploader.upload_stream(
{
folder: "HotSprings",
},
(error, result) => {
if (error) {
reject(error);
return;
}
// resultの型エラーを防ぐために記述した
if (result === undefined) {
return;
}
resolve(result);
},
);
await writeAsyncIterableToWritable(data, uploadStream);
},
);
return uploadPromise;
}
実際にactionから呼び出す
hotsprings.new.tsx
export const action = async ({ request }: ActionFunctionArgs) => {
const uploadHandler: UploadHandler = composeUploadHandlers(
async ({ name, data }) => {
console.log({ name, data });
if (name !== "image") {
return undefined;
}
// アップロードした画像URLを返す
const uploadedImage = await uploadImageToCloudinary(data);
return uploadedImage.secure_url;
},
createMemoryUploadHandler(),
);
const formData = await parseMultipartFormData(request, uploadHandler);
const formDataObj = Object.fromEntries(formData);
const validationResult = CreateHotSpringSchema.safeParse(formDataObj);
if (!validationResult.success) {
return json({
validationErrors: validationResult.error.flatten().fieldErrors,
});
}
const user = await authenticator.isAuthenticated(request, {
failureRedirect: "/login",
});
const newHotSpring = await createHotSpring({
...validationResult.data,
authorId: user.id,
});
return null;
};
サンプルをもとに実装したがいくつか問題直面した
現状の課題
バリデーションの結果に画像の情報が含めたいが、画像情報を取ってこれない。Zod側での型定義に問題あり?それともフォームデータの持ち方に問題がある?
const formData = await parseMultipartFormData(request, uploadHandler);
const formDataObj = Object.fromEntries(formData);
// ここに画像の情報が含まれない🤔
const validationResult = CreateHotSpringSchema.safeParse(formDataObj);
formDataObj
とvalidationResult
の値を出力させてみた
対応1
- getAllメソッドで画像URLを配列で取得する
hotsprings.new.tsx
const formData = await parseMultipartFormData(request, uploadHandler);
// 画像URL値が格納された配列を取得
const imgUrls = formData.getAll("image");
const formDataObj = { ...Object.fromEntries(formData), images: imgUrls };
const validationResult = CreateHotSpringSchema.safeParse(formDataObj);
if (!validationResult.success) {
return json({
validationErrors: validationResult.error.flatten().fieldErrors,
});
}
しかし、formDataObj
の中身を見ると必要のないimageプロパティが残っている。これを削除したいがどうしようか..
対応2
deleteメソッドでFormData
オブジェクトからキーと値を削除してみる
const formData = await parseMultipartFormData(request, uploadHandler);
const imgUrls = formData.getAll("image");
formData.delete("image");
const formDataObj = { ...Object.fromEntries(formData), images: imgUrls };
image
プロパティを削除できた
このスクラップは2024/02/26にクローズされました