Merge branch 'dev/methapon'

This commit is contained in:
Methapon2001 2023-11-28 08:58:32 +07:00
commit 20764b1e3d
No known key found for this signature in database
GPG key ID: 849924FEF46BD132
22 changed files with 2448 additions and 1495 deletions

View file

@ -1,5 +1,4 @@
PUBLIC_KEY=
REALM_URL=
PORT=
@ -15,3 +14,7 @@ ELASTICSEARCH_HOST=localhost
ELASTICSEARCH_PORT=9200
ELASTICSEARCH_INDEX=
AMQ_URL=amqp://admin:1234@localhost:9999
AMQ_QUEUE=queue
AUTH_BYPASS=false # MUST NOT TURN THIS ON IN PRODUCTION

View file

@ -9,8 +9,8 @@
"build": "tsoa spec-and-route && tsc",
"preview": "node ./dist/app.js",
"serve": "node ./dist/app.js",
"docs":"npm run docs:typedoc && npm run docs:swagger",
"docs:typedoc":"typedoc && scp -r be-typedoc projects-doc:~/projects/project-docs/edm/ && rm -r be-typedoc" ,
"docs": "npm run docs:typedoc && npm run docs:swagger",
"docs:typedoc": "typedoc && scp -r be-typedoc projects-doc:~/projects/project-docs/edm/ && rm -r be-typedoc",
"docs:swagger": "scp src/swagger.json projects-doc:~/projects/project-docs/edm/"
},
"keywords": [],
@ -22,6 +22,7 @@
"@types/cors": "^2.8.16",
"@types/jsonwebtoken": "^9.0.5",
"@types/multer": "^1.4.10",
"amqplib": "^0.10.3",
"concurrently": "^8.2.2",
"cors": "^2.8.5",
"dotenv": "^16.3.1",
@ -34,11 +35,13 @@
"tsoa": "^5.1.1"
},
"devDependencies": {
"@types/amqplib": "^0.10.4",
"@types/express": "^4.17.21",
"@types/node": "^20.9.0",
"@types/swagger-ui-express": "^4.1.6",
"nodemon": "^3.0.1",
"ts-node": "^10.9.1",
"typedoc": "^0.25.3",
"typescript": "^5.2.2"
}
}

View file

@ -20,6 +20,9 @@ dependencies:
'@types/multer':
specifier: ^1.4.10
version: 1.4.10
amqplib:
specifier: ^0.10.3
version: 0.10.3
concurrently:
specifier: ^8.2.2
version: 8.2.2
@ -52,6 +55,9 @@ dependencies:
version: 5.1.1
devDependencies:
'@types/amqplib':
specifier: ^0.10.4
version: 0.10.4
'@types/express':
specifier: ^4.17.21
version: 4.17.21
@ -67,12 +73,26 @@ devDependencies:
ts-node:
specifier: ^10.9.1
version: 10.9.1(@types/node@20.9.0)(typescript@5.2.2)
typedoc:
specifier: ^0.25.3
version: 0.25.3(typescript@5.2.2)
typescript:
specifier: ^5.2.2
version: 5.2.2
packages:
/@acuminous/bitsyntax@0.1.2:
resolution: {integrity: sha512-29lUK80d1muEQqiUsSo+3A0yP6CdspgC95EnKBMi22Xlwt79i/En4Vr67+cXhU+cZjbti3TgGGC5wy1stIywVQ==}
engines: {node: '>=0.8'}
dependencies:
buffer-more-ints: 1.0.0
debug: 4.3.4
safe-buffer: 5.1.2
transitivePeerDependencies:
- supports-color
dev: false
/@babel/runtime@7.23.2:
resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==}
engines: {node: '>=6.9.0'}
@ -181,6 +201,12 @@ packages:
validator: 13.11.0
dev: false
/@types/amqplib@0.10.4:
resolution: {integrity: sha512-Y5Sqquh/LqDxSgxYaAAFNM0M7GyONtSDCcFMJk+DQwYEjibPyW6y+Yu9H9omdkKc3epyXULmFN3GTaeBHhn2Hg==}
dependencies:
'@types/node': 20.9.0
dev: true
/@types/body-parser@1.19.5:
resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==}
dependencies:
@ -295,11 +321,27 @@ packages:
hasBin: true
dev: true
/amqplib@0.10.3:
resolution: {integrity: sha512-UHmuSa7n8vVW/a5HGh2nFPqAEr8+cD4dEZ6u9GjP91nHfr1a54RyAKyra7Sb5NH7NBKOUlyQSMXIp0qAixKexw==}
engines: {node: '>=10'}
dependencies:
'@acuminous/bitsyntax': 0.1.2
buffer-more-ints: 1.0.0
readable-stream: 1.1.14
url-parse: 1.5.10
transitivePeerDependencies:
- supports-color
dev: false
/ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
dev: false
/ansi-sequence-parser@1.1.1:
resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==}
dev: true
/ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
@ -430,7 +472,6 @@ packages:
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
dependencies:
balanced-match: 1.0.2
dev: false
/braces@3.0.2:
resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==}
@ -451,6 +492,10 @@ packages:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
dev: false
/buffer-more-ints@1.0.0:
resolution: {integrity: sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==}
dev: false
/busboy@1.6.0:
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
engines: {node: '>=10.16.0'}
@ -1257,6 +1302,10 @@ packages:
call-bind: 1.0.5
dev: false
/isarray@0.0.1:
resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==}
dev: false
/isarray@1.0.0:
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
dev: false
@ -1280,6 +1329,10 @@ packages:
resolution: {integrity: sha512-H/ZGY0nIAg3QcOwE1QN/rK/Fa7gJn7Ii5obwp6zyPO4xiPNwpIMjqy2gwjBEGqzkF/vSWEIBQCBuN19hYiL6Qg==}
dev: false
/jsonc-parser@3.2.0:
resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==}
dev: true
/jsonfile@6.1.0:
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
dependencies:
@ -1299,10 +1352,20 @@ packages:
yallist: 4.0.0
dev: true
/lunr@2.3.9:
resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==}
dev: true
/make-error@1.3.6:
resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
dev: true
/marked@4.3.0:
resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==}
engines: {node: '>= 12'}
hasBin: true
dev: true
/media-typer@0.3.0:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'}
@ -1355,6 +1418,13 @@ packages:
brace-expansion: 2.0.1
dev: false
/minimatch@9.0.3:
resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==}
engines: {node: '>=16 || 14 >=14.17'}
dependencies:
brace-expansion: 2.0.1
dev: true
/minimist@1.2.8:
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
dev: false
@ -1566,6 +1636,10 @@ packages:
strict-uri-encode: 2.0.0
dev: false
/querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
dev: false
/range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
@ -1581,6 +1655,15 @@ packages:
unpipe: 1.0.0
dev: false
/readable-stream@1.1.14:
resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==}
dependencies:
core-util-is: 1.0.3
inherits: 2.0.4
isarray: 0.0.1
string_decoder: 0.10.31
dev: false
/readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
dependencies:
@ -1631,6 +1714,10 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
dev: false
/rxjs@7.8.1:
resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==}
dependencies:
@ -1743,6 +1830,15 @@ packages:
resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==}
dev: false
/shiki@0.14.5:
resolution: {integrity: sha512-1gCAYOcmCFONmErGTrS1fjzJLA7MGZmKzrBNX7apqSwhyITJg2O102uFzXUeBxNnEkDA9vHIKLyeKq0V083vIw==}
dependencies:
ansi-sequence-parser: 1.1.1
jsonc-parser: 3.2.0
vscode-oniguruma: 1.7.0
vscode-textmate: 8.0.0
dev: true
/side-channel@1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
dependencies:
@ -1832,6 +1928,10 @@ packages:
es-abstract: 1.22.3
dev: false
/string_decoder@0.10.31:
resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==}
dev: false
/string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
dependencies:
@ -2014,6 +2114,20 @@ packages:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
dev: false
/typedoc@0.25.3(typescript@5.2.2):
resolution: {integrity: sha512-Ow8Bo7uY1Lwy7GTmphRIMEo6IOZ+yYUyrc8n5KXIZg1svpqhZSWgni2ZrDhe+wLosFS8yswowUzljTAV/3jmWw==}
engines: {node: '>= 16'}
hasBin: true
peerDependencies:
typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x
dependencies:
lunr: 2.3.9
marked: 4.3.0
minimatch: 9.0.3
shiki: 0.14.5
typescript: 5.2.2
dev: true
/typescript@4.9.5:
resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==}
engines: {node: '>=4.2.0'}
@ -2067,6 +2181,13 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
dev: false
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
dev: false
@ -2100,6 +2221,14 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/vscode-oniguruma@1.7.0:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==}
dev: true
/vscode-textmate@8.0.0:
resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==}
dev: true
/web-encoding@1.1.5:
resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==}
dependencies:

View file

