😺
RemixでFirestore Storageへファイルアップロード機能を作る
はじめに
zip ファイルのアップロードとダウンロードが出来る Web アプリを作っていた。Remix 公式ドキュメントに Cloudinary へ画像アップロードするサンプルコードはありましたが、Firebase Storage へのものは無かったので、メモする。
環境
- remix-run/{node,react}: ^2.15.0
- firebase-admin: ^13.0.1
準備
import { Form } from "@remix-run/react";
import { initializeApp, cert } from 'firebase-admin/app';
// Firebase初期化
initializeApp({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
});
export default function Index() {
return (
<Form
method="post"
encType="multipart/form-data"
>
<input type="file" name="file" id="" />
<button type="submit">
upload
</button>
</Form>
);
}
Remix Utilities を使う方法
※try-catch は割愛
import type {
ActionFunctionArgs,
UploadHandler,
} from "@remix-run/node";
import {
unstable_composeUploadHandlers as composeUploadHandlers,
unstable_createFileUploadHandler as createFileUploadHandler,
unstable_createMemoryUploadHandler as createMemoryUploadHandler,
unstable_parseMultipartFormData as parseMultipartFormData,
writeAsyncIterableToWritable
} from "@remix-run/node";
import { getStorage } from "firebase-admin/storage";
/**
* @param data The iterable of the file bytes
* @param name The name of the file in this bucket. (like images/rickroll.mp4)
* @param expires Signed URL expiration
* @returns
*/
async function uploadToFirebaseStorage(data: AsyncIterable<Uint8Array>, filename: string, expires?: number) {
const storage = getStorage().bucket();
const blob = storage.file(filename);
const writableStream = blob.createWriteStream();
await writeAsyncIterableToWritable(data, writableStream);
const [url] = await blob.getSignedUrl({
action: 'read',
expires: expires ?? Date.now() + 60 * 60 * 1000,
});
return url
}
export const action = async ({ request }: ActionFunctionArgs) => {
const uploadHandler: UploadHandler = composeUploadHandlers(
async ({ name, contentType, data, filename }) => {
if (name !== "file" || !filename) {
return undefined;
}
// const fileExtension = filename.split(".").pop()?.toLowerCase();
// if (!ALLOWED_FILE_EXTENSIONS.includes(fileExtension || "")) {
// throw new Error("Invalid file extension");
// }
const signedUrl = await uploadToFirebaseStorage(
data,
filename,
Date.now() + 60 * 60 * 1000
);
return signedUrl;
},
createMemoryUploadHandler()
);
const formData = await parseMultipartFormData(request, uploadHandler)
const imageSrc = formData.get("file")
return json({ imageSrc, error: null });
};
Remix Utilities を使わない方法
※try-catch は割愛
import type { ActionFunctionArgs } from "@remix-run/node";
import { Readable } from 'stream';
import { getStorage } from 'firebase-admin/storage';
export const action = async ({ request }: ActionFunctionArgs) => {
const storage = getStorage().bucket();
const form = await request.formData();
const file = form.get("file") as File
const fileStream = Readable.from(file.stream());
const blob = storage.file(`images/${file.name}`);
await new Promise((resolve, reject) => {
fileStream.pipe(blob.createWriteStream())
.on('finish', resolve)
.on('error', reject)
});
const [url] = await blob.getSignedUrl({
action: 'read',
expires: Date.now() + 60 * 60 * 1000,
});
return json({ imgSrc: url, error: null });
};
Discussion