import path from 'path' import sharp from 'sharp' import fs from 'fs/promises' import config from '../config.mjs' import { HttpError, CorsHandler } from 'flaska' import * as security from './security.mjs' import * as formidable from './formidable.mjs' export default class MediaRoutes { constructor(opts = {}) { Object.assign(this, { security: opts.security || security, formidable: opts.formidable || formidable, sharp: opts.sharp || sharp, fs: opts.fs || fs, }) this.siteCache = new Map() this.collator = new Intl.Collator('is-IS', { numeric: false, sensitivity: 'accent' }) this.cors = CorsHandler({ allowedOrigins: ['*'] }) } register(server) { this.init(server).then(function() {}, function(err) { server.core.log.error(err, 'Error initing media') }) server.flaska.get('/media', [server.queryHandler()], this.listFiles.bind(this)) server.flaska.get('/media/:site', this.listPublicFiles.bind(this)) server.flaska.post('/media', [server.queryHandler()], this.upload.bind(this)) server.flaska.post('/media/noprefix', [server.queryHandler()], this.uploadNoPrefix.bind(this)) server.flaska.post('/media/resize', [server.queryHandler()], this.resize.bind(this)) server.flaska.post('/media/resize/:filename', [server.queryHandler(), server.jsonHandler()], this.resizeExisting.bind(this)) server.flaska.delete('/media/:filename', [server.queryHandler()], this.remove.bind(this)) server.flaska.options('/::path', [server.queryHandler(), this.security.verifyCorsEnabled], this.cors) } init(server) { let folders = Object.keys(config.get('sites')) return Promise.all(folders.map(folder => { return fs.readdir(config.get('uploadFolder') + '/' + folder) .then(files => { return files.map(file => { return { filename: file, size: null } }) }) .catch(err => { server.core.log.error(err, `Error reading folder "${config.get('uploadFolder')}/${folder}"`) return [] }) .then(files => { this.siteCache.set(folder, files) }) })) } filesCacheGet(site) { let files = this.siteCache.get(site) || [] return files.sort((a, b) => { return this.collator.compare(a.filename, b.filename) }) } filesCacheAdd(site, filename, size) { let arr = this.siteCache.get(site) if (!arr) { this.siteCache.set(site, arr = []) } let found = false for (let file of arr) { if (file.filename === filename) { found = true file.size = size break } } if (!found) { arr.push({ filename: filename, size: size }) } } filesCacheRemove(site, filename) { let arr = this.siteCache.get(site) if (!arr) return for (let i = 0; i < arr.length; i++) { if (arr[i].filename === filename) { arr.splice(i, 1) break } } } getSite(ctx) { let site = this.security.verifyToken(ctx) if (this.security.hasCors(site)) { this.cors(ctx) } return site } async listFiles(ctx) { let site = this.getSite(ctx) ctx.body = this.filesCacheGet(site) } async listPublicFiles(ctx) { this.security.throwIfNotPublic(ctx.params.site) if (this.security.hasCors(ctx.params.site)) { this.cors(ctx) } ctx.body = this.filesCacheGet(ctx.params.site) } async upload(ctx, noprefix = false) { let site = this.getSite(ctx) ctx.state.site = site let result = await this.formidable.uploadFile(ctx, ctx.state.site, noprefix) ctx.log.info(`Uploaded ${result.filename}`) let stat = await this.fs.stat(`${config.get('uploadFolder')}/${ctx.state.site}/${result.filename}`) this.filesCacheAdd(ctx.state.site, result.filename, stat.size) ctx.body = { filename: result.filename, path: `/${ctx.state.site}/${result.filename}` } } uploadNoPrefix(ctx) { return this.upload(ctx, true) } async resizeFile(ctx, sourceFile) { this.security.verifyBody(ctx) let keys = Object.keys(ctx.req.body) let allowedOperations = [ 'trim', 'flatten', 'resize', 'blur', 'extend', ] await Promise.all(keys.filter(key => ctx.req.body[key]).map(key => { return Promise.resolve() .then(async () => { let item = ctx.req.body[key] let sharp = this.sharp(`${config.get('uploadFolder')}/${ctx.state.site}/${sourceFile}`) .rotate() for (let operation of allowedOperations) { if (item[operation] != null) { sharp = sharp[operation](item[operation]) } } sharp = sharp[item.format](item[item.format]) let target = sourceFile if (path.extname(target).length > 0) { target = target.slice(0, -path.extname(target).length) } target += `_${key}.${item.format.replace('jpeg', 'jpg')}` if (item.out === 'base64') { let buffer = await sharp.toBuffer() ctx.body[key] = { base64: `data:image/${item.format};base64,` + buffer.toString('base64'), } return } await sharp.toFile(`${config.get('uploadFolder')}/${ctx.state.site}/${target}`) let stat = await this.fs.stat(`${config.get('uploadFolder')}/${ctx.state.site}/${target}`) this.filesCacheAdd(ctx.state.site, target, stat.size) ctx.body[key] = { filename: target, path: `/${ctx.state.site}/${target}`, } }).then( function() {}, function(err) { throw new HttpError(422, `Error processing ${key}: ${err.message}`) } ) })) } async resizeExisting(ctx) { let site = this.getSite(ctx) ctx.state.site = site ctx.body = {} await this.resizeFile(ctx, ctx.params.filename) } async resize(ctx) { await this.upload(ctx) await this.resizeFile(ctx, ctx.body.filename) } async remove(ctx) { let site = this.getSite(ctx) this.filesCacheRemove(site, ctx.params.filename) let root = path.join(config.get('uploadFolder'), site) var unlinkPath = path.join(root, decodeURIComponent(ctx.params.filename)) if (unlinkPath.indexOf(root) !== 0) { throw new HttpError(403, `Error removing ${unlinkPath}: Traversing folder is not allowed`) } await this.fs.unlink(unlinkPath) .catch(function(err) { throw new HttpError(422, `Error removing ${unlinkPath}: ${err.message}`) }) ctx.status = 204 } }