Browse Source

Added image uploading and image compression

Signed-off-by: Dušan Mitrović <dusan@dusanmitrovic.xyz>
master
Dušan Mitrović 3 months ago
parent
commit
c29333e7c7
Signed by: dusan GPG Key ID: 8E81D1BFCE8427E5
12 changed files with 595 additions and 56 deletions
  1. +2
    -0
      app.js
  2. +52
    -0
      config/multer.js
  3. +24
    -0
      middleware/multer-handler.js
  4. +322
    -47
      package-lock.json
  5. +14
    -7
      package.json
  6. +70
    -0
      routes/images.js
  7. +52
    -0
      services/image-conversion-service.js
  8. +34
    -0
      static/css/image-upload.css
  9. +2
    -0
      static/images/uploads/.gitignore
  10. +0
    -0
     
  11. +2
    -2
      static/js/prism.js
  12. +21
    -0
      views/image-upload.hbs

+ 2
- 0
app.js View File

@@ -33,6 +33,7 @@ const blog = require('./routes/blog');
const contact = require('./routes/contact');
const info = require('./routes/info');
const rss = require('./routes/rss');
const images = require('./routes/images');

const app = express();

@@ -74,6 +75,7 @@ app.disable('x-powered-by');
// Routing
app.use('/', home);
app.use('/admin', login);
app.use('/admin', images);
app.use('/blog', blog);
app.use('/contact', contact);
app.use('/info', info);


+ 52
- 0
config/multer.js View File

@@ -0,0 +1,52 @@
/**
* @author Dusan Mitrovic <dusan@dusanmitrovic.xyz>
* @license AGPL-3.0-or-later https://opensource.org/licenses/AGPL-3.0
*
* @summary Configuration for multer
*/
const multer = require('multer');
const path = require('path');
const { v4: uuidv4 } = require('uuid');

/**
* Filter allowed files by size and type
*
* @param {Express.Request} req
* @param {Express.Multer.File} file
* @param {function} callback
*/
const fileFilter = (req, file, callback) => {
const allowedFileTypes = [
'image/jpg',
'image/jpeg',
'image/png'
];

if (file.size > 1024 * 1024 * 8) {
callback(new Error('File size must be less than 8MiB.'), false);
return;
}

if (!allowedFileTypes.includes(file.mimetype)) {
callback(new Error('File must be an image.'), false);
return;
}

callback(null, true);
};

const storage = multer.diskStorage({
destination: (req, file, callback) => {
callback(null, '/tmp');
},
filename(req, file, callback) {
callback(null, uuidv4() + path.extname(file.originalname));
},
});

const uploader = multer({
storage,
fileFilter,
});

module.exports = uploader;

+ 24
- 0
middleware/multer-handler.js View File

@@ -0,0 +1,24 @@
/**
* @author Dusan Mitrovic <dusan@dusanmitrovic.xyz>
* @license AGPL-3.0-or-later https://opensource.org/licenses/AGPL-3.0
*
* @summary Handler for multer errors
*/
const multerHandler = (error, req, res, next) => {
if (error) {
return res.status(422).render('image-upload', {
title: 'Upload an image',
css: [
'/static/css/image-upload.css',
'/static/css/form.css',
],
error: {
message: error.message,
},
});
}

next();
};

module.exports = multerHandler;

+ 322
- 47
package-lock.json View File

