From 594c3654c92464c67fa35703608677abb116f5ba Mon Sep 17 00:00:00 2001 From: static Date: Mon, 12 Jan 2026 05:04:07 +0900 Subject: [PATCH] =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EB=B0=8F=20=EC=8D=B8?= =?UTF-8?q?=EB=84=A4=EC=9D=BC=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20End?= =?UTF-8?q?point=EC=9D=98=20=ED=95=B8=EB=93=A4=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?=ED=95=98=EB=82=98=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/http.ts | 14 ++++-- src/params/thumbnail.ts | 5 ++ .../download/+server.ts | 24 ++++------ .../file/[id]/thumbnail/download/+server.ts | 48 ------------------- .../api/upload/[id]/chunks/[index]/+server.ts | 32 +++++-------- 5 files changed, 39 insertions(+), 84 deletions(-) create mode 100644 src/params/thumbnail.ts rename src/routes/api/file/[id]/{ => [[thumbnail=thumbnail]]}/download/+server.ts (54%) delete mode 100644 src/routes/api/file/[id]/thumbnail/download/+server.ts diff --git a/src/lib/modules/http.ts b/src/lib/modules/http.ts index 4f79ec5..4116c18 100644 --- a/src/lib/modules/http.ts +++ b/src/lib/modules/http.ts @@ -1,7 +1,7 @@ -export const parseRangeHeader = (rangeHeader: string | null) => { - if (!rangeHeader) return undefined; +export const parseRangeHeader = (value: string | null) => { + if (!value) return undefined; - const firstRange = rangeHeader.split(",")[0]!.trim(); + const firstRange = value.split(",")[0]!.trim(); const parts = firstRange.replace(/bytes=/, "").split("-"); return { start: parts[0] ? parseInt(parts[0], 10) : undefined, @@ -12,3 +12,11 @@ export const parseRangeHeader = (rangeHeader: string | null) => { export const getContentRangeHeader = (range?: { start: number; end: number; total: number }) => { return range && { "Content-Range": `bytes ${range.start}-${range.end}/${range.total}` }; }; + +export const parseContentDigestHeader = (value: string | null) => { + if (!value) return undefined; + + const firstDigest = value.split(",")[0]!.trim(); + const match = firstDigest.match(/^sha-256=:([A-Za-z0-9+/=]+):$/); + return match?.[1]; +}; diff --git a/src/params/thumbnail.ts b/src/params/thumbnail.ts new file mode 100644 index 0000000..3faf298 --- /dev/null +++ b/src/params/thumbnail.ts @@ -0,0 +1,5 @@ +import type { ParamMatcher } from "@sveltejs/kit"; + +export const match: ParamMatcher = (param) => { + return param === "thumbnail"; +}; diff --git a/src/routes/api/file/[id]/download/+server.ts b/src/routes/api/file/[id]/[[thumbnail=thumbnail]]/download/+server.ts similarity index 54% rename from src/routes/api/file/[id]/download/+server.ts rename to src/routes/api/file/[id]/[[thumbnail=thumbnail]]/download/+server.ts index 5324365..a79da41 100644 --- a/src/routes/api/file/[id]/download/+server.ts +++ b/src/routes/api/file/[id]/[[thumbnail=thumbnail]]/download/+server.ts @@ -2,14 +2,10 @@ import { error } from "@sveltejs/kit"; import { z } from "zod"; import { parseRangeHeader, getContentRangeHeader } from "$lib/modules/http"; import { authorize } from "$lib/server/modules/auth"; -import { getFileStream } from "$lib/server/services/file"; -import type { RequestHandler } from "./$types"; +import { getFileStream, getFileThumbnailStream } from "$lib/server/services/file"; +import type { RequestHandler, RouteParams } from "./$types"; -const downloadHandler = async ( - locals: App.Locals, - params: Record, - request: Request, -) => { +const downloadHandler = async (locals: App.Locals, params: RouteParams, request: Request) => { const { userId } = await authorize(locals, "activeClient"); const zodRes = z @@ -20,29 +16,29 @@ const downloadHandler = async ( if (!zodRes.success) error(400, "Invalid path parameters"); const { id } = zodRes.data; - const { encContentStream, range } = await getFileStream( + const getStream = params.thumbnail ? getFileThumbnailStream : getFileStream; + const { encContentStream, range } = await getStream( userId, id, parseRangeHeader(request.headers.get("Range")), ); return { stream: encContentStream, + status: range ? 206 : 200, headers: { "Accept-Ranges": "bytes", - "Content-Length": (range.end - range.start + 1).toString(), + "Content-Length": String(range.end - range.start + 1), "Content-Type": "application/octet-stream", ...getContentRangeHeader(range), }, - isRangeRequest: !!range, }; }; export const GET: RequestHandler = async ({ locals, params, request }) => { - const { stream, headers, isRangeRequest } = await downloadHandler(locals, params, request); - return new Response(stream as ReadableStream, { status: isRangeRequest ? 206 : 200, headers }); + const { stream, ...init } = await downloadHandler(locals, params, request); + return new Response(stream as ReadableStream, init); }; export const HEAD: RequestHandler = async ({ locals, params, request }) => { - const { headers, isRangeRequest } = await downloadHandler(locals, params, request); - return new Response(null, { status: isRangeRequest ? 206 : 200, headers }); + return new Response(null, await downloadHandler(locals, params, request)); }; diff --git a/src/routes/api/file/[id]/thumbnail/download/+server.ts b/src/routes/api/file/[id]/thumbnail/download/+server.ts deleted file mode 100644 index 85cdd8c..0000000 --- a/src/routes/api/file/[id]/thumbnail/download/+server.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { z } from "zod"; -import { parseRangeHeader, getContentRangeHeader } from "$lib/modules/http"; -import { authorize } from "$lib/server/modules/auth"; -import { getFileThumbnailStream } from "$lib/server/services/file"; -import type { RequestHandler } from "./$types"; - -const downloadHandler = async ( - locals: App.Locals, - params: Record, - request: Request, -) => { - const { userId } = await authorize(locals, "activeClient"); - - const zodRes = z - .object({ - id: z.coerce.number().int().positive(), - }) - .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - - const { encContentStream, range } = await getFileThumbnailStream( - userId, - id, - parseRangeHeader(request.headers.get("Range")), - ); - return { - stream: encContentStream, - headers: { - "Accept-Ranges": "bytes", - "Content-Length": (range.end - range.start + 1).toString(), - "Content-Type": "application/octet-stream", - ...getContentRangeHeader(range), - }, - isRangeRequest: !!range, - }; -}; - -export const GET: RequestHandler = async ({ locals, params, request }) => { - const { stream, headers, isRangeRequest } = await downloadHandler(locals, params, request); - return new Response(stream as ReadableStream, { status: isRangeRequest ? 206 : 200, headers }); -}; - -export const HEAD: RequestHandler = async ({ locals, params, request }) => { - const { headers, isRangeRequest } = await downloadHandler(locals, params, request); - return new Response(null, { status: isRangeRequest ? 206 : 200, headers }); -}; diff --git a/src/routes/api/upload/[id]/chunks/[index]/+server.ts b/src/routes/api/upload/[id]/chunks/[index]/+server.ts index 47d6397..689d313 100644 --- a/src/routes/api/upload/[id]/chunks/[index]/+server.ts +++ b/src/routes/api/upload/[id]/chunks/[index]/+server.ts @@ -1,6 +1,8 @@ import { error, text } from "@sveltejs/kit"; import { Readable } from "stream"; +import { ReadableStream } from "stream/web"; import { z } from "zod"; +import { parseContentDigestHeader } from "$lib/modules/http"; import { authorize } from "$lib/server/modules/auth"; import { uploadChunk } from "$lib/server/services/upload"; import type { RequestHandler } from "./$types"; @@ -15,29 +17,21 @@ export const POST: RequestHandler = async ({ locals, params, request }) => { }) .safeParse(params); if (!zodRes.success) error(400, "Invalid path parameters"); - const { id: uploadId, index: chunkIndex } = zodRes.data; + const { id: sessionId, index: chunkIndex } = zodRes.data; - // Parse Content-Digest header (RFC 9530) - // Expected format: sha-256=:base64hash: - const contentDigest = request.headers.get("Content-Digest"); - if (!contentDigest) error(400, "Missing Content-Digest header"); - - const digestMatch = contentDigest.match(/^sha-256=:([A-Za-z0-9+/=]+):$/); - if (!digestMatch || !digestMatch[1]) - error(400, "Invalid Content-Digest format, must be sha-256=:base64:"); - const encChunkHash = digestMatch[1]; - - const contentType = request.headers.get("Content-Type"); - if (contentType !== "application/octet-stream" || !request.body) { + const encContentHash = parseContentDigestHeader(request.headers.get("Content-Digest")); + if (!encContentHash) { + error(400, "Invalid request headers"); + } else if (!request.body) { error(400, "Invalid request body"); } - // Convert web ReadableStream to Node Readable - const nodeReadable = Readable.fromWeb( - request.body as unknown as Parameters[0], + await uploadChunk( + userId, + sessionId, + chunkIndex, + Readable.fromWeb(request.body as ReadableStream), + encContentHash, ); - - await uploadChunk(userId, uploadId, chunkIndex, nodeReadable, encChunkHash); - return text("Chunk uploaded", { headers: { "Content-Type": "text/plain" } }); };