'use strict'; const config = require('../config'); const tusboy = require('tusboy').default; const express = require('express'); const morgan = require('morgan'); const compression = require('compression'); const Store = require('./store'); const uuid = require('uuid/v4'); const path = require('path'); const fs = require("fs"); const tusMeta = require('tus-metadata'); const assert = require('assert'); const AES = require("crypto-js/aes"); const debug = require('debug')('psitransfer:main'); const errorPage = fs.readFileSync(path.join(__dirname, '../public/html/error.html')).toString(); const store = new Store(config.uploadDir); const Db = require('./db'); const db = new Db(config.uploadDir, store); db.init(); const app = express(); app.disable('x-powered-by'); app.use(compression()); if(config.accessLog) { app.use(morgan(config.accessLog)); } // Static files app.use('/app', express.static(path.join(__dirname, '../public/app'))); app.use('/assets', express.static(path.join(__dirname, '../public/assets'))); // robots.txt app.get('/robots.txt', (req, res) => { res.sendFile(path.join(__dirname, '../public/robots.txt')); }); // Upload App app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../public/html/upload.html')); }); // Config app.get('/config.json', (req, res) => { res.json({ retentions: config.retentions, defaultRetention: config.defaultRetention, mailTemplate: config.mailTemplate }); }); // List files / Download App app.get('/: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(); res.json(db.get(sid).map(data => { const item = Object.assign(data, {url: `/files/${sid}++${data.key}`}); if(item.metadata.password) { return AES.encrypt(JSON.stringify(data), item.metadata.password).toString(); } else { return item; } })); } else { if(!db.get(req.params.sid)) return next(); res.sendFile(path.join(__dirname, '../public/html/download.html')); } }); // Download single file 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(); 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); // remove one-time files after download if(info.metadata.retention === 'one-time') { res.on('finish', async () => { await db.remove(info.metadata.sid, info.metadata.key); }); } } catch(e) { res.status(404).send(errorPage.replace('%%ERROR%%', e.message)); } }); // Upload file app.use('/files', function(req, res, next) { if(req.method === 'GET') return res.status(405).end(); if(req.method === 'POST') { // validate meta-data // !! tusMeta.encode supports only strings !! const meta = tusMeta.decode(req.get('Upload-Metadata')); try { assert(meta.name, 'tus meta prop missing: name'); 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(',')}]`); meta.key = uuid(); meta.createdAt = Date.now().toString(); // store changed metadata for tusboy req.headers['upload-metadata'] = tusMeta.encode(meta); // for tusboy getKey() req.FID = meta.sid + '++' + meta.key; db.add(meta.sid, meta.key, { "isPartial": true, metadata: meta }); } catch(e) { return res.status(400).end(e.message); } } next(); }, // let tusboy handle the upload tusboy(store, { getKey: req => req.FID, afterComplete: (req, upload, fid) => { db.add(upload.metadata.sid, upload.metadata.key, upload); debug(`Completed upload ${fid}, size=${upload.size} name=${upload.metadata.name}`); }, }) ); app.use((req, res, next) => { res.status(404).send(errorPage.replace('%%ERROR%%', 'Download bucket not found.')); }); module.exports = app;