@ -5,8 +5,10 @@ import cors from "cors";
import { RegisterRoutes } from "./routes";
import errorHandler from "./middlewares/exception";
import rabbitmq from "./rabbitmq";
import swaggerSpecs from "./swagger.json";
import { handler as amqHandler } from "./rabbitmq/handler";
const PORT = +(process.env.PORT || 80);
@ -26,5 +28,7 @@ app.use(swaggerSpecs.basePath, router);
app.use(errorHandler);
app.listen(PORT, "0.0.0.0", () =>
console.log(`Application is running on http://localhost:${PORT}`),
console.log(`[APP] Application is running on http://localhost:${PORT}`),
);
rabbitmq.init(amqHandler).catch((e) => console.error(e));

View file

@ -11,104 +11,141 @@ import {
SuccessResponse,
Tags,
Request,
Response,
Example,
} from "tsoa";
import * as Minio from "minio";
import minioClient from "../storage";
import { EhrFile, EhrFolder } from "../interfaces/ehr-fs";
import HttpStatusCode from "../interfaces/http-status";
import { listFolder, listItem, replaceIllegalChars } from "../utils/minio";
import minioClient from "../minio";
import esClient from "../elasticsearch";
import { copyCond, listFolder, listItem, replaceIllegalChars } from "../utils/minio";
import HttpStatusCode from "../interfaces/http-status";
import { StorageFile, StorageFolder } from "../interfaces/storage-fs";
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")
export class CabinetController extends Controller {
@Get("/")
@Tags("Cabinet")
@SuccessResponse(HttpStatusCode.OK)
public async listCabinet(): Promise<EhrFolder[]> {
const list = await listFolder().catch((e) => console.error(`Error List Folder: ${e}`));
if (!list) {
throw new Error("Error listing folder");
}
@Tags("ตู้เอกสาร")
@Security("bearerAuth")
@Response(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง",
)
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
@Example([
{
path: "ตู้เอกสาร 1/",
name: "ตู้เอกสาร 1",
createdAt: "2021-07-20T12:33:13.018Z",
createdBy: "admin",
},
{
path: "ตู้เอกสาร 2/",
name: "ตู้เอกสาร 2",
createdAt: "2022-01-23T16:05:02.114Z",
createdBy: "admin",
},
])
public async listCabinet(): Promise<StorageFolder[]> {
const list = await listFolder(DEFAULT_BUCKET!).catch((e) =>
console.error(`Error List Folder: ${e}`),
);
if (!list)
throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการตู้เอกสารได้ กรุณาลองใหม่ในภายหลัง");
return list;
}
@Post("/")
@Tags("Cabinet")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.CREATED)
@Tags("ตู้เอกสาร")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
public async createCabinet(
@Request() request: { user: { preferred_username: string } },
@Body() body: { name: string },
@Body()
body: {
/**
* @example "ตู้เอกสาร 1"
*/
name: string;
},
) {
const uploaded = await minioClient
.putObject("ehr", `${replaceIllegalChars(body.name)}/.keep`, "", 0, {
const created = await minioClient
.putObject(DEFAULT_BUCKET!, `${replaceIllegalChars(body.name)}/.keep`, "", 0, {
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
})
.catch((e) => console.error(e));
if (!uploaded) throw new Error("Object storage error occured.");
if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
return this.setStatus(HttpStatusCode.CREATED);
}
/**
* @example cabinetName "ตู้เอกสาร 1"
*/
@Put("/{cabinetName}")
@Tags("Cabinet")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "Success")
@Tags("ตู้เอกสาร")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async editCabinet(
@Path() cabinetName: string,
@Body() body: { name: string },
@Body()
body: {
/**
* @example "ตู้เอกสารใหม่"
*/
name: string;
},
): Promise<void> {
const list = await listItem(`${cabinetName}/`, true);
const cond = new Minio.CopyConditions();
const path = `${cabinetName}/`;
const list = await listItem(DEFAULT_BUCKET!, path, true);
await Promise.all(
list.map(async (current) => {
if (!current.name) return;
const destination = `${replaceIllegalChars(body.name)}/${current.name.slice(
cabinetName.length + 1,
)}`;
const source = `/ehr/${current.name}`;
const destination = `${replaceIllegalChars(body.name)}/${current.name.slice(path.length)}`;
const source = `/${DEFAULT_BUCKET}/${current.name}`;
return await minioClient
.copyObject("ehr", destination, source, cond)
.copyObject(DEFAULT_BUCKET!, destination, source, copyCond)
.then(async () => {
if (!current.name) return;
if (current.name.includes(".keep")) {
return await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
}
await minioClient.removeObject("ehr", current.name);
if (current.name.includes(".keep")) return;
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index",
query: {
match: {
pathname: current.name,
},
},
const search = await esClient.search<
StorageFile & { attachment: Record<string, string> }
>({
index: DEFAULT_INDEX!,
query: { match: { pathname: current.name } },
});
if (search && search.hits.hits.length === 0) {
throw new Error("Data cannot be found in database.");
}
if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล");
const data = search.hits.hits[0];
await esClient.update({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index",
index: DEFAULT_INDEX!,
id: data._id,
doc: { pathname: destination },
});
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
})
.catch((e) => {
console.error(e);
throw new Error("Failed to move.");
throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
});
}),
);
@ -116,25 +153,28 @@ export class CabinetController extends Controller {
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
/**
* @example cabinetName "ตู้เอกสาร 1"
*/
@Delete("/{cabinetName}")
@Tags("Cabinet")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.NO_CONTENT)
@Tags("ตู้เอกสาร")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteCabinet(@Path() cabinetName: string) {
return new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
const objects: string[] = [];
const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/`, true);
const stream = minioClient.listObjectsV2(DEFAULT_BUCKET!, `${cabinetName}/`, true);
stream.on("data", (v) => {
if (!(v && v.name)) return;
objects.push(v.name);
if (v && v.name) objects.push(v.name);
});
stream.on("close", () => minioClient.removeObjects("ehr", objects));
stream.on("error", () => reject(new Error("Object storage error occured.")));
resolve(this.setStatus(HttpStatusCode.NO_CONTENT));
stream.on("close", async () =>
resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)),
);
stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")));
});
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
}

View file

@ -6,120 +6,165 @@ import {
Path,
Post,
Put,
Request,
Route,
Security,
SuccessResponse,
Tags,
Request,
Response,
Example,
} from "tsoa";
import * as Minio from "minio";
import minioClient from "../storage";
import minioClient from "../minio";
import esClient from "../elasticsearch";
import { copyCond, listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio";
import HttpStatusCode from "../interfaces/http-status";
import { StorageFile, StorageFolder } from "../interfaces/storage-fs";
import HttpError from "../interfaces/http-error";
import { listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio";
import esClient from "../elasticsearch";
import { EhrFile, EhrFolder } from "../interfaces/ehr-fs";
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")
export class DrawerController extends Controller {
/**
* @example cabinetName "ตู้เอกสาร 1"
*/
@Get("/")
@Tags("Drawer")
@SuccessResponse(HttpStatusCode.OK)
public async listDrawer(@Path() cabinetName: string): Promise<EhrFolder[]> {
const fullpath = [cabinetName, ""].join("/");
if (!(await pathExist(fullpath))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist.");
}
return listFolder(fullpath);
@Tags("ลิ้นชัก")
@Security("bearerAuth")
@Response(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง",
)
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
@Example([
{
path: "ตู้เอกสาร 1/ลิ้นชัก 1/",
name: "ลิ้นชัก 1",
createdAt: "2021-07-20T12:33:13.018Z",
createdBy: "admin",
},
{
path: "ตู้เอกสาร 1/ลิ้นชัก 2/",
name: "ลิ้นชัก 2",
createdAt: "2022-01-23T16:05:02.114Z",
createdBy: "admin",
},
])
public async listDrawer(@Path() cabinetName: string): Promise<StorageFolder[]> {
const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/`).catch((e) =>
console.error(`Error List Folder: ${e}`),
);
if (!list)
throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการลิ้นชักได้ กรุณาลองใหม่ในภายหลัง");
return list;
}
/**
* @example cabinetName "ตู้เอกสาร 1"
*/
@Post("/")
@Tags("Drawer")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.CREATED)
@Tags("ลิ้นชัก")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.NOT_FOUND, "ไม่พบลิ้นชัก")
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
public async createDrawer(
@Request() request: { user: { preferred_username: string } },
@Path() cabinetName: string,
@Body() body: { name: string },
@Body()
body: {
/**
* @example "ลิ้นชัก 1"
*/
name: string;
},
) {
if (!(await pathExist(`${cabinetName}/`))) {
throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet cannot be found.");
const basePath = `${cabinetName}/`;
if (!(await pathExist(DEFAULT_BUCKET!, basePath))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างลิ้นชัก");
}
const uploaded = await minioClient
.putObject("ehr", `${cabinetName}/${replaceIllegalChars(body.name)}/.keep`, "", 0, {
const created = await minioClient
.putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, {
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
})
.catch((e) => console.error(e));
if (!uploaded) {
throw new Error("Object storage error occured.");
}
if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
return this.setStatus(HttpStatusCode.CREATED);
}
/**
* @example cabinetName "ตู้เอกสาร 1"
* @example drawerName "ลิ้นชัก 1"
*/
@Put("/{drawerName}")
@Tags("Drawer")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.NO_CONTENT)
@Tags("ลิ้นชัก")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async editDrawer(
@Path() cabinetName: string,
@Path() drawerName: string,
@Body() body: { name: string },
@Body()
body: {
/**
* @example "ลิ้นชักใหม่"
*/
name: string;
},
): Promise<void> {
const fullpath = `${cabinetName}/${drawerName}/`;
const list = await listItem(fullpath, true);
const cond = new Minio.CopyConditions();
const path = `${cabinetName}/${drawerName}/`;
const list = await listItem(DEFAULT_BUCKET!, path, true);
await Promise.all(
list.map(async (current) => {
if (!current.name) return;
const destination = `${cabinetName}/${replaceIllegalChars(body.name)}/${current.name.slice(
fullpath.length,
path.length,
)}`;
const source = `/ehr/${current.name}`;
const source = `/${DEFAULT_BUCKET}/${current.name}`;
return await minioClient
.copyObject("ehr", destination, source, cond)
.copyObject(DEFAULT_BUCKET!, destination, source, copyCond)
.then(async () => {
if (!current.name) return;
if (current.name.includes(".keep")) {
return await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
}
await minioClient.removeObject("ehr", current.name);
if (current.name.includes(".keep")) return;
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index",
query: {
match: {
pathname: current.name,
},
},
const search = await esClient.search<
StorageFile & { attachment: Record<string, string> }
>({
index: DEFAULT_INDEX!,
query: { match: { pathname: current.name } },
});
if (search && search.hits.hits.length === 0) {
throw new Error("Data cannot be found in database.");
}
if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล");
const data = search.hits.hits[0];
await esClient.update({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index",
index: DEFAULT_INDEX!,
id: data._id,
doc: { pathname: destination },
});
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
})
.catch((e) => {
console.error(e);
throw new Error("Failed to move.");
throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
});
}),
);
@ -127,25 +172,32 @@ export class DrawerController extends Controller {
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
/**
* @example cabinetName "ตู้เอกสาร 1"
* @example drawerName "ลิ้นชัก 1"
*/
@Delete("/{drawerName}")
@Tags("Drawer")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.NO_CONTENT)
@Tags("ลิ้นชัก")
@Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) {
return new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
const objects: string[] = [];
const stream = minioClient.listObjectsV2("ehr", `${cabinetName}/${drawerName}/`, true);
const stream = minioClient.listObjectsV2(
DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/`,
true,
);
stream.on("data", (v) => {
if (!(v && v.name)) return;
objects.push(v.name);
if (v && v.name) objects.push(v.name);
});
stream.on("close", () => minioClient.removeObjects("ehr", objects));
stream.on("error", () => reject(new Error("Object storage error occured.")));
resolve(true);
stream.on("close", async () =>
resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)),
);
stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")));
});
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
}

View file

@ -1,299 +1,280 @@
import {
Body,
Controller,
Delete,
FormField,
Get,
Patch,
Path,
Post,
Request,
Response,
Route,
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 { StorageFile } from "../interfaces/storage-fs";
import HttpError from "../interfaces/http-error";
import { EhrFile } from "../interfaces/ehr-fs";
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 {
@Post("/")
@Tags("File")
@Security("bearerAuth")
@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,
@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}`;
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,
})
.catch((e) => console.error(e));
if (!info) throw new Error("Object storage error occured.");
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index',
query: {
match: {
pathname: pathname,
},
},
});
const exist = search.hits.hits.find((v) => v._source?.pathname === pathname);
const metadata: Partial<EhrFile> = {
pathname,
fileName: filename,
fileSize: file.size,
fileType: file.mimetype,
title: title,
description: description,
category: category.split(","),
keyword: keyword.split(","),
};
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,
},
});
}
return this.setStatus(HttpStatusCode.CREATED);
}
@Get("/")
@Tags("File")
@SuccessResponse(HttpStatusCode.OK)
@Tags("ไฟล์")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async getFile(
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
): Promise<EhrFile[]> {
const search = await esClient.search<
EhrFile & {
attachment: Record<string, string>;
}
>({
index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index',
): Promise<StorageFile[]> {
const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
index: DEFAULT_INDEX!,
query: {
prefix: {
pathname: `${cabinetName}/${drawerName}/${folderName}/`,
},
},
size: 10000,
});
// 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 StorageFile;
}
})
.flatMap((v) => (v ? [v] : []));
.filter((v: StorageFile | undefined): v is StorageFile => !!v);
return records;
}
@Post("/")
@Tags("ไฟล์")
@Security("bearerAuth", ["admin"])
@Response(
HttpStatusCode.NOT_FOUND,
"ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ",
)
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
public async uploadFile(
@Request() request: { user: { preferred_username: string } },
@Body()
body: {
file: string;
title?: string;
description?: string;
category?: string;
keyword?: string;
},
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
) {
const basePath = `${cabinetName}/${drawerName}/${folderName}/`;
const pathname = `${basePath}${body.file}`;
if (!(await pathExist(DEFAULT_BUCKET!, basePath))) {
throw new HttpError(
HttpStatusCode.NOT_FOUND,
"ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ",
);
}
const result = await esClient
.search<StorageFile & { attachment?: Record<string, unknown> }>({
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));
}
const rec = result ? result.hits.hits[0]._source : false;
const metadata: Partial<StorageFile> = {
pathname,
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",
};
await esClient.index({
index: DEFAULT_INDEX!,
document: metadata,
});
return {
...body,
createdAt: metadata.createdAt,
createdBy: metadata.createdBy,
updatedAt: metadata.updatedAt,
updatedBy: metadata.updatedBy,
upload: await minioClient.presignedPutObject(DEFAULT_BUCKET!, pathname),
};
}
@Patch("/{fileName}")
@Tags("File")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK)
@Tags("ไฟล์")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม")
@Response(HttpStatusCode.NO_CONTENT, "สำเร็จ")
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async updateFile(
@Request() request: { user: { preferred_username: string } },
@Path() cabinetName: string,
@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<EhrFile & { attachment: Record<string, string> }>({
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<void | { upload: string }> {
const basePath = `${cabinetName}/${drawerName}/${folderName}/`;
const pathname = `${basePath}${fileName}`;
if (search && search.hits.hits.length === 0) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found");
if (
!Boolean(
await minioClient.statObject(DEFAULT_BUCKET!, `${pathname}`).catch((e) => {
if (e.code === "NotFound") return false;
throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
}),
)
) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์");
}
const data = search.hits.hits[0];
// assume user will probably replace file by re-upload but maybe just rename
if (body.file) {
const destination = `${basePath}${body.file}`;
const source = `/${DEFAULT_BUCKET}/${basePath}${fileName}`;
const copy = await minioClient.copyObject(DEFAULT_BUCKET!, destination, source, copyCond);
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 (copy) {
const search = await esClient
.search<StorageFile & { attachment?: Record<string, unknown> }>({
index: DEFAULT_INDEX!,
query: { match: { pathname } },
})
.catch((e) => console.error(e));
if (!esResult) throw new Error("An error occured, cannot perform this action.");
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(DEFAULT_BUCKET!, pathname));
} else {
await minioClient.removeObject(DEFAULT_BUCKET!, pathname);
}
}
} 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}`,
);
const info = await minioClient
.putObject("ehr", pathname, file.buffer, file.size, {
"Content-Type": file.mimetype,
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
const search = await esClient
.search<StorageFile & { attachment?: Record<string, unknown> }>({
index: DEFAULT_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
? {
upload: await minioClient.presignedPutObject(
DEFAULT_BUCKET!,
`${basePath}${body.file ?? fileName}`,
),
}
: this.setStatus(HttpStatusCode.NO_CONTENT);
}
@Delete("/{fileName}")
@Tags("File")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK)
@Tags("ไฟล์")
@Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async deleteFile(
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
@Path() fileName: string,
) {
const search = await esClient.search<
EhrFile & {
attachment: Record<string, string>;
}
>({
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.");
await minioClient.removeObject("ehr", `${cabinetName}/${drawerName}/${folderName}/${fileName}`);
await minioClient.removeObject(
DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/${folderName}/${fileName}`,
);
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
@Get("/{fileName}")
@Tags("File")
@SuccessResponse(HttpStatusCode.OK)
@Tags("ดาวน์โหลด")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async downloadFile(
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
@Path() fileName: string,
) {
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index',
const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
index: DEFAULT_INDEX!,
query: {
match: {
pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}`,
},
match: { pathname: `${cabinetName}/${drawerName}/${folderName}/${fileName}` },
},
});
@ -301,18 +282,12 @@ export class FileController extends Controller {
throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found");
}
const data = search.hits.hits[0]._source;
if (!data) {
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "Found data but no info.");
}
const { attachment, ...rest } = data;
const { attachment, ...rest } = search.hits.hits[0]._source!;
return {
...rest,
download: await minioClient.presignedGetObject(
"ehr",
DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/${folderName}/${fileName}`,
),
};

