// src/routes/cloudStorage.routes.ts import express, { Request, Response } from "express"; // import storage from "../storage"; import { cloudStorage as storage } from "../storage/cloudStorage-storage"; import { serializeFile } from "../utils/prismaFileUtils"; import { CloudFolder } from "@repo/db/types"; const router = express.Router(); /* ---------- Helpers ---------- */ function parsePositiveInt(v: unknown, fallback: number) { const n = Number(v); if (!Number.isFinite(n) || n < 0) return fallback; return Math.floor(n); } function sendError( res: Response, status: number, message: string, details?: any ) { return res.status(status).json({ error: true, message, details }); } /* ---------- Paginated child FOLDERS for a parent ---------- GET /items/folders?parentId=&limit=&offset= parentId may be "null" or numeric or absent (means root) */ router.get( "/items/folders", async (req: Request, res: Response): Promise => { const rawParent = req.query.parentId; const parentId = rawParent === undefined ? null : rawParent === "null" ? null : Number(rawParent); if (parentId !== null && (!Number.isInteger(parentId) || parentId <= 0)) { return sendError(res, 400, "Invalid parentId"); } const limit = parsePositiveInt(req.query.limit, 10); // default 10 folders/page const offset = parsePositiveInt(req.query.offset, 0); try { // Prefer a storage method that lists folders by parent, otherwise filter let data: CloudFolder[] = []; if (typeof (storage as any).listFoldersByParent === "function") { data = await (storage as any).listFoldersByParent( parentId, limit, offset ); const total = (await (storage as any).countFoldersByParent?.(parentId)) ?? data.length; return res.json({ error: false, data, total, limit, offset }); } // Fallback: use recent and filter (less efficient). Recommend implementing listFoldersByParent in storage. const recent = await storage.listRecentFolders(1000, 0); const folders = (recent || []).filter( (f: any) => (f as any).parentId === parentId ); const paged = folders.slice(offset, offset + limit); return res.json({ error: false, data: paged, total: folders.length, limit, offset, }); } catch (err) { return sendError(res, 500, "Failed to load child folders", err); } } ); /* ---------- Paginated files for a folder ---------- GET /items/files?parentId=&limit=&offset= parentId may be "null" or numeric or absent (means root) */ router.get( "/items/files", async (req: Request, res: Response): Promise => { const rawParent = req.query.parentId; const parentId = rawParent === undefined ? null : rawParent === "null" ? null : Number(rawParent); if (parentId !== null && (!Number.isInteger(parentId) || parentId <= 0)) { return sendError(res, 400, "Invalid parentId"); } const limit = parsePositiveInt(req.query.limit, 20); // default 20 files/page const offset = parsePositiveInt(req.query.offset, 0); try { const files = await storage.listFilesInFolder(parentId, limit, offset); const total = await storage.countFilesInFolder(parentId); const serialized = files.map(serializeFile); return res.json({ error: false, data: serialized, total, limit, offset }); } catch (err) { return sendError(res, 500, "Failed to load files for folder", err); } } ); /* ---------- Recent folders (global) ---------- GET /folders/recent?limit=&offset= */ router.get( "/folders/recent", async (req: Request, res: Response): Promise => { const limit = parsePositiveInt(req.query.limit, 50); const offset = parsePositiveInt(req.query.offset, 0); try { const folders = await storage.listRecentFolders(limit, offset); const total = await storage.countFolders(); return res.json({ error: false, data: folders, total, limit, offset }); } catch (err) { return sendError(res, 500, "Failed to load recent folders"); } } ); /* ---------- Folder CRUD ---------- POST /folders { userId, name, parentId? } PUT /folders/:id { name?, parentId? } DELETE /folders/:id */ router.post("/folders", async (req: Request, res: Response): Promise => { const { userId, name, parentId } = req.body; if (!userId || typeof name !== "string" || !name.trim()) { return sendError(res, 400, "Missing or invalid userId/name"); } try { const created = await storage.createFolder( userId, name.trim(), parentId ?? null ); return res.status(201).json({ error: false, data: created }); } catch (err) { return sendError(res, 500, "Failed to create folder"); } }); router.put( "/folders/:id", async (req: Request, res: Response): Promise => { // coerce possibly-undefined param to string before parsing const id = Number.parseInt(req.params.id ?? "", 10); if (!Number.isInteger(id) || id <= 0) return sendError(res, 400, "Invalid folder id"); const updates: any = {}; if (typeof req.body.name === "string") updates.name = req.body.name.trim(); if (req.body.parentId !== undefined) updates.parentId = req.body.parentId; try { const updated = await storage.updateFolder(id, updates); if (!updated) return sendError(res, 404, "Folder not found or update failed"); return res.json({ error: false, data: updated }); } catch (err) { return sendError(res, 500, "Failed to update folder"); } } ); router.delete( "/folders/:id", async (req: Request, res: Response): Promise => { const id = Number.parseInt(req.params.id ?? "", 10); if (!Number.isInteger(id) || id <= 0) return sendError(res, 400, "Invalid folder id"); try { const ok = await storage.deleteFolder(id); if (!ok) return sendError(res, 404, "Folder not found or delete failed"); return res.json({ error: false, data: { id } }); } catch (err) { return sendError(res, 500, "Failed to delete folder"); } } ); /* ---------- Files inside folder (pagination) ---------- GET /folders/:id/files?limit=&offset= id = "null" lists files with folderId = null responses serialized */ router.get( "/folders/:id/files", async (req: Request, res: Response): Promise => { const rawId = req.params.id; const folderId = rawId === "null" ? null : Number.parseInt(rawId ?? "", 10); if (folderId !== null && (!Number.isInteger(folderId) || folderId <= 0)) { return sendError(res, 400, "Invalid folder id"); } const limit = parsePositiveInt(req.query.limit, 50); const offset = parsePositiveInt(req.query.offset, 0); try { const files = await storage.listFilesInFolder(folderId, limit, offset); const total = await storage.countFilesInFolder(folderId); const serialized = files.map(serializeFile); return res.json({ error: false, data: serialized, total, limit, offset }); } catch (err) { return sendError(res, 500, "Failed to list files for folder"); } } ); /* ---------- File CRUD (init, update metadata, delete) ---------- POST /folders/:id/files { userId, name, mimeType?, expectedSize?, totalChunks? } PUT /files/:id { name?, mimeType?, folderId? } DELETE /files/:id */ router.post( "/folders/:id/files", async (req: Request, res: Response): Promise => { const rawId = req.params.id; const folderId = rawId === "null" ? null : Number.parseInt(rawId ?? "", 10); if (folderId !== null && (!Number.isInteger(folderId) || folderId <= 0)) { return sendError(res, 400, "Invalid folder id"); } const { userId, name, mimeType } = req.body; if (!userId || typeof name !== "string" || !name.trim()) { return sendError(res, 400, "Missing or invalid userId/name"); } // coerce size & chunks let expectedSize: bigint | null = null; if (req.body.expectedSize != null) { try { expectedSize = BigInt(String(req.body.expectedSize)); } catch { return sendError(res, 400, "Invalid expectedSize"); } } let totalChunks: number | null = null; if (req.body.totalChunks != null) { const tc = Number(req.body.totalChunks); if (!Number.isFinite(tc) || tc <= 0) return sendError(res, 400, "Invalid totalChunks"); totalChunks = Math.floor(tc); } try { const created = await storage.initializeFileUpload( userId, name.trim(), mimeType ?? null, expectedSize, totalChunks, folderId ); return res .status(201) .json({ error: false, data: serializeFile(created as any) }); } catch { return sendError(res, 500, "Failed to create file"); } } ); /* ---------- 2. CHUNKS (raw upload) ---------- */ router.post( "/files/:id/chunks", // only here: use express.raw so req.body is Buffer express.raw({ type: () => true, limit: "100mb" }), async (req: Request, res: Response): Promise => { const id = Number.parseInt(req.params.id ?? "", 10); const seq = Number.parseInt( String(req.query.seq ?? req.body.seq ?? ""), 10 ); if (!Number.isInteger(id) || id <= 0) return sendError(res, 400, "Invalid file id"); if (!Number.isInteger(seq) || seq < 0) return sendError(res, 400, "Invalid seq"); const body = req.body as Buffer; console.log( `[chunk upload] fileId=${id} seq=${seq} contentType=${String(req.headers["content-type"])} bodyIsBuffer=${Buffer.isBuffer(body)} bodyLength=${body?.length ?? 0}` ); if (!body || !(body instanceof Buffer)) { return sendError(res, 400, "Expected raw binary body (Buffer)"); } try { await storage.appendFileChunk(id, seq, body); return res.json({ error: false, data: { fileId: id, seq } }); } catch (err: any) { console.error( "[chunk upload] appendFileChunk failed:", err && (err.stack || err.message || err) ); return sendError(res, 500, "Failed to add chunk"); } } ); /* ---------- 3. COMPLETE ---------- */ router.post( "/files/:id/complete", async (req: Request, res: Response): Promise => { const id = Number.parseInt(req.params.id ?? "", 10); if (!Number.isInteger(id) || id <= 0) return sendError(res, 400, "Invalid file id"); try { const result = await storage.finalizeFileUpload(id); return res.json({ error: false, data: result }); } catch (err: any) { return sendError(res, 500, err?.message || "Failed to complete file"); } } ); router.put("/files/:id", async (req: Request, res: Response): Promise => { const id = Number.parseInt(req.params.id ?? "", 10); if (!Number.isInteger(id) || id <= 0) return sendError(res, 400, "Invalid file id"); const updates: any = {}; if (typeof req.body.name === "string") updates.name = req.body.name.trim(); if (typeof req.body.mimeType === "string") updates.mimeType = req.body.mimeType; if (req.body.folderId !== undefined) updates.folderId = req.body.folderId; try { const updated = await storage.updateFile(id, updates); if (!updated) return sendError(res, 404, "File not found or update failed"); return res.json({ error: false, data: serializeFile(updated as any) }); } catch (err) { return sendError(res, 500, "Failed to update file metadata"); } }); router.delete( "/files/:id", async (req: Request, res: Response): Promise => { const id = Number.parseInt(req.params.id ?? "", 10); if (!Number.isInteger(id) || id <= 0) return sendError(res, 400, "Invalid file id"); try { const ok = await storage.deleteFile(id); if (!ok) return sendError(res, 404, "File not found or delete failed"); return res.json({ error: false, data: { id } }); } catch (err) { return sendError(res, 500, "Failed to delete file"); } } ); /* ---------- Download (stream) ---------- GET /files/:id/download */ router.get( "/files/:id/download", async (req: Request, res: Response): Promise => { const id = Number.parseInt(req.params.id ?? "", 10); if (!Number.isInteger(id) || id <= 0) return sendError(res, 400, "Invalid file id"); try { const file = await storage.getFile(id); if (!file) return sendError(res, 404, "File not found"); const filename = (file.name ?? `file-${(file as any).id}`).replace( /["\\]/g, "" ); if ((file as any).mimeType) res.setHeader("Content-Type", (file as any).mimeType); res.setHeader( "Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"` ); await storage.streamFileTo(res, id); if (!res.writableEnded) res.end(); } catch (err) { if (res.headersSent) return res.end(); return sendError(res, 500, "Failed to stream file"); } } ); /* ---------- Search endpoints (separate) ---------- GET /search/folders?q=&limit=&offset= GET /search/files?q=&type=&limit=&offset= */ router.get( "/search/folders", async (req: Request, res: Response): Promise => { const q = String(req.query.q ?? "").trim(); const limit = parsePositiveInt(req.query.limit, 20); const offset = parsePositiveInt(req.query.offset, 0); if (!q) return sendError(res, 400, "Missing search query parameter 'q'"); try { const { data, total } = await storage.searchFolders(q, limit, offset); return res.json({ error: false, data, total, limit, offset }); } catch (err) { return sendError(res, 500, "Folder search failed"); } } ); router.get( "/search/files", async (req: Request, res: Response): Promise => { const q = String(req.query.q ?? "").trim(); const type = typeof req.query.type === "string" ? req.query.type.trim() : undefined; const limit = parsePositiveInt(req.query.limit, 20); const offset = parsePositiveInt(req.query.offset, 0); if (!q && !type) return sendError( res, 400, "Provide at least one of 'q' or 'type' to search files" ); try { const { data, total } = await storage.searchFiles(q, type, limit, offset); const serialized = data.map(serializeFile); return res.json({ error: false, data: serialized, total, limit, offset }); } catch (err) { return sendError(res, 500, "File search failed"); } } ); export default router;