diff --git a/Services/server/src/controllers/drawerController.ts b/Services/server/src/controllers/drawerController.ts index 6afb601..329a1b4 100644 --- a/Services/server/src/controllers/drawerController.ts +++ b/Services/server/src/controllers/drawerController.ts @@ -69,7 +69,7 @@ export class DrawerController extends Controller { }), ) ) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบลิ้นชัก"); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างลิ้นชัก"); } const created = await minioClient diff --git a/Services/server/src/controllers/fileController.ts b/Services/server/src/controllers/fileController.ts index 6fb9393..310a008 100644 --- a/Services/server/src/controllers/fileController.ts +++ b/Services/server/src/controllers/fileController.ts @@ -1,7 +1,7 @@ import { + Body, Controller, Delete, - FormField, Get, Patch, Path, @@ -11,14 +11,22 @@ import { Security, SuccessResponse, Tags, - UploadedFile, } from "tsoa"; + import esClient from "../elasticsearch"; -import minioClient from "../storage"; +import minioClient from "../minio"; + import HttpStatusCode from "../interfaces/http-status"; -import { pathExist } from "../utils/minio"; -import HttpError from "../interfaces/http-error"; 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 { @@ -28,87 +36,57 @@ export class FileController extends Controller { @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, + @Body() + body: { + file: string; + title: string; + description: string; + category: string; + keyword: 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}`; + const pathname = `${cabinetName}/${drawerName}/${folderName}/${body.file}`; if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/`))) { throw new HttpError( - HttpStatusCode.PRECONDITION_FAILED, - "Cabinet, drawer or folder cannot be found.", + HttpStatusCode.NOT_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 rec = await popInfo(pathname); const metadata: Partial = { pathname, - fileName: filename, - fileSize: file.size, - fileType: file.mimetype, - title: title, - description: description, - category: category.split(","), - keyword: keyword.split(","), + 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", }; - 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, - }, - }); - } + await esClient.index({ + index: "dev-index", + document: metadata, + }); - return this.setStatus(HttpStatusCode.CREATED); + return { + ...body, + createdAt: metadata.createdAt, + createdBy: metadata.createdBy, + updatedAt: metadata.updatedAt, + updatedBy: metadata.updatedBy, + upload: await minioClient.presignedPutObject("ehr", pathname), + }; } @Get("/") @@ -124,7 +102,7 @@ export class FileController extends Controller { attachment: Record; } >({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", query: { prefix: { pathname: `${cabinetName}/${drawerName}/${folderName}/`, @@ -132,16 +110,14 @@ export class FileController extends Controller { }, }); - // 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; + if (v._source) { + const { attachment, ...rest } = v._source; + return rest satisfies EhrFile; + } }) - .flatMap((v) => (v ? [v] : [])); + .filter((v: EhrFile | undefined): v is EhrFile => !!v); return records; } @@ -156,86 +132,88 @@ export class FileController extends Controller { @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}`, - }, - }, - }); + @Body() + body: { + file?: string; + title?: string; + description?: string; + category?: string; + keyword?: string; + }, + ): Promise { + const 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}`, + 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, + // 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 (!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, - }, - }); + 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 this.setStatus(HttpStatusCode.NO_CONTENT); + return body.file + ? this.setStatus(HttpStatusCode.NO_CONTENT) + : { + upload: await minioClient.presignedPutObject( + "ehr", + `${cabinetName}/${drawerName}/${folderName}/${body.file ?? fileName}`, + ), + }; } @Delete("/{fileName}") @@ -248,31 +226,24 @@ export class FileController extends Controller { @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}`, + const result = await esClient + .deleteByQuery({ + 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."); + 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}`); @@ -289,11 +260,9 @@ export class FileController extends Controller { @Path() fileName: string, ) { const search = await esClient.search }>({ - index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index', + index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index", query: { - match: { - pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`, - }, + match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}` }, }, }); @@ -318,3 +287,26 @@ export class FileController extends Controller { }; } } + +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; +} diff --git a/Services/server/src/controllers/folderController.ts b/Services/server/src/controllers/folderController.ts index f838b6c..80251a6 100644 --- a/Services/server/src/controllers/folderController.ts +++ b/Services/server/src/controllers/folderController.ts @@ -53,7 +53,7 @@ export class FolderController extends Controller { @Post("/") @Tags("Folder") @Security("bearerAuth", ["admin"]) - @Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม") + @Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม") @Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์") @SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ") public async createFolder( @@ -72,7 +72,7 @@ export class FolderController extends Controller { }), ) ) { - throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบแฟ้ม"); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม"); } const created = await minioClient