@@ -32,6 +32,91 @@
"defer-to-connect": "^1.0.1"
}
},
"@types/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==",
"dev": true,
"requires": {
"@types/connect": "*",
"@types/node": "*"
}
},
"@types/connect": {
"version": "3.4.34",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz",
"integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/express": {
"version": "4.17.9",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.9.tgz",
"integrity": "sha512-SDzEIZInC4sivGIFY4Sz1GG6J9UObPwCInYJjko2jzOf/Imx/dlpume6Xxwj1ORL82tBbmN4cPDIDkLbWHk9hw==",
"dev": true,
"requires": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "*",
"@types/qs": "*",
"@types/serve-static": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.17.17",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.17.tgz",
"integrity": "sha512-YYlVaCni5dnHc+bLZfY908IG1+x5xuibKZMGv8srKkvtul3wUuanYvpIj9GXXoWkQbaAdR+kgX46IETKUALWNQ==",
"dev": true,
"requires": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*"
}
},
"@types/mime": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz",
"integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==",
"dev": true
},
"@types/multer": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.5.tgz",
"integrity": "sha512-9b/0a8JyrR0r2nQhL73JR86obWL7cogfX12augvlrvcpciCo/hkvEsgu80Z4S2g2DHGVXHr8pUIi1VhqFJ8Ufw==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/node": {
"version": "14.14.20",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz",
"integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==",
"dev": true
},
"@types/qs": {
"version": "6.9.5",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.5.tgz",
"integrity": "sha512-/JHkVHtx/REVG0VVToGRGH2+23hsYLHdyG+GrvoUGlGAd0ErauXDyvHtRI/7H7mzLm+tBCKA7pfcpkQ1lf58iQ==",
"dev": true
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
"integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
"dev": true
},
"@types/serve-static": {
"version": "1.13.8",
"resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz",
"integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==",
"dev": true,
"requires": {
"@types/mime": "*",
"@types/node": "*"
}
},
"abab": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz",
@@ -128,6 +213,11 @@
"picomatch": "^2.0.4"
}
},
"append-field": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
"integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY="
},
"aproba": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
@@ -177,6 +267,16 @@
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
},
"array-parallel": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/array-parallel/-/array-parallel-0.1.3.tgz",
"integrity": "sha1-j3hTCJJu1apHjEfmTRszS2wMlH0="
},
"array-series": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/array-series/-/array-series-0.1.5.tgz",
"integrity": "sha1-3103v8XC7wdV4qpPkv6ufUtaly8="
},
"array-slice": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz",
@@ -383,6 +483,43 @@
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz",
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow=="
},
"buffer-from": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
"busboy": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz",
"integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=",
"requires": {
"dicer": "0.2.5",
"readable-stream": "1.1.x"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
@@ -475,14 +612,14 @@
}
},
"chokidar": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.4.3.tgz",
"integrity": "sha512-DtM3g7juCXQxFVSNPNByEC2+NImtBuxQQvWlHunpJIS5Ocr0lG306cC7FCi7cEA0fzmybPUIl4txBIobk1gGOQ==",
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.0.tgz",
"integrity": "sha512-JgQM9JS92ZbFR4P90EvmzNpSGhpPBGBSj10PILeDyYFwp4h2/D9OM03wsJ4zW1fEp4ka2DGrnUeD7FuvQ2aZ2Q==",
"dev": true,
"requires": {
"anymatch": "~3.1.1",
"braces": "~3.0.2",
"fsevents": "~2.1.2",
"fsevents": "~2.3.1",
"glob-parent": "~5.1.0",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
@@ -615,9 +752,9 @@
}
},
"commander": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz",
"integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="
},
"component-emitter": {
"version": "1.3.0",
@@ -629,6 +766,17 @@
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"concat-stream": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
"integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
"requires": {
"buffer-from": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^2.2.2",
"typedarray": "^0.0.6"
}
},
"configstore": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz",
@@ -686,6 +834,31 @@
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"cross-spawn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
"integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=",
"requires": {
"lru-cache": "^4.0.1",
"which": "^1.2.9"
},
"dependencies": {
"lru-cache": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
"requires": {
"pseudomap": "^1.0.2",
"yallist": "^2.1.2"
}
},
"yallist": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
}
}
},
"crypto-random-string": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
@@ -850,6 +1023,38 @@
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
},
"dicer": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz",
"integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=",
"requires": {
"readable-stream": "1.1.x",
"streamsearch": "0.1.2"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"readable-stream": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
"integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.1",
"isarray": "0.0.1",
"string_decoder": "~0.10.x"
}
},
"string_decoder": {
"version": "0.10.31",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
"integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
}
}
},
"domexception": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
@@ -866,9 +1071,9 @@
}
},
"dompurify": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.3.tgz",
"integrity": "sha512-8Hv7Q0FuwD9rWoB6qI2eZsfKbGXfoUVuGHHrE15vgk4ReOKwOkSgbqb2OMFtc0d5besOEkoLkcyuV10zQ2X5gw=="
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.2.6.tgz",
"integrity": "sha512-7b7ZArhhH0SP6W2R9cqK6RjaU82FZ2UPM7RO8qN1b1wyvC/NY1FNWcX1Pu00fFOAnzEORtwXe4bPaClg6pUybQ=="
},
"dot-prop": {
"version": "5.3.0",
@@ -1128,12 +1333,12 @@
}
},
"express-validator": {
"version": "6.8.0",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.8.0.tgz",
"integrity": "sha512-zEHxjly2Rx0vzJOgWJBCTk1vNNwxqp0a8S8WtKaW912oTmnQGSdh/XuuNzkt+tRBgw66z9u+ah+Sv8SH5SJyUQ==",
"version": "6.9.2",
"resolved": "https://registry.npmjs.org/express-validator/-/express-validator-6.9.2.tgz",
"integrity": "sha512-Yqlsw2/uBobtBVkP+gnds8OMmVAEb3uTI4uXC93l0Ym5JGHgr8Vd4ws7oSo7GGYpWn5YCq4UePMEppKchURXrw==",
"requires": {
"lodash": "^4.17.20",
"validator": "^13.5.1"
"validator": "^13.5.2"
}
},
"extend": {
@@ -1385,9 +1590,9 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz",
"integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz",
"integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==",
"dev": true,
"optional": true
},
@@ -1502,12 +1707,12 @@
}
},
"global-dirs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz",
"integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz",
"integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==",
"dev": true,
"requires": {
"ini": "^1.3.5"
"ini": "1.3.7"
}
},
"global-modules": {
@@ -1532,6 +1737,17 @@
"which": "^1.2.14"
}
},
"gm": {
"version": "1.23.1",
"resolved": "https://registry.npmjs.org/gm/-/gm-1.23.1.tgz",
"integrity": "sha1-Lt7rlYCE0PjqeYjl2ZWxx9/BR3c=",
"requires": {
"array-parallel": "~0.1.3",
"array-series": "~0.1.5",
"cross-spawn": "^4.0.0",
"debug": "^3.1.0"
}
},
"got": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz",
@@ -1702,6 +1918,14 @@
"minimatch": "^3.0.4"
}
},
"image-size": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.9.3.tgz",
"integrity": "sha512-5SakFa79uhUVSjKeQE30GVzzLJ0QNzB53+I+/VD1vIesD6GP6uatWIlgU0uisFNLt1u0d6kBydp7yfk+lLJhLQ==",
"requires": {
"queue": "6.0.1"
}
},
"import-lazy": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
@@ -2065,30 +2289,30 @@
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
},
"knex": {
"version": "0.21.12",
"resolved": "https://registry.npmjs.org/knex/-/knex-0.21.12.tgz",
"integrity": "sha512-AEyyiTM9p/x/Pb38TPZkvphKPmn8UWxP7MdIphzjAOielOfFFeU6pjP6y3M7UJ7rxrQsCrAYHwdonLQ3l1JCDw==",
"version": "0.21.15",
"resolved": "https://registry.npmjs.org/knex/-/knex-0.21.15.tgz",
"integrity": "sha512-STHnPIIkExZVz0X3zIDaWC/q9EcTAfuuRE5Rev7NpOhF1QVh24K5iT2FXD0nWoJ1BUSeDs5QdgdTaz9H8oEtfg==",
"requires": {
"colorette": "1.2.1",
"commander": "^5.1.0",
"debug": "4.1.1",
"commander": "^6.2.0",
"debug": "4.3.1",
"esm": "^3.2.25",
"getopts": "2.2.5",
"interpret": "^2.2.0",
"liftoff": "3.1.0",
"lodash": "^4.17.20",
"pg-connection-string": "2.3.0",
"pg-connection-string": "2.4.0",
"tarn": "^3.0.1",
"tildify": "2.0.0",
"v8flags": "^3.2.0"
},
"dependencies": {
"debug": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
"requires": {
"ms": "^2.1.1"
"ms": "2.1.2"
}
}
}
@@ -2201,9 +2425,9 @@
}
},
"marked": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-1.2.6.tgz",
"integrity": "sha512-7vVuSEZ8g/HH3hK/BH/+7u/NJj7x9VY4EHzujLDcqAQLiOUeFJYAsfSAyoWtR17lKrx7b08qyIno4lffwrzTaA=="
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/marked/-/marked-1.2.7.tgz",
"integrity": "sha512-No11hFYcXr/zkBvL6qFmAp1z6BKY3zqLMHny/JN/ey+al7qwCM2+CMBL9BOgqMxZU36fz4cCWfn2poWIf7QRXA=="
},
"media-typer": {
"version": "0.3.0",
@@ -2339,6 +2563,14 @@
}
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"moment": {
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
@@ -2349,6 +2581,21 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multer": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz",
"integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==",
"requires": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
"concat-stream": "^1.5.2",
"mkdirp": "^0.5.1",
"object-assign": "^4.1.1",
"on-finished": "^2.3.0",
"type-is": "^1.6.4",
"xtend": "^4.0.0"
}
},
"mysql2": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-2.2.5.tgz",
@@ -2483,9 +2730,9 @@
"integrity": "sha512-89ps+SBGpo0D4Bi5ZrxcrCiRFaMmkCt+gItMXQGzEtZVR3uAD3QAQIDoxTWnx3ky0Dwwy/dhFrQ+6NNGXpw/qQ=="
},
"nodemon": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.6.tgz",
"integrity": "sha512-4I3YDSKXg6ltYpcnZeHompqac4E6JeAMpGm8tJnB9Y3T0ehasLa4139dJOcCrB93HHrUMsCrKtoAlXTqT5n4AQ==",
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.7.tgz",
"integrity": "sha512-XHzK69Awgnec9UzHr1kc8EomQh4sjTQ8oRf8TsGrSmHDx9/UmiGG9E/mM3BuTfNeFwdNBvrqQq/RHL0xIeyFOA==",
"dev": true,
"requires": {
"chokidar": "^3.2.2",
@@ -2640,11 +2887,11 @@
}
},
"objection": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/objection/-/objection-2.2.3.tgz",
"integrity": "sha512-uNya9GuHlNeix7H0URthVE3+CmAlXmxkU69LAcRnncLjujJ8l1YX8JCB2GVSErTYS3Oc2xneF1ZWaR/MS8r63g==",
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/objection/-/objection-2.2.6.tgz",
"integrity": "sha512-1oceoMuWMtUBTGZ6Y3aIfsjhX83JDpETOVm73ZnyuJx0KsAcf+ZLAtSHWChCQw1DZydHQOMxHlq5vTQ4HYPLAw==",
"requires": {
"ajv": "^6.12.0",
"ajv": "^6.12.6",
"db-errors": "^0.2.3"
}
},
@@ -2791,9 +3038,9 @@
"integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
},
"pg-connection-string": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.3.0.tgz",
"integrity": "sha512-ukMTJXLI7/hZIwTW7hGMZJ0Lj0S2XQBCJ4Shv4y1zgQ/vqVea+FLhzywvPj0ujSuofu+yA4MYHGZPTsgjBgJ+w=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.4.0.tgz",
"integrity": "sha512-3iBXuv7XKvxeMrIgym7njT+HlZkwZqqGX4Bu9cci8xHZNT+Um1gWKqCsAzcC0d95rcKMU5WBg6YRUcHyV0HZKQ=="
},
"picomatch": {
"version": "2.2.2",
@@ -2876,6 +3123,14 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"queue": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.1.tgz",
"integrity": "sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg==",
"requires": {
"inherits": "~2.0.3"
}
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
@@ -3045,6 +3300,11 @@
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
}
}
},
@@ -3469,6 +3729,11 @@
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
},
"streamsearch": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
"integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo="
},
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
@@ -3697,6 +3962,11 @@
"mime-types": "~2.1.24"
}
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
@@ -3871,9 +4141,9 @@
"integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
},
"v8flags": {
"version": "3.2.0",
@@ -4052,6 +4322,11 @@
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",


+ 14
- 7
package.json View File

@@ -12,28 +12,35 @@
"author": "",
"license": "AGPL-3.0-or-later",
"devDependencies": {
"nodemon": "^2.0.6"
"@types/express": "^4.17.9",
"@types/multer": "^1.4.5",
"@types/node": "^14.14.20",
"nodemon": "^2.0.7"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"argon2": "^0.26.2",
"connect-redis": "^4.0.4",
"dompurify": "^2.2.3",
"dompurify": "^2.2.6",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-handlebars": "^4.0.6",
"express-session": "^1.17.1",
"express-validator": "^6.8.0",
"express-validator": "^6.9.2",
"feed": "^4.2.1",
"gm": "^1.23.1",
"image-size": "^0.9.3",
"jsdom": "^16.4.0",
"knex": "^0.21.12",
"marked": "^1.2.6",
"knex": "^0.21.15",
"marked": "^1.2.7",
"method-override": "^3.0.0",
"moment": "^2.29.1",
"multer": "^1.4.2",
"mysql2": "^2.2.5",
"nodemailer": "^6.4.17",
"objection": "^2.2.3",
"objection": "^2.2.6",
"redis": "^3.0.2",
"slugify": "^1.4.6"
"slugify": "^1.4.6",
"uuid": "^8.3.2"
}
}

