From 896ab4482f16db957bc3b32da6f311eb664a7616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Bl=C3=A4sta?= Date: Wed, 18 Dec 2024 11:08:09 +0100 Subject: [PATCH 1/2] feature: backend implementing attachment server interface --- conf/config.js | 3 + handlers/attachment/attachments.js | 118 ++++++++++++++++++++++++ handlers/attachment/index.js | 56 ++++++++++++ package-lock.json | 142 ++++++++++++++++++++++------- package.json | 2 + routes/index.js | 2 + 6 files changed, 292 insertions(+), 31 deletions(-) create mode 100644 handlers/attachment/attachments.js create mode 100644 handlers/attachment/index.js diff --git a/conf/config.js b/conf/config.js index 6fc6e70..837511d 100644 --- a/conf/config.js +++ b/conf/config.js @@ -148,5 +148,8 @@ module.exports = { grant_type: 'client_credentials', scope: 'am_application_scope default', query: '{"feature.typ": {"eq": "detaljplan"}, "detaljplan.objektidentitet": {"eq": "$planid$"}, "detaljplan.status": {"in": ["laga kraft"]}}' + }, + attachment: { + filepath: "C:\\attachment\\" } } diff --git a/handlers/attachment/attachments.js b/handlers/attachment/attachments.js new file mode 100644 index 0000000..7ee2a9b --- /dev/null +++ b/handlers/attachment/attachments.js @@ -0,0 +1,118 @@ +var conf = require('../../conf/config'); +var ex = require('express'); +const fs = require('fs'); +const path = require('path'); +const mime = require('mime-types'); +const getUuid = require('uuid-by-string'); + +let configOptions = {}; +const router = ex.Router(); + +if (conf['attachment']) { + configOptions = Object.assign({}, conf['attachment']); +} + +/** + * List all attachments belonging to this object and group + * + * @function + * @name listAttachments + * @kind function + * @param {any} req + * @param {any} res + * @param {any} next + * @returns {Promise} + */ +function listAttachments(req, res, next) { + const dir = path.join(configOptions.filepath, req.params.layer, req.params.object); + let fileInfos = []; + // Check directory for files + if (!fs.existsSync(dir)) { + // No files found return empty + res.json({ "attachmentInfos": fileInfos }); + } else { + const groups = fs.readdirSync(dir); + groups.forEach(group => { + const groupPath = path.join(dir, group); + const fileNames = fs.readdirSync(groupPath); + // Go through the files in directory + fileNames.forEach(filename => { + const filePath = path.join(groupPath, filename); + const fileInfo = { + "id": getUuid(`${group}_${filename}`, 5), // Create a stable uuid for the file from the group and filename, uuid version 5 + "contentType": mime.lookup(filename), + "size": fs.statSync(filePath).size, + "name": filename, + "group": group + }; + fileInfos.push(fileInfo); + }); + }); + res.json({ "attachmentInfos": fileInfos }); + } +} + +/** + * Get the the document with id from a specific object and group + * + * @function + * @name fetchDoc + * @kind function + * @param {any} req + * @param {any} res + * @param {any} next + * @returns {Promise} + */ +function fetchDoc(req, res, next) { + const dir = path.join(configOptions.filepath, req.params.layer, req.params.object); + const groups = fs.readdirSync(dir); + groups.forEach(group => { + const groupPath = path.join(dir, group); + const fileNames = fs.readdirSync(groupPath); + + fileNames.forEach(filename => { + if (getUuid(`${group}_${filename}`, 5) === req.params.id) { + const filePath = path.join(dir, group, filename); + res.sendFile(filePath); + } + }); + }); +} + +/** + * Delete a attachment file identified by uuid + * + * @function + * @name deleteAttachment + * @kind function + * @param {any} req + * @param {any} res + * @param {any} next + * @returns {void} + */ +function deleteAttachment(req, res, next) { + const result = { "deleteAttachmentResults": [] }; + const idsToDelete = req.body.attachmentIds.split(','); + const dir = path.join(configOptions.filepath, req.params.layer, req.params.object); + + if (fs.existsSync(dir)) { + const groups = fs.readdirSync(dir); + groups.forEach(group => { + const fileNames = fs.readdirSync(path.join(dir, group)); + fileNames.forEach(filename => { + if (idsToDelete.includes(getUuid(`${group}_${filename}`, 5))) { + const filePath = path.join(dir, group, filename); + fs.rmSync(filePath, { recursive: true }); + result.deleteAttachmentResults.push({ + "objectId": getUuid(`${group}_${filename}`, 5), // Create a stable uuid for the file from the group and filename, uuid version 5 + "globalId": null, + "success": true + }); + } + }); + }); + } + res.json(result); +} + +module.exports = { listAttachments, fetchDoc, deleteAttachment }; \ No newline at end of file diff --git a/handlers/attachment/index.js b/handlers/attachment/index.js new file mode 100644 index 0000000..067627c --- /dev/null +++ b/handlers/attachment/index.js @@ -0,0 +1,56 @@ +var conf = require('../../conf/config'); +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const { listAttachments, fetchDoc, deleteAttachment } = require('./attachments'); +const multer = require('multer'); +const getUuid = require('uuid-by-string'); +const attachmentRouter = express.Router(); + +// Let the upload middleware buffer the file in memory +const storage = multer.memoryStorage(); +const upload = multer({ storage: storage }); + +let configOptions = {}; + +if (conf['attachment']) { + configOptions = Object.assign({}, conf['attachment']); +} + +attachmentRouter.get('/:layer/:object/attachments/', listAttachments); +attachmentRouter.get('/:layer/:object/attachments/:id', fetchDoc); +attachmentRouter.post('/:layer/:object/deleteAttachments/', deleteAttachment); +attachmentRouter.post('/:layer/:object/addAttachment', upload.single('attachment'), function (req, res, next) { + const dir = path.join(configOptions.filepath, req.params.layer, req.params.object, req.body.group); + // Create directory if doesn't already exists + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + if (typeof req.file !== 'undefined') { + // Get the filename and convert to utf8 from latin1 + const fileName = path.join(dir, Buffer.from(req.file.originalname, 'latin1').toString('utf8')); + // Write the file to disc + fs.writeFileSync(fileName, req.file.buffer); + // Create response object to send back after successful saved + const retval = { + "addAttachmentResult": { + "objectId": getUuid(`${req.body.group}_${fileName}`, 5), + "globalId": null, + "success": true + } + } + res.json(retval); + } else { + // Something went wrong with posting the file + const retval = { + "addAttachmentResult": { + "objectId": null, + "globalId": null, + "success": false + } + } + res.json(retval); + } + }); + +module.exports = attachmentRouter; diff --git a/package-lock.json b/package-lock.json index 770e692..d9831f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "express-rate-limit": "^7.4.1", "http-proxy": "^1.18.1", "iconv-lite": "^0.6.3", + "multer": "^1.4.5-lts.1", "node-fetch": "^3.3.2", "node-gyp": "^10.2.0", "openid-client": "^5.6.2", @@ -34,6 +35,7 @@ "serve-favicon": "^2.5.0", "sqlite3": "^5.1.7", "utf8": "^3.0.0", + "uuid-by-string": "^4.0.0", "wkt": "^0.1.1", "xml2js": "^0.6.2", "xmlbuilder": "^15.1.1" @@ -569,6 +571,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -612,11 +620,6 @@ "node": ">= 6" } }, - "node_modules/archiver-utils/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "node_modules/archiver-utils/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -1451,11 +1454,6 @@ "readable-stream": "^2.0.2" } }, - "node_modules/duplexer2/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "node_modules/duplexer2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -2691,6 +2689,12 @@ "node": ">=8" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", @@ -2736,6 +2740,18 @@ "license": "MIT", "optional": true }, + "node_modules/js-md5": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.7.3.tgz", + "integrity": "sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ==", + "license": "MIT" + }, + "node_modules/js-sha1": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/js-sha1/-/js-sha1-0.6.0.tgz", + "integrity": "sha512-01gwBFreYydzmU9BmZxpVk6svJJHrVxEN3IOiGl6VO93bVKYETJ0sIth6DASI6mIFdt7NmfX9UiByRzsYHGU9w==", + "license": "MIT" + }, "node_modules/jsbn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", @@ -2850,11 +2866,6 @@ "setimmediate": "^1.0.5" } }, - "node_modules/jszip/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "node_modules/jszip/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -2916,11 +2927,6 @@ "node": ">= 0.6.3" } }, - "node_modules/lazystream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "node_modules/lazystream/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -3362,6 +3368,81 @@ "node": ">=18" } }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/multer/node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/multer/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/multer/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/multer/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/multer/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/mysql": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz", @@ -3377,12 +3458,6 @@ "node": ">= 0.6" } }, - "node_modules/mysql/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "optional": true - }, "node_modules/mysql/node_modules/readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -5660,11 +5735,6 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==" }, - "node_modules/unzipper/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, "node_modules/unzipper/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -5726,6 +5796,16 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uuid-by-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/uuid-by-string/-/uuid-by-string-4.0.0.tgz", + "integrity": "sha512-88ZSfcSkN04juiLqSsuyteqlSrXNFdsEPzSv3urnElDXNsZUXQN0smeTnh99x2DE15SCUQNgqKBfro54CuzHNQ==", + "license": "MIT", + "dependencies": { + "js-md5": "^0.7.3", + "js-sha1": "^0.6.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index d990db8..de52483 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "express-rate-limit": "^7.4.1", "http-proxy": "^1.18.1", "iconv-lite": "^0.6.3", + "multer": "^1.4.5-lts.1", "node-fetch": "^3.3.2", "node-gyp": "^10.2.0", "openid-client": "^5.6.2", @@ -37,6 +38,7 @@ "serve-favicon": "^2.5.0", "sqlite3": "^5.1.7", "utf8": "^3.0.0", + "uuid-by-string": "^4.0.0", "wkt": "^0.1.1", "xml2js": "^0.6.2", "xmlbuilder": "^15.1.1" diff --git a/routes/index.js b/routes/index.js index e6d0983..a5c1db5 100644 --- a/routes/index.js +++ b/routes/index.js @@ -25,6 +25,7 @@ var lmBuilding = require('../handlers/lmbuilding'); var auth = require('../handlers/auth'); var clients = require('../handlers/clients'); var ngp = require('../handlers/ngp'); +var attachment = require('../handlers/attachment'); /* GET start page. */ router.get('/', function (req, res) { @@ -55,5 +56,6 @@ router.all('/converttogeojson/*', convertToGeojson); router.use('/auth', auth); router.use('/clients', clients); router.use('/ngp', ngp); +router.use('/attachment', attachment); module.exports = router; From db7ddbcd52ac2a12f9a7926d7fe0309985e40ce0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Bl=C3=A4sta?= Date: Wed, 18 Dec 2024 11:19:18 +0100 Subject: [PATCH 2/2] doc of attachment --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index ce204b8..b37058a 100644 --- a/README.md +++ b/README.md @@ -156,3 +156,15 @@ Configured services at: /origoserver/attachments/ngp/dpdocuments/something-like-the-layername/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/attachments /origoserver/attachments/ngp/dpdocuments/something-like-the-layername/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/attachments/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +- Attachment - list, get, add and delete files to features + + Configure attachment in conf/config.js + + /origoserver/attachment/{something-like-the-layername}/{a unique identifier}/attachments + + /origoserver/attachment/{something-like-the-layername}/{a unique identifier}/attachments/{uuid} + + /origoserver/attachment/{something-like-the-layername}/{a unique identifier}/addAttachment + + /origoserver/attachment/{something-like-the-layername}/{a unique identifier}/deleteAttachments