From 9da7a8f55389260e6a6948680c42f84a57413e5a Mon Sep 17 00:00:00 2001 From: Louis Murerwa Date: Sun, 12 May 2024 18:20:42 -0700 Subject: [PATCH 1/4] Add File Upload Interface --- packages/ocular/src/services/file-service | 52 +++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 packages/ocular/src/services/file-service diff --git a/packages/ocular/src/services/file-service b/packages/ocular/src/services/file-service new file mode 100644 index 00000000..7891a12e --- /dev/null +++ b/packages/ocular/src/services/file-service @@ -0,0 +1,52 @@ +import { AbstractFileService } from "@ocular/types"; + +export class FileService extends AbstractFileService { + static identifier = "localfs"; + protected uploadDir_: string; + protected backendUrl_: string; + + upload(file: Express.Multer.File): Promise { + if (!file) { + throw new MedusaError(MedusaError.Types.INVALID_DATA, `No file provided`); + } + + if (!file.filename) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `No filename provided` + ); + } + + const parsedFilename = path.parse(file.filename); + + if (parsedFilename.dir) { + this.ensureDirExists(parsedFilename.dir); + } + + const fileKey = path.join( + parsedFilename.dir, + `${Date.now()}-${parsedFilename.base}` + ); + + const filePath = this.getUploadFilePath(fileKey); + const fileUrl = this.getUploadFileUrl(fileKey); + + const content = Buffer.from(file.content, "binary"); + await fs.writeFile(filePath, content); + + return { + key: fileKey, + url: fileUrl, + }; + } + // uploadProtected(file: Express.Multer.File): Promise; + // delete(fileData: DeleteFileType): Promise; + // getUploadStreamDescriptor( + // fileData: UploadStreamDescriptorType + // ): Promise; + + // getDownloadStream( + // fileData: GetUploadedFileType + // ): Promise; + // getPresignedDownloadUrl(fileData: GetUploadedFileType): Promise; +} From f8619eab35398d7390c9fcd4de92b3fc86bd051c Mon Sep 17 00:00:00 2001 From: Louis Murerwa Date: Mon, 13 May 2024 08:51:27 -0700 Subject: [PATCH 2/4] Add File Service --- packages/ocular/src/services/file-service | 84 ++++++++++++++--- packages/types/src/interfaces/file-service.ts | 91 +++++++++---------- packages/utils/src/common/errors.ts | 27 +++--- 3 files changed, 128 insertions(+), 74 deletions(-) diff --git a/packages/ocular/src/services/file-service b/packages/ocular/src/services/file-service index 7891a12e..bf66f112 100644 --- a/packages/ocular/src/services/file-service +++ b/packages/ocular/src/services/file-service @@ -1,18 +1,37 @@ -import { AbstractFileService } from "@ocular/types"; +import { + AbstractFileService, + FileDeleteData, + FileGetData, + FileUploadData, + FileUploadResult, +} from "@ocular/types"; +import { AutoflowAiError, AutoflowAiErrorTypes } from "@ocular/utils"; +import path from "path"; +import fs from "fs/promises"; export class FileService extends AbstractFileService { static identifier = "localfs"; protected uploadDir_: string; protected backendUrl_: string; - upload(file: Express.Multer.File): Promise { + protected constructor( + protected readonly container: Record, + protected readonly config?: Record // eslint-disable-next-line @typescript-eslint/no-empty-function + ) { + super(container, config); + } + + async upload(file: FileUploadData): Promise { if (!file) { - throw new MedusaError(MedusaError.Types.INVALID_DATA, `No file provided`); + throw new AutoflowAiError( + AutoflowAiErrorTypes.INVALID_DATA, + `No file provided` + ); } if (!file.filename) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, + throw new AutoflowAiError( + AutoflowAiErrorTypes.INVALID_DATA, `No filename provided` ); } @@ -39,14 +58,49 @@ export class FileService extends AbstractFileService { url: fileUrl, }; } - // uploadProtected(file: Express.Multer.File): Promise; - // delete(fileData: DeleteFileType): Promise; - // getUploadStreamDescriptor( - // fileData: UploadStreamDescriptorType - // ): Promise; - - // getDownloadStream( - // fileData: GetUploadedFileType - // ): Promise; - // getPresignedDownloadUrl(fileData: GetUploadedFileType): Promise; + + async delete(file: FileDeleteData): Promise { + const filePath = this.getUploadFilePath(file.fileKey); + try { + await fs.access(filePath, fs.constants.F_OK); + await fs.unlink(filePath); + } catch (e) { + // The file does not exist, so it's a noop. + } + + return; + } + + async getPresignedDownloadUrl(fileData: FileGetData): Promise { + try { + await fs.access( + this.getUploadFilePath(fileData.fileKey), + fs.constants.F_OK + ); + } catch { + throw new AutoflowAiError( + AutoflowAiErrorTypes.FILE_NOT_FOUND, + `File with key ${fileData.fileKey} not found` + ); + } + + return this.getUploadFileUrl(fileData.fileKey); + } + + private getUploadFilePath = (fileKey: string) => { + return path.join(this.uploadDir_, fileKey); + }; + + private getUploadFileUrl = (fileKey: string) => { + return path.join(this.backendUrl_, this.getUploadFilePath(fileKey)); + }; + + private async ensureDirExists(dirPath: string) { + const relativePath = path.join(this.uploadDir_, dirPath); + try { + await fs.access(relativePath, fs.constants.F_OK); + } catch (e) { + await fs.mkdir(relativePath, { recursive: true }); + } + } } diff --git a/packages/types/src/interfaces/file-service.ts b/packages/types/src/interfaces/file-service.ts index 04b39866..06bbd66a 100644 --- a/packages/types/src/interfaces/file-service.ts +++ b/packages/types/src/interfaces/file-service.ts @@ -1,38 +1,42 @@ import { TransactionBaseService } from "./transaction-base-service"; -import { - DeleteFileType, - FileServiceGetUploadStreamResult, - FileServiceUploadResult, - GetUploadedFileType, - UploadStreamDescriptorType, -} from "@medusajs/types"; -export interface IFileService extends TransactionBaseService { - upload(file: Express.Multer.File): Promise; - uploadProtected(file: Express.Multer.File): Promise; - delete(fileData: DeleteFileType): Promise; - getUploadStreamDescriptor( - fileData: UploadStreamDescriptorType - ): Promise; +export type FileUploadResult = { + url: string; + key: string; +}; - getDownloadStream( - fileData: GetUploadedFileType - ): Promise; - getPresignedDownloadUrl(fileData: GetUploadedFileType): Promise; +export type FileGetData = { + fileKey: string; + isPrivate?: boolean; + [x: string]: unknown; +}; + +export type FileDeleteData = { + fileKey: string; + [x: string]: unknown; +}; + +export type FileUploadData = { + filename: string; + mimeType: string; + content: string; +}; +export interface IFileProvider { + upload(file: FileUploadData): Promise; + delete(fileData: FileDeleteData): Promise; + getPresignedDownloadUrl(fileData: FileGetData): Promise; +} + +export interface FileServiceOptions { + upload_dir?: string; + backend_url?: string; } -export abstract class AbstractFileService +export class AbstractFileService extends TransactionBaseService - implements IFileService + implements IFileProvider { - /** - * @ignore - */ static _isFileService = true; - - /** - * @ignore - */ static isFileService(object): object is AbstractFileService { return object?.constructor?._isFileService; } @@ -44,25 +48,20 @@ export abstract class AbstractFileService super(container, config); } - abstract upload( - fileData: Express.Multer.File - ): Promise; - - abstract uploadProtected( - fileData: Express.Multer.File - ): Promise; - - abstract delete(fileData: DeleteFileType): Promise; - - abstract getUploadStreamDescriptor( - fileData: UploadStreamDescriptorType - ): Promise; + getIdentifier() { + return (this.constructor as any).identifier; + } - abstract getDownloadStream( - fileData: GetUploadedFileType - ): Promise; + async upload(file: FileUploadData): Promise { + throw Error("upload must be overridden by the child class"); + } + async delete(file: FileDeleteData): Promise { + throw Error("delete must be overridden by the child class"); + } - abstract getPresignedDownloadUrl( - fileData: GetUploadedFileType - ): Promise; + async getPresignedDownloadUrl(fileData: FileGetData): Promise { + throw Error( + "getPresignedDownloadUrl must be overridden by the child class" + ); + } } diff --git a/packages/utils/src/common/errors.ts b/packages/utils/src/common/errors.ts index f71c22b6..045711fd 100644 --- a/packages/utils/src/common/errors.ts +++ b/packages/utils/src/common/errors.ts @@ -6,6 +6,7 @@ export const AutoflowAiErrorTypes = { /** Errors stemming from the database */ DB_ERROR: "database_error", DUPLICATE_ERROR: "duplicate_error", + FILE_NOT_FOUND: "file_not_found", INVALID_ARGUMENT: "invalid_argument", INVALID_DATA: "invalid_data", UNAUTHORIZED: "unauthorized", @@ -14,18 +15,18 @@ export const AutoflowAiErrorTypes = { UNEXPECTED_STATE: "unexpected_state", CONFLICT: "conflict", PAYMENT_AUTHORIZATION_ERROR: "payment_authorization_error", -} +}; /** * Standardized error to be used across Autoflow project. * @extends Error */ export class AutoflowAiError extends Error { - public type: string - public message: string - public code?: string - public date: Date - public static Types = AutoflowAiErrorTypes + public type: string; + public message: string; + public code?: string; + public date: Date; + public static Types = AutoflowAiErrorTypes; /** * Creates a standardized error to be used across AutoflowAI project. @@ -35,15 +36,15 @@ export class AutoflowAiError extends Error { * @param {Array} params - params */ constructor(type: string, message: string, code?: string, ...params: any) { - super(...params) + super(...params); if (Error.captureStackTrace) { - Error.captureStackTrace(this, AutoflowAiError) + Error.captureStackTrace(this, AutoflowAiError); } - this.type = type - this.code = code - this.message = message - this.date = new Date() + this.type = type; + this.code = code; + this.message = message; + this.date = new Date(); } -} \ No newline at end of file +} From 880d3ef8693f81e80cafedb0ec7af52ef32391e2 Mon Sep 17 00:00:00 2001 From: Louis Murerwa Date: Mon, 13 May 2024 09:30:36 -0700 Subject: [PATCH 3/4] Clean Up --- .../ocular/src/services/__tests__/file.ts | 92 +++++++++++++++++++ .../{rate-limiter.ts => rate-limiter} | 0 .../src/services/{file-service => file.ts} | 6 +- packages/ocular/src/services/index.ts | 1 + 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 packages/ocular/src/services/__tests__/file.ts rename packages/ocular/src/services/__tests__/{rate-limiter.ts => rate-limiter} (100%) rename packages/ocular/src/services/{file-service => file.ts} (94%) diff --git a/packages/ocular/src/services/__tests__/file.ts b/packages/ocular/src/services/__tests__/file.ts new file mode 100644 index 00000000..e88f2894 --- /dev/null +++ b/packages/ocular/src/services/__tests__/file.ts @@ -0,0 +1,92 @@ +import FileService from "../file"; // adjust the import path according to your project structure +import fs from "fs/promises"; +import path from "path"; + +jest.mock("fs/promises"); + +describe("FileService", () => { + let fileService; + let mockFile; + + const RealDate = Date; + + function mockDate(isoDate: string) { + global.Date = class extends RealDate { + constructor() { + super(); + return new RealDate(isoDate); + } + static now() { + return new RealDate(isoDate).getTime(); + } + } as DateConstructor; + } + + beforeEach(() => { + fileService = new FileService({}, {}); + mockFile = { + filename: "test.txt", + content: "Hello, world!", + }; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it("should throw an error if no file is provided", async () => { + await expect(fileService.upload()).rejects.toThrow("No file provided"); + }); + + it("should throw an error if no filename is provided", async () => { + mockFile.filename = ""; + await expect(fileService.upload(mockFile)).rejects.toThrow( + "No filename provided" + ); + }); + + it("should write the file content to the correct path and return the key and url", async () => { + mockDate("2022-01-01T00:00:00Z"); + const spy = jest.spyOn(fs, "writeFile"); + const fileKey = path.join( + path.parse(mockFile.filename).dir, + `${Date.now()}-${path.parse(mockFile.filename).base}` + ); + const filePath = path.join(fileService.uploadDir_, fileKey); + + const fileData = await fileService.upload(mockFile); + + expect(spy).toHaveBeenCalledWith( + filePath, + Buffer.from(mockFile.content, "binary") + ); + + expect(fileData).toEqual({ + key: "1640995200000-test.txt", + url: "http:/localhost:9000/uploads/1640995200000-test.txt", + }); + }); + + it("should delete the file at the correct path", async () => { + const spy = jest.spyOn(fs, "unlink"); + const fileKey = "test.txt"; + const filePath = path.join(fileService.uploadDir_, fileKey); + + await fileService.delete({ fileKey }); + + expect(spy).toHaveBeenCalledWith(filePath); + }); + + it("should get presigned download url", async () => { + const fileKey = "test.txt"; + const filePath = path.join( + "http:/localhost:9000", + fileService.uploadDir_, + fileKey + ); + + await expect( + fileService.getPresignedDownloadUrl({ fileKey }) + ).resolves.toBe(filePath); + }); +}); diff --git a/packages/ocular/src/services/__tests__/rate-limiter.ts b/packages/ocular/src/services/__tests__/rate-limiter similarity index 100% rename from packages/ocular/src/services/__tests__/rate-limiter.ts rename to packages/ocular/src/services/__tests__/rate-limiter diff --git a/packages/ocular/src/services/file-service b/packages/ocular/src/services/file.ts similarity index 94% rename from packages/ocular/src/services/file-service rename to packages/ocular/src/services/file.ts index bf66f112..828276e8 100644 --- a/packages/ocular/src/services/file-service +++ b/packages/ocular/src/services/file.ts @@ -9,16 +9,18 @@ import { AutoflowAiError, AutoflowAiErrorTypes } from "@ocular/utils"; import path from "path"; import fs from "fs/promises"; -export class FileService extends AbstractFileService { +export default class FileService extends AbstractFileService { static identifier = "localfs"; protected uploadDir_: string; protected backendUrl_: string; - protected constructor( + constructor( protected readonly container: Record, protected readonly config?: Record // eslint-disable-next-line @typescript-eslint/no-empty-function ) { super(container, config); + (this.uploadDir_ = "/uploads"), + (this.backendUrl_ = "http://localhost:9000"); } async upload(file: FileUploadData): Promise { diff --git a/packages/ocular/src/services/index.ts b/packages/ocular/src/services/index.ts index 740175a5..104a2249 100644 --- a/packages/ocular/src/services/index.ts +++ b/packages/ocular/src/services/index.ts @@ -13,3 +13,4 @@ export { default as ChatService } from "./chat"; export { default as QueueService } from "./queue"; export { default as RateLimiterService } from "./rate-limiter"; export { default as DocumentMetadataService } from "./document-metadata"; +export { default as FileService } from "./file"; From 9643e08e7667a2521d16542dd2b8c01eea1c2784 Mon Sep 17 00:00:00 2001 From: Louis Murerwa Date: Mon, 13 May 2024 12:23:15 -0700 Subject: [PATCH 4/4] Add File Service --- packages/ocular/src/api/middlewares/index.ts | 16 ++-- .../src/api/middlewares/transform-body.ts | 20 +++++ .../api/routes/admin/files/create-upload.ts | 43 ++++++++++ .../api/routes/admin/files/delete-upload.ts | 32 +++++++ .../src/api/routes/admin/files/index.ts | 41 +++++++++ packages/ocular/src/api/routes/admin/index.ts | 37 ++++---- packages/ocular/src/services/file.ts | 84 ++++++------------- packages/types/src/interfaces/file-service.ts | 15 ++-- 8 files changed, 192 insertions(+), 96 deletions(-) create mode 100644 packages/ocular/src/api/middlewares/transform-body.ts create mode 100644 packages/ocular/src/api/routes/admin/files/create-upload.ts create mode 100644 packages/ocular/src/api/routes/admin/files/delete-upload.ts create mode 100644 packages/ocular/src/api/routes/admin/files/index.ts diff --git a/packages/ocular/src/api/middlewares/index.ts b/packages/ocular/src/api/middlewares/index.ts index b25381f7..f8d2fcfb 100644 --- a/packages/ocular/src/api/middlewares/index.ts +++ b/packages/ocular/src/api/middlewares/index.ts @@ -1,14 +1,14 @@ -import { default as authenticateAdmin } from "./authenticate-admin" -import { default as authenticate} from "./authenticate" -import { default as wrap } from "./await-middleware" -import { default as registeredLoggedinUser } from "./logged-in-user" +import { default as authenticateAdmin } from "./authenticate-admin"; +import { default as authenticate } from "./authenticate"; +import { default as wrap } from "./await-middleware"; +import { default as registeredLoggedinUser } from "./logged-in-user"; - -export { transformQuery } from "./transform-query" +export { transformQuery } from "./transform-query"; +export { transformBody } from "./transform-body"; export default { authenticateAdmin, authenticate, registeredLoggedinUser, - wrap -} \ No newline at end of file + wrap, +}; diff --git a/packages/ocular/src/api/middlewares/transform-body.ts b/packages/ocular/src/api/middlewares/transform-body.ts new file mode 100644 index 00000000..bce1ba50 --- /dev/null +++ b/packages/ocular/src/api/middlewares/transform-body.ts @@ -0,0 +1,20 @@ +import { ValidatorOptions } from "class-validator"; +import { NextFunction, Request, Response } from "express"; +import { ClassConstructor } from "../../types/global"; +import { validator } from "@ocular/utils"; + +export function transformBody( + plainToClass: ClassConstructor, + config: ValidatorOptions = { + forbidUnknownValues: false, + } +): (req: Request, res: Response, next: NextFunction) => Promise { + return async (req: Request, res: Response, next: NextFunction) => { + try { + req.validatedBody = await validator(plainToClass, req.body, config); + next(); + } catch (e) { + next(e); + } + }; +} diff --git a/packages/ocular/src/api/routes/admin/files/create-upload.ts b/packages/ocular/src/api/routes/admin/files/create-upload.ts new file mode 100644 index 00000000..da50801e --- /dev/null +++ b/packages/ocular/src/api/routes/admin/files/create-upload.ts @@ -0,0 +1,43 @@ +import fs from "fs"; + +/** + * @oas [post] / + * operationId: "PostUploads" + * summary: "Uploads an array of files" + * description: "Uploads an array of files to the specific fileservice that is installed." + * x-authenticated: true + * tags: + * - Uploads + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * uploads + */ +export default async (req, res) => { + try { + const fileService = req.scope.resolve("fileService"); + + const result = await Promise.all( + req.files.map(async (f) => { + return fileService.upload(f).then((result) => { + fs.unlinkSync(f.path); + return result; + }); + }) + ); + + res.status(200).json({ uploads: result }); + } catch (err) { + console.log(err); + throw err; + } +}; + +export class IAdminPostUploadsFileReq { + originalName: string; + path: string; +} diff --git a/packages/ocular/src/api/routes/admin/files/delete-upload.ts b/packages/ocular/src/api/routes/admin/files/delete-upload.ts new file mode 100644 index 00000000..21263258 --- /dev/null +++ b/packages/ocular/src/api/routes/admin/files/delete-upload.ts @@ -0,0 +1,32 @@ +import { IsString } from "class-validator"; + +/** + * [delete] /uploads + * operationId: "AdminDeleteUploads" + * summary: "Removes an uploaded file" + * description: "Removes an uploaded file using the installed fileservice" + * x-authenticated: true + * tags: + * - Uploads + * responses: + * 200: + * description: OK + */ +export default async (req, res) => { + const validated = req.validatedBody as AdminDeleteUploadsReq; + + const fileService = req.scope.resolve("fileService"); + + await fileService.delete({ + fileKey: validated.file_key, + }); + + res + .status(200) + .send({ id: validated.file_key, object: "file", deleted: true }); +}; + +export class AdminDeleteUploadsReq { + @IsString() + file_key: string; +} diff --git a/packages/ocular/src/api/routes/admin/files/index.ts b/packages/ocular/src/api/routes/admin/files/index.ts new file mode 100644 index 00000000..19742116 --- /dev/null +++ b/packages/ocular/src/api/routes/admin/files/index.ts @@ -0,0 +1,41 @@ +import { Router } from "express"; +import multer from "multer"; +import middlewares, { transformBody } from "../../../middlewares"; +import { AdminDeleteUploadsReq } from "./delete-upload"; + +const route = Router(); +const upload = multer({ dest: "uploads/" }); + +export default (app) => { + app.use("/uploads", route); + + route.post( + "/", + upload.array("files"), + middlewares.wrap(require("./create-upload").default) + ); + + route.delete( + "/", + transformBody(AdminDeleteUploadsReq), + middlewares.wrap(require("./delete-upload").default) + ); + return app; +}; + +export type AdminUploadsRes = { + uploads: { url: string }[]; +}; + +export type AdminDeleteUploadsRes = { + id: string; + object: string; + deleted: boolean; +}; + +export type AdminUploadsDownloadUrlRes = { + download_url: string; +}; + +export * from "./create-upload"; +export * from "./delete-upload"; diff --git a/packages/ocular/src/api/routes/admin/index.ts b/packages/ocular/src/api/routes/admin/index.ts index 5bfee31e..cc7068ed 100644 --- a/packages/ocular/src/api/routes/admin/index.ts +++ b/packages/ocular/src/api/routes/admin/index.ts @@ -1,26 +1,23 @@ -import { Router } from "express" -import middlewares from "../../middlewares" -import users from "./users/index" -import apps from "./apps" -// import components from "./member/components" -// import search from "./member/search" -import organisation from "./organisation" +import { Router } from "express"; +import middlewares from "../../middlewares"; +import users from "./users/index"; +import apps from "./apps"; +import uploads from "./files"; +import organisation from "./organisation"; export default (app, container, config) => { - const route = Router() - app.use("/admin",route) - + const route = Router(); + app.use("/admin", route); + // Create User Routes Admin Routes - users(route) + users(route); // Authenticated routes - route.use(middlewares.authenticateAdmin()) - route.use(middlewares.registeredLoggedinUser) - - apps(route) - // components(route) - organisation(route) + route.use(middlewares.authenticateAdmin()); + route.use(middlewares.registeredLoggedinUser); - // users(route) - return app -} \ No newline at end of file + apps(route); + organisation(route); + uploads(route); + return app; +}; diff --git a/packages/ocular/src/services/file.ts b/packages/ocular/src/services/file.ts index 828276e8..77c60670 100644 --- a/packages/ocular/src/services/file.ts +++ b/packages/ocular/src/services/file.ts @@ -7,7 +7,8 @@ import { } from "@ocular/types"; import { AutoflowAiError, AutoflowAiErrorTypes } from "@ocular/utils"; import path from "path"; -import fs from "fs/promises"; +import fs from "fs"; +import { parse } from "path"; export default class FileService extends AbstractFileService { static identifier = "localfs"; @@ -19,26 +20,13 @@ export default class FileService extends AbstractFileService { protected readonly config?: Record // eslint-disable-next-line @typescript-eslint/no-empty-function ) { super(container, config); - (this.uploadDir_ = "/uploads"), + (this.uploadDir_ = "local-uploads"), (this.backendUrl_ = "http://localhost:9000"); + this.ensureDirExists(""); } - async upload(file: FileUploadData): Promise { - if (!file) { - throw new AutoflowAiError( - AutoflowAiErrorTypes.INVALID_DATA, - `No file provided` - ); - } - - if (!file.filename) { - throw new AutoflowAiError( - AutoflowAiErrorTypes.INVALID_DATA, - `No filename provided` - ); - } - - const parsedFilename = path.parse(file.filename); + async upload(file: Express.Multer.File): Promise { + const parsedFilename = parse(file.originalname); if (parsedFilename.dir) { this.ensureDirExists(parsedFilename.dir); @@ -49,60 +37,40 @@ export default class FileService extends AbstractFileService { `${Date.now()}-${parsedFilename.base}` ); - const filePath = this.getUploadFilePath(fileKey); - const fileUrl = this.getUploadFileUrl(fileKey); + return new Promise((resolve, reject) => { + fs.copyFile(file.path, `${this.uploadDir_}/${fileKey}`, (err) => { + if (err) { + reject(err); + throw err; + } - const content = Buffer.from(file.content, "binary"); - await fs.writeFile(filePath, content); + const fileUrl = `${this.backendUrl_}/${this.uploadDir_}/${fileKey}`; - return { - key: fileKey, - url: fileUrl, - }; + resolve({ url: fileUrl, key: fileKey }); + }); + }); } async delete(file: FileDeleteData): Promise { - const filePath = this.getUploadFilePath(file.fileKey); try { - await fs.access(filePath, fs.constants.F_OK); - await fs.unlink(filePath); - } catch (e) { - // The file does not exist, so it's a noop. - } - - return; - } - - async getPresignedDownloadUrl(fileData: FileGetData): Promise { - try { - await fs.access( - this.getUploadFilePath(fileData.fileKey), - fs.constants.F_OK - ); + const filePath = `${this.uploadDir_}/${file.fileKey}`; + console.log(filePath); + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + } } catch { throw new AutoflowAiError( AutoflowAiErrorTypes.FILE_NOT_FOUND, - `File with key ${fileData.fileKey} not found` + `File with key ${file.fileKey} not found` ); } - - return this.getUploadFileUrl(fileData.fileKey); } - private getUploadFilePath = (fileKey: string) => { - return path.join(this.uploadDir_, fileKey); - }; - - private getUploadFileUrl = (fileKey: string) => { - return path.join(this.backendUrl_, this.getUploadFilePath(fileKey)); - }; - - private async ensureDirExists(dirPath: string) { + private ensureDirExists(dirPath: string) { const relativePath = path.join(this.uploadDir_, dirPath); - try { - await fs.access(relativePath, fs.constants.F_OK); - } catch (e) { - await fs.mkdir(relativePath, { recursive: true }); + + if (!fs.existsSync(relativePath)) { + fs.mkdirSync(relativePath, { recursive: true }); } } } diff --git a/packages/types/src/interfaces/file-service.ts b/packages/types/src/interfaces/file-service.ts index 06bbd66a..35b960f9 100644 --- a/packages/types/src/interfaces/file-service.ts +++ b/packages/types/src/interfaces/file-service.ts @@ -17,14 +17,15 @@ export type FileDeleteData = { }; export type FileUploadData = { + originalname: string; filename: string; mimeType: string; - content: string; + path: string; }; + export interface IFileProvider { - upload(file: FileUploadData): Promise; + upload(file: Express.Multer.File): Promise; delete(fileData: FileDeleteData): Promise; - getPresignedDownloadUrl(fileData: FileGetData): Promise; } export interface FileServiceOptions { @@ -52,16 +53,10 @@ export class AbstractFileService return (this.constructor as any).identifier; } - async upload(file: FileUploadData): Promise { + async upload(file: Express.Multer.File): Promise { throw Error("upload must be overridden by the child class"); } async delete(file: FileDeleteData): Promise { throw Error("delete must be overridden by the child class"); } - - async getPresignedDownloadUrl(fileData: FileGetData): Promise { - throw Error( - "getPresignedDownloadUrl must be overridden by the child class" - ); - } }