diff --git a/Services/server/src/controllers/storageController.ts b/Services/server/src/controllers/storageController.ts new file mode 100644 index 0000000..1b15615 --- /dev/null +++ b/Services/server/src/controllers/storageController.ts @@ -0,0 +1,366 @@ +import { Body, Controller, Delete, Example, Post, Put, Route, SuccessResponse } 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."); +if (!process.env.ELASTICSEARCH_INDEX) throw Error("Default ElasticSearch index must be specified."); + +const DEFAULT_BUCKET = process.env.MINIO_BUCKET; +const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX; +const MINIO_ERROR_MESSAGE = "เกิดข้อผิดพลาดกับระบบจัดการไฟล์"; +const PATH_NOT_FOUND_MESSAGE = "ไม่พบตำแหน่งที่ต้องการนำข้อมูลเข้า"; +const PATH_ALREADY_EXIST = "ตำแหน่งดังกล่าวมีในระบบแล้ว"; + +interface ListRequestBody { + operation: "folder" | "file"; + path: string[]; +} + +interface CreateFolderBody { + path: string[]; + name: string; +} + +interface PutFolderBody { + from: { + path: string[]; + name: string; + }; + to: { + path: string[]; + name: string; + }; +} + +interface DeleteFolderBody { + path: string[]; +} + +interface CreateFileBody { + path: string[]; + file: string; + title?: string; + description?: string; + category?: string[]; + keyword?: string[]; +} + +async function listFolder(path: string[]) { + const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => { + const item: { pathname: string; name: string }[] = []; + + const stream = minioClient.listObjectsV2(DEFAULT_BUCKET, path.join("/") + "/"); + stream.on("data", (v) => { + if (v && v.prefix) + item.push({ + pathname: v.prefix, + name: v.prefix.split("/").filter(Boolean).at(-1)!, + }); + }); + stream.on("end", () => resolve(item)); + stream.on("error", () => reject(new Error(MINIO_ERROR_MESSAGE))); + }); + + const folder = await Promise.all( + list.map(async (v) => { + // Get stat from hidden object that used to mark as folder as minio doesn't really have folder + const stat = await minioClient + .statObject(DEFAULT_BUCKET, `${v.pathname}.keep`) + .catch((e) => console.error(`MinIO Error: ${e}`)); + + if (!stat) return undefined; + + const { createdat, createdby } = stat.metaData; + + return { + ...v, + createdAt: createdat ?? "n/a", + createdBy: createdby ?? "n/a", + } satisfies StorageFolder; + }), + ); + + return folder.filter((v: StorageFolder | undefined): v is StorageFolder => !!v); +} + +async function listFile(path: string[]) { + const result = await esClient + .search }>({ + index: DEFAULT_INDEX!, + sort: [{ pathname: "asc" }], + query: { + match: { + path: path.join("/") + "/", + }, + }, + size: 10000, + }) + .then((r) => r.hits.hits); + + const records = result + .map((v) => { + if (v._source) { + const { attachment, ...rest } = v._source; + return rest satisfies StorageFile; + } + }) + .filter((v: StorageFile | undefined): v is StorageFile => !!v); + + return records; +} + +async function checkPathExist(bucket: string, path: string) { + return Boolean( + await minioClient.statObject(bucket, `${path}/.keep`).catch((e) => { + if (e.code === "NotFound") return false; + console.error(`Storage Error: ${e}`); + throw new Error(MINIO_ERROR_MESSAGE); + }), + ); +} + +@Route("storage") +export class StorageController extends Controller { + @Post("list") + @Example([ + { + path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1", + name: "แฟ้ม 1", + createdAt: "2021-07-20T12:33:13.018Z", + createdBy: "admin", + }, + { + path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 2", + name: "แฟ้ม 2", + createdAt: "2022-01-23T16:05:02.114Z", + createdBy: "admin", + }, + ]) + @Example([ + { + pathname: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1.pdf", + path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/", + title: "เอกสาร", + description: "เอกสารการเงิน", + category: ["บัญชี"], + keyword: ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"], + upload: false, + fileName: "เอกสาร 1.pdf", + fileSize: 10240, + fileType: "application/pdf", + createdAt: "2021-07-20T12:33:13.018Z", + createdBy: "admin", + updatedAt: "2021-07-20T12:33:13.018Z", + updatedBy: "admin", + }, + ]) + public async getList(@Body() body: ListRequestBody) { + const path = body.path.filter(Boolean); + + if (body.operation === "folder") return await listFolder(path); + if (body.operation === "file") return await listFile(path); + } + + @Post("folder") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") + public async postFolder(@Body() body: CreateFolderBody) { + const { path, name } = body; + + if (!(await checkPathExist(DEFAULT_BUCKET, path.join("/")))) { + throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE); + } + + const created = await minioClient + .putObject( + DEFAULT_BUCKET, + `${path.join("/")}/${name.replace(/[/\\?%*:|"<>#]/g, "-").trim()}/.keep`, + "", + 0, + { + createdAt: new Date().toISOString(), + createdBy: "n/a", + }, + ) + .catch((e) => console.error(`MinIO Error: ${e}`)); + + if (!created) throw new Error(MINIO_ERROR_MESSAGE); + + return this.setStatus(HttpStatusCode.NO_CONTENT); + } + + /** + * ยา้ย Folder ภายใต้ Folder (Path) หนึ่ง ไปภายใน Folder (Path) หนึ่งและสามารถเปลี่ยนชื่อได้ + */ + @Put("folder") + public async moveFolder(@Body() body: PutFolderBody) { + const src = `${body.from.path.join("/")}/${body.from.name}`; + const dst = `${body.to.path.join("/")}/${body.to.name}`; + + if (!(await checkPathExist(DEFAULT_BUCKET, src))) { + throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE); + } + if (await checkPathExist(DEFAULT_BUCKET, dst)) { + throw new HttpError(HttpStatusCode.CONFLICT, PATH_ALREADY_EXIST); + } + if (!(await checkPathExist(DEFAULT_BUCKET, body.to.path.join("/")))) { + throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "ไม่พบตำแหน่งที่ต้องการย้าย"); + } + + const list = await new Promise<{ pathname: string }[]>((resolve, reject) => { + const stream = minioClient.listObjectsV2(DEFAULT_BUCKET, `${src}/`, true); + const item: { pathname: string }[] = []; + + stream.on("data", (v) => { + if (v && v.name) item.push({ pathname: v.name }); + }); + stream.on("end", () => resolve(item)); + stream.on("error", () => reject(new Error(MINIO_ERROR_MESSAGE))); + }); + + await Promise.all( + list.map(async (v) => { + const from = `/${DEFAULT_BUCKET}/${v.pathname}`; + const to = `${dst}/${v.pathname.slice(`${src}/`.length)}`; + + const result = await minioClient + .copyObject(DEFAULT_BUCKET, to, from, copyCond) + .catch((e) => { + console.error(`MinIO Error: ${e}`); + throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้"); + }); + + if (result) { + if (v.pathname.includes(".keep")) { + return await minioClient + .removeObject(DEFAULT_BUCKET, v.pathname) + .catch((e) => console.error(`MinIO Error: ${e}`)); + } + + const search = await esClient.search< + StorageFile & { attachment: Record } + >({ + index: DEFAULT_INDEX!, + query: { match: { pathname: v.pathname } }, + }); + + if (search && search.hits.hits.length === 0) { + await minioClient + .removeObject(DEFAULT_BUCKET, v.pathname) + .catch((e) => console.error(`MinIO Error: ${e}`)); + return console.log( + `Trying to update file in storage but not exist in database: ${from} > ${to}`, + ); + } + + const data = search.hits.hits[0]; + + await esClient + .update({ + index: DEFAULT_INDEX!, + id: data._id, + doc: { + pathname: to, + path: to.split("/").slice(0, -1).join("/") + "/", + }, + refresh: "wait_for", + }) + .catch((e) => console.error(`ElasticSearch Error: ${e}`)); + + await minioClient + .removeObject(DEFAULT_BUCKET, v.pathname) + .catch((e) => console.error(`MinIO Error: ${e}`)); + } + }), + ); + + return this.setStatus(HttpStatusCode.NO_CONTENT); + } + + /** + * ลบ Folder หรือ File ออกจากระบบ + */ + @Delete("folder") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") + public async deleteStorage(@Body() body: DeleteFolderBody) { + await new Promise((resolve, reject) => { + const objects: string[] = []; + const stream = minioClient.listObjectsV2(DEFAULT_BUCKET, body.path.join("/") + "/", true); + + stream.on("data", (v) => v && v.name && objects.push(v.name)); + stream.on("close", async () => { + resolve(await minioClient.removeObjects(DEFAULT_BUCKET, objects)); + }); + stream.on("error", () => reject(new Error(MINIO_ERROR_MESSAGE))); + }); + + return this.setStatus(HttpStatusCode.NO_CONTENT); + } + + /** + * ร้องขอการอัพโหลดไฟล์ โดยเมื่อร้องขอจะได้ URL สำหรับอัพโหลดไฟล์มาพร้อมข้อมูลของไฟล์ที่จะทำการอัพโหลด + */ + @Post("file") + @SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ") + public async postFile(@Body() body: CreateFileBody) { + const { path, file } = body; + + if (!(await checkPathExist(DEFAULT_BUCKET, path.join("/")))) { + throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE); + } + + const result = await esClient + .search }>({ + index: DEFAULT_INDEX!, + query: { match: { pathname: path.join("/") + "/" } }, + }) + .catch((e) => console.error(`MinIO Error: ${e}`)); + + const metadata: StorageFile = { + path: path.join("/") + "/", + pathname: path.join("/") + "/" + file.replace(/[/\\?%*:|"<>#]/g, "-").trim(), + fileName: file.replace(/[/\\?%*:|"<>#]/g, "-").trim(), + fileSize: 0, // Will be get by minio object storage after file is uploaded + fileType: "", // Will be determined by minio object storage after file is uploaded + title: body.title ?? file.replace(/[/\\?%*:|"<>#]/g, "-").trim(), // default to same as filename + description: body.description ?? "", + category: body.category ?? [], + keyword: body.keyword ?? [], + upload: false, // flag + createdAt: new Date().toISOString(), + createdBy: "n/a", + updatedAt: new Date().toISOString(), + updatedBy: "n/a", + }; + + // 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(`MinIO Error: ${e}`)); + + const rec = result.hits.hits[0]._source; + + // File exist get created at and created by field but all other will be replaced + // by provided infomation or blank if not provided + metadata.createdAt = rec.createdAt; + metadata.createdBy = rec.createdBy; + } + + await esClient.index({ + index: DEFAULT_INDEX!, + document: metadata, + refresh: "wait_for", // Must have or else it doesn't wait for updated index resulted in data not found on fetch + }); + + const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, metadata.pathname); + + return { ...metadata, uploadUrl: presignedUrl }; + } +} diff --git a/Services/server/src/routes.ts b/Services/server/src/routes.ts index 7b3897c..ba2ba9e 100644 --- a/Services/server/src/routes.ts +++ b/Services/server/src/routes.ts @@ -13,6 +13,8 @@ import { FolderController } from './controllers/folderController'; // 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 import { SearchController } from './controllers/searchController'; // 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 +import { StorageController } from './controllers/storageController'; +// 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 import { SubFolderController } from './controllers/subFolderController'; // 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 import { SubFolderFileController } from './controllers/subFolderFileController'; @@ -65,6 +67,54 @@ 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 + "ListRequestBody": { + "dataType": "refObject", + "properties": { + "operation": {"dataType":"union","subSchemas":[{"dataType":"enum","enums":["folder"]},{"dataType":"enum","enums":["file"]}],"required":true}, + "path": {"dataType":"array","array":{"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 + "CreateFolderBody": { + "dataType": "refObject", + "properties": { + "path": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "name": {"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 + "PutFolderBody": { + "dataType": "refObject", + "properties": { + "from": {"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true},"path":{"dataType":"array","array":{"dataType":"string"},"required":true}},"required":true}, + "to": {"dataType":"nestedObjectLiteral","nestedProperties":{"name":{"dataType":"string","required":true},"path":{"dataType":"array","array":{"dataType":"string"},"required":true}},"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 + "DeleteFolderBody": { + "dataType": "refObject", + "properties": { + "path": {"dataType":"array","array":{"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 + "CreateFileBody": { + "dataType": "refObject", + "properties": { + "path": {"dataType":"array","array":{"dataType":"string"},"required":true}, + "file": {"dataType":"string","required":true}, + "title": {"dataType":"string"}, + "description": {"dataType":"string"}, + "category": {"dataType":"array","array":{"dataType":"string"}}, + "keyword": {"dataType":"array","array":{"dataType":"string"}}, + }, + "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); @@ -656,6 +706,131 @@ 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.post('/storage/list', + ...(fetchMiddlewares(StorageController)), + ...(fetchMiddlewares(StorageController.prototype.getList)), + + function StorageController_getList(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"ref":"ListRequestBody"}, + }; + + // 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.getList.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, 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.post('/storage/folder', + ...(fetchMiddlewares(StorageController)), + ...(fetchMiddlewares(StorageController.prototype.postFolder)), + + function StorageController_postFolder(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"ref":"CreateFolderBody"}, + }; + + // 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.postFolder.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.put('/storage/folder', + ...(fetchMiddlewares(StorageController)), + ...(fetchMiddlewares(StorageController.prototype.moveFolder)), + + function StorageController_moveFolder(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"ref":"PutFolderBody"}, + }; + + // 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.moveFolder.apply(controller, validatedArgs as any); + promiseHandler(controller, promise, response, undefined, 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/folder', + ...(fetchMiddlewares(StorageController)), + ...(fetchMiddlewares(StorageController.prototype.deleteStorage)), + + function StorageController_deleteStorage(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"ref":"DeleteFolderBody"}, + }; + + // 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.deleteStorage.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.post('/storage/file', + ...(fetchMiddlewares(StorageController)), + ...(fetchMiddlewares(StorageController.prototype.postFile)), + + function StorageController_postFile(request: any, response: any, next: any) { + const args = { + body: {"in":"body","name":"body","required":true,"ref":"CreateFileBody"}, + }; + + // 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.postFile.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 7dd5811..404bc9e 100644 --- a/Services/server/src/swagger.json +++ b/Services/server/src/swagger.json @@ -174,6 +174,146 @@ }, "type": "object", "additionalProperties": false + }, + "ListRequestBody": { + "properties": { + "operation": { + "type": "string", + "enum": [ + "folder", + "file" + ] + }, + "path": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "operation", + "path" + ], + "type": "object", + "additionalProperties": false + }, + "CreateFolderBody": { + "properties": { + "path": { + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "type": "string" + } + }, + "required": [ + "path", + "name" + ], + "type": "object", + "additionalProperties": false + }, + "PutFolderBody": { + "properties": { + "from": { + "properties": { + "name": { + "type": "string" + }, + "path": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "name", + "path" + ], + "type": "object" + }, + "to": { + "properties": { + "name": { + "type": "string" + }, + "path": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "name", + "path" + ], + "type": "object" + } + }, + "required": [ + "from", + "to" + ], + "type": "object", + "additionalProperties": false + }, + "DeleteFolderBody": { + "properties": { + "path": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "path" + ], + "type": "object", + "additionalProperties": false + }, + "CreateFileBody": { + "properties": { + "path": { + "items": { + "type": "string" + }, + "type": "array" + }, + "file": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "category": { + "items": { + "type": "string" + }, + "type": "array" + }, + "keyword": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "path", + "file" + ], + "type": "object", + "additionalProperties": false } }, "securitySchemes": { @@ -1662,6 +1802,274 @@ } } }, + "/storage/list": { + "post": { + "operationId": "GetList", + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/StorageFolder" + }, + "type": "array" + }, + { + "items": { + "$ref": "#/components/schemas/StorageFile" + }, + "type": "array" + } + ] + }, + "examples": { + "Example 1": { + "value": [ + { + "path": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1", + "name": "แฟ้ม 1", + "createdAt": "2021-07-20T12:33:13.018Z", + "createdBy": "admin" + }, + { + "path": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 2", + "name": "แฟ้ม 2", + "createdAt": "2022-01-23T16:05:02.114Z", + "createdBy": "admin" + } + ] + }, + "Example 2": { + "value": [ + { + "pathname": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1.pdf", + "path": "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/", + "title": "เอกสาร", + "description": "เอกสารการเงิน", + "category": [ + "บัญชี" + ], + "keyword": [ + "เงิน", + "บัญชี", + "รายจ่าย", + "รายรับ" + ], + "upload": false, + "fileName": "เอกสาร 1.pdf", + "fileSize": 10240, + "fileType": "application/pdf", + "createdAt": "2021-07-20T12:33:13.018Z", + "createdBy": "admin", + "updatedAt": "2021-07-20T12:33:13.018Z", + "updatedBy": "admin" + } + ] + } + } + } + } + } + }, + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListRequestBody" + } + } + } + } + } + }, + "/storage/folder": { + "post": { + "operationId": "PostFolder", + "responses": { + "204": { + "description": "สำเร็จ" + } + }, + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFolderBody" + } + } + } + } + }, + "put": { + "operationId": "MoveFolder", + "responses": { + "204": { + "description": "No content" + } + }, + "description": "ยา้ย Folder ภายใต้ Folder (Path) หนึ่ง ไปภายใน Folder (Path) หนึ่งและสามารถเปลี่ยนชื่อได้", + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PutFolderBody" + } + } + } + } + }, + "delete": { + "operationId": "DeleteStorage", + "responses": { + "204": { + "description": "สำเร็จ" + } + }, + "description": "ลบ Folder หรือ File ออกจากระบบ", + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteFolderBody" + } + } + } + } + } + }, + "/storage/file": { + "post": { + "operationId": "PostFile", + "responses": { + "204": { + "description": "สำเร็จ", + "content": { + "application/json": { + "schema": { + "properties": { + "createdBy": { + "type": "string" + }, + "createdAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "updatedBy": { + "type": "string" + }, + "updatedAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string", + "format": "date-time" + } + ] + }, + "upload": { + "type": "boolean" + }, + "path": { + "type": "string" + }, + "keyword": { + "items": { + "type": "string" + }, + "type": "array" + }, + "category": { + "items": { + "type": "string" + }, + "type": "array" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + }, + "fileType": { + "type": "string" + }, + "fileSize": { + "type": "number", + "format": "double" + }, + "fileName": { + "type": "string" + }, + "pathname": { + "type": "string" + }, + "uploadUrl": { + "type": "string" + } + }, + "required": [ + "createdBy", + "createdAt", + "updatedBy", + "updatedAt", + "upload", + "path", + "keyword", + "category", + "description", + "title", + "fileType", + "fileSize", + "fileName", + "pathname", + "uploadUrl" + ], + "type": "object" + } + } + } + } + }, + "description": "ร้องขอการอัพโหลดไฟล์ โดยเมื่อร้องขอจะได้ URL สำหรับอัพโหลดไฟล์มาพร้อมข้อมูลของไฟล์ที่จะทำการอัพโหลด", + "security": [], + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFileBody" + } + } + } + } + } + }, "/cabinet/{cabinetName}/drawer/{drawerName}/folder/{folderName}/subfolder": { "get": { "operationId": "ListFolder",