View file

@ -6,88 +6,133 @@ import {
Path,
Post,
Put,
Request,
Route,
Security,
SuccessResponse,
Tags,
Request,
Response,
Example,
} from "tsoa";
import * as Minio from "minio";
import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status";
import { listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio";
import { EhrFile, EhrFolder } from "../interfaces/ehr-fs";
import minioClient from "../storage";
import minioClient from "../minio";
import esClient from "../elasticsearch";
import { copyCond, listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio";
import HttpStatusCode from "../interfaces/http-status";
import { StorageFile, StorageFolder } from "../interfaces/storage-fs";
import HttpError from "../interfaces/http-error";
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")
export class FolderController extends Controller {
/**
* @example cabinetName "ตู้เอกสาร 1"
* @example drawerName "ลิ้นชัก 1"
*/
@Get("/")
@Tags("Folder")
@SuccessResponse(HttpStatusCode.OK)
@Tags("แฟ้ม")
@Security("bearerAuth")
@Response(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง",
)
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
@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",
},
])
public async listFolder(
@Path() cabinetName: string,
@Path() drawerName: string,
): Promise<EhrFolder[]> {
const fullpath = [cabinetName, drawerName, ""].join("/");
if (!(await pathExist(fullpath))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist.");
}
return listFolder(fullpath);
): Promise<StorageFolder[]> {
const list = await listFolder(DEFAULT_BUCKET!, `${cabinetName}/${drawerName}`).catch((e) =>
console.error(`Error List Folder: ${e}`),
);
if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง");
return list;
}
/**
* @example cabinetName "ตู้เอกสาร 1"
* @example drawerName "ลิ้นชัก 1"
*/
@Post("/")
@Tags("Folder")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.CREATED)
@Tags("แฟ้ม")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม")
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
public async createFolder(
@Request() request: { user: { preferred_username: string } },
@Body() body: { name: string },
@Body()
body: {
/**
* @example "แฟ้ม 1"
*/
name: string;
},
@Path() cabinetName: string,
@Path() drawerName: string,
) {
if (!(await pathExist(`${cabinetName}/${drawerName}/`))) {
throw new HttpError(HttpStatusCode.PRECONDITION_FAILED, "Cabinet or drawer cannot be found.");
const basePath = `${cabinetName}/${drawerName}/`;
if (!(await pathExist(DEFAULT_BUCKET!, basePath))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างลิ้นชัก");
}
const uploaded = await minioClient
.putObject(
"ehr",
`${cabinetName}/${drawerName}/${replaceIllegalChars(body.name)}/.keep`,
"",
0,
{
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
},
)
const created = await minioClient
.putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, {
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
})
.catch((e) => console.error(e));
if (!uploaded) {
throw new Error("Object storage error occured.");
}
if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
return this.setStatus(HttpStatusCode.CREATED);
}
/**
* @example cabinetName "ตู้เอกสาร 1"
* @example drawerName "ลิ้นชัก 1"
* @example folderName "แฟ้ม 1"
*/
@Put("/{folderName}")
@Tags("Folder")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.NO_CONTENT)
@Tags("แฟ้ม")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async editFolder(
@Body() body: { name: string },
@Body()
body: {
/**
* @example "แฟ้มใหม่"
*/
name: string;
},
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
) {
const fullpath = `${cabinetName}/${drawerName}/${folderName}`;
const list = await listItem(fullpath, true);
const cond = new Minio.CopyConditions();
const path = `${cabinetName}/${drawerName}/${folderName}`;
const list = await listItem(DEFAULT_BUCKET!, path, true);
await Promise.all(
list.map(async (current) => {
@ -95,42 +140,38 @@ export class FolderController extends Controller {
const destination = `${cabinetName}/${drawerName}/${replaceIllegalChars(
body.name,
)}/${current.name.slice(fullpath.length)}`;
const source = `/ehr/${current.name}`;
)}/${current.name.slice(path.length)}`;
const source = `/${DEFAULT_BUCKET}/${current.name}`;
return await minioClient
.copyObject("ehr", destination, source, cond)
.copyObject(DEFAULT_BUCKET!, destination, source, copyCond)
.then(async () => {
if (!current.name) return;
if (current.name.includes(".keep")) {
return await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
}
await minioClient.removeObject("ehr", current.name);
if (current.name.includes(".keep")) return;
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index",
query: {
match: {
pathname: current.name,
},
},
const search = await esClient.search<
StorageFile & { attachment: Record<string, string> }
>({
index: DEFAULT_INDEX!,
query: { match: { pathname: current.name } },
});
if (search && search.hits.hits.length === 0) {
throw new Error("Data cannot be found in database.");
}
if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล");
const data = search.hits.hits[0];
await esClient.update({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index",
index: DEFAULT_INDEX!,
id: data._id,
doc: { pathname: destination },
});
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
})
.catch((e) => {
console.error(e);
throw new Error("Failed to move.");
throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
});
}),
);
@ -138,33 +179,37 @@ export class FolderController extends Controller {
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
/**
* @example cabinetName "ตู้เอกสาร 1"
* @example drawerName "ลิ้นชัก 1"
* @example folderName "แฟ้ม 1"
*/
@Delete("/{folderName}")
@Tags("Folder")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.NO_CONTENT)
@Tags("แฟ้ม")
@Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteFolder(
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
) {
return new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
const objects: string[] = [];
const stream = minioClient.listObjectsV2(
"ehr",
DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/${folderName}`,
true,
);
stream.on("data", (v) => {
if (!(v && v.name)) return;
objects.push(v.name);
if (v && v.name) objects.push(v.name);
});
stream.on("close", () => minioClient.removeObjects("ehr", objects));
stream.on("error", () => reject(new Error("Object storage error occured.")));
resolve(this.setStatus(HttpStatusCode.NO_CONTENT));
stream.on("close", async () =>
resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)),
);
stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")));
});
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
}

View file

@ -1,23 +1,29 @@
import { Body, Controller, Post, Route, SuccessResponse, Tags } from "tsoa";
import { Body, Controller, Post, Route, Security, SuccessResponse, Tags } from "tsoa";
import HttpStatusCode from "../interfaces/http-status";
import esClient from "../elasticsearch";
import { Search } from "../interfaces/search";
import { EhrFile } from "../interfaces/ehr-fs";
import { StorageFile } from "../interfaces/storage-fs";
const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX;
if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified.");
@Route("/search")
export class SearchController extends Controller {
@Post("/")
@Tags("Search")
@SuccessResponse(HttpStatusCode.OK)
public async searchFile(@Body() search: Search): Promise<EhrFile[]> {
const result = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index',
@Tags("ค้นหา")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async searchFile(@Body() search: Search): Promise<StorageFile[]> {
const result = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
index: DEFAULT_INDEX,
query: {
bool: {
must: search.AND?.map((v) => ({ match: { [v.field]: v.value } })),
should: search.OR?.map((v) => ({ match: { [v.field]: v.value } })),
},
},
size: 10000,
});
return result.hits.hits.length > 0

View file

@ -0,0 +1,104 @@
import { Body, Controller, Get, Path, Post, Put, Query, Route, SuccessResponse } from "tsoa";
import { replaceIllegalChars } from "../utils/minio";
import minioClient from "../minio";
import HttpStatusCode from "../interfaces/http-status";
import HttpError from "../interfaces/http-error";
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("/storage/d")
export class StorageController extends Controller {
@Get()
@SuccessResponse(HttpStatusCode.OK, "Success")
public async getFolder(@Query() path: string, @Query() bucket?: string) {
const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => {
const item: { pathname: string; name: string }[] = [];
const stream = minioClient.listObjectsV2(bucket ?? DEFAULT_BUCKET!, path);
stream.on("data", (v) => {
if (v && v.prefix) {
item.push({
pathname: v.prefix,
name: v.prefix.slice(path?.length).split("/")[0],
});
}
});
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์")));
});
return list;
}
@Post()
@SuccessResponse(HttpStatusCode.CREATED, "Success")
public async createFolder(@Query() path: string, @Query() bucket?: string) {
const fragments = path.split("/").filter(Boolean);
await Promise.all(
fragments.map(async (_, i, a) => {
const path = [...a.slice(0, i + 1)].map((x) => replaceIllegalChars(x)).join("/");
const created = await minioClient
.putObject(bucket ?? DEFAULT_BUCKET!, `${path}/.keep`, "", 0)
.catch((e) => console.error(e));
if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
}),
);
return this.setStatus(HttpStatusCode.CREATED);
}
@Put()
@SuccessResponse(HttpStatusCode.NO_CONTENT, "Success")
public async updateFolder(
@Body()
body: {
from: {
bucket: string;
path: string;
};
to: {
bucket: string;
path: string;
};
},
) {
const src = body.from.path.split("/").filter(Boolean).join("/");
const dst = body.to.path.split("/").filter(Boolean).join("/");
if (
!Boolean(
await minioClient
.statObject(DEFAULT_BUCKET!, `${dst.replace(/^\/|\/$/g, "")}/.keep`)
.catch((e) => {
if (e.code === "NotFound") return false;
throw new Error(`Minio Error: ${e}`);
}),
)
)
throw new HttpError(HttpStatusCode.NOT_FOUND, "Destination Not Found");
const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => {
const item: { pathname: string; name: string }[] = [];
const stream = minioClient.listObjectsV2(body.from.bucket, src);
stream.on("data", (v) => {
if (v && v.prefix) {
item.push({
pathname: v.prefix,
name: v.prefix.slice(src.length).split("/")[0],
});
}
});
stream.on("error", (e) => reject(new Error(`Minio Error: ${e}`)));
stream.on("end", () => resolve(item));
});
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
}

