storage-upload/api/media/routes.mjs
Jonatan Nilsson afde7fb89a
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded
cors: Add proper full cors support to all the media routes
2023-11-15 13:21:22 +00:00

230 lines
6.4 KiB
JavaScript

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
}
}