+ 70
- 0
routes/images.js View File

@@ -0,0 +1,70 @@
/**
* @author Dusan Mitrovic <dusan@dusanmitrovic.xyz>
* @license AGPL-3.0-or-later https://opensource.org/licenses/AGPL-3.0
*
* @summary HTTP routes for image upload
*/
const express = require('express');
const router = express.Router();
const imageSize = require('image-size');
const authorizationMiddleware = require('../middleware/auth');
const multerHandler = require('../middleware/multer-handler');
const ImageConversionService = require('../services/image-conversion-service');
const path = require('path');
const multer = require('../config/multer');

router.get(
'/images',
authorizationMiddleware,
(req, res) => {
return res.render('image-upload', {
title: 'Upload an image',
css: [
'/static/css/image-upload.css',
'/static/css/form.css',
],
});
}
);

router.post(
'/images',
authorizationMiddleware,
multer.single('uploaded_file'),
multerHandler,
(req, res) => {
const {file} = req;

if (undefined === file) {
return res.status(422).render('image-upload', {
title: 'Upload an image',
css: [
'/static/css/image-upload.css',
'/static/css/form.css',
],
error: {
message: 'No file received, but it is required.',
},
});
}

const size = imageSize(file.path);

const imageURI = ImageConversionService.convert({
size: size.width,
inputPath: file.path,
outputName: path.parse(file.filename).name + '.jpg',
});

return res.render('image-upload', {
title: 'Upload an image',
css: [
'/static/css/image-upload.css',
'/static/css/form.css',
],
imageURI,
});
}
);