View file

@ -6,44 +6,83 @@ import {
Path,
Post,
Put,
Request,
Route,
Security,
SuccessResponse,
Tags,
Request,
Response,
Example,
} from "tsoa";
import * as Minio from "minio";
import HttpError from "../interfaces/http-error";
import HttpStatusCode from "../interfaces/http-status";
import { listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio";
import { EhrFile, EhrFolder } from "../interfaces/ehr-fs";
import minioClient from "../storage";
import minioClient from "../minio";
import esClient from "../elasticsearch";
import { copyCond, listFolder, listItem, pathExist, replaceIllegalChars } from "../utils/minio";
import HttpStatusCode from "../interfaces/http-status";
import { StorageFile, StorageFolder } from "../interfaces/storage-fs";
import HttpError from "../interfaces/http-error";
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}/subfolder")
export class SubFolderController extends Controller {
/**
* @example cabinetName "ตู้เอกสาร 1"
* @example drawerName "ลิ้นชัก 1"
* @example folderName "แฟ้ม 1"
*/
@Get("/")
@Tags("SubFolder")
@SuccessResponse(HttpStatusCode.OK)
@Tags("แฟ้มย่อย")
@Security("bearerAuth")
@Response(
HttpStatusCode.INTERNAL_SERVER_ERROR,
"เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง",
)
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
@Example([
{
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/แฟ้มย่อย 1",
name: "แฟ้มย่อย 1",
createdAt: "2021-07-20T12:33:13.018Z",
createdBy: "admin",
},
{
path: "ตู้เอกสาร 1/ลิ้นชัก 1/แฟ้ม 1/แฟ้มย่อย 2",
name: "แฟ้มย่อย 2",
createdAt: "2022-01-23T16:05:02.114Z",
createdBy: "admin",
},
])
public async listFolder(
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
): Promise<EhrFolder[]> {
const fullpath = [cabinetName, drawerName, folderName, ""].join("/");
if (!(await pathExist(fullpath))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "Provided path does not exist.");
}
return listFolder(fullpath);
): Promise<StorageFolder[]> {
const list = await listFolder(
DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/${folderName}`,
).catch((e) => console.error(`Error List Folder: ${e}`));
if (!list) throw new Error("เกิดข้อผิดพลาด ไม่สามารถแสดงรายการแฟ้มได้ กรุณาลองใหม่ในภายหลัง");
return list;
}
/**
* @example cabinetName "ตู้เอกสาร 1"
* @example drawerName "ลิ้นชัก 1"
* @example folderName "แฟ้ม 1"
*/
@Post("/")
@Tags("SubFolder")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.CREATED)
@Tags("แฟ้มย่อย")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม")
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
public async createFolder(
@Request() request: { user: { preferred_username: string } },
@Body() body: { name: string },
@ -51,49 +90,50 @@ export class SubFolderController extends Controller {
@Path() drawerName: string,
@Path() folderName: string,
) {
if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}`))) {
throw new HttpError(
HttpStatusCode.PRECONDITION_FAILED,
"Cabinet, drawer or folder cannot be found.",
);
const basePath = `${cabinetName}/${drawerName}/${folderName}/`;
if (!(await pathExist(DEFAULT_BUCKET!, basePath))) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างลิ้นชัก");
}
const uploaded = await minioClient
.putObject(
"ehr",
`${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(body.name)}/.keep`,
"",
0,
{
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
},
)
const created = await minioClient
.putObject(DEFAULT_BUCKET!, `${basePath}${replaceIllegalChars(body.name)}/.keep`, "", 0, {
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
})
.catch((e) => console.error(e));
if (!uploaded) {
throw new Error("Object storage error occured.");
}
if (!created) throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
return this.setStatus(HttpStatusCode.CREATED);
}
/**
* @example cabinetName "ตู้เอกสาร 1"
* @example drawerName "ลิ้นชัก 1"
* @example folderName "แฟ้ม 1"
* @example subFolderName "แฟ้มย่อย 1"
*/
@Put("/{subFolderName}")
@Tags("SubFolder")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.NO_CONTENT)
@Tags("แฟ้มย่อย")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async editFolder(
@Body() body: { name: string },
@Body()
body: {
/**
* @example "แฟ้มใหม่"
*/
name: string;
},
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
@Path() subFolderName: string,
) {
const fullpath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`;
const list = await listItem(fullpath, true);
const cond = new Minio.CopyConditions();
const path = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`;
const list = await listItem(DEFAULT_BUCKET!, path, true);
await Promise.all(
list.map(async (current) => {
@ -101,42 +141,38 @@ export class SubFolderController extends Controller {
const destination = `${cabinetName}/${drawerName}/${folderName}/${replaceIllegalChars(
body.name,
)}/${current.name.slice(fullpath.length)}`;
const source = `/ehr/${current.name}`;
)}/${current.name.slice(path.length)}`;
const source = `/${DEFAULT_BUCKET}/${current.name}`;
return await minioClient
.copyObject("ehr", destination, source, cond)
.copyObject(DEFAULT_BUCKET!, destination, source, copyCond)
.then(async () => {
if (!current.name) return;
if (current.name.includes(".keep")) {
return await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
}
await minioClient.removeObject("ehr", current.name);
if (current.name.includes(".keep")) return;
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index",
query: {
match: {
pathname: current.name,
},
},
const search = await esClient.search<
StorageFile & { attachment: Record<string, string> }
>({
index: DEFAULT_INDEX!,
query: { match: { pathname: current.name } },
});
if (search && search.hits.hits.length === 0) {
throw new Error("Data cannot be found in database.");
}
if (search && search.hits.hits.length === 0) throw new Error("ไม่พบข้อมูลในฐานข้อมูล");
const data = search.hits.hits[0];
await esClient.update({
index: process.env.ELASTICSEARCH_INDEX ?? "ehr-index",
index: DEFAULT_INDEX!,
id: data._id,
doc: { pathname: destination },
});
await minioClient.removeObject(DEFAULT_BUCKET!, current.name);
})
.catch((e) => {
console.error(e);
throw new Error("Failed to move.");
throw new Error("เกิดข้อผิดพลาด ไม่สามารถย้ายไฟล์ได้");
});
}),
);
@ -144,34 +180,39 @@ export class SubFolderController extends Controller {
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
/**
* @example cabinetName "ตู้เอกสาร 1"
* @example drawerName "ลิ้นชัก 1"
* @example folderName "แฟ้ม 1"
* @example subFolderName "แฟ้มย่อย 1"
*/
@Delete("/{subFolderName}")
@Tags("SubFolder")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.NO_CONTENT)
@Tags("แฟ้มย่อย")
@Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
public async deleteFolder(
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
@Path() subFolderName: string,
) {
return new Promise((resolve, reject) => {
await new Promise<void>((resolve, reject) => {
const objects: string[] = [];
const stream = minioClient.listObjectsV2(
"ehr",
DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/${folderName}/${subFolderName}`,
true,
);
stream.on("data", (v) => {
if (!(v && v.name)) return;
objects.push(v.name);
if (v && v.name) objects.push(v.name);
});
stream.on("close", () => minioClient.removeObjects("ehr", objects));
stream.on("error", () => reject(new Error("Object storage error occured.")));
resolve(this.setStatus(HttpStatusCode.NO_CONTENT));
stream.on("close", async () =>
resolve(await minioClient.removeObjects(DEFAULT_BUCKET!, objects)),
);
stream.on("error", () => reject(new Error("เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")));
});
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
}

View file

