Merge feature-download-zip

This commit is contained in:
Christoph Wiechert
2017-05-08 14:18:50 +02:00
5 changed files with 104 additions and 11 deletions

View File

@@ -87,6 +87,7 @@ DEBUG=psitransfer:* npm start
## Side notes
* There is no (end-to-end) payload encryption (yet).
* `Download all as ZIP` does not support resuming the download.
:star2: Contribution is highly welcome :metal:

View File

@@ -19,7 +19,16 @@
i.fa.fa-key
| decrypt
.panel.panel-primary(v-if='!needsPassword')
.panel-heading Files
.panel-heading
strong Files
div.pull-right(style="margin-top:-5px;")
span.btn-group(v-if="downloadsAvailable")
a.btn.btn-sm.btn-default(@click="downloadAll('zip')", title="Archive download is not resumeable!")
i.fa.fa-fw.fa-fw.fa-download
| zip
a.btn.btn-sm.btn-default(@click="downloadAll('tar.gz')", title="Archive download is not resumeable!")
i.fa.fa-fw.fa-fw.fa-download
| tar.gz
.panel-body
table.table.table-hover.table-striped(style='margin-bottom: 0')
tbody
@@ -32,10 +41,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 }}
</template>
@@ -44,6 +51,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 +59,7 @@
export default {
name: 'app',
components: { FileIcon, Clipboard },
data () {
return {
files: [],
@@ -63,6 +72,13 @@
host: document.location.protocol + '//' + document.location.host
}
},
computed: {
downloadsAvailable: function() {
return this.files.filter(f => !f.downloaded || f.metadata.retention !== 'one-time').length > 0
}
},
methods: {
download(file) {
if(file.downloaded && file.metadata.retention === 'one-time') {
@@ -73,6 +89,20 @@
file.downloaded = true;
},
downloadAll(format) {
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() + '.' + format;
this.files.forEach(f => {
f.downloaded = true;
});
},
copied(file, $event) {
file.downloaded = $event === 'copied';
},

View File

@@ -11,7 +11,10 @@ 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 zlib = require('zlib');
const errorPage = fs.readFileSync(path.join(__dirname, '../public/html/error.html')).toString();
const store = new Store(config.uploadDir);
@@ -72,11 +75,64 @@ app.get('/:sid', (req, res, next) => {
});
// Download single file
// Download files
app.get('/files/:fid', async(req, res, next) => {
// let tusboy handle HEAD with Tus Header
// let tusboy handle HEAD requests with Tus Header
if(req.method === 'HEAD' && req.get('Tus-Resumable')) return next();
// Download all files
if(req.params.fid.match(/^[a-z0-9+]+\.(tar\.gz|zip)$/)) {
const sid = req.params.fid.split('++')[0];
const format = req.params.fid.endsWith('.zip') ? 'zip' : 'tar.gz';
const bucket = db.get(sid);
if(!bucket) return res.status(404).send(errorPage.replace('%%ERROR%%', 'Download bucket not found.'));
if(req.params.fid !== sid + '++' + MD5(bucket.map(f => f.key).join()).toString() + '.' + format) {
res.status(404).send(errorPage.replace('%%ERROR%%', 'Invalid link'));
return;
}
debug(`Download Bucket ${sid}`);
if(format === 'zip') res.header('ContentType', 'application/zip');
if(format === 'tar.gz') res.header('ContentType', 'application/x-gtar');
res.header('Content-Disposition', `attachment; filename="${sid}.${format}"`);
const archive = archiver(format === 'zip' ? 'zip' : 'tar');
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}
);
});
if(format === 'tar.gz') {
archive.pipe(zlib.createGzip()).pipe(res);
} else {
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

View File

@@ -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});
}

View File

@@ -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",