module.exports = router;

+ 52
- 0
services/image-conversion-service.js View File

@@ -0,0 +1,52 @@
/**
* @author Dusan Mitrovic <dusan@dusanmitrovic.xyz>
* @license AGPL-3.0-or-later https://opensource.org/licenses/AGPL-3.0
*
* @summary Does basic image manipulation using imagemagick
*/
const imageMagick = require('gm').subClass({imageMagick: true});
const getAppURI = require('../utilities/get-app-uri');
const path = require('path');
const fs = require('fs');

class ImageConversionService {
/**
* Converts an image to JPEG to reduce file size
*/
static convert({inputPath, outputName, size} = options) {
const outputPath = path.join(
__dirname,
'../static/images/uploads/',
outputName
);

imageMagick(inputPath)
// Image width, height scales acordingly
.resize(size > 1200 ? 1200 : size)
// Slice the chroma values in half to reduce file size
.samplingFactor('4:2:0', '')
// Quality in range of 1-100
.quality(75)
// Desired colorspace
.colorspace('sRGB')
// interlacing type
.interlace('JPEG')
// Strip off all EXIF metadata
.strip()
.write(outputPath, error => {
if (error) {
console.error(error);
}

fs.unlink(inputPath, error => {
if (error) {
console.error(error);
}
});
});

return `${getAppURI()}/static/images/uploads/${outputName}`;
}
}