@ -1,159 +1,158 @@
import {
Body,
Controller,
Delete,
FormField,
Get,
Patch,
Path,
Post,
Request,
Response,
Route,
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 { StorageFile } from "../interfaces/storage-fs";
import HttpError from "../interfaces/http-error";
import { EhrFile } from "../interfaces/ehr-fs";
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}/subfolder/{subFolderName}/file",
)
export class SubFolderFileController extends Controller {
@Post("/")
@Tags("SubFolder File")
@Security("bearerAuth")
@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,
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
@Path() subFolderName: string,
) {
const filename = Buffer.from(file.originalname, "latin1").toString("utf-8");
const pathname = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${filename}`;
if (!(await pathExist(`${cabinetName}/${drawerName}/${folderName}/${subFolderName}`))) {
throw new HttpError(
HttpStatusCode.PRECONDITION_FAILED,
"Cabinet, drawer, folder or subfolder 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,
})
.catch((e) => console.error(e));
if (!info) throw new Error("Object storage error occured.");
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index',
query: {
match: {
pathname: pathname,
},
},
});
const exist = search.hits.hits.find((v) => v._source?.pathname === pathname);
const metadata: Partial<EhrFile> = {
pathname,
fileName: filename,
fileSize: file.size,
fileType: file.mimetype,
title: title,
description: description,
category: category.split(","),
keyword: keyword.split(","),
};
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,
},
});
}
return this.setStatus(HttpStatusCode.CREATED);
}
@Get("/")
@Tags("SubFolder File")
@SuccessResponse(HttpStatusCode.OK)
@Tags("ไฟล์")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async getFile(
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
@Path() subFolderName: string,
) {
const search = await esClient.search<
EhrFile & {
attachment: Record<string, string>;
}
>({
index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index',
): Promise<StorageFile[]> {
const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
index: DEFAULT_INDEX!,
query: {
prefix: {
pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}`,
},
},
size: 10000,
});
// 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 StorageFile;
}
})
.flatMap((v) => (v ? [v] : []));
.filter((v: StorageFile | undefined): v is StorageFile => !!v);
return records;
}
@Post("/")
@Tags("ไฟล์")
@Security("bearerAuth", ["admin"])
@Response(
HttpStatusCode.NOT_FOUND,
"ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ",
)
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
public async uploadFile(
@Request() request: { user: { preferred_username: string } },
@Body()
body: {
file: string;
title?: string;
description?: string;
category?: string;
keyword?: string;
},
@Path() cabinetName: string,
@Path() drawerName: string,
@Path() folderName: string,
@Path() subFolderName: string,
) {
const basePath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`;
const pathname = `${basePath}${body.file}`;
if (!(await pathExist(DEFAULT_BUCKET!, basePath))) {
throw new HttpError(
HttpStatusCode.NOT_FOUND,
"ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ",
);
}
const result = await esClient
.search<StorageFile & { attachment?: Record<string, unknown> }>({
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));
}
const rec = result ? result.hits.hits[0]._source : false;
const metadata: Partial<StorageFile> = {
pathname,
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",
};
await esClient.index({
index: DEFAULT_INDEX!,
document: metadata,
});
return {
...body,
createdAt: metadata.createdAt,
createdBy: metadata.createdBy,
updatedAt: metadata.updatedAt,
updatedBy: metadata.updatedBy,
upload: await minioClient.presignedPutObject(DEFAULT_BUCKET!, pathname),
};
}
@Patch("/{fileName}")
@Tags("SubFolder File")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK)
@Tags("ไฟล์")
@Security("bearerAuth", ["admin"])
@Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม")
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async updateFile(
@Request() request: { user: { preferred_username: string } },
@Path() cabinetName: string,
@ -161,92 +160,98 @@ export class SubFolderFileController extends Controller {
@Path() folderName: string,
@Path() subFolderName: string,
@Path() fileName: 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;
},
) {
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index',
query: {
match: {
pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`,
},
},
});
const basePath = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/`;
const pathname = `${basePath}${fileName}`;
if (search && search.hits.hits.length === 0) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found");
if (
!Boolean(
await minioClient.statObject(DEFAULT_BUCKET!, `${pathname}`).catch((e) => {
if (e.code === "NotFound") return false;
throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
}),
)
) {
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์");
}
const data = search.hits.hits[0];
// assume user will probably replace file by re-upload but maybe just rename
if (body.file) {
const destination = `${basePath}${body.file}`;
const source = `/${DEFAULT_BUCKET}/${basePath}${fileName}`;
const copy = await minioClient.copyObject(DEFAULT_BUCKET!, destination, source, copyCond);
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 (copy) {
const search = await esClient
.search<StorageFile & { attachment?: Record<string, unknown> }>({
index: DEFAULT_INDEX!,
query: { match: { pathname } },
})
.catch((e) => console.error(e));
if (!esResult) throw new Error("An error occured, cannot perform this action.");
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(DEFAULT_BUCKET!, pathname));
} else {
await minioClient.removeObject(DEFAULT_BUCKET!, pathname);
}
}
} else {
const filename = Buffer.from(file.originalname, "latin1").toString("utf-8");
const pathname = `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${filename}`;
await minioClient.removeObject(
"ehr",
`${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`,
);
const info = await minioClient
.putObject("ehr", pathname, file.buffer, file.size, {
"Content-Type": file.mimetype,
createdAt: new Date().toISOString(),
createdBy: request.user.preferred_username,
const search = await esClient
.search<StorageFile & { attachment?: Record<string, unknown> }>({
index: DEFAULT_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
? {
upload: await minioClient.presignedPutObject(
DEFAULT_BUCKET!,
`${basePath}${body.file ?? fileName}`,
),
}
: this.setStatus(HttpStatusCode.NO_CONTENT);
}
@Delete("/{fileName}")
@Tags("SubFolder File")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK)
@Tags("ไฟล์")
@Security("bearerAuth", ["admin"])
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
public async deleteFile(
@Path() cabinetName: string,
@Path() drawerName: string,
@ -254,42 +259,16 @@ export class SubFolderFileController extends Controller {
@Path() subFolderName: string,
@Path() fileName: string,
) {
const search = await esClient.search<
EhrFile & {
attachment: Record<string, string>;
}
>({
index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index',
query: {
match: {
pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${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.");
await minioClient.removeObject(
"ehr",
`${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`,
DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/${folderName}/${fileName}/${subFolderName}/`,
);
return this.setStatus(HttpStatusCode.NO_CONTENT);
}
@Get("/{fileName}")
@Tags("File")
@Tags("ดาวน์โหลด")
@Security("bearerAuth")
@SuccessResponse(HttpStatusCode.OK)
public async downloadFile(
@Path() cabinetName: string,
@ -298,8 +277,8 @@ export class SubFolderFileController extends Controller {
@Path() subFolderName: string,
@Path() fileName: string,
) {
const search = await esClient.search<EhrFile & { attachment: Record<string, string> }>({
index: process.env.ELASTICSEARCH_INDEX ?? 'ehr-index',
const search = await esClient.search<StorageFile & { attachment: Record<string, string> }>({
index: DEFAULT_INDEX!,
query: {
match: {
pathname: `${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`,
@ -311,19 +290,13 @@ export class SubFolderFileController extends Controller {
throw new HttpError(HttpStatusCode.NOT_FOUND, "Not found");
}
const data = search.hits.hits[0]._source;
if (!data) {
throw new HttpError(HttpStatusCode.INTERNAL_SERVER_ERROR, "Found data but no info.");
}
const { attachment, ...rest } = data;
const { attachment, ...rest } = search.hits.hits[0]._source!;
return {
...rest,
download: await minioClient.presignedGetObject(
"ehr",
`${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`,
DEFAULT_BUCKET!,
`${cabinetName}/${drawerName}/${folderName}/${fileName}`,
),
};
}

View file

@ -1,4 +1,4 @@
export interface EhrFolder {
export interface StorageFolder {
/**
* @prop Full path to this folder. It is used as key as there are no files or directories at the same location.
*/
@ -12,7 +12,7 @@ export interface EhrFolder {
createdBy: string | Date;
}
export interface EhrFile {
export interface StorageFile {
/**
* @prop Full path to this folder. It is used as key as there are no files or directories at the same location.
*/
@ -27,6 +27,11 @@ export interface EhrFile {
category: string[];
keyword: string[];
/**
* @private For internal use only.
*/
upload: boolean;
updatedAt: string | Date;
updatedBy: string;
createdAt: string | Date;

View file

@ -0,0 +1,149 @@
import { StorageFile } from "../interfaces/storage-fs";
import esClient from "../elasticsearch";
import minioClient from "../minio";
const DEFAULT_INDEX = process.env.ELASTICSEARCH_INDEX;
if (!DEFAULT_INDEX) throw Error("Default ElasticSearch index must be specified.");
// for failed queue that will come later
const cachedBuffer: Record<string, Buffer> = {};
const cachedMetadata: Record<string, { size: number; type: string }> = {};
export async function handler(key: string, event: string): Promise<boolean> {
console.info(`[AMQ] Messages received - key: ${key}, event: ${event}`);
const [bucket, ...fragment] = key.split("/");
const pathname = fragment.join("/");
if (event === "s3:ObjectRemoved:Delete") {
return await ensureDelete(pathname);
}
if (!cachedBuffer[key]) {
const stream = await minioClient.getObject(bucket, pathname);
const buffer = Buffer.concat(await stream.toArray());
cachedBuffer[key] = buffer;
}
if (!cachedMetadata[key]) {
const stat = await minioClient.statObject(bucket, pathname);
cachedMetadata[key] = { size: stat.size, type: stat.metaData["content-type"] };
}
const rec = await popInfo(pathname);
const result = rec
? await handleFoundRecord(rec, cachedBuffer[key], cachedMetadata[key])
: await handleNotFoundRecord(pathname, cachedBuffer[key], cachedMetadata[key]);
if (result) {
delete cachedBuffer[key];
delete cachedMetadata[key];
}
return result;
}
// Get info and delete it from ElasticSearch to re-index
async function popInfo(pathname: string) {
const result = await esClient
.search<StorageFile & { attachment?: Record<string, unknown> }>({
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;
}
if (result && result.hits.hits.length === 0) console.info("Index Not Found");
return false;
}
/**
* If there is data in database then delete it
*/
async function ensureDelete(pathname: string) {
await esClient
.deleteByQuery({
index: DEFAULT_INDEX!,
query: { match: { pathname } },
conflicts: "proceed",
})
.catch((e) => console.error(e));
return true;
}
/**
* Handle when record in elasticsearch cannot be found.
* This will insert empty metadata.
*/
async function handleNotFoundRecord(
pathname: string,
buffer: Buffer,
stat: { size: number; type: string },
) {
const filename = pathname.split("/").at(-1);
const base64 = Buffer.from(buffer).toString("base64");
const metadata = {
pathname,
fileName: filename ?? "n/a", // should not possible to fallback, but just in case.
fileSize: stat.size,
fileType: stat.type,
title: "",
description: "",
category: [],
keyword: [],
upload: true,
createdAt: new Date().toISOString(),
createdBy: "n/a",
updatedAt: new Date().toISOString(),
updatedBy: "n/a",
} satisfies Partial<StorageFile>;
const result = await esClient
.index({
pipeline: "attachment",
index: DEFAULT_INDEX!,
document: { data: base64, ...metadata },
})
.catch((e) => console.error(e));
if (result) return true;
return false;
}
async function handleFoundRecord(
metadata: StorageFile,
buffer: Buffer,
stat: { size: number; type: string },
) {
metadata.fileSize = stat.size;
metadata.fileType = stat.type;
metadata.upload = true;
const result = await esClient
.index({
pipeline: "attachment",
index: DEFAULT_INDEX!,
document: { data: Buffer.from(buffer).toString("base64"), ...metadata },
})
.catch((e) => console.error(e));
if (result) return true;
return false;
}

View file

@ -0,0 +1,40 @@
import amqp from "amqplib";
export async function init(cb: (key: string, event: string) => boolean | Promise<boolean>) {
if (!process.env.AMQ_URL || !process.env.AMQ_QUEUE) return;
const { AMQ_URL: url, AMQ_QUEUE: queue } = process.env;
const connection = await amqp.connect(url);
const channel = await connection.createChannel();
channel.assertQueue(queue, { durable: true });
channel.prefetch(1);
console.log("[AMQ] Listening for message...");
channel.consume(
queue,
async (msg) => {
if (!msg) return;
const parsed: Record<string, unknown> = JSON.parse(msg.content.toString());
if (typeof parsed.Key !== "string" || parsed.Key.includes(".keep")) return channel.ack(msg);
if (typeof parsed.EventName !== "string" || parsed.EventName.includes("Copy")) {
return channel.ack(msg);
}
const key = parsed.Key;
const event = parsed.EventName;
if (await cb(key, event)) return channel.ack(msg);
return await new Promise((resolve) => setTimeout(() => resolve(channel.nack(msg)), 3000));
},
{ noAck: false },
);
}
export default { init };

View file

@ -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';
@ -20,13 +22,11 @@ import { expressAuthentication } from './utils/auth';
// @ts-ignore - no great way to install types from subpackage
const promiseAny = require('promise.any');
import type { RequestHandler, Router } from 'express';
const multer = require('multer');
const upload = multer();
// 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 models: TsoaRoute.Models = {
"EhrFolder": {
"StorageFolder": {
"dataType": "refObject",
"properties": {
"pathname": {"dataType":"string","required":true},
@ -37,7 +37,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
"EhrFile": {
"StorageFile": {
"dataType": "refObject",
"properties": {
"pathname": {"dataType":"string","required":true},
@ -48,6 +48,7 @@ const models: TsoaRoute.Models = {
"description": {"dataType":"string","required":true},
"category": {"dataType":"array","array":{"dataType":"string"},"required":true},
"keyword": {"dataType":"array","array":{"dataType":"string"},"required":true},
"upload": {"dataType":"boolean","required":true},
"updatedAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true},
"updatedBy": {"dataType":"string","required":true},
"createdAt": {"dataType":"union","subSchemas":[{"dataType":"string"},{"dataType":"datetime"}],"required":true},
@ -76,6 +77,7 @@ export function RegisterRoutes(app: Router) {
// Please look into the "controllerPathGlobs" config option described in the readme: https://github.com/lukeautry/tsoa
// ###########################################################################################################
app.get('/cabinet',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.listCabinet)),
@ -100,7 +102,7 @@ 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('/cabinet',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.createCabinet)),
@ -127,7 +129,7 @@ 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('/cabinet/:cabinetName',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.editCabinet)),
@ -154,7 +156,7 @@ 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.delete('/cabinet/:cabinetName',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(CabinetController)),
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.deleteCabinet)),
@ -180,6 +182,7 @@ 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.get('/cabinet/:cabinetName/drawer',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.listDrawer)),
@ -205,7 +208,7 @@ 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('/cabinet/:cabinetName/drawer',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.createDrawer)),
@ -233,7 +236,7 @@ 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('/cabinet/:cabinetName/drawer/:drawerName',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.editDrawer)),
@ -261,7 +264,7 @@ 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.delete('/cabinet/:cabinetName/drawer/:drawerName',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(DrawerController)),
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.deleteDrawer)),
@ -287,42 +290,8 @@ 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file',
authenticateMiddleware([{"bearerAuth":[]}]),
upload.single('file'),
...(fetchMiddlewares<RequestHandler>(FileController)),
...(fetchMiddlewares<RequestHandler>(FileController.prototype.uploadFile)),
function FileController_uploadFile(request: any, response: any, next: any) {
const args = {
request: {"in":"request","name":"request","required":true,"dataType":"object"},
file: {"in":"formData","name":"file","required":true,"dataType":"file"},
title: {"in":"formData","name":"title","required":true,"dataType":"string"},
description: {"in":"formData","name":"description","required":true,"dataType":"string"},
keyword: {"in":"formData","name":"keyword","required":true,"dataType":"string"},
category: {"in":"formData","name":"category","required":true,"dataType":"string"},
cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"},
drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"},
folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"},
};
// 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 FileController();
const promise = controller.uploadFile.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 201, 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/file',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(FileController)),
...(fetchMiddlewares<RequestHandler>(FileController.prototype.getFile)),
@ -349,9 +318,38 @@ 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file',
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(FileController)),
...(fetchMiddlewares<RequestHandler>(FileController.prototype.uploadFile)),
function FileController_uploadFile(request: any, response: any, next: any) {
const args = {
request: {"in":"request","name":"request","required":true,"dataType":"object"},
body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string"},"category":{"dataType":"string"},"description":{"dataType":"string"},"title":{"dataType":"string"},"file":{"dataType":"string","required":true}}},
cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"},
drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"},
folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"},
};
// 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 FileController();
const promise = controller.uploadFile.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 201, 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.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName',
authenticateMiddleware([{"bearerAuth":[]}]),
upload.single('file'),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(FileController)),
...(fetchMiddlewares<RequestHandler>(FileController.prototype.updateFile)),
@ -362,11 +360,7 @@ export function RegisterRoutes(app: Router) {
drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"},
folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"},
fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"},
file: {"in":"formData","name":"file","dataType":"file"},
title: {"in":"formData","name":"title","dataType":"string"},
description: {"in":"formData","name":"description","dataType":"string"},
keyword: {"in":"formData","name":"keyword","dataType":"string"},
category: {"in":"formData","name":"category","dataType":"string"},
body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string"},"category":{"dataType":"string"},"description":{"dataType":"string"},"title":{"dataType":"string"},"file":{"dataType":"string"}}},
};
// 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
@ -386,7 +380,7 @@ 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(FileController)),
...(fetchMiddlewares<RequestHandler>(FileController.prototype.deleteFile)),
@ -415,6 +409,7 @@ 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.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(FileController)),
...(fetchMiddlewares<RequestHandler>(FileController.prototype.downloadFile)),
@ -443,6 +438,7 @@ 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.get('/cabinet/:cabinetName/drawer/:drawerName/folder',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.listFolder)),
@ -469,7 +465,7 @@ 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('/cabinet/:cabinetName/drawer/:drawerName/folder',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.createFolder)),
@ -498,7 +494,7 @@ 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.editFolder)),
@ -527,7 +523,7 @@ 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(FolderController)),
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.deleteFolder)),
@ -555,6 +551,7 @@ 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('/search',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(SearchController)),
...(fetchMiddlewares<RequestHandler>(SearchController.prototype.searchFile)),
@ -580,6 +577,7 @@ 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.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.listFolder)),
@ -607,7 +605,7 @@ 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.createFolder)),
@ -637,7 +635,7 @@ 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.editFolder)),
@ -667,7 +665,7 @@ 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderController)),
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.deleteFolder)),
@ -695,43 +693,8 @@ 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file',
authenticateMiddleware([{"bearerAuth":[]}]),
upload.single('file'),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController)),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController.prototype.uploadFile)),
function SubFolderFileController_uploadFile(request: any, response: any, next: any) {
const args = {
request: {"in":"request","name":"request","required":true,"dataType":"object"},
file: {"in":"formData","name":"file","required":true,"dataType":"file"},
title: {"in":"formData","name":"title","required":true,"dataType":"string"},
description: {"in":"formData","name":"description","required":true,"dataType":"string"},
keyword: {"in":"formData","name":"keyword","required":true,"dataType":"string"},
category: {"in":"formData","name":"category","required":true,"dataType":"string"},
cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"},
drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"},
folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"},
subFolderName: {"in":"path","name":"subFolderName","required":true,"dataType":"string"},
};
// 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 SubFolderFileController();
const promise = controller.uploadFile.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 201, 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/:subFolderName/file',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController)),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController.prototype.getFile)),
@ -759,9 +722,39 @@ 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('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file',
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController)),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController.prototype.uploadFile)),
function SubFolderFileController_uploadFile(request: any, response: any, next: any) {
const args = {
request: {"in":"request","name":"request","required":true,"dataType":"object"},
body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string"},"category":{"dataType":"string"},"description":{"dataType":"string"},"title":{"dataType":"string"},"file":{"dataType":"string","required":true}}},
cabinetName: {"in":"path","name":"cabinetName","required":true,"dataType":"string"},
drawerName: {"in":"path","name":"drawerName","required":true,"dataType":"string"},
folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"},
subFolderName: {"in":"path","name":"subFolderName","required":true,"dataType":"string"},
};
// 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 SubFolderFileController();
const promise = controller.uploadFile.apply(controller, validatedArgs as any);
promiseHandler(controller, promise, response, 201, 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.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file/:fileName',
authenticateMiddleware([{"bearerAuth":[]}]),
upload.single('file'),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController)),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController.prototype.updateFile)),
@ -773,11 +766,7 @@ export function RegisterRoutes(app: Router) {
folderName: {"in":"path","name":"folderName","required":true,"dataType":"string"},
subFolderName: {"in":"path","name":"subFolderName","required":true,"dataType":"string"},
fileName: {"in":"path","name":"fileName","required":true,"dataType":"string"},
file: {"in":"formData","name":"file","dataType":"file"},
title: {"in":"formData","name":"title","dataType":"string"},
description: {"in":"formData","name":"description","dataType":"string"},
keyword: {"in":"formData","name":"keyword","dataType":"string"},
category: {"in":"formData","name":"category","dataType":"string"},
body: {"in":"body","name":"body","required":true,"dataType":"nestedObjectLiteral","nestedProperties":{"keyword":{"dataType":"string"},"category":{"dataType":"string"},"description":{"dataType":"string"},"title":{"dataType":"string"},"file":{"dataType":"string"}}},
};
// 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
@ -797,7 +786,7 @@ 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.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file/:fileName',
authenticateMiddleware([{"bearerAuth":[]}]),
authenticateMiddleware([{"bearerAuth":["admin"]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController)),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController.prototype.deleteFile)),
@ -827,6 +816,7 @@ 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.get('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file/:fileName',
authenticateMiddleware([{"bearerAuth":[]}]),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController)),
...(fetchMiddlewares<RequestHandler>(SubFolderFileController.prototype.downloadFile)),

