aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/app/api/assets
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--apps/web/app/api/assets/[assetId]/route.ts85
-rw-r--r--packages/api/utils/upload.ts (renamed from apps/web/app/api/assets/route.ts)64
2 files changed, 25 insertions, 124 deletions
diff --git a/apps/web/app/api/assets/[assetId]/route.ts b/apps/web/app/api/assets/[assetId]/route.ts
deleted file mode 100644
index 8abb9080..00000000
--- a/apps/web/app/api/assets/[assetId]/route.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { createContextFromRequest } from "@/server/api/client";
-import { and, eq } from "drizzle-orm";
-
-import { assets } from "@karakeep/db/schema";
-import {
- createAssetReadStream,
- getAssetSize,
- readAssetMetadata,
-} from "@karakeep/shared/assetdb";
-
-export const dynamic = "force-dynamic";
-export async function GET(
- request: Request,
- { params }: { params: { assetId: string } },
-) {
- const ctx = await createContextFromRequest(request);
- if (!ctx.user) {
- return Response.json({ error: "Unauthorized" }, { status: 401 });
- }
-
- const assetDb = await ctx.db.query.assets.findFirst({
- where: and(eq(assets.id, params.assetId), eq(assets.userId, ctx.user.id)),
- });
-
- if (!assetDb) {
- return Response.json({ error: "Asset not found" }, { status: 404 });
- }
-
- 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) : size - 1;
-
- const stream = createAssetReadStream({
- userId: ctx.user.id,
- assetId: params.assetId,
- start,
- end,
- });
-
- 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/apps/web/app/api/assets/route.ts b/packages/api/utils/upload.ts
index e2e1e63e..d96a0f60 100644
--- a/apps/web/app/api/assets/route.ts
+++ b/packages/api/utils/upload.ts
@@ -3,10 +3,7 @@ import * as os from "os";
import * as path from "path";
import { Readable } from "stream";
import { pipeline } from "stream/promises";
-import { createContextFromRequest } from "@/server/api/client";
-import { TRPCError } from "@trpc/server";
-import type { ZUploadResponse } from "@karakeep/shared/types/uploads";
import { assets, AssetTypes } from "@karakeep/db/schema";
import {
newAssetId,
@@ -18,20 +15,34 @@ import { AuthedContext } from "@karakeep/trpc";
const MAX_UPLOAD_SIZE_BYTES = serverConfig.maxAssetSizeMb * 1024 * 1024;
-export const dynamic = "force-dynamic";
-
// Helper to convert Web Stream to Node Stream (requires Node >= 16.5 / 14.18)
-function webStreamToNode(webStream: ReadableStream<Uint8Array>): Readable {
+export function webStreamToNode(
+ webStream: ReadableStream<Uint8Array>,
+): Readable {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
return Readable.fromWeb(webStream as any); // Type assertion might be needed
}
-export async function uploadFromPostData(
+export function toWebReadableStream(
+ nodeStream: fs.ReadStream,
+): ReadableStream<Uint8Array> {
+ const reader = nodeStream as unknown as Readable;
+
+ return new ReadableStream({
+ start(controller) {
+ reader.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
+ reader.on("end", () => controller.close());
+ reader.on("error", (err) => controller.error(err));
+ },
+ });
+}
+
+export async function uploadAsset(
user: AuthedContext["user"],
db: AuthedContext["db"],
- formData: FormData,
+ formData: { file: File } | { image: File },
): Promise<
- | { error: string; status: number }
+ | { error: string; status: 400 | 413 }
| {
assetId: string;
contentType: string;
@@ -39,10 +50,11 @@ export async function uploadFromPostData(
size: number;
}
> {
- const data = formData.get("file") ?? formData.get("image");
-
- if (!(data instanceof File)) {
- return { error: "Bad request", status: 400 };
+ let data: File;
+ if ("file" in formData) {
+ data = formData.file;
+ } else {
+ data = formData.image;
}
const contentType = data.type;
@@ -96,29 +108,3 @@ export async function uploadFromPostData(
}
}
}
-
-export async function POST(request: Request) {
- const ctx = await createContextFromRequest(request);
- if (ctx.user === null) {
- return Response.json({ error: "Unauthorized" }, { status: 401 });
- }
- if (serverConfig.demoMode) {
- throw new TRPCError({
- message: "Mutations are not allowed in demo mode",
- code: "FORBIDDEN",
- });
- }
- const formData = await request.formData();
-
- const resp = await uploadFromPostData(ctx.user, ctx.db, formData);
- if ("error" in resp) {
- return Response.json({ error: resp.error }, { status: resp.status });
- }
-
- return Response.json({
- assetId: resp.assetId,
- contentType: resp.contentType,
- size: resp.size,
- fileName: resp.fileName,
- } satisfies ZUploadResponse);
-}