module.exports = ImageConversionService;

+ 34
- 0
static/css/image-upload.css View File

@@ -0,0 +1,34 @@
#image-upload-page {
width: 700px;
}

#image-upload-page img {
margin-top: 1em;
width: 100%;
height: auto;
}

.upload-error {
text-align: center;
color: var(--error-color);
background-color: var(--grey-bg);
margin-top: 1em;
padding: 1em 2em;
}

.upload-output {
text-align: center;
background-color: var(--grey-bg);
margin-top: 1em;
padding: 0.5em 1em;
}

.upload-output a {
color: var(--success-color);
}

@media screen and (max-width: 700px) {
#image-upload-page {
width: 100%;
}
}

+ 2
- 0
static/images/uploads/.gitignore View File

@@ -0,0 +1,2 @@
*.jpg
*.png

+ 0
- 0
View File


+ 2
- 2
static/js/prism.js
File diff suppressed because it is too large
View File


+ 21
- 0
views/image-upload.hbs View File

@@ -0,0 +1,21 @@
<section id="image-upload-page" class="container">
<form class="form" action="/admin/images/" enctype="multipart/form-data" method="POST">
<input type="file" class="form-input" name="uploaded_file" required>

<button class="form-button" type="submit">Upload</button>
</form>

{{#if error}}
<p class="upload-error">
{{error.message}}
</p>
{{/if}}

{{#if imageURI}}
<p class="upload-output">
<a href="{{imageURI}}" rel="noopener" target="_blank">
Success, click here to view the image.
</a>
</p>
{{/if}}
</section>

Loading…
Cancel
Save