File diff suppressed because it is too large Load diff

View file

@ -14,26 +14,30 @@ const jwtVerify = createVerifier({
},
});
export function expressAuthentication(
export async function expressAuthentication(
request: express.Request,
securityName: string,
_scopes?: string[],
scopes?: string[],
) {
return new Promise(async (resolve, reject) => {
if (securityName !== "bearerAuth") reject(new Error("Unknown authentication method."));
if (process.env.AUTH_BYPASS) return { preferred_username: "bypassed" };
const token = request.headers["authorization"]?.includes("Bearer ")
? request.headers["authorization"].split(" ")[1]
: null;
if (securityName !== "bearerAuth") throw new Error("Unknown authentication method.");
if (!token) return reject(new HttpError(HttpStatusCode.UNAUTHORIZED, "No token provided."));
const token = request.headers["authorization"]?.includes("Bearer ")
? request.headers["authorization"].split(" ")[1]
: null;
const payload = await jwtVerify(token).catch((_) => null);
if (!token) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "No token provided.");
if (!payload) {
return reject(new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided."));
}
const payload = await jwtVerify(token).catch((_) => null);
return resolve(payload);
});
if (!payload) {
throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.");
}
if (scopes && !scopes.some((v) => payload.resource_access[payload.azp].roles.includes(v))) {
throw new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action.");
}
return payload;
}

View file

@ -1,37 +1,27 @@
import { EhrFolder } from "../interfaces/ehr-fs";
import { StorageFolder } from "../interfaces/storage-fs";
import * as Minio from "minio";
import minioClient from "../storage";
import minioClient from "../minio";
/**
* Remove slash at the start and ensure slash at the end of the path
* @param path - path to be check and ensure
* @returns path without / at start and end with trailing slash
*/
function safePath(path: string) {
return path.replace(/^\/|\/$/g, "") + "/";
}
/**
* Replace illegal character eg. ? % < > / \ : | that can't be in path with "-".
* Replace illegal character eg. ? % < > / \ : | that can't be in path with other char with dash by default.
* @param path - string to check and replace
* @returns path with illegal character replaced with "-"
* @param replace - string to replace illegal character
* @returns illegal character replaced path
*/
export function replaceIllegalChars(path: string, replaceChar = "-") {
return path.replace(/[/\\?%*:|"<>]/g, replaceChar);
export function replaceIllegalChars(path: string, replace = "-") {
return path.replace(/[/\\?%*:|"<>]/g, replace);
}
/**
* Utility function to check for .keep file if it is exist or not.
* @returns true if .keep exist, false otherwise
* Check if folder really exist by using ".keep" object.
*/
export async function pathExist(path: string): Promise<boolean> {
return await minioClient
.statObject("ehr", `${safePath(path)}.keep`)
.then((_) => true)
.catch((e) => {
export async function pathExist(bucket: string, path: string): Promise<boolean> {
return Boolean(
await minioClient.statObject(bucket, `${path.replace(/^\/|\/$/g, "")}/.keep`).catch((e) => {
if (e.code === "NotFound") return false;
throw new Error("Object Storage Error");
});
throw new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์");
}),
);
}
/**
@ -39,55 +29,62 @@ export async function pathExist(path: string): Promise<boolean> {
* @param path - path to list
* @return list of folder with metadata
*/
export function listFolder(path?: string): Promise<EhrFolder[]> {
if (path) path = safePath(path);
export async function listFolder(bucket: string, path?: string): Promise<StorageFolder[]> {
if (path) path = `${path.replace(/^\/|\/$/g, "")}/`;
return new Promise((resolve, reject) => {
const folder: EhrFolder[] = [];
const stream = minioClient.listObjectsV2("ehr", path ?? "");
const list = await new Promise<{ pathname: string; name: string }[]>((resolve, reject) => {
const item: { pathname: string; name: string }[] = [];
const stream = minioClient.listObjectsV2(bucket, path ?? "");
stream.on("data", (v) => {
if (!(v && v.prefix)) return;
folder.push({
pathname: v.prefix,
name: v.prefix.slice(path?.length).split("/")[0],
createdAt: "N/A",
createdBy: "N/A",
});
});
stream.on("end", async () => {
for (let i = 0; i < folder.length; i++) {
const stat = await minioClient
.statObject("ehr", `${folder[i].pathname}.keep`)
.catch((e) => console.error(`Error List Folder: ${folder[i].pathname}`, e));
if (!stat) continue;
folder[i] = {
...folder[i],
createdAt: stat.metaData.createdat ?? "N/A",
createdBy: stat.metaData.createdby ?? "N/A",
};
if (v && v.prefix) {
item.push({
pathname: v.prefix,
name: v.prefix.slice(path?.length).split("/")[0],
});
}
resolve(folder);
});
stream.on("error", () => reject(new Error("Object storage error occured.")));
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์")));
});
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(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: (typeof folder)[number]): v is StorageFolder => !!v);
}
export async function listItem(path: string, recursive = false): Promise<Minio.BucketItem[]> {
export async function listItem(
bucket: string,
path: string,
recursive = false,
): Promise<Minio.BucketItem[]> {
return new Promise((resolve, reject) => {
const stream = minioClient.listObjectsV2("ehr", path, recursive);
const stream = minioClient.listObjectsV2(bucket, path, recursive);
const item: Minio.BucketItem[] = [];
stream.on("data", (v) => {
if (v && v.name) item.push(v);
});
stream.on("end", () => resolve(item));
stream.on("error", () => reject(new Error("Object storage error occured.")));
stream.on("error", () => reject(new Error("เกิดข้อผิดพลาดกับระบบจัดการไฟล์")));
});
}
export const copyCond = new Minio.CopyConditions();

View file

@ -0,0 +1,44 @@
import "dotenv/config";
import esClient from "../src/elasticsearch";
import minioClient from "../src/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.");
esClient.ingest.putPipeline({
id: "attachment",
body: {
description: "Extract attachment information",
processors: [
{
attachment: {
field: "data",
},
},
{
remove: {
field: "data",
},
},
],
},
});
esClient.indices.putMapping({
index: DEFAULT_INDEX,
body: {
properties: {
pathname: {
type: "keyword",
},
},
},
});
minioClient.makeBucket(DEFAULT_BUCKET!, (e) => {
if (!e) console.log("Configuration needed for Bucket Notification to AMQP");
console.error(e);
});

View file

@ -25,7 +25,16 @@
"description": "Keycloak Bearer Token",
"in": "header"
}
}
},
"tags": [
{ "name": "ตู้เอกสาร" },
{ "name": "ลิ้นชัก" },
{ "name": "แฟ้ม" },
{ "name": "แฟ้มย่อย" },
{ "name": "ไฟล์" },
{ "name": "ดาวน์โหลด" },
{ "name": "ค้นหา" }
]
},
"routes": {
"routesDir": "src",