diff --git a/README.md b/README.md index e86149dc..42c2982b 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ It's an alternative to paid services like Dropbox, WeTransfer. * Download all files as zip/tar.gz archive * Modal-style file preview * Requires Node >=7.4 or use `--harmony-async-await` flag -* Password protected download list ([AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)) +* Password protected download list ([AES](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard)) +* `/admin` Page lists bucket information **See the blog posts about PsiTransfer: https://psi.cx/tags/PsiTransfer/ and checkout the [Documentation](https://github.com/psi-4ward/psitransfer/tree/master/docs)** diff --git a/app/src/Admin.vue b/app/src/Admin.vue new file mode 100644 index 00000000..dc694e54 --- /dev/null +++ b/app/src/Admin.vue @@ -0,0 +1,171 @@ + + + + + + diff --git a/app/src/admin.js b/app/src/admin.js new file mode 100644 index 00000000..b7149fdd --- /dev/null +++ b/app/src/admin.js @@ -0,0 +1,34 @@ +import 'babel-polyfill'; +import Vue from 'vue'; +import Admin from './Admin.vue'; + +function parseDate(str) { + if(!str) return str; + return new Date(str); +} + +function formatDate(dt) { + if(dt === null) return ""; + const f = function(d) { + return d < 10 ? '0' + d : d; + }; + return dt.getFullYear() + '-' + f(dt.getMonth() + 1) + '-' + f(dt.getDate()) + ' ' + f(dt.getHours()) + ':' + f(dt.getMinutes()); +} +function isDate(d) { + return Object.prototype.toString.call(d) === '[object Date]'; +} + +Vue.filter('date', function(val, format) { + if(!isDate(val)) { + val = parseDate(val); + } + return isDate(val) ? formatDate(val, format) : val; +}); + + +new Vue({ + el: '#admin', + render: h => h(Admin) +}); + +window.PSITRANSFER_VERSION = PSITRANSFER_VERSION; diff --git a/app/webpack.config.js b/app/webpack.config.js index 19e5c806..4453aadd 100644 --- a/app/webpack.config.js +++ b/app/webpack.config.js @@ -12,6 +12,7 @@ module.exports = { entry: { upload: './src/upload.js', download: './src/download.js', + admin: './src/admin.js', }, output: { path: path.resolve(__dirname, '../public/app'), diff --git a/config.dev.js b/config.dev.js index 5beea94a..73c8c497 100644 --- a/config.dev.js +++ b/config.dev.js @@ -14,6 +14,7 @@ module.exports = { "1209600": "2 Weeks" }, "defaultRetention": 3600, - "sslKeyFile": './tmp/cert.key', - "sslCertFile": './tmp/cert.pem', + "adminPass": "admin" + // "sslKeyFile": './tmp/cert.key', + // "sslCertFile": './tmp/cert.pem', }; diff --git a/config.js b/config.js index 7a53dadd..07be49c1 100644 --- a/config.js +++ b/config.js @@ -26,6 +26,8 @@ const config = { "2419200": "4 Weeks", "4838400": "8 Weeks" }, + // admin password, set to false to disable /admin page + "adminPass": false, "defaultRetention": 604800, // expire every file after maxAge (eg never downloaded one-time files) "maxAge": 3600*24*75, // 75 days diff --git a/lib/db.js b/lib/db.js index ec2552d1..48bd1f86 100644 --- a/lib/db.js +++ b/lib/db.js @@ -116,6 +116,15 @@ module.exports = class DB { } + async updateLastDownload(sid, key) { + debug(`Update last download ${sid}++${key}`); + const data = this.get(sid).find(item => item.key === key); + if(!data) return; + data.metadata.lastDownload = Date.now(); + await this.store.update(`${sid}++${key}`, data); + } + + get(sid) { return this.db[sid]; } diff --git a/lib/endpoints.js b/lib/endpoints.js index 333628b2..2e85b076 100644 --- a/lib/endpoints.js +++ b/lib/endpoints.js @@ -15,6 +15,7 @@ const MD5 = require("crypto-js/md5"); const debug = require('debug')('psitransfer:main'); const archiver = require('archiver'); const zlib = require('zlib'); +const _ = require('lodash'); const errorPage = fs.readFileSync(path.join(__dirname, '../public/html/error.html')).toString(); const store = new Store(config.uploadDir); @@ -54,6 +55,32 @@ app.get('/config.json', (req, res) => { }); +app.get('/admin', (req, res) => { + res.sendFile(path.join(__dirname, '../public/html/admin.html')); +}); +app.get('/admin/data.json', (req, res) => { + if(!config.adminPass || !req.get('x-passwd')) return res.status(401).send('Unauthorized'); + if(req.get('x-passwd') !== config.adminPass) return res.status(403).send('Forbidden'); + + const result = _.chain(db.db) + .cloneDeep() + .forEach(bucket => { + bucket.forEach(file => { + if(file.metadata.password) { + file.metadata._password = true; + delete file.metadata.password; + delete file.metadata.key; + delete file.key; + delete file.url; + } + }) + }) + .value(); + + res.json(result); +}); + + // List files / Download App app.get('/:sid', (req, res, next) => { if(req.url.endsWith('.json')) { @@ -128,6 +155,8 @@ app.get('/files/:fid', async(req, res, next) => { bucket.forEach(async info => { if(info.metadata.retention === 'one-time') { await db.remove(info.metadata.sid, info.metadata.key); + } else { + await db.updateLastDownload(info.metadata.sid, info.metadata.key); } }); }); @@ -145,11 +174,13 @@ app.get('/files/:fid', async(req, res, next) => { res.download(store.getFilename(req.params.fid), info.metadata.name); // remove one-time files after download - if(info.metadata.retention === 'one-time') { - res.on('finish', async () => { + res.on('finish', async () => { + if(info.metadata.retention === 'one-time') { await db.remove(info.metadata.sid, info.metadata.key); - }); - } + } else { + await db.updateLastDownload(info.metadata.sid, info.metadata.key); + } + }); } catch(e) { res.status(404).send(errorPage.replace('%%ERROR%%', e.message)); } diff --git a/lib/store.js b/lib/store.js index 9b13340e..724ee2a0 100644 --- a/lib/store.js +++ b/lib/store.js @@ -45,6 +45,13 @@ class Store { } + async update(fid, data) { + debug(`Update File ${this.getFilename(fid)}`); + await fsp.writeJson(this.getFilename(fid) + '.json', data); + return data; + } + + async info(fid) { try { const info = await fsp.readJson(this.getFilename(fid) + '.json'); diff --git a/public/html/admin.html b/public/html/admin.html new file mode 100644 index 00000000..1ad56150 --- /dev/null +++ b/public/html/admin.html @@ -0,0 +1,36 @@ + + + + + PsiTransfer Admin + + + + + + + + + + +
+

+ + PsiTransfer Admin +

+
+
+
+ + + + + + + + +