diff --git a/app.js b/app.js index 16f6ec79..fb753b8c 100755 --- a/app.js +++ b/app.js @@ -34,6 +34,11 @@ if(config.sslPort && config.sslKeyFile && config.sslCertFile) { }); } +if(config['fileCommentMaxLength'] > 255 ) { + console.error("fileCommentMaxLength must be lower than 256. Please update your config file!"); + shutdown(); +} + // graceful shutdown function shutdown() { diff --git a/app/src/Upload.vue b/app/src/Upload.vue index 27a2350e..958e4988 100644 --- a/app/src/Upload.vue +++ b/app/src/Upload.vue @@ -90,7 +90,7 @@ computed: { ...mapState(['state']), - ...mapState('config', ['uploadPassRequired', 'uploadPass', 'requireBucketPassword', 'disableQrCode']), + ...mapState('config', ['uploadPassRequired', 'uploadPass', 'requireBucketPassword', 'disableQrCode', 'fileCommentMaxLength']), ...mapState('upload', ['sid', 'files', 'password']), ...mapGetters(['error', 'disabled']), ...mapGetters('upload', ['percentUploaded', 'shareUrl', 'bucketSize', 'bytesUploaded']), @@ -105,7 +105,15 @@ showUploadBtn() { return this.files.length && !this.disabled + && !this.isUploadBtnDisabled && (this.requireBucketPassword && this.password || !this.requireBucketPassword) + }, + isUploadBtnDisabled(){ + let error = false; + this.files.forEach(file => { + if(file.comment.length > this.fileCommentMaxLength) error = true; + }) + return error; } }, diff --git a/app/src/Upload/Files.vue b/app/src/Upload/Files.vue index dd18968c..587ea67b 100644 --- a/app/src/Upload/Files.vue +++ b/app/src/Upload/Files.vue @@ -23,6 +23,7 @@ small ({{ file.humanSize }}) p input.form-control.input-sm(type="text", :placeholder="$root.lang.comment", v-model="file.comment", :disabled="disabled") + div.text-end(v-show="file.comment.length > $store.state.config.fileCommentMaxLength") {{ file.comment.length }} / {{ $store.state.config.fileCommentMaxLength }} .alert.alert-danger(v-if="file.error") icon.fa-fw(name="exclamation-triangle") | {{ file.error }} diff --git a/config.dev.js b/config.dev.js index 732fadf9..18cb9434 100644 --- a/config.dev.js +++ b/config.dev.js @@ -18,7 +18,8 @@ module.exports = { "uploadAppPath": '/', // "maxFileSize": Math.pow(2, 20) * 15, // "maxBucketSize": Math.pow(2, 20) * 20, - "mailFrom": "PsiTransfer " + "mailFrom": "PsiTransfer ", // "sslKeyFile": './tmp/cert.key', // "sslCertFile": './tmp/cert.pem', + "fileCommentMaxLength": 200, }; diff --git a/config.js b/config.js index aaf62d64..d76374ec 100644 --- a/config.js +++ b/config.js @@ -62,10 +62,11 @@ const config = { "plugins": ['file-downloaded-webhook', 'file-uploaded-webhook'], // Disable the QR code button for download url sharing, set to true to disable "disableQrCode": false, + "fileCommentMaxLength": 200, }; // Load NODE_ENV specific config -const envConfFile = path.resolve(__dirname, `config.${ process.env.NODE_ENV }.js`); +const envConfFile = path.resolve(__dirname, `config.${process.env.NODE_ENV}.js`); if (process.env.NODE_ENV && fsp.existsSync(envConfFile)) { Object.assign(config, require(envConfFile)); } @@ -94,14 +95,14 @@ config.uploadAppPath = config.baseUrl.substr(0, config.baseUrl.length - 1) + con // Load language files config.languages = { - [config.defaultLanguage]: require(`./lang/${ config.defaultLanguage }`) // default language + [config.defaultLanguage]: require(`./lang/${config.defaultLanguage}`) // default language }; fs.readdirSync(path.resolve(__dirname, 'lang')).forEach(lang => { lang = lang.replace('.js', ''); if (lang === config.defaultLanguage) return; config.languages[lang] = { ...config.languages[config.defaultLanguage], - ...require(`./lang/${ lang }`) + ...require(`./lang/${lang}`) }; }); diff --git a/lib/endpoints.js b/lib/endpoints.js index 945cbc46..c0f49e9b 100644 --- a/lib/endpoints.js +++ b/lib/endpoints.js @@ -43,7 +43,7 @@ if (config.accessLog) { if (config.forceHttps) { app.enable('trust proxy'); - app.use(function(req, res, next) { + app.use(function (req, res, next) { if (req.secure) return next(); const target = config.forceHttps === 'true' ? 'https://' + req.headers.host : config.forceHttps; res.redirect(target + req.url); @@ -51,8 +51,8 @@ if (config.forceHttps) { } // Static files -app.use(`${ config.baseUrl }app`, express.static(path.join(__dirname, '../public/app'))); -app.use(`${ config.baseUrl }assets`, express.static(path.join(__dirname, '../public/assets'))); +app.use(`${config.baseUrl}app`, express.static(path.join(__dirname, '../public/app'))); +app.use(`${config.baseUrl}assets`, express.static(path.join(__dirname, '../public/assets'))); // Resolve language app.use((req, res, next) => { @@ -62,7 +62,7 @@ app.use((req, res, next) => { }); // robots.txt -app.get(`${ config.baseUrl }robots.txt`, (req, res) => { +app.get(`${config.baseUrl}robots.txt`, (req, res) => { res.sendFile(path.join(__dirname, '../public/robots.txt')); }); @@ -82,13 +82,13 @@ app.get(config.uploadAppPath, (req, res) => { }); // Return translations -app.get(`${ config.baseUrl }lang.json`, (req, res) => { +app.get(`${config.baseUrl}lang.json`, (req, res) => { eventBus.emit('getLang', req.translations); res.json(req.translations); }); // Config -app.get(`${ config.baseUrl }config.json`, (req, res) => { +app.get(`${config.baseUrl}config.json`, (req, res) => { // Upload password protection if (config.uploadPass) { const bfTimeout = 200; @@ -110,6 +110,7 @@ app.get(`${ config.baseUrl }config.json`, (req, res) => { maxFileSize: config.maxFileSize, maxBucketSize: config.maxBucketSize, disableQrCode: config.disableQrCode, + fileCommentMaxLength: config.fileCommentMaxLength, }; eventBus.emit('getFrontendConfig', frontendConfig); @@ -117,12 +118,12 @@ app.get(`${ config.baseUrl }config.json`, (req, res) => { res.json(frontendConfig); }); -app.get(`${ config.baseUrl }admin`, (req, res, next) => { +app.get(`${config.baseUrl}admin`, (req, res, next) => { if (!config.adminPass) return next(); res.send(adminPage({ ...pugVars, lang: req.translations })); }); -app.get(`${ config.baseUrl }admin/data.json`, (req, res, next) => { +app.get(`${config.baseUrl}admin/data.json`, (req, res, next) => { if (!config.adminPass) return next(); const bfTimeout = 500; @@ -154,7 +155,7 @@ app.get(`${ config.baseUrl }admin/data.json`, (req, res, next) => { // List files / Download App -app.get(`${ config.baseUrl }:sid`, (req, res, next) => { +app.get(`${config.baseUrl}:sid`, (req, res, next) => { if (req.url.endsWith('.json')) { const sid = req.params.sid.substr(0, req.params.sid.length - 5); if (!db.get(sid)) return res.status(404).end(); @@ -162,13 +163,13 @@ app.get(`${ config.baseUrl }:sid`, (req, res, next) => { const downloadPassword = req.get('x-download-pass'); const items = db.get(sid).map(item => ({ ...item, - url: `${ config.baseUrl }files/${ sid }++${ item.key }` + url: `${config.baseUrl}files/${sid}++${item.key}` })); res.header('Cache-control', 'private, max-age=0, no-cache, no-store, must-revalidate'); // Currently, every item in a bucket must have the same password - if(items.some(item => item.metadata.password && item.metadata.password !== downloadPassword)) { + if (items.some(item => item.metadata.password && item.metadata.password !== downloadPassword)) { setTimeout(() => res.status(401).send('Unauthorized'), 500); return; } @@ -187,7 +188,7 @@ app.get(`${ config.baseUrl }:sid`, (req, res, next) => { // Download files -app.get(`${ config.baseUrl }files/:fid`, async (req, res, next) => { +app.get(`${config.baseUrl}files/:fid`, async (req, res, next) => { // let tusboy handle HEAD requests with Tus Header if (req.method === 'HEAD' && req.get('Tus-Resumable')) return next(); @@ -199,11 +200,11 @@ app.get(`${ config.baseUrl }files/:fid`, async (req, res, next) => { const bucket = db.get(sid); if (!bucket) return res.status(404).send(errorPage({ - ...pugVars, - error: 'Download bucket not found.', - lang: req.translations, - uploadAppPath: config.uploadAppPath || config.baseUrl, - })); + ...pugVars, + error: 'Download bucket not found.', + lang: req.translations, + uploadAppPath: config.uploadAppPath || config.baseUrl, + })); if (req.params.fid !== sid + '++' + MD5(bucket.map(f => f.key).join()).toString() + '.' + format) { res.status(404).send(errorPage({ @@ -214,10 +215,10 @@ app.get(`${ config.baseUrl }files/:fid`, async (req, res, next) => { })); return; } - debug(`Download Bucket ${ sid }`); + debug(`Download Bucket ${sid}`); - const filename = `${ sid }.${ format }`; - res.header('Content-Disposition', `attachment; filename="${ filename }"`); + const filename = `${sid}.${format}`; + res.header('Content-Disposition', `attachment; filename="${filename}"`); try { res.on('finish', async () => { @@ -242,10 +243,10 @@ app.get(`${ config.baseUrl }files/:fid`, async (req, res, next) => { console.error(e); } - if(format === 'zip') { + if (format === 'zip') { res.header('ContentType', 'application/zip'); const archive = archiver('zip'); - archive.on('error', function(err) { + archive.on('error', function (err) { console.error(err); }); archive.pipe(res); @@ -270,7 +271,7 @@ app.get(`${ config.baseUrl }files/:fid`, async (req, res, next) => { const entry = pack.entry({ name: info.metadata.name, size: info.size }); readStream.on('error', reject); entry.on('error', reject); - entry.on('finish',resolve); + entry.on('finish', resolve); readStream.pipe(entry); }); } @@ -281,7 +282,7 @@ app.get(`${ config.baseUrl }files/:fid`, async (req, res, next) => { } // Download single file - debug(`Download ${ req.params.fid }`); + debug(`Download ${req.params.fid}`); try { const info = await store.info(req.params.fid); // throws on 404 res.download(store.getFilename(req.params.fid), info.metadata.name); @@ -314,8 +315,8 @@ app.get(`${ config.baseUrl }files/:fid`, async (req, res, next) => { // Upload file -app.use(`${ config.uploadAppPath }files`, - function(req, res, next) { +app.use(`${config.uploadAppPath}files`, + function (req, res, next) { // Upload password protection if (config.uploadPass) { const bfTimeout = 500; @@ -341,7 +342,7 @@ app.use(`${ config.uploadAppPath }files`, assert(meta.sid, 'tus meta prop missing: sid'); assert(meta.retention, 'tus meta prop missing: retention'); assert(Object.keys(config.retentions).indexOf(meta.retention) >= 0, - `invalid tus meta prop retention. Value ${ meta.retention } not in [${ Object.keys(config.retentions).join(',') }]`); + `invalid tus meta prop retention. Value ${meta.retention} not in [${Object.keys(config.retentions).join(',')}]`); const uploadLength = req.get('Upload-Length'); assert(uploadLength, 'missing Upload-Length header'); @@ -354,11 +355,11 @@ app.use(`${ config.uploadAppPath }files`, if (config.maxFileSize && config.maxFileSize < +uploadLength) { return res .status(413) - .json({ message: `File exceeds maximum upload size ${ config.maxFileSize }.` }); + .json({ message: `File exceeds maximum upload size ${config.maxFileSize}.` }); } else if (config.maxBucketSize && db.bucketSize(meta.sid) + +uploadLength > config.maxBucketSize) { return res .status(413) - .json({ message: `Bucket exceeds maximum upload size ${ config.maxBucketSize }.` }); + .json({ message: `Bucket exceeds maximum upload size ${config.maxBucketSize}.` }); } // store changed metadata for tusboy @@ -386,7 +387,7 @@ app.use(`${ config.uploadAppPath }files`, maxUploadLength: config.maxFileSize || Infinity, afterComplete: (req, upload, fid) => { db.add(upload.metadata.sid, upload.metadata.key, upload); - debug(`Completed upload ${ fid }, size=${ upload.size } name=${ upload.metadata.name }`); + debug(`Completed upload ${fid}, size=${upload.size} name=${upload.metadata.name}`); eventBus.emit('fileUploaded', upload); }, diff --git a/public/assets/styles.css b/public/assets/styles.css index b6d8aea3..e458fe94 100644 --- a/public/assets/styles.css +++ b/public/assets/styles.css @@ -155,6 +155,9 @@ h1 .fa-icon { margin-left: 15px; } +.text-end{ + text-align: end; +} /* Dark Mode */ @media (prefers-color-scheme: dark) {