diff --git a/.gitignore b/.gitignore index 0d1a7d3..63a355a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,9 @@ node_modules data -custom/*.js +custom/* public/dist/*.js public/dist/*.map public/dist/index.html config.js *.sublime-workspace -npm-debug.log -/custom/images/logo.png \ No newline at end of file +npm-debug.log \ No newline at end of file diff --git a/config.defaults.js b/config.defaults.js index 08e4962..5aa5b7c 100644 --- a/config.defaults.js +++ b/config.defaults.js @@ -36,21 +36,28 @@ module.exports = { alphabet: '1234567890abcdef', length: 8, + defaultExpiration: null, maxExpiration: null /* - To set a maximum expiration of 7 days: maxExpiration: { units: ExpirationUnits.Days, value: 7 } + + defaultExpiration follows the same format */ }, + // How often codes and uploads are checked for expiration and deleted + cleanupInterval: 300, + notifications: { + // How often the notification queue is flushed, in seconds interval: 10, + maxAttempt: 12, adminUrl: 'http://localhost:3001/admin/', diff --git a/emails/uploadnotification/html.ejs b/emails/uploadnotification/html.ejs index 897d53c..7711bf7 100644 --- a/emails/uploadnotification/html.ejs +++ b/emails/uploadnotification/html.ejs @@ -29,5 +29,8 @@ function humanFileSize(bytes, si)
  • <%= file.name %> (<%= humanFileSize(file.size, true) %>)
  • <% }) %> +<% if (upload.expirationDate !== null) { %> +These files will be automatically deleted after <%=upload.expirationDate.toLocaleString('en-US')%> +<% } %>

    You can download these files by logging in to <%= adminUrl %>

    Cheers,
    Recv

    \ No newline at end of file diff --git a/index.js b/index.js index d8846ce..cb92fd1 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,7 @@ else const Repository = require('./lib/repository'); const NotificationWorker = require('./lib/workers/notification'); +const ExpirationWorker = require('./lib/workers/expiration'); const express = require('express'); const bodyParser = require('body-parser'); @@ -116,6 +117,8 @@ const webpackConfigFactory = require('./webpack.config.js'); var notificationWorker = new NotificationWorker(repository); notificationWorker.start(config.notifications.interval * 1000); + var expirationWorker = new ExpirationWorker(repository); + expirationWorker.start(config.cleanupInterval * 1000); var server = app.listen(config.port, () => console.log('Recv running on port ' + server.address().port)); } diff --git a/lib/api/admin.js b/lib/api/admin.js index d25c5f3..9686485 100644 --- a/lib/api/admin.js +++ b/lib/api/admin.js @@ -214,13 +214,14 @@ module.exports = (repository) => })); - router.get('/maxExpiration', asyncHandler(async (req, res) => + router.get('/expiration', asyncHandler(async (req, res) => { await checkAuthorization(req, res, repository, async (user) => { - res.send(config.code.maxExpiration !== null - ? config.code.maxExpiration - : { units: '', value: 1 }); + res.send({ + max: config.code.maxExpiration, + default: config.code.defaultExpiration + }); }); })); diff --git a/lib/api/token.js b/lib/api/token.js index 93270f9..4a90b48 100644 --- a/lib/api/token.js +++ b/lib/api/token.js @@ -21,7 +21,7 @@ module.exports = (repository) => jwt.sign({ code: req.body.code, codeUserId: code.userId, - codeExpirationDate: code.expirationDate + codeExpirationTime: code.expirationDate !== null ? code.expirationDate.getTime() : null }, config.jwtSecret, (err, token) => { if (err) diff --git a/lib/api/upload.js b/lib/api/upload.js index d2467d7..9ea6cd0 100644 --- a/lib/api/upload.js +++ b/lib/api/upload.js @@ -45,7 +45,7 @@ module.exports = (repository, tusServer) => var router = express.Router(); // Upload API - router.get('/message/:code', asyncHandler(async (req, res) => + router.get('/info/:code', asyncHandler(async (req, res) => { var code = await repository.codes.get(req.params.code); if (code === null) @@ -54,19 +54,23 @@ module.exports = (repository, tusServer) => return; } - if (!code.messageHTML) + let info = { + message: null, + expirationDate: code.expirationDate !== null ? code.expirationDate.getTime() : null + }; + + if (code.messageHTML) { - res.sendStatus(204); - return; + var user = await repository.users.get(code.userId); + var name = user !== null ? user.name : null; + + info.message = { + name: name, + message: code.messageHTML + }; } - var user = await repository.users.get(code.userId); - var name = user !== null ? user.name : null; - - res.send({ - name: name, - message: code.messageHTML - }); + res.send(info); })); router.post('/complete', asyncHandler(async (req, res) => @@ -102,7 +106,13 @@ module.exports = (repository, tusServer) => return; } - var uploadId = await repository.uploads.insert(decoded.codeUserId, decoded.code, req.body.files, decoded.codeExpirationDate); + var uploadId = await repository.uploads.insert( + decoded.codeUserId, + decoded.code, + req.body.files, + decoded.codeExpirationTime !== null ? new Date(decoded.codeExpirationTime) : null + ); + await repository.notifications.insert({ userId: decoded.codeUserId, uploadId: uploadId diff --git a/lib/repository/code.js b/lib/repository/code.js index a85c830..3c2c723 100644 --- a/lib/repository/code.js +++ b/lib/repository/code.js @@ -2,6 +2,7 @@ const map = require('lodash/map'); const retry = require('async-retry'); const generate = require('nanoid/generate'); const markdown = require('markdown').markdown; +const async = require('async'); const ExpirationUnits = require('../expirationunits'); @@ -197,6 +198,40 @@ class CodeRepository }); }); } + + + deleteExpired() + { + var self = this; + + return new Promise((resolve, reject) => + { + let now = new Date(); + + self.store.find({ $where: function() { return this.expirationDate !== null && this.expirationDate < now }}, (err, docs) => + { + if (err) + { + reject(err); + return; + } + + async.eachOfSeries(docs, + async (doc) => + { + console.log('Expired code: ' + doc._id); + await self.delete(doc._id); + }, + (err) => + { + if (err) + reject(err); + else + resolve(); + }); + }); + }); + } } diff --git a/lib/repository/upload.js b/lib/repository/upload.js index 679e404..798c09e 100644 --- a/lib/repository/upload.js +++ b/lib/repository/upload.js @@ -2,6 +2,7 @@ const map = require('lodash/map'); const filter = require('lodash/filter'); const resolvePath = require('resolve-path'); const fs = require('mz/fs'); +const async = require('async'); class Upload @@ -205,6 +206,40 @@ class UploadRepository }); }); } + + + deleteExpired() + { + var self = this; + + return new Promise((resolve, reject) => + { + let now = new Date(); + + self.store.find({ $where: function() { return this.expirationDate !== null && this.expirationDate < now }}, (err, docs) => + { + if (err) + { + reject(err); + return; + } + + async.eachOfSeries(docs, + async (doc) => + { + console.log('Expired upload: ' + doc._id); + await self.delete(doc._id); + }, + (err) => + { + if (err) + reject(err); + else + resolve(); + }); + }); + }); + } } diff --git a/lib/workers/abstractintervalworker.js b/lib/workers/abstractintervalworker.js new file mode 100644 index 0000000..d3b0d40 --- /dev/null +++ b/lib/workers/abstractintervalworker.js @@ -0,0 +1,45 @@ +class AbstractIntervalWorker +{ + start(interval) + { + var self = this; + + self.stop(); + self.timer = setInterval(async () => + { + if (self.ticking) + return; + + self.ticking = true; + try + { + await self.tick(); + } + catch (err) + { + console.log(err); + } + self.ticking = false; + }, interval); + } + + + stop() + { + var self = this; + if (self.timer) + { + clearInterval(self.timer); + self.timer = null; + } + } + + + /* Implement this: + async tick() + { + } + */ +} + +module.exports = AbstractIntervalWorker; \ No newline at end of file diff --git a/lib/workers/expiration.js b/lib/workers/expiration.js new file mode 100644 index 0000000..f5fd857 --- /dev/null +++ b/lib/workers/expiration.js @@ -0,0 +1,23 @@ +const async = require('async'); +const AbstractIntervalWorker = require('./abstractintervalworker'); + + +class ExpirationWorker extends AbstractIntervalWorker +{ + constructor(repository) + { + super(); + this.repository = repository; + } + + + async tick() + { + var self = this; + + await self.repository.uploads.deleteExpired(); + await self.repository.codes.deleteExpired(); + } +} + +module.exports = ExpirationWorker; \ No newline at end of file diff --git a/lib/workers/notification.js b/lib/workers/notification.js index a10c142..071d73e 100644 --- a/lib/workers/notification.js +++ b/lib/workers/notification.js @@ -1,45 +1,21 @@ -const config = require('../../config'); const async = require('async'); const nodemailer = require('nodemailer'); const Email = require('email-templates'); +const fs = require('mz/fs'); +const path = require('path'); +const getPaths = require('get-paths'); +const AbstractIntervalWorker = require('./abstractintervalworker'); -class NotificationWorker +class NotificationWorker extends AbstractIntervalWorker { constructor(repository) { + super(); this.repository = repository; } - start(interval) - { - var self = this; - - self.stop(); - self.timer = setInterval(async () => - { - if (self.ticking) - return; - - self.ticking = true; - await self.tick(); - self.ticking = false; - }, interval); - } - - - stop() - { - var self = this; - if (self.timer) - { - clearInterval(self.timer); - self.timer = null; - } - } - - async tick() { var self = this; @@ -88,6 +64,39 @@ class NotificationWorker } }); + + // Override the default template locator with one that checks + // the custom folder first, then falls back to the configured folder + let getTemplatePathFromRoot = function(root, view, ext) + { + return new Promise(async (resolve, reject) => { + try { + const paths = await getPaths( + root, + view, + ext + ); + const filePath = path.resolve(root, paths.rel); + resolve({ filePath, paths }); + } catch (err) { + reject(err); + } + }); + }; + + email.getTemplatePath = async function(view) + { + try + { + return await getTemplatePathFromRoot(path.resolve('custom/emails'), view, this.config.views.options.extension); + } + catch (err) + { + return await getTemplatePathFromRoot(this.config.views.root, view, this.config.views.options.extension); + } + }; + + email .send({ template: 'uploadnotification', diff --git a/package.json b/package.json index c613973..bbdcc87 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Recv - self-hosted web file transfer", "main": "index.js", "scripts": { - "dev": "supervisor -w index.js,lib index.js", + "dev": "supervisor -w index.js,lib,config.js,config.defaults.js index.js", "devbuild": "webpack-cli --mode development", "build": "webpack-cli --mode production", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/public/src/app.js b/public/src/app.js index fcc437c..4026f3c 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -53,6 +53,21 @@ Vue.filter('formatDateTime', (value) => } }); +Vue.filter('formatDate', (value) => +{ + if (value) + { + return moment(String(value)).format(i18n.t('dateFormat')) + } +}); + +Vue.filter('formatTime', (value) => +{ + if (value) + { + return moment(String(value)).format(i18n.t('timeFormat')) + } +}); // All credit goes to: https://stackoverflow.com/a/14919494 function humanFileSize(bytes, si) diff --git a/public/src/locale/en.js b/public/src/locale/en.js index e63729d..d0ee050 100644 --- a/public/src/locale/en.js +++ b/public/src/locale/en.js @@ -2,6 +2,8 @@ export default { title: 'File upload - Recv', disclaimer: '', dateTimeFormat: 'MM/DD/YYYY hh:mm a', + dateFormat: 'MM/DD/YYYY', + timeFormat: 'hh:mm a', landing: { invitePlaceholder: 'Code', @@ -16,7 +18,20 @@ export default { invalidLogin: 'The specified username or password is incorrect' }, - messageFrom: 'Message from ', + messageFrom: 'Message from {owner}', + uploadDisclaimer: '', + expirationNotice: { + never: '', + ever: 'Files uploaded for this invite code will be automatically deleted on {date} at {time}' + }, + + expirationValues: { + h: '0 hours | 1 hour | {count} hours', + d: '0 days | 1 day | {count} days', + w: '0 weeks | 1 week | {count} weeks', + M: '0 months | 1 month | {count} months', + y: '0 years | 1 year | {count} years' + }, uppyDashboard: { done: 'Done', @@ -33,7 +48,7 @@ export default { save: 'Save', delete: 'Delete', - diskspace: '%{available} disk space available of %{total} total', + diskspace: '{available} disk space available of {total} total', login: { usernamePlaceholder: 'Username or e-mail address', @@ -85,14 +100,7 @@ export default { y: 'Years:' }, - expirationHint: { - format: 'The maximum allowed expiration is %{valueUnits}', - h: '0 hours | 1 hour | {count} hours', - d: '0 days | 1 day | {count} days', - w: '0 weeks | 1 week | {count} weeks', - M: '0 months | 1 month | {count} months', - y: '0 years | 1 year | {count} years' - } + expirationHint: 'The maximum allowed expiration is {valueUnits}' } }, diff --git a/public/src/locale/nl.js b/public/src/locale/nl.js index 07338df..874386a 100644 --- a/public/src/locale/nl.js +++ b/public/src/locale/nl.js @@ -2,6 +2,8 @@ export default { title: 'Bestandsoverdracht - Recv', disclaimer: '', dateTimeFormat: 'DD-MM-YYYY HH:mm', + dateFormat: 'DD-MM-YYYY', + timeFormat: 'HH:mm', landing: { invitePlaceholder: 'Code', @@ -16,7 +18,20 @@ export default { invalidLogin: 'De ingevoerde gebruikersnaam of wachtwoord is incorrect' }, - messageFrom: 'Bericht van ', + messageFrom: 'Bericht van {owner}', + uploadDisclaimer: '', + expirationNotice: { + never: '', + ever: 'Bestanden die worden geupload voor deze code worden automatisch verwijderd op {date} om {time}' + }, + + expirationValues: { + h: '0 uren | 1 uur | {count} uren', + d: '0 dagen | 1 dag | {count} dagen', + w: '0 weken | 1 week | {count} weken', + M: '0 maanden | 1 maand | {count} maanden', + y: '0 jaren | 1 jaar | {count} jaren' + }, uppyDashboard: { done: 'Gereed', @@ -33,7 +48,7 @@ export default { save: 'Opslaan', delete: 'Verwijderen', - diskspace: '%{available} schijfruimte beschikbaar van %{total} totaal', + diskspace: '{available} schijfruimte beschikbaar van {total} totaal', login: { usernamePlaceholder: 'Gebruikersnaam of e-mail adres', @@ -85,14 +100,7 @@ export default { y: 'Jaren:' }, - expirationHint: { - format: 'De maximaal toegestane verlooptermijn is %{valueUnits}', - h: '0 uren | 1 uur | {value} uren', - d: '0 dagen | 1 dag | {value} dagen', - w: '0 weken | 1 week | {value} weken', - M: '0 maanden | 1 maand | {value} maanden', - y: '0 jaren | 1 jaar | {value} jaren' - } + expirationHint: 'De maximaal toegestane verlooptermijn is {valueUnits}' } }, diff --git a/public/src/route/Landing.vue b/public/src/route/Landing.vue index deb5623..c839862 100644 --- a/public/src/route/Landing.vue +++ b/public/src/route/Landing.vue @@ -8,13 +8,15 @@ -
    +
    {{ $t('disclaimer') }}
    - \ No newline at end of file + \ No newline at end of file diff --git a/public/src/route/Upload.vue b/public/src/route/Upload.vue index a81c5a4..aaec57f 100644 --- a/public/src/route/Upload.vue +++ b/public/src/route/Upload.vue @@ -1,10 +1,19 @@