diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-05-18 16:58:08 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-05-18 16:58:08 +0100 |
| commit | 3505cb7d6416d101a4fcb1be27fc22e0171bacd2 (patch) | |
| tree | ef9f55504b8a5b20add8c0ebe916972ab4ab0178 /apps/web/app/api/assets | |
| parent | 74e74fa6425f072107de3a9bc9dd8f91c5ac9a7d (diff) | |
| download | karakeep-3505cb7d6416d101a4fcb1be27fc22e0171bacd2.tar.zst | |
refactor: Migrate from NextJs's API routes to Hono based routes for the API (#1432)
* Setup Hono and migrate the highlights API there
* Implement the tags and lists endpoint
* Implement the bookmarks and users endpoints
* Add the trpc error code adapter
* Remove the old nextjs handlers
* fix api key not found handling
* Fix trpc error handling
* Fix 204 handling
* Fix search ordering
* Implement the singlefile endpoint
* Implement the asset serving endpoints
* Implement webauth
* Add hono as a catch all route under api
* fix tests
Diffstat (limited to 'apps/web/app/api/assets')
| -rw-r--r-- | apps/web/app/api/assets/[assetId]/route.ts | 85 | ||||
| -rw-r--r-- | apps/web/app/api/assets/route.ts | 124 |
2 files changed, 0 insertions, 209 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/apps/web/app/api/assets/route.ts deleted file mode 100644 index e2e1e63e..00000000 --- a/apps/web/app/api/assets/route.ts +++ /dev/null @@ -1,124 +0,0 @@ -import * as fs from "fs"; -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, - saveAssetFromFile, - SUPPORTED_UPLOAD_ASSET_TYPES, -} from "@karakeep/shared/assetdb"; -import serverConfig from "@karakeep/shared/config"; -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 { - // 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( - user: AuthedContext["user"], - db: AuthedContext["db"], - formData: FormData, -): Promise< - | { error: string; status: number } - | { - assetId: string; - contentType: string; - fileName: string; - size: number; - } -> { - const data = formData.get("file") ?? formData.get("image"); - - if (!(data instanceof File)) { - return { error: "Bad request", status: 400 }; - } - - const contentType = data.type; - const fileName = data.name; - if (!SUPPORTED_UPLOAD_ASSET_TYPES.has(contentType)) { - return { error: "Unsupported asset type", status: 400 }; - } - if (data.size > MAX_UPLOAD_SIZE_BYTES) { - return { error: "Asset is too big", status: 413 }; - } - - let tempFilePath: string | undefined; - - try { - tempFilePath = path.join(os.tmpdir(), `karakeep-upload-${Date.now()}`); - await pipeline( - webStreamToNode(data.stream()), - fs.createWriteStream(tempFilePath), - ); - const [assetDb] = await db - .insert(assets) - .values({ - id: newAssetId(), - // Initially, uploads are uploaded for unknown purpose - // And without an attached bookmark. - assetType: AssetTypes.UNKNOWN, - bookmarkId: null, - userId: user.id, - contentType, - size: data.size, - fileName, - }) - .returning(); - - await saveAssetFromFile({ - userId: user.id, - assetId: assetDb.id, - assetPath: tempFilePath, - metadata: { contentType, fileName }, - }); - - return { - assetId: assetDb.id, - contentType, - size: data.size, - fileName, - }; - } finally { - if (tempFilePath) { - await fs.promises.unlink(tempFilePath).catch(() => ({})); - } - } -} - -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); -} |
