From f4b9f870870227c2a5d1e603f02d2e5df50b211d Mon Sep 17 00:00:00 2001 From: static Date: Tue, 14 Jan 2025 18:06:41 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EC=97=85?= =?UTF-8?q?=EB=A1=9C=EB=93=9C=ED=95=A0=20=EB=95=8C=20=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A6=AC=EB=B0=8D=EC=9D=B4=20=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EA=B3=A0=20=EB=B2=84=ED=8D=BC=EB=A7=81=ED=95=98=EB=8D=98=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + pnpm-lock.yaml | 8 ++++ src/lib/server/services/file.ts | 9 ++-- .../(main)/directory/[[id]]/+page.svelte | 13 +++-- src/routes/api/file/upload/+server.ts | 48 +++++++++++++++++-- 5 files changed, 66 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index dfa988e..91c3c3e 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "vite": "^5.4.11" }, "dependencies": { + "@fastify/busboy": "^3.1.1", "argon2": "^0.41.1", "better-sqlite3": "^11.7.2", "drizzle-orm": "^0.33.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dccbc50..43441bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + '@fastify/busboy': + specifier: ^3.1.1 + version: 3.1.1 argon2: specifier: ^0.41.1 version: 0.41.1 @@ -602,6 +605,9 @@ packages: resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/busboy@3.1.1': + resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2543,6 +2549,8 @@ snapshots: dependencies: levn: 0.4.1 + '@fastify/busboy@3.1.1': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index cb5559b..8a05eb6 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -2,7 +2,8 @@ import { error } from "@sveltejs/kit"; import { createReadStream, createWriteStream } from "fs"; import { mkdir, stat, unlink } from "fs/promises"; import { dirname } from "path"; -import { Readable, Writable } from "stream"; +import { Readable } from "stream"; +import { pipeline } from "stream/promises"; import { v4 as uuidv4 } from "uuid"; import { IntegrityError } from "$lib/server/db/error"; import { @@ -94,7 +95,7 @@ const safeUnlink = async (path: string) => { export const uploadFile = async ( params: Omit, - encContentStream: ReadableStream, + encContentStream: Readable, ) => { const oneMinuteAgo = new Date(Date.now() - 60 * 1000); const oneMinuteLater = new Date(Date.now() + 60 * 1000); @@ -106,9 +107,7 @@ export const uploadFile = async ( await mkdir(dirname(path), { recursive: true }); try { - await encContentStream.pipeTo( - Writable.toWeb(createWriteStream(path, { flags: "wx", mode: 0o600 })), - ); + await pipeline(encContentStream, createWriteStream(path, { flags: "wx", mode: 0o600 })); await registerFile({ ...params, path, diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 5361fd7..866adbe 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -60,9 +60,16 @@ data.id, $masterKeyStore?.get(1)!, $hmacSecretStore?.get(1)!, - ).then(() => { - info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME - }); + ) + .then(() => { + // TODO: FIXME + info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); + window.alert("파일이 업로드되었어요."); + }) + .catch(() => { + // TODO: FIXME + window.alert("파일 업로드에 실패했어요."); + }); }; const loadAndUploadFile = async () => { diff --git a/src/routes/api/file/upload/+server.ts b/src/routes/api/file/upload/+server.ts index 0b72402..66624f6 100644 --- a/src/routes/api/file/upload/+server.ts +++ b/src/routes/api/file/upload/+server.ts @@ -1,19 +1,57 @@ +import Busboy from "@fastify/busboy"; import { error, text } from "@sveltejs/kit"; +import { Readable, Writable } from "stream"; import { authorize } from "$lib/server/modules/auth"; import { fileUploadRequest } from "$lib/server/schemas"; import { uploadFile } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; +const parseFormData = async (contentType: string, body: ReadableStream) => { + return new Promise<{ metadata: string; content: Readable }>((resolve, reject) => { + let metadata: string | null = null; + let content: Readable | null = null; + + const bb = Busboy({ headers: { "content-type": contentType } }); + bb.on("field", (fieldname, val) => { + if (fieldname !== "metadata") return reject(new Error("Invalid request body")); + if (metadata || content) return reject(new Error("Invalid request body")); // metadata must be first + metadata = val; + }); + bb.on("file", (fieldname, file) => { + if (fieldname !== "content") return reject(new Error("Invalid request body")); + if (!metadata || content) return reject(new Error("Invalid request body")); // metadata must be first + content = file; + resolve({ metadata, content }); + }); + bb.on("finish", () => reject(new Error("Invalid request body"))); + bb.on("error", (e) => reject(e)); + + body.pipeTo(Writable.toWeb(bb)); + }); +}; + export const POST: RequestHandler = async ({ locals, request }) => { const { userId } = await authorize(locals, "activeClient"); - const form = await request.formData(); - const metadata = form.get("metadata"); - const content = form.get("content"); - if (typeof metadata !== "string" || !(content instanceof File)) { + const contentTypeHeader = request.headers.get("Content-Type"); + if (!contentTypeHeader?.startsWith("multipart/form-data") || !request.body) { error(400, "Invalid request body"); } + let metadata; + let content; + + try { + const formData = await parseFormData(contentTypeHeader, request.body); + metadata = formData.metadata; + content = formData.content; + } catch (e) { + if (e instanceof Error && e.message === "Invalid request body") { + error(400, "Invalid request body"); + } + throw e; + } + const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata)); if (!zodRes.success) error(400, "Invalid request body"); const { @@ -53,7 +91,7 @@ export const POST: RequestHandler = async ({ locals, request }) => { encLastModifiedAt: lastModifiedAt, encLastModifiedAtIv: lastModifiedAtIv, }, - content.stream(), + content, ); return text("File uploaded", { headers: { "Content-Type": "text/plain" } }); };