import config from "config"; import {NextFunction, Request, Response} from "express"; import formidable from "formidable"; import * as fs from "fs"; import path from "path"; import {RequireAuthMiddleware, RequireRequestAuthMiddleware} from "swaf/auth/AuthComponent"; import {route} from "swaf/common/Routing"; import Controller from "swaf/Controller"; import FileUploadMiddleware from "swaf/FileUploadMiddleware"; import {BadRequestError, ForbiddenHttpError, ServerError} from "swaf/HttpError"; import {logger} from "swaf/Logger"; import FileModel from "../models/FileModel.js"; import generateSlug from "../SlugGenerator.js"; export default class FileController extends Controller { public routes(): void { this.get('/files/:page([0-9]+)?', this.getFileUploader, 'file-uploader', RequireAuthMiddleware); this.get('/files/upload-script', this.downloadLinuxScript, 'file-linux-script'); this.post('/files/upload', this.postFileFrontend, 'post-file-frontend', RequireAuthMiddleware, FileUploadFormMiddleware); this.post('/files/delete/:slug', FileController.deleteFileRoute, 'delete-file-frontend', RequireAuthMiddleware); } protected async getFileUploader(req: Request, res: Response): Promise { const allowedDomains = config.get('allowed_url_domains'); const user = req.as(RequireAuthMiddleware).getUser(); const files = await FileModel.paginateForUser(req, 25, user.getOrFail('id')); res.render('file-uploader', { max_upload_size: config.get('max_upload_size'), allowed_domains: allowedDomains, default_domain: allowedDomains[config.get('default_url_domain_for_files')], files: files.map(file => ({ ...file, shouldBeDeleted: file.shouldBeDeleted(), url: file.getURL(), expires_at: file.expires_at?.toISOString(), })), pagination: files.pagination?.serialize(), }); } protected async downloadLinuxScript(req: Request, res: Response): Promise { res.download(path.resolve(__dirname, '..', 'assets/files/upload_file.sh'), 'upload_file.sh'); } protected async postFileFrontend(req: Request, res: Response): Promise { req.body.type = 'file'; await FileController.handleFileUpload( req.body.autogen_url === undefined && req.body.slug ? req.body.slug : await generateSlug(10), req, res, ); } public static async handleFileUpload( slug: string, req: Request, res: Response, requestAuth: boolean = false, ): Promise { // Check for file upload if (Object.keys(req.files).indexOf('upload') < 0) { throw new BadRequestError('No file received.', 'You must upload exactly one (1) file.', req.url); } const upload = req.files['upload']; if (Array.isArray(upload)) { throw new BadRequestError( 'Uploading multiple files at once is unsupported.', 'Please only upload one file at a time.', req.url, ); } // TTL let ttl = config.get('default_file_ttl'); if (req.body.never_expire !== undefined) ttl = 0; if (req.body.ttl !== undefined) ttl = parseInt(req.body.ttl); else if (req.body.expire_after_days !== undefined) ttl = parseInt(req.body.expire_after_days) * 24 * 3600; const user = (requestAuth ? req.as(RequireRequestAuthMiddleware) : req.as(RequireAuthMiddleware)).getUser(); const file = FileModel.create({ user_id: user.id, slug: slug, real_name: upload.originalFilename, storage_type: 'local', storage_path: 'storage/uploads/' + slug, size: upload.size, expires_at: ttl <= 0 ? null : new Date(Date.now() + ttl * 1000), }); await file.save(); fs.renameSync(upload.filepath, file.getOrFail('storage_path')); const domain = req.body.url_domain || config.get('allowed_url_domains')[config.get('default_url_domain_for_files')]; res.format({ json: () => res.json({ url: file.getURL(domain), }), text: () => res.send(file.getURL(domain)), html: () => { req.flash('success', 'Upload success!'); req.flash('url', file.getURL(domain)); res.redirect(route('file-uploader')); }, }); } public static async deleteFileRoute( req: Request, res: Response, next: NextFunction, requestAuth: boolean = false, ): Promise { const slug = req.params.slug; if (!slug) throw new BadRequestError('Cannot delete nothing.', 'Please provide a slug.', req.url); const file = await FileModel.getBySlug(req.params.slug); if (!file) return next(); const user = (requestAuth ? req.as(RequireRequestAuthMiddleware) : req.as(RequireAuthMiddleware)).getUser(); if (!file.canDelete(user.getOrFail('id'))) throw new ForbiddenHttpError('file', req.url); switch (file.storage_type) { case 'local': await FileController.deleteFile(file); break; default: throw new ServerError(`This file cannot be deleted. Deletion protocol for ${file.storage_type} storage type not implemented.`); } res.format({ json: () => res.json({ status: 'success', }), text: () => res.send('success'), html: () => { req.flash('success', 'Successfully deleted file.'); res.redirect(route('file-uploader')); }, }); } public static async deleteFile(file: FileModel): Promise { fs.unlinkSync(file.getOrFail('storage_path')); await file.delete(); logger.info('Deleted', file.storage_path, `(${file.real_name})`); } } export class FileUploadFormMiddleware extends FileUploadMiddleware { protected getDefaultField(): string { return 'upload'; } protected getFormidableOptions(): formidable.Options { return { uploadDir: 'storage/tmp', maxFileSize: config.get('max_upload_size') * 1024 * 1024, }; } }