import { BusBoyFile, AssFile } from 'ass'; import fs from 'fs-extra'; import bb from 'express-busboy'; import crypto from 'crypto'; import { Router } from 'express'; import { Readable } from 'stream'; import * as data from '../data'; import { log } from '../log'; import { App } from '../app'; import { random } from '../generators'; import { UserConfig } from '../UserConfig'; import { getFileS3, uploadFileS3 } from '../s3'; import { rateLimiterMiddleware } from '../ratelimit'; const router = Router({ caseSensitive: true }); //@ts-ignore // Required since bb.extends expects express.Application, not a Router (but it still works) bb.extend(router, { upload: true, restrictMultiple: true, allowedPath: (url: string) => url === '/', limits: { fileSize: () => (UserConfig.ready ? UserConfig.config.maximumFileSize : 50) * 1000000 // MB } }); // Render or redirect router.get('/', (req, res) => UserConfig.ready ? res.render('index', { version: App.pkgVersion }) : res.redirect('/setup')); // Upload flow router.post('/', rateLimiterMiddleware("upload", UserConfig.config.rateLimit?.upload), async (req, res) => { // Check user config if (!UserConfig.ready) return res.status(500).type('text').send('Configuration missing!'); // Does the file actually exist if (!req.files || !req.files['file']) return res.status(400).type('text').send('No file was provided!'); else log.debug('Upload request received', `Using ${UserConfig.config.s3 != null ? 'S3' : 'local'} storage`); // Type-check the file data const bbFile: BusBoyFile = req.files['file']; // Prepare file move const uploads = UserConfig.config.uploadsDir; const timestamp = Date.now().toString(); const fileKey = `${timestamp}_${bbFile.filename}`; const destination = `${uploads}${uploads.endsWith('/') ? '' : '/'}${fileKey}`; // S3 configuration const s3 = UserConfig.config.s3 != null ? UserConfig.config.s3 : false; try { // Get the file size const size = (await fs.stat(bbFile.file)).size; // Get the hash const sha256 = crypto.createHash('sha256').update(await fs.readFile(bbFile.file)).digest('base64'); // * Move the file if (!s3) await fs.move(bbFile.file, destination); else await uploadFileS3(await fs.readFile(bbFile.file), fileKey, bbFile.mimetype, size, sha256); // Build ass metadata const assFile: AssFile = { fakeid: random({ length: UserConfig.config.idSize }), // todo: more generators size, sha256, fileKey, timestamp, mimetype: bbFile.mimetype, filename: bbFile.filename, uploader: '0', // todo: users save: {}, }; // Set the save location if (!s3) assFile.save.local = destination; else { // Using S3 doesn't move temp file, delete it now await fs.rm(bbFile.file); assFile.save.s3 = true; } // * Save metadata data.put('files', assFile.fakeid, assFile); log.debug('File saved to', !s3 ? assFile.save.local! : 'S3'); return res.type('json').send({ resource: `${req.ass.host}/${assFile.fakeid}` }); } catch (err) { log.error('Failed to upload file', bbFile.filename); console.error(err); return res.status(500).send(err); } }); router.get('/:fakeId', (req, res) => res.redirect(`/direct/${req.params.fakeId}`)); router.get('/direct/:fakeId', async (req, res) => { if (!UserConfig.ready) res.redirect('/setup'); // Get the ID const fakeId = req.params.fakeId; // Get the file metadata let _data; try { _data = await data.get('files', fakeId); } catch (err) { log.error('Failed to get', fakeId); console.error(err); return res.status(500).send(); } if (!_data) return res.status(404).send(); else { const meta = _data as AssFile; // File data can come from either S3 or local filesystem let output: Readable | NodeJS.ReadableStream; // Try to retrieve the file if (!!meta.save.s3) { const file = await getFileS3(meta.fileKey); if (!file.Body) return res.status(500).send('Unknown error'); output = file.Body as Readable; } else output = fs.createReadStream(meta.save.local!); // Configure response headers res.type(meta.mimetype) .header('Content-Disposition', `inline; filename="${meta.filename}"`) .header('Cache-Control', 'public, max-age=31536000, immutable') .header('Accept-Ranges', 'bytes'); // Send the file (thanks to https://stackoverflow.com/a/67373050) output.pipe(res); } }); export { router };