파일을 업로드할 때 스트리밍이 되지 않고 버퍼링하던 버그 수정

This commit is contained in:
static
2025-01-14 18:06:41 +09:00
parent 6015a9bca4
commit f4b9f87087
5 changed files with 66 additions and 13 deletions

View File

@@ -50,6 +50,7 @@
"vite": "^5.4.11" "vite": "^5.4.11"
}, },
"dependencies": { "dependencies": {
"@fastify/busboy": "^3.1.1",
"argon2": "^0.41.1", "argon2": "^0.41.1",
"better-sqlite3": "^11.7.2", "better-sqlite3": "^11.7.2",
"drizzle-orm": "^0.33.0", "drizzle-orm": "^0.33.0",

8
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
'@fastify/busboy':
specifier: ^3.1.1
version: 3.1.1
argon2: argon2:
specifier: ^0.41.1 specifier: ^0.41.1
version: 0.41.1 version: 0.41.1
@@ -602,6 +605,9 @@ packages:
resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} 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': '@humanfs/core@0.19.1':
resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
engines: {node: '>=18.18.0'} engines: {node: '>=18.18.0'}
@@ -2543,6 +2549,8 @@ snapshots:
dependencies: dependencies:
levn: 0.4.1 levn: 0.4.1
'@fastify/busboy@3.1.1': {}
'@humanfs/core@0.19.1': {} '@humanfs/core@0.19.1': {}
'@humanfs/node@0.16.6': '@humanfs/node@0.16.6':

View File

@@ -2,7 +2,8 @@ import { error } from "@sveltejs/kit";
import { createReadStream, createWriteStream } from "fs"; import { createReadStream, createWriteStream } from "fs";
import { mkdir, stat, unlink } from "fs/promises"; import { mkdir, stat, unlink } from "fs/promises";
import { dirname } from "path"; 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 { v4 as uuidv4 } from "uuid";
import { IntegrityError } from "$lib/server/db/error"; import { IntegrityError } from "$lib/server/db/error";
import { import {
@@ -94,7 +95,7 @@ const safeUnlink = async (path: string) => {
export const uploadFile = async ( export const uploadFile = async (
params: Omit<NewFileParams, "path">, params: Omit<NewFileParams, "path">,
encContentStream: ReadableStream<Uint8Array>, encContentStream: Readable,
) => { ) => {
const oneMinuteAgo = new Date(Date.now() - 60 * 1000); const oneMinuteAgo = new Date(Date.now() - 60 * 1000);
const oneMinuteLater = 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 }); await mkdir(dirname(path), { recursive: true });
try { try {
await encContentStream.pipeTo( await pipeline(encContentStream, createWriteStream(path, { flags: "wx", mode: 0o600 }));
Writable.toWeb(createWriteStream(path, { flags: "wx", mode: 0o600 })),
);
await registerFile({ await registerFile({
...params, ...params,
path, path,

View File

@@ -60,9 +60,16 @@
data.id, data.id,
$masterKeyStore?.get(1)!, $masterKeyStore?.get(1)!,
$hmacSecretStore?.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 () => { const loadAndUploadFile = async () => {

View File

@@ -1,19 +1,57 @@
import Busboy from "@fastify/busboy";
import { error, text } from "@sveltejs/kit"; import { error, text } from "@sveltejs/kit";
import { Readable, Writable } from "stream";
import { authorize } from "$lib/server/modules/auth"; import { authorize } from "$lib/server/modules/auth";
import { fileUploadRequest } from "$lib/server/schemas"; import { fileUploadRequest } from "$lib/server/schemas";
import { uploadFile } from "$lib/server/services/file"; import { uploadFile } from "$lib/server/services/file";
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
const parseFormData = async (contentType: string, body: ReadableStream<Uint8Array>) => {
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 }) => { export const POST: RequestHandler = async ({ locals, request }) => {
const { userId } = await authorize(locals, "activeClient"); const { userId } = await authorize(locals, "activeClient");
const form = await request.formData(); const contentTypeHeader = request.headers.get("Content-Type");
const metadata = form.get("metadata"); if (!contentTypeHeader?.startsWith("multipart/form-data") || !request.body) {
const content = form.get("content");
if (typeof metadata !== "string" || !(content instanceof File)) {
error(400, "Invalid 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)); const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata));
if (!zodRes.success) error(400, "Invalid request body"); if (!zodRes.success) error(400, "Invalid request body");
const { const {
@@ -53,7 +91,7 @@ export const POST: RequestHandler = async ({ locals, request }) => {
encLastModifiedAt: lastModifiedAt, encLastModifiedAt: lastModifiedAt,
encLastModifiedAtIv: lastModifiedAtIv, encLastModifiedAtIv: lastModifiedAtIv,
}, },
content.stream(), content,
); );
return text("File uploaded", { headers: { "Content-Type": "text/plain" } }); return text("File uploaded", { headers: { "Content-Type": "text/plain" } });
}; };