Skip to content
This repository has been archived by the owner on Sep 9, 2024. It is now read-only.

Adding Upload Files API Functionality #81

Merged
merged 4 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions packages/ocular/src/api/middlewares/index.ts
Original file line number Diff line number Diff line change
@@ -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
}
wrap,
};
20 changes: 20 additions & 0 deletions packages/ocular/src/api/middlewares/transform-body.ts
Original file line number Diff line number Diff line change
@@ -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<T>(
plainToClass: ClassConstructor<T>,
config: ValidatorOptions = {
forbidUnknownValues: false,
}
): (req: Request, res: Response, next: NextFunction) => Promise<void> {
return async (req: Request, res: Response, next: NextFunction) => {
try {
req.validatedBody = await validator(plainToClass, req.body, config);
next();
} catch (e) {
next(e);
}
};
}
43 changes: 43 additions & 0 deletions packages/ocular/src/api/routes/admin/files/create-upload.ts
Original file line number Diff line number Diff line change
@@ -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;
}
32 changes: 32 additions & 0 deletions packages/ocular/src/api/routes/admin/files/delete-upload.ts
Original file line number Diff line number Diff line change
@@ -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;
}
41 changes: 41 additions & 0 deletions packages/ocular/src/api/routes/admin/files/index.ts
Original file line number Diff line number Diff line change
@@ -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";
37 changes: 17 additions & 20 deletions packages/ocular/src/api/routes/admin/index.ts
Original file line number Diff line number Diff line change
@@ -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
}
apps(route);
organisation(route);
uploads(route);
return app;
};
92 changes: 92 additions & 0 deletions packages/ocular/src/services/__tests__/file.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
76 changes: 76 additions & 0 deletions packages/ocular/src/services/file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import {
AbstractFileService,
FileDeleteData,
FileGetData,
FileUploadData,
FileUploadResult,
} from "@ocular/types";
import { AutoflowAiError, AutoflowAiErrorTypes } from "@ocular/utils";
import path from "path";
import fs from "fs";
import { parse } from "path";

export default class FileService extends AbstractFileService {
static identifier = "localfs";
protected uploadDir_: string;
protected backendUrl_: string;

constructor(
protected readonly container: Record<string, unknown>,
protected readonly config?: Record<string, unknown> // eslint-disable-next-line @typescript-eslint/no-empty-function
) {
super(container, config);
(this.uploadDir_ = "local-uploads"),
(this.backendUrl_ = "http://localhost:9000");
this.ensureDirExists("");
}

async upload(file: Express.Multer.File): Promise<FileUploadResult> {
const parsedFilename = parse(file.originalname);

if (parsedFilename.dir) {
this.ensureDirExists(parsedFilename.dir);
}

const fileKey = path.join(
parsedFilename.dir,
`${Date.now()}-${parsedFilename.base}`
);

return new Promise((resolve, reject) => {
fs.copyFile(file.path, `${this.uploadDir_}/${fileKey}`, (err) => {
if (err) {
reject(err);
throw err;
}

const fileUrl = `${this.backendUrl_}/${this.uploadDir_}/${fileKey}`;

resolve({ url: fileUrl, key: fileKey });
});
});
}

async delete(file: FileDeleteData): Promise<void> {
try {
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 ${file.fileKey} not found`
);
}
}

private ensureDirExists(dirPath: string) {
const relativePath = path.join(this.uploadDir_, dirPath);

if (!fs.existsSync(relativePath)) {
fs.mkdirSync(relativePath, { recursive: true });
}
}
}
1 change: 1 addition & 0 deletions packages/ocular/src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Loading