167 lines
6.0 KiB
JavaScript
167 lines
6.0 KiB
JavaScript
'use strict';
|
|
const express = require('express');
|
|
const config = require('../config');
|
|
const auth = require('./auth');
|
|
const settings = require('./settings');
|
|
const mime = require('./mime');
|
|
const utils = require('./utils');
|
|
const { pipeArchive } = require('./archive');
|
|
|
|
/**
|
|
* Admin area: manage the uploaded files and the settings (name, logo).
|
|
* Mounted at <baseUrl>admin, only available when adminPass is set.
|
|
*/
|
|
module.exports = function createAdmin({ db, store, renderPage }) {
|
|
const router = express.Router();
|
|
|
|
// not configured: there is no admin area
|
|
router.use((req, res, next) => config.adminPass ? next() : next('router'));
|
|
|
|
router.get('/', (req, res) => {
|
|
res.send(renderPage(req, 'admin'));
|
|
});
|
|
|
|
const guard = (req, res, next) => auth.isAdmin(req) ? next() : auth.deny(req, res);
|
|
|
|
function fileInfo(sid, file) {
|
|
const m = file.metadata;
|
|
const oneTime = m.retention === 'one-time';
|
|
return {
|
|
key: file.key,
|
|
name: m.name,
|
|
comment: m.comment || '',
|
|
size: file.isPartial ? +m.uploadLength : file.size,
|
|
// unfinished uploads, they use disk space as well
|
|
partial: !!file.isPartial,
|
|
retention: m.retention,
|
|
createdAt: +m.createdAt,
|
|
lastDownload: m.lastDownload ? +m.lastDownload : null,
|
|
hasPassword: !!m.password,
|
|
internalBypass: m.internalBypass === 'true',
|
|
expiresAt: db.expiresAt(file),
|
|
// previews of one-time files would use them up
|
|
previewType: oneTime || file.isPartial ? null : mime.previewKind(m.name),
|
|
url: `${ config.baseUrl }files/${ sid }++${ file.key }`,
|
|
thumbUrl: m.thumb && !oneTime ? `${ config.baseUrl }files/${ sid }++${ file.key }.thumb` : null,
|
|
};
|
|
}
|
|
|
|
router.get('/data.json', guard, (req, res) => {
|
|
const buckets = Object.keys(db.db)
|
|
.map(sid => ({ sid, files: db.db[sid].map(file => fileInfo(sid, file)) }))
|
|
.filter(bucket => bucket.files.length > 0);
|
|
res.set('Cache-Control', 'no-store');
|
|
res.json({
|
|
buckets,
|
|
now: Date.now(),
|
|
totalSize: buckets.reduce((sum, b) => sum + b.files.reduce((s, f) => s + (f.size || 0), 0), 0),
|
|
});
|
|
});
|
|
|
|
// Browsers can not send a header with a plain download link, so the page asks
|
|
// for a link which is valid for one minute.
|
|
router.post('/download-link', guard, (req, res) => {
|
|
const { sid, key, format } = req.body || {};
|
|
if (!utils.isSafeBucketFid(sid)) return res.status(400).json({ message: 'Invalid bucket id' });
|
|
const files = (db.get(sid) || []).filter(file => !file.isPartial);
|
|
if (!files.length) return res.status(404).json({ message: 'Bucket not found' });
|
|
if (key) {
|
|
if (!files.some(file => file.key === key)) return res.status(404).json({ message: 'File not found' });
|
|
} else if (!['zip', 'tar.gz'].includes(format)) {
|
|
return res.status(400).json({ message: 'Invalid format' });
|
|
}
|
|
const { token } = auth.issue({ sid, key: key || null, format: key ? null : format }, 60, 'admin-download');
|
|
res.json({ url: `${ config.baseUrl }admin/dl?t=${ encodeURIComponent(token) }` });
|
|
});
|
|
|
|
// Downloads of the admin neither consume one-time files nor count as download
|
|
router.get('/dl', (req, res) => {
|
|
const data = auth.verify(String(req.query.t || ''), 'admin-download');
|
|
if (!data) return res.status(403).send('Link expired');
|
|
const files = (db.get(data.sid) || []).filter(file => !file.isPartial);
|
|
res.set('Cache-Control', 'no-store, no-transform');
|
|
|
|
if (data.key) {
|
|
const file = files.find(f => f.key === data.key);
|
|
if (!file) return res.status(404).send('File not found');
|
|
res.set('Content-Disposition', utils.contentDisposition(file.metadata.name, file.key));
|
|
return res.sendFile(store.getFilename(`${ data.sid }++${ file.key }`), err => {
|
|
if (err && !res.headersSent) res.status(404).end();
|
|
});
|
|
}
|
|
|
|
if (!files.length) return res.status(404).send('Bucket not found');
|
|
res.set('Content-Disposition', utils.contentDisposition(`${ data.sid }.${ data.format }`, 'files'));
|
|
return pipeArchive(res, files, data.format, store);
|
|
});
|
|
|
|
router.delete('/buckets/:sid', guard, async (req, res, next) => {
|
|
try {
|
|
if (!utils.isSafeBucketFid(req.params.sid)) return res.status(400).json({ message: 'Invalid bucket id' });
|
|
const removed = await db.removeBucket(req.params.sid);
|
|
if (!removed) return res.status(404).json({ message: 'Bucket not found' });
|
|
res.json({ removed });
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
});
|
|
|
|
router.delete('/buckets/:sid/files/:key', guard, async (req, res, next) => {
|
|
try {
|
|
const { sid, key } = req.params;
|
|
if (!utils.isSafeBucketFid(sid) || !utils.UUID_V4_RE.test(key)) {
|
|
return res.status(400).json({ message: 'Invalid id' });
|
|
}
|
|
if (!(db.get(sid) || []).some(file => file.key === key)) {
|
|
return res.status(404).json({ message: 'File not found' });
|
|
}
|
|
await db.remove(sid, key);
|
|
res.json({ removed: 1 });
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
});
|
|
|
|
// Settings: name, logo, footer
|
|
router.get('/settings', guard, (req, res) => {
|
|
res.set('Cache-Control', 'no-store');
|
|
res.json({ brand: settings.brand(), defaults: settings.defaults(), maxLogoSize: settings.MAX_LOGO_SIZE });
|
|
});
|
|
|
|
router.put('/settings', guard, (req, res, next) => {
|
|
try {
|
|
res.json({ brand: settings.update(req.body || {}) });
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
});
|
|
|
|
router.post('/settings/logo', guard,
|
|
express.raw({ type: () => true, limit: settings.MAX_LOGO_SIZE }),
|
|
(req, res, next) => {
|
|
try {
|
|
res.json({ brand: settings.setLogo(req.body) });
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
}
|
|
);
|
|
|
|
router.delete('/settings/logo', guard, (req, res, next) => {
|
|
try {
|
|
res.json({ brand: settings.clearLogo() });
|
|
} catch (e) {
|
|
next(e);
|
|
}
|
|
});
|
|
|
|
router.use((err, req, res, next) => {
|
|
if (res.headersSent) return next(err);
|
|
const status = err.status || err.statusCode || 500;
|
|
if (status >= 500) console.error(err);
|
|
res.status(status).json({ message: status >= 500 ? 'Internal error' : err.message });
|
|
});
|
|
|
|
return router;
|
|
};
|