aboutsummaryrefslogtreecommitdiffstats
path: root/packages/api/utils/upload.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/api/utils/upload.ts')
-rw-r--r--packages/api/utils/upload.ts110
1 files changed, 110 insertions, 0 deletions
diff --git a/packages/api/utils/upload.ts b/packages/api/utils/upload.ts
new file mode 100644
index 00000000..d96a0f60
--- /dev/null
+++ b/packages/api/utils/upload.ts
@@ -0,0 +1,110 @@
+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 { 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;
+
+// Helper to convert Web Stream to Node Stream (requires Node >= 16.5 / 14.18)
+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 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: { file: File } | { image: File },
+): Promise<
+ | { error: string; status: 400 | 413 }
+ | {
+ assetId: string;
+ contentType: string;
+ fileName: string;
+ size: number;
+ }
+> {
+ let data: File;
+ if ("file" in formData) {
+ data = formData.file;
+ } else {
+ data = formData.image;
+ }
+
+ 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(() => ({}));
+ }
+ }
+}