import { Body, Controller, Delete, Get, Patch, Path, Post, Request, Response, 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}/subfolder/{subFolderName}/file", ) export class SubFolderFileController extends Controller { @Get("/") @Tags("ไฟล์") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async getFile( @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @Path() subFolderName: string, ): Promise { const search = await esClient.search }>({ index: DEFAULT_INDEX!, query: { prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`, }, }, size: 10000, }); 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; } @Post("/") @Tags("ไฟล์") @Security("bearerAuth", ["admin"]) @Response( HttpStatusCode.NOT_FOUND, "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ", ) @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, @Path() subFolderName: string, ) { const pathname = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${body.file}`; if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/${subFolderName}`))) { throw new HttpError( HttpStatusCode.NOT_FOUND, "ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ", ); } 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)); } const rec = result ? result.hits.hits[0]._source : false; 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: DEFAULT_INDEX!, document: metadata, }); return { ...body, createdAt: metadata.createdAt, createdBy: metadata.createdBy, updatedAt: metadata.updatedAt, updatedBy: metadata.updatedBy, upload: await minioClient.presignedPutObject(DEFAULT_BUCKET!, pathname), }; } @Patch("/{fileName}") @Tags("ไฟล์") @Security("bearerAuth", ["admin"]) @Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม") @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async updateFile( @Request() request: { user: { preferred_username: string } }, @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @Path() subFolderName: string, @Path() fileName: string, @Body() body: { file?: string; title?: string; description?: string; category?: string; keyword?: string; }, ) { const basePath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`; const pathname = `${basePath}${fileName}`; if ( !Boolean( await minioClient.statObject(DEFAULT_BUCKET!, `${pathname}`).catch((e) => { if (e.code === "NotFound") return false; throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์"); }), ) ) { throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์"); } // assume user will probably replace file by re-upload but maybe just rename if (body.file) { const destination = `${basePath}${body.file}`; const source = `/${DEFAULT_BUCKET}/${basePath}${fileName}`; const copy = await minioClient.copyObject(DEFAULT_BUCKET!, destination, source, copyCond); if (copy) { const search = await esClient .search }>({ index: DEFAULT_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(DEFAULT_BUCKET!, pathname)); } else { await minioClient.removeObject(DEFAULT_BUCKET!, pathname); } } } else { const search = await esClient .search }>({ index: DEFAULT_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 ? { upload: await minioClient.presignedPutObject( DEFAULT_BUCKET!, `${basePath}${body.file ?? fileName}`, ), } : this.setStatus(HttpStatusCode.NO_CONTENT); } @Delete("/{fileName}") @Tags("ไฟล์") @Security("bearerAuth", ["admin"]) @SuccessResponse(HttpStatusCode.OK, "สำเร็จ") public async deleteFile( @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @Path() subFolderName: string, @Path() fileName: string, ) { await minioClient.removeObject( DEFAULT_BUCKET!, `${cabinetName}/${drawerName}/${folderName}/${fileName}/${subFolderName}/`, ); return this.setStatus(HttpStatusCode.NO_CONTENT); } @Get("/{fileName}") @Tags("ดาวน์โหลด") @Security("bearerAuth") @SuccessResponse(HttpStatusCode.OK) public async downloadFile( @Path() cabinetName: string, @Path() drawerName: string, @Path() folderName: string, @Path() subFolderName: string, @Path() fileName: string, ) { const search = await esClient.search }>({ index: DEFAULT_INDEX!, query: { match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`, }, }, }); if (search && search.hits.hits.length === 0) { throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found"); } const { attachment, ...rest } = search.hits.hits[0]._source!; return { ...rest, download: await minioClient.presignedGetObject( DEFAULT_BUCKET!, `${cabinetName}/${drawerName}/${folderName}/${fileName}`, ), }; } }