feat: file move and delete endpoint

This commit is contained in:
Methapon2001 2023-12-12 12:01:56 +07:00
parent 8fbeda892b
commit 07d97a9091
No known key found for this signature in database
GPG key ID: 849924FEF46BD132
4 changed files with 520 additions and 44 deletions

View file

@ -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));

View file

@ -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<FileBody, "file" | "path"> {
/** หากต้องการอัพโหลดไฟล์ด้วยให้ส่งค่าเป็นจริง */
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<StorageFile & { attachment: Record<string, string> }>({
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<void>((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<StorageFile & { attachment: Record<string, any> }>({
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);
}
}

View file

@ -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<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(StorageController)),
...(fetchMiddlewares<RequestHandler>(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<RequestHandler>(SubFolderController)),

View file

@ -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"
}
}
}