diff --git a/apps/web/app/api/assets/[assetId]/route.ts b/apps/web/app/api/assets/[assetId]/route.ts index 3bff79ba..66ec6754 100644 --- a/apps/web/app/api/assets/[assetId]/route.ts +++ b/apps/web/app/api/assets/[assetId]/route.ts @@ -2,7 +2,11 @@ import { createContextFromRequest } from "@/server/api/client"; import { and, eq } from "drizzle-orm"; import { assets } from "@hoarder/db/schema"; -import { readAsset } from "@hoarder/shared/assetdb"; +import { + createAssetReadStream, + getAssetSize, + readAssetMetadata, +} from "@hoarder/shared/assetdb"; export const dynamic = "force-dynamic"; export async function GET( @@ -22,35 +26,60 @@ export async function GET( return Response.json({ error: "Asset not found" }, { status: 404 }); } - const { asset, metadata } = await readAsset({ - userId: ctx.user.id, - assetId: params.assetId, - }); + const [metadata, size] = await Promise.all([ + readAssetMetadata({ + userId: ctx.user.id, + assetId: params.assetId, + }), + + getAssetSize({ + userId: ctx.user.id, + assetId: params.assetId, + }), + ]); const range = request.headers.get("Range"); if (range) { const parts = range.replace(/bytes=/, "").split("-"); const start = parseInt(parts[0], 10); - const end = parts[1] ? parseInt(parts[1], 10) : asset.length - 1; - - // TODO: Don't read the whole asset into memory in the first place - const chunk = asset.subarray(start, end + 1); - return new Response(chunk, { - status: 206, // Partial Content - headers: { - "Content-Range": `bytes ${start}-${end}/${asset.length}`, - "Accept-Ranges": "bytes", - "Content-Length": chunk.length.toString(), - "Content-type": metadata.contentType, - }, + const end = parts[1] ? parseInt(parts[1], 10) : size - 1; + + const stream = createAssetReadStream({ + userId: ctx.user.id, + assetId: params.assetId, + start, + end, }); - } else { - return new Response(asset, { - status: 200, - headers: { - "Content-Length": asset.length.toString(), - "Content-type": metadata.contentType, + + return new Response( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + stream as any, + { + status: 206, // Partial Content + headers: { + "Content-Range": `bytes ${start}-${end}/${size}`, + "Accept-Ranges": "bytes", + "Content-Length": (end - start + 1).toString(), + "Content-type": metadata.contentType, + }, }, + ); + } else { + const stream = createAssetReadStream({ + userId: ctx.user.id, + assetId: params.assetId, }); + + return new Response( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + stream as any, + { + status: 200, + headers: { + "Content-Length": size.toString(), + "Content-type": metadata.contentType, + }, + }, + ); } } diff --git a/packages/shared/assetdb.ts b/packages/shared/assetdb.ts index fb7d2461..2ef69279 100644 --- a/packages/shared/assetdb.ts +++ b/packages/shared/assetdb.ts @@ -123,6 +123,25 @@ export async function readAsset({ return { asset, metadata }; } +export function createAssetReadStream({ + userId, + assetId, + start, + end, +}: { + userId: string; + assetId: string; + start?: number; + end?: number; +}) { + const assetDir = getAssetDir(userId, assetId); + + return fs.createReadStream(path.join(assetDir, "asset.bin"), { + start, + end, + }); +} + export async function readAssetMetadata({ userId, assetId,