From e7194e22c4d5ecb08259565e97c8bb2c35d4835a Mon Sep 17 00:00:00 2001 From: Akhil Meka Date: Fri, 11 Apr 2025 12:17:58 +0530 Subject: [PATCH] changes to file routes --- src/file/file.controller.ts | 156 +++++++++++++++++++++--------------- src/file/file.route.ts | 66 ++++++++++----- src/file/file.schema.ts | 65 ++++++++++----- src/file/file.service.ts | 131 +++++++++++++++++++++++++++--- 4 files changed, 306 insertions(+), 112 deletions(-) diff --git a/src/file/file.controller.ts b/src/file/file.controller.ts index 517fcdb..94edf9e 100644 --- a/src/file/file.controller.ts +++ b/src/file/file.controller.ts @@ -3,56 +3,114 @@ import { FastifyReply, FastifyRequest } from "fastify"; import { generateId } from "../utils/id"; import { completeMultiPartUpload, - deleteFileS3, getFileUrlS3, getUploadUrl, getUploadUrlMultiPart, - uploadFileS3, } from "../utils/s3"; -import { createFile, getFile } from "./file.service"; -import { UploadMultiPartCompleteRequest } from "./file.schema"; +import { + createFile, + deleteFile, + getChildren, + getFile, + updateFile, + uploadDone, +} from "./file.service"; +import { + CreateFileInput, + UpdateFileInput, + UploadMultiPartCompleteRequest, +} from "./file.schema"; -export async function fileUploadHandler( +export async function createFileHandler( req: FastifyRequest, res: FastifyReply ) { - const file = await req.file(); - - if (!file) return res.code(400).send({ error: "file not found in the body" }); + const input = req.body as CreateFileInput; try { - const chunks = []; - for await (const chunk of file.file) { - // @ts-ignore - chunks.push(Buffer.from(chunk)); + const file = await createFile(input, req.user); + const resObj = { + file, + }; + + if (file.mimeType != "folder") { + const signedUrl = await getUploadUrl(file.pid); + resObj["signedUrl"] = signedUrl; } - - const fileData = Buffer.concat(chunks); - const key = generateId(); - await uploadFileS3(key, fileData); - - const fileRec = await createFile( - key, - file.filename, - file.mimetype, - req.user.tenantId - ); - - return res.code(201).send(fileRec); + return res.code(201).send(resObj); } catch (err) { return err; } } -export async function fileUploadS3UrlHandler( +export async function uploadDoneHandler( req: FastifyRequest, res: FastifyReply ) { - try { - const key = generateId(); - const signedUrl = await getUploadUrl(key); + const { fileId } = req.params as { fileId: string }; - return res.code(200).send({ key, signedUrl }); + try { + const file = await uploadDone(fileId, req.user.tenantId); + return res.code(200).send(); + } catch (err) { + return err; + } +} + +export async function getFileHandler(req: FastifyRequest, res: FastifyReply) { + const { fileId } = req.params as { fileId: string }; + + try { + const file = await getFile(fileId, req.user.tenantId); + if (!file) return res.code(404).send({ error: "resource not found" }); + + return res.code(200).send(file); + } catch (err) { + return err; + } +} + +export async function getChildrenHandler( + req: FastifyRequest, + res: FastifyReply +) { + const { fileId } = req.params as { fileId: string }; + + try { + const files = await getChildren(fileId, req.user.tenantId); + return res.code(200).send(files); + } catch (err) { + return err; + } +} + +export async function updateFileHandler( + req: FastifyRequest, + res: FastifyReply +) { + const { fileId } = req.params as { fileId: string }; + const input = req.body as UpdateFileInput; + + try { + const updatedFile = await updateFile(fileId, input, req.user.tenantId); + if (!updatedFile) + return res.code(404).send({ error: "resource not found" }); + + return res.code(200).send(updatedFile); + } catch (err) { + return err; + } +} + +export async function deleteFileHandler( + req: FastifyRequest, + res: FastifyReply +) { + const { fileId } = req.params as { fileId: string }; + + try { + await deleteFile(fileId, req.user.tenantId); + return res.code(204).send(); } catch (err) { return err; } @@ -95,45 +153,17 @@ export async function fileDownloadHandler( res: FastifyReply ) { const { fileId } = req.params as { fileId: string }; - const { direct } = req.query as { direct: boolean }; - - try { - let id: string; - let name: string | null; - - if (!direct) { - const file = await getFile(fileId, req.user.tenantId); - if (file === null) - return res.code(404).send({ error: "resource not found" }); - - id = file.id; - name = file.name; - } else { - id = fileId; - name = null; - } - - const signedUrl = await getFileUrlS3(id, name); - return res.code(200).send({ url: signedUrl }); - } catch (err) { - return err; - } -} - -export async function deleteFileHandler( - req: FastifyRequest, - res: FastifyReply -) { - const { fileId } = req.params as { fileId: string }; try { const file = await getFile(fileId, req.user.tenantId); if (file === null) return res.code(404).send({ error: "resource not found" }); - await deleteFileS3(file.pid); - await file.deleteOne(); - return res.code(204).send(); + if (file.mimeType == "folder") + return res.code(400).send({ error: "cannot download a folder" }); + + const signedUrl = await getFileUrlS3(file.pid, file.name); + return res.code(200).send({ url: signedUrl }); } catch (err) { return err; } diff --git a/src/file/file.route.ts b/src/file/file.route.ts index b7dcd90..c67d605 100644 --- a/src/file/file.route.ts +++ b/src/file/file.route.ts @@ -1,27 +1,40 @@ import { FastifyInstance } from "fastify"; import { + createFileHandler, deleteFileHandler, fileDownloadHandler, - fileUploadHandler, - fileUploadS3UrlHandler, fileUploadS3UrlMultiPartHandler, finishMulitPartUploadHandler, + getChildrenHandler, + getFileHandler, + updateFileHandler, + uploadDoneHandler, } from "./file.controller"; import { $file } from "./file.schema"; export async function fileRoutes(fastify: FastifyInstance) { fastify.post( - "/upload", + "/", { schema: { - response: { - 201: $file("uploadFileResponse"), - }, + body: $file("createFileInput"), }, config: { requiredClaims: ["file:upload"] }, preHandler: [fastify.authorize], }, - fileUploadHandler + createFileHandler + ); + + fastify.post( + "/:fileId/done", + { + schema: { + params: { type: "object", properties: { fileId: { type: "string" } } }, + }, + config: { requiredClaims: ["file:upload"] }, + preHandler: [fastify.authorize], + }, + uploadDoneHandler ); fastify.get( @@ -29,18 +42,35 @@ export async function fileRoutes(fastify: FastifyInstance) { { schema: { params: { type: "object", properties: { fileId: { type: "string" } } }, - querystring: { - type: "object", - properties: { direct: { type: "boolean" } }, - }, - response: { - 200: $file("downloadFileResponse"), - }, }, config: { requiredClaims: ["file:download"] }, preHandler: [fastify.authorize], }, - fileDownloadHandler + getFileHandler + ); + + fastify.get( + "/:fileId/children", + { + schema: { + params: { type: "object", properties: { fileId: { type: "string" } } }, + }, + config: { requiredClaims: ["file:download"] }, + preHandler: [fastify.authorize], + }, + getChildrenHandler + ); + + fastify.post( + "/:fileId", + { + schema: { + body: $file("updateFileInput"), + }, + config: { requiredClaims: ["file:upload"] }, + preHandler: [fastify.authorize], + }, + updateFileHandler ); fastify.delete( @@ -56,12 +86,12 @@ export async function fileRoutes(fastify: FastifyInstance) { ); fastify.get( - "/", + "/:fileId/download", { - config: { requiredClaims: ["file:upload"] }, + config: { requiredClaims: ["file:download"] }, preHandler: [fastify.authorize], }, - fileUploadS3UrlHandler + fileDownloadHandler ); fastify.get( diff --git a/src/file/file.schema.ts b/src/file/file.schema.ts index 8fc595f..322448e 100644 --- a/src/file/file.schema.ts +++ b/src/file/file.schema.ts @@ -2,29 +2,48 @@ import { z } from "zod"; import mongoose from "mongoose"; import { buildJsonSchemas } from "fastify-zod"; -export const fileModel = mongoose.model( - "file", - new mongoose.Schema({ - tenantId: String, - pid: { - type: String, - unique: true, - required: true, - }, - name: { - type: String, - required: true, - }, - mimeType: String, - createdAt: Date, - createdBy: Date, - }) -); +const fileSchema = new mongoose.Schema({ + tenantId: String, + pid: { + type: String, + unique: true, + required: true, + }, + parentId: String, + name: { + type: String, + required: true, + }, + size: Number, + mimeType: { + type: String, + required: true, + }, + status: String, + createdAt: Date, + updatedAt: Date, + createdBy: { + type: mongoose.Types.ObjectId, + ref: "user", + }, + isDeleted: Boolean, +}); -const uploadFileResponse = z.object({ - pid: z.string(), +fileSchema.index({ pid: 1, parentId: 1, isDeleted: 1 }); + +export const fileModel = mongoose.model("file", fileSchema); + +const createFileInput = z.object({ + parentId: z.string(), name: z.string(), + size: z.number().optional(), mimeType: z.string(), + root: z.boolean().optional(), +}); + +const updateFileInput = z.object({ + parentId: z.string().optional(), + name: z.string().optional(), }); const downloadFileResponse = z.object({ @@ -73,9 +92,13 @@ export type UploadMultiPartCompleteRequest = z.infer< typeof uploadMultipartCompleteRequest >; +export type CreateFileInput = z.infer; +export type UpdateFileInput = z.infer; + export const { schemas: fileSchemas, $ref: $file } = buildJsonSchemas( { - uploadFileResponse, + createFileInput, + updateFileInput, downloadFileResponse, uploadMultipartCompleteRequest, }, diff --git a/src/file/file.service.ts b/src/file/file.service.ts index c46141d..d079c76 100644 --- a/src/file/file.service.ts +++ b/src/file/file.service.ts @@ -1,22 +1,133 @@ -import { fileModel } from "./file.schema"; +import { AuthenticatedUser } from "../auth"; +import { generateId } from "../utils/id"; +import { deleteFileS3 } from "../utils/s3"; +import { CreateFileInput, fileModel, UpdateFileInput } from "./file.schema"; + +export const ErrNotFound = "not_found"; +export const ParentNotFound = "parent_not_found"; +export const ParentNotFolder = "parent_is_not_folder"; export async function createFile( - pid: string, - name: string, - mimeType: string, - tenantId: string + input: CreateFileInput, + user: AuthenticatedUser ) { + if (!input.root) { + const parent = await fileModel.findOne({ + $and: [ + { tenantId: user.tenantId }, + { pid: input.parentId }, + { isDeleted: false }, + ], + }); + + if (!parent) throw ParentNotFound; + if (parent.mimeType != "folder") throw ParentNotFolder; + } + return await fileModel.create({ - tenantId: tenantId, - pid: pid, - name: name, - mimeType: mimeType, + tenantId: user.tenantId, + pid: generateId(), + status: input.mimeType == "folder" ? "done" : "penidng", createdAt: new Date(), + createdBy: user.userId, + isDeleted: false, + ...input, }); } +export async function uploadDone(fileId: string, tenantId: string) { + return await fileModel.findOneAndUpdate( + { + $and: [{ tenantId: tenantId }, { pid: fileId }], + }, + { status: "done" }, + { new: true } + ); +} + export async function getFile(fileId: string, tenantId: string) { return await fileModel.findOne({ - $and: [{ tenantId: tenantId }, { pid: fileId }], + $and: [{ tenantId: tenantId }, { pid: fileId }, { isDeleted: false }], }); } + +export async function getChildren(parentId: string, tenantId: string) { + return await fileModel.find({ + $and: [ + { tenantId: tenantId }, + { parentId: parentId }, + { isDeleted: false }, + { status: "done" }, + ], + }); +} + +export async function updateFile( + fileId: string, + input: UpdateFileInput, + tenantId: string +) { + if (input.parentId) { + const parentInDb = await fileModel.findOne({ + $and: [ + { tenantId: tenantId }, + { pid: input.parentId }, + { isDeleted: false }, + ], + }); + + if (!parentInDb) throw ParentNotFound; + if (parentInDb.mimeType != "folder") throw ParentNotFolder; + } + + return await fileModel.findOneAndUpdate( + { + $and: [{ tenantId: tenantId }, { pid: fileId }, { isDeleted: false }], + }, + { + ...input, + updatedAt: new Date(), + }, + { new: true } + ); +} + +async function getAllDescendents(folderId: string, tenantId: string) { + const idArr: Array = []; + + async function traverseFolder(folderId: string) { + const children = await getChildren(folderId, tenantId); + for (const child of children) { + if (child.mimeType == "folder") { + await traverseFolder(child.pid); + } + idArr.push(child.pid); + } + } + + await traverseFolder(folderId); + + console.log(idArr); + return idArr; +} + +export async function deleteFile(fileId: string, tenantId: string) { + const file = await fileModel.findOne({ + $and: [{ tenantId: tenantId }, { pid: fileId }], + }); + + if (!file) throw ErrNotFound; + + if (file.mimeType != "folder") { + await deleteFileS3(file.pid); + return await fileModel.deleteOne({ + $and: [{ tenantId: tenantId }, { pid: fileId }], + }); + } else { + const descendents = await getAllDescendents(file.pid, tenantId); + return await fileModel.updateMany( + { pid: { $in: descendents } }, + { $set: { isDeleted: true } } + ); + } +}