import { Controller, Delete, FormField, Get, Patch, Path, Post, Request, Route, Security, SuccessResponse, Tags, UploadedFile, } from "tsoa"; import esClient from "../elasticsearch"; import minioClient from "../storage"; import HttpStatusCode from "../interfaces/http-status"; import { pathExist } from "../utils/minio"; import HttpError from "../interfaces/http-error"; import { EhrFile } from "../interfaces/ehr-fs"; @Route("/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/file") export class FileController extends Controller { @Post("/") @Tags("File") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.CREATED) public async uploadFile( @Request() request: { user: { preferred_username: string } }, @UploadedFile() file: Express.Multer.File, @FormField() title: string, @FormField() description: string, @FormField() keyword: string, @FormField() category: string, @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, ) { const filename = Buffer.from(file.originalname, "latin1").toString("utf-8"); const pathname = `${cabinetName}/${drawerName}/${folderName}/${filename}`; if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/`))) { throw new HttpError( HttpStatusCode.PRECONDITION_FAILED, "Cabinet, drawer or folder cannot be found.", ); } const info = await minioClient .putObject("ehr", pathname, file.buffer, file.size, { "Content-Type": file.mimetype, createdAt: new Date().toISOString(), createdBy: request.user.preferred_username, }) .catch((e) => console.error(e)); if (!info) throw new Error("Object storage error occured."); const search = await esClient.search }>({ index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', query: { match: { pathname: pathname, }, }, }); const exist = search.hits.hits.find((v) => v._source?.pathname === pathname); const metadata: Partial = { pathname, fileName: filename, fileSize: file.size, fileType: file.mimetype, title: title, description: description, category: category.split(","), keyword: keyword.split(","), }; if (!exist) { await esClient.index({ pipeline: "attachment", index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', document: { data: Buffer.from(file.buffer).toString("base64"), createdAt: new Date().toISOString(), createdBy: request.user.preferred_username, updatedAt: new Date().toISOString(), updatedBy: request.user.preferred_username, ...metadata, }, }); } else { await esClient.delete({ index: exist._index, id: exist._id }); await esClient.index({ pipeline: "attachment", index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', document: { data: Buffer.from(file.buffer).toString("base64"), createdAt: exist._source?.createdAt, createdBy: exist._source?.createdBy, updatedAt: new Date().toISOString(), updatedBy: request.user.preferred_username, ...metadata, }, }); } return this.setStatus(HttpStatusCode.CREATED); } @Get("/") @Tags("File") @SuccessResponse(HttpStatusCode.OK) public async getFile( @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, ): Promise { const search = await esClient.search< EhrFile & { attachment: Record; } >({ index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', query: { prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/`, }, }, }); // Use flatMap for return type only. Filter does not change type after filter out undefined or null const records = search.hits.hits .map((v) => { if (!v._source) return; const { attachment, ...rest } = v._source; return rest; }) .flatMap((v) => (v ? [v] : [])); return records; } @Patch("/{fileName}") @Tags("File") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) public async updateFile( @Request() request: { user: { preferred_username: string } }, @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @Path() fileName: string, @UploadedFile() file?: Express.Multer.File, @FormField() title?: string, @FormField() description?: string, @FormField() keyword?: string, @FormField() category?: string, ) { const search = await esClient.search }>({ index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', query: { match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, }, }, }); if (search && search.hits.hits.length === 0) { throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); } const data = search.hits.hits[0]; if (!file) { const esResult = await esClient .update({ index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', id: data._id, doc: { title, description, keyword: keyword?.split(","), category: category?.split(","), updatedAt: new Date().toISOString(), updatedBy: request.user.preferred_username, }, }) .catch((e) => console.error(e)); if (!esResult) throw new Error("An error occured, cannot perform this action."); } else { const filename = Buffer.from(file.originalname, "latin1").toString("utf-8"); const pathname = `${cabinetName}/${drawerName}/${folderName}/${filename}`; await minioClient.removeObject( "ehr", `${cabinetName}/${drawerName}/${folderName}/${fileName}`, ); const info = await minioClient .putObject("ehr", pathname, file.buffer, file.size, { "Content-Type": file.mimetype, createdAt: new Date().toISOString(), createdBy: request.user.preferred_username, }) .catch((e) => console.error(e)); if (!info) throw new Error("Object storage error occured."); await esClient.delete({ index: data._index, id: data._id }); await esClient.index({ pipeline: "attachment", index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', document: { data: Buffer.from(file.buffer).toString("base64"), pathname, fileName: filename, fileSize: file.size, fileType: file.mimetype, title: title, description: description, category: category?.split(","), keyword: keyword?.split(","), createdAt: data._source?.createdAt, createdBy: data._source?.createdBy, updatedAt: new Date().toISOString(), updatedBy: request.user.preferred_username, }, }); } return this.setStatus(HttpStatusCode.NO_CONTENT); } @Delete("/{fileName}") @Tags("File") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) public async deleteFile( @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @Path() fileName: string, ) { const search = await esClient.search< EhrFile & { attachment: Record; } >({ index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', query: { match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, }, }, }); if (search && search.hits.hits.length === 0) { throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); } const esResult = await esClient .delete({ index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', id: search.hits.hits[0]._id, }) .catch((e) => console.error(e)); if (!esResult) throw new Error("An error occured, cannot perform this action."); await minioClient.removeObject("ehr", `${cabinetName}/${drawerName}/${folderName}/${fileName}`); return this.setStatus(HttpStatusCode.NO_CONTENT); } @Get("/{fileName}") @Tags("File") @SuccessResponse(HttpStatusCode.OK) public async downloadFile( @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @Path() fileName: string, ) { const search = await esClient.search }>({ index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', query: { match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, }, }, }); if (search && search.hits.hits.length === 0) { throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); } const data = search.hits.hits[0]._source; if (!data) { throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "Found data but no info."); } const { attachment, ...rest } = data; return { ...rest, download: await minioClient.presignedGetObject( "ehr", `${cabinetName}/${drawerName}/${folderName}/${fileName}`, ), }; } }