diff --git a/app/src/Download.vue b/app/src/Download.vue index 860f91c..2670073 100644 --- a/app/src/Download.vue +++ b/app/src/Download.vue @@ -19,7 +19,11 @@ i.fa.fa-key | decrypt .panel.panel-primary(v-if='!needsPassword') - .panel-heading Files + .panel-heading + a.pull-right(style="color:#fff", @click="downloadAll", v-if="downloadsAvailable") + i.fa.fa-fw.fa-download + | Download ZIP + | Files .panel-body table.table.table-hover.table-striped(style='margin-bottom: 0') tbody @@ -32,10 +36,8 @@ a i.fa.fa-fw.fa-copy i.fa.fa-check.text-success.pull-right(v-show='file.downloaded') - | - strong {{ file.metadata.name }} - | - small ({{ humanFileSize(file.size) }}) + strong {{ file.metadata.name }} + small(v-if="Number.isFinite(file.size)") ({{ humanFileSize(file.size) }}) p {{ file.metadata.comment }} @@ -44,6 +46,7 @@ "use strict"; import AES from 'crypto-js/aes'; import encUtf8 from 'crypto-js/enc-utf8'; + import MD5 from 'crypto-js/md5'; import FileIcon from './common/FileIcon.vue'; import Clipboard from './common/Clipboard.vue'; @@ -51,6 +54,7 @@ export default { name: 'app', components: { FileIcon, Clipboard }, + data () { return { files: [], @@ -63,6 +67,13 @@ host: document.location.protocol + '//' + document.location.host } }, + + computed: { + downloadsAvailable: function() { + return this.files.some(f => !f.downloaded || f.metadata.retention !== 'one-time') + } + }, + methods: { download(file) { if(file.downloaded && file.metadata.retention === 'one-time') { @@ -73,6 +84,20 @@ file.downloaded = true; }, + downloadAll() { + document.location.href = document.location.protocol + '//' + document.location.host + + '/files/' + this.sid + '++' + + MD5( + this.files + .filter(f => !f.downloaded || f.metadata.retention !== 'one-time') + .map(f => f.key).join() + ).toString() + '.zip'; + + this.files.forEach(f => { + f.downloaded = true; + }); + }, + copied(file, $event) { file.downloaded = $event === 'copied'; }, diff --git a/lib/endpoints.js b/lib/endpoints.js index 734f17a..a1f6e83 100644 --- a/lib/endpoints.js +++ b/lib/endpoints.js @@ -11,7 +11,9 @@ const fs = require("fs"); const tusMeta = require('tus-metadata'); const assert = require('assert'); const AES = require("crypto-js/aes"); +const MD5 = require("crypto-js/md5"); const debug = require('debug')('psitransfer:main'); +const archiver = require('archiver'); const errorPage = fs.readFileSync(path.join(__dirname, '../public/html/error.html')).toString(); const store = new Store(config.uploadDir); @@ -73,6 +75,50 @@ app.get('/files/:fid', async(req, res, next) => { // let tusboy handle HEAD with Tus Header if(req.method === 'HEAD' && req.get('Tus-Resumable')) return next(); + // Download all files + if(req.params.fid.endsWith('.zip')) { + const sid = req.params.fid.split('++')[0]; + const bucket = db.get(sid); + if(req.params.fid !== sid + '++' + MD5(bucket.map(f => f.key).join()).toString() + '.zip') { + res.status(404).send(errorPage.replace('%%ERROR%%', 'Invalid link')); + return; + } + debug(`Download Bucket ${sid}`); + + res.header('ContentType', 'application/zip'); + res.header('Content-Disposition', 'attachment; filename="' + sid + '.zip"'); + + const archive = archiver('zip'); + archive.on('error', function(err) { + console.error(err); + }); + + bucket.forEach(info => { + archive.append( + fs.createReadStream(store.getFilename(info.metadata.sid + '++' + info.key)), + {name: info.metadata.name} + ); + }); + + archive.pipe(res); + archive.finalize(); + + try { + res.on('finish', async () => { + bucket.forEach(async info => { + if(info.metadata.retention === 'one-time') { + await db.remove(info.metadata.sid, info.metadata.key); + } + }); + }); + } catch(e) { + console.error(e); + } + + return; + } + + // Download single file debug(`Download ${req.params.fid}`); try { const info = await store.info(req.params.fid); // throws on 404 diff --git a/lib/store.js b/lib/store.js index bc51c4d..9b13340 100644 --- a/lib/store.js +++ b/lib/store.js @@ -97,7 +97,7 @@ class Store { if(!end) end = info.size - 1; contentLength = end - start + 1 } - cb({ contentLength, metadata: info.metadata, info }); + if(cb) cb({ contentLength, metadata: info.metadata, info }); }); return fsp.createReadStream(this.getFilename(fid), {start, end}); } diff --git a/package.json b/package.json index 5b74e5b..8f396c5 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,18 @@ "name": "psitransfer", "version": "1.0.0", "description": "Open self-hoste file-sharing solution", - "keywords": ["share","upload","transfer","files","wetransfer"], + "keywords": [ + "share", + "upload", + "transfer", + "files", + "wetransfer" + ], "repository": "psi-4ward/psitransfer", "bugs": "https://github.com/psi-4ward/psitransfer/issues", "main": "app.js", "dependencies": { + "archiver": "^1.3.0", "compression": "^1.6.2", "crypto-js": "^3.1.9-1", "debug": "^2.6.0", @@ -18,8 +25,7 @@ "tusboy": "^1.1.1", "uuid": "^3.0.1" }, - "devDependencies": { - }, + "devDependencies": {}, "scripts": { "start": "NODE_ENV=production node app.js", "dev": "NODE_ENV=dev DEBUG=psitransfer:* nodemon -i app -i dist -i data app.js",