diff --git a/Services/server/src/app.ts b/Services/server/src/app.ts index fdff76d..d735caa 100644 --- a/Services/server/src/app.ts +++ b/Services/server/src/app.ts @@ -42,17 +42,11 @@ const io = new Server(server, { }); setInstance(io); - -io.on("connection", (socket) => { - console.log("User Connected"); - - socket.on("disconnected", () => { - console.log("User Disconnected"); - }); -}); +io.on("connection", () => console.log("[Socket.IO] User connected.")); +io.on("disconnected", () => console.log("[Socket.IO] User disconnected.")); server.listen(PORT, "0.0.0.0", () => console.log(`[APP] Application is running on http://localhost:${PORT}`), ); -rabbitmq.init(amqHandler).catch((e) => console.error(e)); +// rabbitmq.init(amqHandler).catch((e) => console.error(e)); diff --git a/Services/server/src/controllers/storageController.ts b/Services/server/src/controllers/storageController.ts index 1b15615..cb6866e 100644 --- a/Services/server/src/controllers/storageController.ts +++ b/Services/server/src/controllers/storageController.ts @@ -1,9 +1,12 @@ -import { Body, Controller, Delete, Example, Post, Put, Route, SuccessResponse } from "tsoa"; +import { Body, Controller, Delete, Example, Post, Put, Route, SuccessResponse, Tags } from "tsoa"; + import minioClient from "../minio"; import esClient from "../elasticsearch"; + import HttpError from "../interfaces/http-error"; import HttpStatusCode from "../interfaces/http-status"; import { StorageFile, StorageFolder } from "../interfaces/storage-fs"; + import { copyCond } from "../utils/minio"; if (!process.env.MINIO_BUCKET) throw Error("Default MinIO bucket must be specified."); @@ -20,35 +23,73 @@ interface ListRequestBody { path: string[]; } -interface CreateFolderBody { +interface FolderBody { + /** @example ["แฟ้ม 1", "แฟ้ม 2"] */ path: string[]; + /** @example "แฟ้ม 3" */ name: string; } interface PutFolderBody { from: { + /** @example ["แฟ้ม 1", "แฟ้ม 2"] */ path: string[]; + /** @example "แฟ้ม 3" */ name: string; }; to: { + /** @example ["แฟ้ม 1", "แฟ้ม 2"] */ path: string[]; + /** @example "แฟ้ม 3 แก้ไข" */ name: string; }; } interface DeleteFolderBody { + /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3 แก้ไข"] */ path: string[]; } -interface CreateFileBody { +interface FileBody { + /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */ path: string[]; + /** @example "ไฟล์ 1.xlsx" */ file: string; + /** @example "การเงิน" */ title?: string; + /** @example "การเงิน" */ description?: string; + /** @example ["การเงิน", "รายงาน"] */ category?: string[]; + /** @example ["การเงิน", "รายรับ", "รายจ่าย"] */ keyword?: string[]; } +interface PutFileBody extends Omit { + /** หากต้องการอัพโหลดไฟล์ด้วยให้ส่งค่าเป็นจริง */ + from: { + /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */ + path: string[]; + /** @example "ไฟล์ 1.xlsx" */ + file: string; + }; + to?: { + /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */ + path: string[]; + /** @example "ไฟล์ 1 แก้ไข.xlsx" */ + file: string; + }; + /** @example false */ + upload?: boolean; +} + +interface DeleteFileBody { + /** @example ["แฟ้ม 1", "แฟ้ม 2", "แฟ้ม 3"] */ + path: string[]; + /** @example "ไฟล์ 1 แก้ไข.xlsx" */ + file: string; +} + async function listFolder(path: string[]) { const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => { const item: { pathname: string; name: string }[] = []; @@ -90,13 +131,9 @@ async function listFolder(path: string[]) { async function listFile(path: string[]) { const result = await esClient .search }>({ - index: DEFAULT_INDEX!, + index: DEFAULT_INDEX, sort: [{ pathname: "asc" }], - query: { - match: { - path: path.join("/") + "/", - }, - }, + query: { match: { path: path.join("/") + "/" } }, size: 10000, }) .then((r) => r.hits.hits); @@ -114,8 +151,12 @@ async function listFile(path: string[]) { } async function checkPathExist(bucket: string, path: string) { + return await checkFileExist(bucket, `${path}/.keep`); +} + +async function checkFileExist(bucket: string, pathname: string) { return Boolean( - await minioClient.statObject(bucket, `${path}/.keep`).catch((e) => { + await minioClient.statObject(bucket, pathname).catch((e) => { if (e.code === "NotFound") return false; console.error(`Storage Error: ${e}`); throw new Error(MINIO_ERROR_MESSAGE); @@ -158,6 +199,7 @@ export class StorageController extends Controller { updatedBy: "admin", }, ]) + @Tags("Storage Folder", "Storage File") public async getList(@Body() body: ListRequestBody) { const path = body.path.filter(Boolean); @@ -166,8 +208,9 @@ export class StorageController extends Controller { } @Post("folder") + @Tags("Storage Folder") @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") - public async postFolder(@Body() body: CreateFolderBody) { + public async postFolder(@Body() body: FolderBody) { const { path, name } = body; if (!(await checkPathExist(DEFAULT_BUCKET, path.join("/")))) { @@ -196,6 +239,8 @@ export class StorageController extends Controller { * ยา้ย Folder ภายใต้ Folder (Path) หนึ่ง ไปภายใน Folder (Path) หนึ่งและสามารถเปลี่ยนชื่อได้ */ @Put("folder") + @Tags("Storage Folder") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async moveFolder(@Body() body: PutFolderBody) { const src = `${body.from.path.join("/")}/${body.from.name}`; const dst = `${body.to.path.join("/")}/${body.to.name}`; @@ -284,6 +329,7 @@ export class StorageController extends Controller { * ลบ Folder หรือ File ออกจากระบบ */ @Delete("folder") + @Tags("Storage Folder") @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") public async deleteStorage(@Body() body: DeleteFolderBody) { await new Promise((resolve, reject) => { @@ -304,8 +350,9 @@ export class StorageController extends Controller { * ร้องขอการอัพโหลดไฟล์ โดยเมื่อร้องขอจะได้ URL สำหรับอัพโหลดไฟล์มาพร้อมข้อมูลของไฟล์ที่จะทำการอัพโหลด */ @Post("file") + @Tags("Storage File") @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") - public async postFile(@Body() body: CreateFileBody) { + public async postFile(@Body() body: FileBody) { const { path, file } = body; if (!(await checkPathExist(DEFAULT_BUCKET, path.join("/")))) { @@ -363,4 +410,131 @@ export class StorageController extends Controller { return { ...metadata, uploadUrl: presignedUrl }; } + + @Put("file") + @Tags("Storage File") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") + public async moveFile(@Body() body: PutFileBody) { + const search = await esClient + .search }>({ + index: DEFAULT_INDEX, + query: { + match: { pathname: body.from.path.join("/") + `/${body.from.file}` }, + }, + }) + .catch((e) => console.error(`ElasticSearch Error: ${e}`)); + + if (!search) { + throw new Error("เกิดข้อผิดพลาดกับระบบฐานข้อมูล กรุณาลองใหม่ในภายหลัง"); + } + if (search && search.hits.hits.length === 0) { + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์ดังกล่าว"); + } + if (!(await checkFileExist(DEFAULT_BUCKET, body.from.path.join("/") + `/${body.from.file}`))) { + await esClient.delete({ + index: DEFAULT_INDEX, + id: search.hits.hits[0]._id, + }); + throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์ดังกล่าว"); + } + if (body.to && !(await checkPathExist(DEFAULT_BUCKET, body.to.path.join("/")))) { + throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "ไม่พบตำแหน่งที่ต้องการย้าย"); + } + if ( + body.to && + (await checkFileExist(DEFAULT_BUCKET, body.to.path.join("/") + `/${body.to.file}`)) + ) { + throw new HttpError( + HttpStatusCode.PRECONDITION_FAILED, + "พบไฟล์ในต้ำแหน่งปลายทาง ไม่สามารถย้ายได้", + ); + } + if (!search.hits.hits[0]._source) { + // This should not possible. + // Just in case the result found with no associated data. + await esClient.delete({ + index: DEFAULT_INDEX, + id: search.hits.hits[0]._id, + }); + throw new Error("ไม่พบข้อมูลในฐานข้อมูล"); + } + + const id = search.hits.hits[0]._id; + + const { to, from, upload, ...metadata } = body; + + if (from && to) { + const src = [DEFAULT_BUCKET, ...from.path, ""].join("/") + from.file; + const dst = to.path.join("/") + `/${to.file}`; + + const result = await minioClient.copyObject(DEFAULT_BUCKET, dst, src, copyCond).catch((e) => { + console.error(`MinIO Error: ${e}`); + throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้"); + }); + + if (result) { + await esClient + .update({ + index: DEFAULT_INDEX, + id: id, + doc: { + ...metadata, + path: to.path.join("/") + "/", + pathname: dst, + updatedAt: new Date().toISOString(), + updatedBy: "n/a", + }, + refresh: "wait_for", + }) + .then(async () => await minioClient.removeObject(DEFAULT_INDEX, src)) + .catch((e) => console.error(`ElasticSearch Error: ${e}`)); + + if (upload) { + const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, dst); + return { uploadUrl: presignedUrl }; + } + } + } + + if (from) { + await esClient + .update({ + index: DEFAULT_INDEX, + id: id, + doc: { + ...metadata, + updatedAt: new Date().toISOString(), + updatedBy: "n/a", + }, + refresh: "wait_for", + }) + .catch((e) => console.error(`ElasticSearch Error: ${e}`)); + if (upload) { + const src = from.path.join("/") + `/${from.file}`; + const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, src); + return { uploadUrl: presignedUrl }; + } + } + + return this.setStatus(HttpStatusCode.NO_CONTENT); + } + + @Delete("file") + @Tags("Storage File") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") + public async deleteFile(@Body() body: DeleteFileBody) { + const pathname = body.path.join("/") + body.file; + + await minioClient + .removeObject(DEFAULT_BUCKET, pathname) + .catch((e) => console.error(`MinIO Error: ${e}`)); + await esClient + .deleteByQuery({ + index: DEFAULT_INDEX, + query: { match: { pathname } }, + }) + .catch((e) => console.error(`ElasticSearch Error: ${e}`)); + + return this.setStatus(HttpStatusCode.NO_CONTENT); + } } diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index ba2ba9e..90cab3b 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -76,7 +76,7 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "CreateFolderBody": { + "FolderBody": { "dataType": "refObject", "properties": { "path": {"dataType":"array","array":{"dataType":"string"},"required":true}, @@ -102,7 +102,7 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - "CreateFileBody": { + "FileBody": { "dataType": "refObject", "properties": { "path": {"dataType":"array","array":{"dataType":"string"},"required":true}, @@ -115,6 +115,29 @@ const models: TsoaRoute.Models = { "additionalProperties": false, }, // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "PutFileBody": { + "dataType": "refObject", + "properties": { + "title": {"dataType":"string"}, + "description": {"dataType":"string"}, + "category": {"dataType":"array","array":{"dataType":"string"}}, + "keyword": {"dataType":"array","array":{"dataType":"string"}}, + "from": {"dataType":"nestedObjectLiteral","nestedProperties":{"file":{"dataType":"string","required":true},"path":{"dataType":"array","array":{"dataType":"string"},"required":true}},"required":true}, + "to": {"dataType":"nestedObjectLiteral","nestedProperties":{"file":{"dataType":"string","required":true},"path":{"dataType":"array","array":{"dataType":"string"},"required":true}}}, + "upload": {"dataType":"boolean"}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + "DeleteFileBody": { + "dataType": "refObject", + "properties": { + "path": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "file": {"dataType":"string","required":true}, + }, + "additionalProperties": false, + }, + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa }; const validationService = new ValidationService(models); @@ -737,7 +760,7 @@ export function RegisterRoutes(app: Router) { function StorageController_postFolder(request: any, response: any, next: any) { const args = { - body: {"in":"body","name":"body","required":true,"ref":"CreateFolderBody"}, + body: {"in":"body","name":"body","required":true,"ref":"FolderBody"}, }; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa @@ -775,7 +798,7 @@ export function RegisterRoutes(app: Router) { const promise = controller.moveFolder.apply(controller, validatedArgs as any); - promiseHandler(controller, promise, response, undefined, next); + promiseHandler(controller, promise, response, 204, next); } catch (err) { return next(err); } @@ -812,7 +835,7 @@ export function RegisterRoutes(app: Router) { function StorageController_postFile(request: any, response: any, next: any) { const args = { - body: {"in":"body","name":"body","required":true,"ref":"CreateFileBody"}, + body: {"in":"body","name":"body","required":true,"ref":"FileBody"}, }; // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa @@ -831,6 +854,56 @@ export function RegisterRoutes(app: Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.put('/storage/file', + ...(fetchMiddlewares(StorageController)), + ...(fetchMiddlewares(StorageController.prototype.moveFile)), + + function StorageController_moveFile(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"ref":"PutFileBody"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new StorageController(); + + + const promise = controller.moveFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 204, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + app.delete('/storage/file', + ...(fetchMiddlewares(StorageController)), + ...(fetchMiddlewares(StorageController.prototype.deleteFile)), + + function StorageController_deleteFile(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"ref":"DeleteFileBody"}, + }; + + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa + + let validatedArgs: any[] = []; + try { + validatedArgs = getValidatedArgs(args, request, response); + + const controller = new StorageController(); + + + const promise = controller.deleteFile.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, 204, next); + } catch (err) { + return next(err); + } + }); + // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa app.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder', authenticateMiddleware([{"bearerAuth":[]}]), ...(fetchMiddlewares(SubFolderController)), diff --git a/Services/server/src/swagger.json b/Services/server/src/swagger.json index 404bc9e..75665c8 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -198,16 +198,21 @@ "type": "object", "additionalProperties": false }, - "CreateFolderBody": { + "FolderBody": { "properties": { "path": { "items": { "type": "string" }, - "type": "array" + "type": "array", + "example": [ + "แฟ้ม 1", + "แฟ้ม 2" + ] }, "name": { - "type": "string" + "type": "string", + "example": "แฟ้ม 3" } }, "required": [ @@ -222,13 +227,18 @@ "from": { "properties": { "name": { - "type": "string" + "type": "string", + "example": "แฟ้ม 3" }, "path": { "items": { "type": "string" }, - "type": "array" + "type": "array", + "example": [ + "แฟ้ม 1", + "แฟ้ม 2" + ] } }, "required": [ @@ -240,13 +250,18 @@ "to": { "properties": { "name": { - "type": "string" + "type": "string", + "example": "แฟ้ม 3 แก้ไข" }, "path": { "items": { "type": "string" }, - "type": "array" + "type": "array", + "example": [ + "แฟ้ม 1", + "แฟ้ม 2" + ] } }, "required": [ @@ -269,7 +284,12 @@ "items": { "type": "string" }, - "type": "array" + "type": "array", + "example": [ + "แฟ้ม 1", + "แฟ้ม 2", + "แฟ้ม 3 แก้ไข" + ] } }, "required": [ @@ -278,34 +298,167 @@ "type": "object", "additionalProperties": false }, - "CreateFileBody": { + "FileBody": { "properties": { "path": { "items": { "type": "string" }, - "type": "array" + "type": "array", + "example": [ + "แฟ้ม 1", + "แฟ้ม 2", + "แฟ้ม 3" + ] }, "file": { - "type": "string" + "type": "string", + "example": "ไฟล์ 1.xlsx" }, "title": { - "type": "string" + "type": "string", + "example": "การเงิน" }, "description": { - "type": "string" + "type": "string", + "example": "การเงิน" }, "category": { "items": { "type": "string" }, - "type": "array" + "type": "array", + "example": [ + "การเงิน", + "รายงาน" + ] }, "keyword": { "items": { "type": "string" }, - "type": "array" + "type": "array", + "example": [ + "การเงิน", + "รายรับ", + "รายจ่าย" + ] + } + }, + "required": [ + "path", + "file" + ], + "type": "object", + "additionalProperties": false + }, + "PutFileBody": { + "properties": { + "title": { + "type": "string", + "example": "การเงิน" + }, + "description": { + "type": "string", + "example": "การเงิน" + }, + "category": { + "items": { + "type": "string" + }, + "type": "array", + "example": [ + "การเงิน", + "รายงาน" + ] + }, + "keyword": { + "items": { + "type": "string" + }, + "type": "array", + "example": [ + "การเงิน", + "รายรับ", + "รายจ่าย" + ] + }, + "from": { + "properties": { + "file": { + "type": "string", + "example": "ไฟล์ 1.xlsx" + }, + "path": { + "items": { + "type": "string" + }, + "type": "array", + "example": [ + "แฟ้ม 1", + "แฟ้ม 2", + "แฟ้ม 3" + ] + } + }, + "required": [ + "file", + "path" + ], + "type": "object", + "description": "หากต้องการอัพโหลดไฟล์ด้วยให้ส่งค่าเป็นจริง" + }, + "to": { + "properties": { + "file": { + "type": "string", + "example": "ไฟล์ 1 แก้ไข.xlsx" + }, + "path": { + "items": { + "type": "string" + }, + "type": "array", + "example": [ + "แฟ้ม 1", + "แฟ้ม 2", + "แฟ้ม 3" + ] + } + }, + "required": [ + "file", + "path" + ], + "type": "object" + }, + "upload": { + "type": "boolean", + "example": false + } + }, + "required": [ + "from" + ], + "type": "object", + "additionalProperties": false + }, + "DeleteFileBody": { + "properties": { + "path": { + "items": { + "type": "string" + }, + "type": "array", + "example": [ + "แฟ้ม 1", + "แฟ้ม 2", + "แฟ้ม 3" + ] + }, + "file": { + "type": "string", + "example": "ไฟล์ 1 แก้ไข.xlsx" } }, "required": [ @@ -1875,6 +2028,10 @@ } } }, + "tags": [ + "Storage Folder", + "Storage File" + ], "security": [], "parameters": [], "requestBody": { @@ -1897,6 +2054,9 @@ "description": "สำเร็จ" } }, + "tags": [ + "Storage Folder" + ], "security": [], "parameters": [], "requestBody": { @@ -1904,7 +2064,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateFolderBody" + "$ref": "#/components/schemas/FolderBody" } } } @@ -1914,10 +2074,13 @@ "operationId": "MoveFolder", "responses": { "204": { - "description": "No content" + "description": "สำเร็จ" } }, "description": "ยา้ย Folder ภายใต้ Folder (Path) หนึ่ง ไปภายใน Folder (Path) หนึ่งและสามารถเปลี่ยนชื่อได้", + "tags": [ + "Storage Folder" + ], "security": [], "parameters": [], "requestBody": { @@ -1939,6 +2102,9 @@ } }, "description": "ลบ Folder หรือ File ออกจากระบบ", + "tags": [ + "Storage Folder" + ], "security": [], "parameters": [], "requestBody": { @@ -2056,6 +2222,9 @@ } }, "description": "ร้องขอการอัพโหลดไฟล์ โดยเมื่อร้องขอจะได้ URL สำหรับอัพโหลดไฟล์มาพร้อมข้อมูลของไฟล์ที่จะทำการอัพโหลด", + "tags": [ + "Storage File" + ], "security": [], "parameters": [], "requestBody": { @@ -2063,7 +2232,73 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateFileBody" + "$ref": "#/components/schemas/FileBody" + } + } + } + } + }, + "put": { + "operationId": "MoveFile", + "responses": { + "204": { + "description": "สำเร็จ", + "content": { + "application/json": { + "schema": { + "anyOf": [ + {}, + { + "properties": { + "uploadUrl": { + "type": "string" + } + }, + "required": [ + "uploadUrl" + ], + "type": "object" + } + ] + } + } + } + } + }, + "tags": [ + "Storage File" + ], + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutFileBody" + } + } + } + } + }, + "delete": { + "operationId": "DeleteFile", + "responses": { + "204": { + "description": "สำเร็จ" + } + }, + "tags": [ + "Storage File" + ], + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteFileBody" } } }