feat(cloud-page) - wip - uploading files till now
This commit is contained in:
@@ -1,7 +1,450 @@
|
||||
import { Router } from "express";
|
||||
// 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 = Router();
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
// 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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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<any> => {
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user