Merge branch 'dev/methapon'
This commit is contained in:
commit
20764b1e3d
22 changed files with 2448 additions and 1495 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
131
Services/server/pnpm-lock.yaml
generated
131
Services/server/pnpm-lock.yaml
generated
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
104
Services/server/src/controllers/storageController.ts
Normal file
104
Services/server/src/controllers/storageController.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
149
Services/server/src/rabbitmq/handler.ts
Normal file
149
Services/server/src/rabbitmq/handler.ts
Normal 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;
|
||||
}
|
||||
40
Services/server/src/rabbitmq/index.ts
Normal file
40
Services/server/src/rabbitmq/index.ts
Normal 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 };
|
||||
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
44
Services/server/tools/prepare.ts
Normal file
44
Services/server/tools/prepare.ts
Normal 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);
|
||||
});
|
||||
|
|
@ -25,7 +25,16 @@
|
|||
"description": "Keycloak Bearer Token",
|
||||
"in": "header"
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
{ "name": "ตู้เอกสาร" },
|
||||
{ "name": "ลิ้นชัก" },
|
||||
{ "name": "แฟ้ม" },
|
||||
{ "name": "แฟ้มย่อย" },
|
||||
{ "name": "ไฟล์" },
|
||||
{ "name": "ดาวน์โหลด" },
|
||||
{ "name": "ค้นหา" }
|
||||
]
|
||||
},
|
||||
"routes": {
|
||||
"routesDir": "src",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue