fix: path leading slash and remove unused endpoint

This commit is contained in:
Methapon2001 2023-12-15 15:15:50 +07:00
parent d1abd6684e
commit 51528afc74
No known key found for this signature in database
GPG key ID: 849924FEF46BD132
12 changed files with 295 additions and 5364 deletions

View file

@ -6,6 +6,7 @@ import {
Post,
Put,
Request,
Response,
Route,
Security,
SuccessResponse,
@ -114,6 +115,10 @@ interface DownloadFileBody {
file: string;
}
function stripLeadingSlash(str: string) {
return str.replace(/^\//, "");
}
async function folderSize(path: string[]) {
const size = await new Promise<number>((resolve, reject) => {
const stream = minioClient.listObjectsV2(
@ -138,7 +143,7 @@ async function listFolder(path: string[], hidden: boolean = false) {
const stream = minioClient.listObjectsV2(
DEFAULT_BUCKET,
path.length === 0 ? "" : path.join("/") + "/",
stripLeadingSlash(`${path.join("/")}/`),
);
stream.on("data", (v) => {
if (v && v.prefix)
@ -182,7 +187,7 @@ async function listFile(path: string[], hidden: boolean = false) {
sort: [{ pathname: "asc" }],
query: {
bool: {
must: { match: { path: path.join("/") + "/" } },
must: { match: { path: stripLeadingSlash(`${path.join("/")}/`) } },
must_not: !hidden ? { match: { hidden: true } } : undefined,
},
},
@ -202,14 +207,14 @@ async function listFile(path: string[], hidden: boolean = false) {
return records;
}
async function checkPathExist(bucket: string, path: string) {
if (path.split("/").filter(Boolean).length === 0) return true; // root does not contain any mark
return await checkFileExist(bucket, `${path}/.keep`);
async function checkPathExist(bucket: string, path: string[]) {
if (path.filter(Boolean).length === 0) return true; // root does not contain any mark
return await checkFileExist(bucket, `${path.filter(Boolean).join("/")}/.keep`);
}
async function checkFileExist(bucket: string, pathname: string) {
return Boolean(
await minioClient.statObject(bucket, pathname).catch((e) => {
await minioClient.statObject(bucket, stripLeadingSlash(pathname)).catch((e) => {
if (e.code === "NotFound") return false;
console.error(`Storage Error: ${e}`);
throw new Error(MINIO_ERROR_MESSAGE);
@ -219,16 +224,21 @@ async function checkFileExist(bucket: string, pathname: string) {
@Route("storage")
export class StorageController extends Controller {
/**
* @summary
*/
@Post("list")
@Tags("Storage Folder", "Storage File")
@Security("bearerAuth")
@Example([
{
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1",
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
name: "แฟ้ม 1",
createdAt: "2021-07-20T12:33:13.018Z",
createdBy: "admin",
},
{
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 2",
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 2/",
name: "แฟ้ม 2",
createdAt: "2022-01-23T16:05:02.114Z",
createdBy: "admin",
@ -243,6 +253,7 @@ export class StorageController extends Controller {
category: ["บัญชี"],
keyword: ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"],
upload: false,
hidden: false,
fileName: "เอกสาร 1.pdf",
fileSize: 10240,
fileType: "application/pdf",
@ -252,15 +263,21 @@ export class StorageController extends Controller {
updatedBy: "admin",
},
])
@Tags("Storage Folder", "Storage File")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async getList(@Body() body: ListRequestBody) {
const path = body.path.filter(Boolean);
if (!(await checkPathExist(DEFAULT_BUCKET, path))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
}
if (body.operation === "folder") return await listFolder(path, body.hidden);
if (body.operation === "file") return await listFile(path);
}
/**
* @summary
*/
@Post("folder")
@Tags("Storage Folder")
@Security("bearerAuth", ["management-role", "admin"])
@ -271,7 +288,7 @@ export class StorageController extends Controller {
) {
const { path, name } = body;
if (!(await checkPathExist(DEFAULT_BUCKET, path.join("/")))) {
if (!(await checkPathExist(DEFAULT_BUCKET, path))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
}
@ -283,7 +300,7 @@ export class StorageController extends Controller {
const created = await minioClient
.putObject(
DEFAULT_BUCKET,
`${path.join("/")}/${name.replace(/[/\\?%*:|"<>#]/g, "-").trim()}/.keep`,
stripLeadingSlash(`${path.join("/")}/${name.replace(/[/\\?%*:|"<>#]/g, "-").trim()}/.keep`),
"",
0,
meta,
@ -293,7 +310,9 @@ export class StorageController extends Controller {
if (!created) throw new Error(MINIO_ERROR_MESSAGE);
io.getInstance()?.emit("FolderCreate", {
pathname: `${path.join("/")}/${name.replace(/[/\\?%*:|"<>#]/g, "-").trim()}/`,
pathname: stripLeadingSlash(
`${path.join("/")}/${name.replace(/[/\\?%*:|"<>#]/g, "-").trim()}/`,
),
name: name.replace(/[/\\?%*:|"<>#]/g, "-").trim(),
...meta,
});
@ -302,23 +321,26 @@ export class StorageController extends Controller {
}
/**
* Folder Folder (Path) Folder (Path)
*
*
*
* @summary
*/
@Put("folder")
@Tags("Storage Folder")
@Security("bearerAuth", ["management-role", "admin"])
@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}`;
const src = stripLeadingSlash(`${body.from.path.join("/")}/${body.from.name}`);
const dst = stripLeadingSlash(`${body.to.path.join("/")}/${body.to.name}`);
if (!(await checkPathExist(DEFAULT_BUCKET, src))) {
if (!(await checkPathExist(DEFAULT_BUCKET, src.split("/")))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
}
if (await checkPathExist(DEFAULT_BUCKET, dst)) {
if (await checkPathExist(DEFAULT_BUCKET, dst.split("/"))) {
throw new HttpError(HttpStatusCode.CONFLICT, PATH_ALREADY_EXIST);
}
if (!(await checkPathExist(DEFAULT_BUCKET, body.to.path.join("/")))) {
if (!(await checkPathExist(DEFAULT_BUCKET, body.to.path))) {
throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "ไม่พบตำแหน่งที่ต้องการย้าย");
}
@ -336,7 +358,7 @@ export class StorageController extends Controller {
await Promise.all(
list.map(async (v) => {
const from = `/${DEFAULT_BUCKET}/${v.pathname}`;
const to = `${dst}/${v.pathname.slice(`${src}/`.length)}`;
const to = stripLeadingSlash(`${dst}/${v.pathname.slice(`${src}/`.length)}`);
const result = await minioClient
.copyObject(DEFAULT_BUCKET, to, from, copyCond)
@ -376,7 +398,7 @@ export class StorageController extends Controller {
id: data._id,
doc: {
pathname: to,
path: to.split("/").slice(0, -1).join("/") + "/",
path: stripLeadingSlash(`${to.split("/").slice(0, -1).join("/")}/`),
},
refresh: "wait_for",
})
@ -390,8 +412,8 @@ export class StorageController extends Controller {
);
io.getInstance()?.emit("FolderMove", {
from: `${src}/`,
to: `${dst}/`,
from: stripLeadingSlash(`${src}/`),
to: stripLeadingSlash(`${dst}/`),
});
return this.setStatus(HttpStatusCode.NO_CONTENT);
@ -400,6 +422,10 @@ export class StorageController extends Controller {
@Post("folder/size")
@Tags("Storage Folder")
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
@Example({
size: 10240,
})
public async folderSize(@Body() body: FolderBody) {
return { size: await folderSize([...body.path, body.name]) };
}
@ -412,9 +438,17 @@ export class StorageController extends Controller {
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteStorage(@Body() body: DeleteFolderBody) {
if (body.path.length === 0) {
throw new HttpError(HttpStatusCode.BAD_REQUEST, "ไม่สามารถลบได้");
}
await new Promise<void>((resolve, reject) => {
const objects: string[] = [];
const stream = minioClient.listObjectsV2(DEFAULT_BUCKET, body.path.join("/") + "/", true);
const stream = minioClient.listObjectsV2(
DEFAULT_BUCKET,
stripLeadingSlash(`${body.path.join("/")}/`),
true,
);
stream.on("data", (v) => v && v.name && objects.push(v.name));
stream.on("close", async () => {
@ -435,30 +469,53 @@ export class StorageController extends Controller {
@Tags("Storage File")
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
@Example({
pathname: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1.pdf",
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
title: "เอกสาร",
description: "เอกสารการเงิน",
category: ["บัญชี"],
keyword: ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"],
upload: false,
hidden: 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",
uploadUrl: "s3.storage.upload",
})
public async postFile(
@Request() request: { user: { preferred_username: string } },
@Body() body: FileBody,
) {
const { path, file } = body;
const validFileName = file.replace(/[/\\?%*:|"<>#]/g, "-").trim();
if (!(await checkPathExist(DEFAULT_BUCKET, path.join("/")))) {
if (!(await checkPathExist(DEFAULT_BUCKET, path))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, PATH_NOT_FOUND_MESSAGE);
}
const result = await esClient
.search<StorageFile & { attachment?: Record<string, unknown> }>({
index: DEFAULT_INDEX!,
query: { match: { pathname: path.join("/") + "/" } },
query: {
match: {
pathname: stripLeadingSlash(`${path.join("/")}/${validFileName}`),
},
},
})
.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(),
path: stripLeadingSlash(`${path.join("/")}/`),
pathname: stripLeadingSlash(`${path.join("/")}/${validFileName}`),
fileName: validFileName,
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
title: body.title ?? validFileName, // default to same as filename
description: body.description ?? "",
category: body.category ?? [],
keyword: body.keyword ?? [],
@ -504,6 +561,8 @@ export class StorageController extends Controller {
@Tags("Storage File")
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
@Response(HttpStatusCode.NO_CONTENT, "สำเร็จ")
@Example({ uploadUrl: "s3.storage.upload" })
public async moveFile(
@Request() request: { user: { preferred_username: string } },
@Body() body: PutFileBody,
@ -512,7 +571,9 @@ export class StorageController extends Controller {
.search<StorageFile & { attachment: Record<string, any> }>({
index: DEFAULT_INDEX,
query: {
match: { pathname: body.from.path.join("/") + `/${body.from.file}` },
match: {
pathname: stripLeadingSlash(`${body.from.path.join("/")}/${body.from.file}`),
},
},
})
.catch((e) => console.error(`ElasticSearch Error: ${e}`));
@ -523,7 +584,12 @@ export class StorageController extends Controller {
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}`))) {
if (
!(await checkFileExist(
DEFAULT_BUCKET,
stripLeadingSlash(`${body.from.path.join("/")}/${body.from.file}`),
))
) {
await esClient.delete({
index: DEFAULT_INDEX,
id: search.hits.hits[0]._id,
@ -531,10 +597,15 @@ export class StorageController extends Controller {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์ดังกล่าว");
}
if (body.to && JSON.stringify(body.from) !== JSON.stringify(body.to)) {
if (!(await checkPathExist(DEFAULT_BUCKET, body.to.path.join("/")))) {
if (!(await checkPathExist(DEFAULT_BUCKET, body.to.path))) {
throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "ไม่พบตำแหน่งที่ต้องการย้าย");
}
if (await checkFileExist(DEFAULT_BUCKET, body.to.path.join("/") + `/${body.to.file}`)) {
if (
await checkFileExist(
DEFAULT_BUCKET,
stripLeadingSlash(`${body.to.path.join("/")}/${body.to.file}`),
)
) {
throw new HttpError(
HttpStatusCode.PRECONDITION_FAILED,
"พบไฟล์ในต้ำแหน่งปลายทาง ไม่สามารถย้ายได้",
@ -563,7 +634,7 @@ export class StorageController extends Controller {
if (from && to && JSON.stringify(from) !== JSON.stringify(to)) {
const src = [DEFAULT_BUCKET, ...from.path, ""].join("/") + from.file;
const dst = to.path.join("/") + `/${to.file}`;
const dst = stripLeadingSlash(`${to.path.join("/")}/${to.file}`);
const result = await minioClient.copyObject(DEFAULT_BUCKET, dst, src, copyCond).catch((e) => {
console.error(`MinIO Error: ${e}`);
@ -577,7 +648,7 @@ export class StorageController extends Controller {
id: id,
doc: {
...metadata,
path: to.path.join("/") + "/",
path: stripLeadingSlash(`${to.path.join("/")}/`),
pathname: dst,
fileName: to.file,
...dateMeta,
@ -586,7 +657,10 @@ export class StorageController extends Controller {
})
.then(
async () =>
await minioClient.removeObject(DEFAULT_BUCKET, from.path.join("/") + `/${from.file}`),
await minioClient.removeObject(
DEFAULT_BUCKET,
stripLeadingSlash(`${from.path.join("/")}/${from.file}`),
),
)
.catch((e) => console.error(`ElasticSearch Error: ${e}`));
@ -595,7 +669,7 @@ export class StorageController extends Controller {
to: {
...source,
...metadata,
path: to.path.join("/") + "/",
path: stripLeadingSlash(`${to.path.join("/")}/`),
pathname: dst,
fileName: to.file,
...dateMeta,
@ -633,7 +707,7 @@ export class StorageController extends Controller {
});
if (upload) {
const src = from.path.join("/") + `/${from.file}`;
const src = stripLeadingSlash(`${from.path.join("/")}/${from.file}`);
const presignedUrl = await minioClient.presignedPutObject(DEFAULT_BUCKET, src);
return { uploadUrl: presignedUrl };
}
@ -650,7 +724,7 @@ export class StorageController extends Controller {
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteFile(@Body() body: DeleteFileBody) {
const pathname = body.path.join("/") + `/${body.file}`;
const pathname = stripLeadingSlash(`${body.path.join("/")}/${body.file}`);
await minioClient
.removeObject(DEFAULT_BUCKET, pathname)
@ -671,8 +745,26 @@ export class StorageController extends Controller {
@Tags("Download")
@Security("bearerAuth", ["management-role", "admin"])
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
@Example({
pathname: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/เอกสาร 1.pdf",
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/",
title: "เอกสาร",
description: "เอกสารการเงิน",
category: ["บัญชี"],
keyword: ["เงิน", "บัญชี", "รายจ่าย", "รายรับ"],
upload: false,
hidden: 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",
downloadUrl: "s3.storage.download",
})
public async downloadFile(@Body() body: DownloadFileBody) {
const pathname = body.path.join("/") + `/${body.file}`;
const pathname = stripLeadingSlash(`${body.path.join("/")}/${body.file}`);
const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
index: DEFAULT_INDEX,