import { Body, Controller, Delete, Get, Patch, Path, Post, Request, Route, Security, SuccessResponse, Tags, } from "tsoa"; import esClient from "../elasticsearch"; import minioClient from "../minio"; import HttpStatusCode from "../interfaces/http-status"; import { EhrFile } from "../interfaces/ehr-fs"; import HttpError from "../interfaces/http-error"; import { copyCond, pathExist } from "../utils/minio"; const DEFAULT_BUCKET = process.env.MINIO_BUCKET; const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; if (!DEFAULT_BUCKET) throw Error("Default MinIO bucket must be specified."); if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified."); @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 } }, @Body() body: { file: string; title: string; description: string; category: string; keyword: string; }, @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, ) { const pathname = `${cabinetName}/${drawerName}/${folderName}/${body.file}`; if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/`))) { throw new HttpError( HttpStatusCode.NOT_FOUND, "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ", ); } const rec = await popInfo(pathname); const metadata: Partial = { pathname, fileName: body.file, fileSize: 0, fileType: "", title: body.title, description: body.description, category: body.category.split(","), keyword: body.keyword.split(","), upload: false, createdAt: new Date().toISOString(), createdBy: rec ? rec.createdBy : "n/a", updatedAt: new Date().toISOString(), updatedBy: request.user.preferred_username ?? "n/a", }; await esClient.index({ index: "dev-index", document: metadata, }); return { ...body, createdAt: metadata.createdAt, createdBy: metadata.createdBy, updatedAt: metadata.updatedAt, updatedBy: metadata.updatedBy, upload: await minioClient.presignedPutObject("ehr", pathname), }; } @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}/`, }, }, }); const records = search.hits.hits .map((v) => { if (v._source) { const { attachment, ...rest } = v._source; return rest satisfies EhrFile; } }) .filter((v: EhrFile | undefined): v is EhrFile => !!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, @Body() body: { file?: string; title?: string; description?: string; category?: string; keyword?: string; }, ): Promise { 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.", ); } // assume user will replace file by re-upload if (body.file) { const destination = `${cabinetName}/${drawerName}/${folderName}/${body.file}`; const source = `ehr/${cabinetName}/${drawerName}/${folderName}/${fileName}`; const copy = await minioClient.copyObject("ehr", destination, source, copyCond); if (copy) { const search = await esClient .search }>({ index: "my-test-index", query: { match: { pathname } }, }) .catch((e) => console.error(e)); if (search && search.hits.hits.length > 0 && search.hits.hits[0]._source) { const { _index: index, _id: id } = search.hits.hits[0]; await esClient .update({ index, id, doc: { pathname: destination, updatedAt: new Date().toISOString(), updatedBy: request.user.preferred_username ?? "n/a", }, }) .then(() => minioClient.removeObject("ehr", pathname)); } else { await minioClient.removeObject("ehr", pathname); } } } else { const search = await esClient .search }>({ index: "my-test-index", query: { match: { pathname } }, }) .catch((e) => console.error(e)); if (search && search.hits.hits.length > 0 && search.hits.hits[0]._source) { const { _index: index, _id: id } = search.hits.hits[0]; await esClient.update({ index, id, doc: { ...body, keyword: body.keyword?.split(","), category: body.category?.split(","), updatedAt: new Date().toISOString(), updatedBy: request.user.preferred_username ?? "n/a", }, }); } } return body.file ? this.setStatus(HttpStatusCode.NO_CONTENT) : { upload: await minioClient.presignedPutObject( "ehr", `${cabinetName}/${drawerName}/${folderName}/${body.file ?? fileName}`, ), }; } @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 result = await esClient .deleteByQuery({ index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", query: { match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, }, }, }) .catch((e) => console.error(e)); if (result && result.total === 0) { throw new HttpError(HttpStatusCode.NOT_FOUND, "Data not found"); } if (!result) { 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}`, ), }; } } async function popInfo(pathname: string) { const result = await esClient .search }>({ index: DEFAULT_INDEX!, query: { match: { pathname } }, }) .catch((e) => console.error(e)); // pathname is unique and should not have multiple record with same path if (result && result.hits.hits.length > 0 && result.hits.hits[0]._source) { await esClient .delete({ index: DEFAULT_INDEX!, id: result.hits.hits[0]._id, }) .catch((e) => console.error(e)); return result.hits.hits[0]._source; } return false; }