aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app
diff options
context:
space:
mode:
authorMohamed Bassem (aider) <me@mbassem.com>2025-01-05 14:01:40 +0000
committerMohamed Bassem <me@mbassem.com>2025-01-05 14:11:50 +0000
commitc98722c4781379c680d9ec9efc066e555bad318c (patch)
tree7e4918b96613c1c6b6998450e3c580d5392de48c /apps/web/app
parent88dc6f911bdae49ca72fc82d6da9dce84853bce0 (diff)
downloadkarakeep-c98722c4781379c680d9ec9efc066e555bad318c.tar.zst
refactor: Implement file streaming for asset route. Fixes #818
Diffstat (limited to 'apps/web/app')
-rw-r--r--apps/web/app/api/assets/[assetId]/route.ts75
1 files changed, 52 insertions, 23 deletions
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,
+ },
+ },
+ );
}
}