mirror of
https://github.com/karakeep-app/karakeep.git
synced 2025-12-12 20:35:52 +01:00
125 lines
3.4 KiB
TypeScript
125 lines
3.4 KiB
TypeScript
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);
|
|
}
|