Added few more operation support in the API

dev
Jonatan Nilsson 2022-01-06 09:51:43 +00:00
parent 078f6c518c
commit 1abe64cb09
6 changed files with 238 additions and 170 deletions

View File

@ -114,6 +114,14 @@ export default class MediaRoutes {
let keys = Object.keys(ctx.req.body) let keys = Object.keys(ctx.req.body)
let allowedOperations = [
'trim',
'flatten',
'resize',
'blur',
'extend',
]
await Promise.all(keys.map(key => { await Promise.all(keys.map(key => {
return Promise.resolve() return Promise.resolve()
.then(async () => { .then(async () => {
@ -121,8 +129,10 @@ export default class MediaRoutes {
let sharp = this.sharp(`./public/${ctx.state.site}/${ctx.body.filename}`) let sharp = this.sharp(`./public/${ctx.state.site}/${ctx.body.filename}`)
.rotate() .rotate()
if (item.resize) { for (let operation of allowedOperations) {
sharp = sharp.resize(item.resize) if (item[operation] != null) {
sharp = sharp[operation](item[operation])
}
} }
sharp = sharp[item.format](item[item.format]) sharp = sharp[item.format](item[item.format])

View File

@ -32,6 +32,17 @@ export function throwIfNotPublic(site) {
} }
} }
const validObjectOperations = [
'resize',
'extend',
'flatten',
]
const validNumberOperations = [
'blur',
'trim',
]
export function verifyBody(ctx) { export function verifyBody(ctx) {
let keys = Object.keys(ctx.req.body) let keys = Object.keys(ctx.req.body)
@ -49,7 +60,7 @@ export function verifyBody(ctx) {
if (typeof(item.format) !== 'string' if (typeof(item.format) !== 'string'
|| !item.format || !item.format
|| item.format === 'resize' || validObjectOperations.includes(item.format)
|| item.format === 'out') { || item.format === 'out') {
throw new HttpError(`Body item ${key} missing valid format`, 422) throw new HttpError(`Body item ${key} missing valid format`, 422)
} }
@ -68,10 +79,20 @@ export function verifyBody(ctx) {
} }
} }
if (item.resize != null) { for (let operation of validObjectOperations) {
if (typeof(item.resize) !== 'object' if (item[operation] != null) {
|| Array.isArray(item.resize)) { if (typeof(item[operation]) !== 'object'
throw new HttpError(`Body item ${key} key resize was invalid`, 422) || Array.isArray(item[operation])) {
throw new HttpError(`Body item ${key} key ${operation} was invalid`, 422)
}
}
}
for (let operation of validNumberOperations) {
if (item[operation] != null) {
if (typeof(item[operation]) !== 'number') {
throw new HttpError(`Body item ${key} key ${operation} was invalid`, 422)
}
} }
} }
} }

View File

@ -113,62 +113,3 @@ Client.prototype.upload = function(url, file, method = 'POST', body = {}) {
}) })
}) })
} }
/*
export function createClient(host = config.get('server:port'), opts) {
let options = defaults(opts, {})
let prefix = `http://localhost:${host}`
options.headers['x-request-id'] = 'asdf'
client.auth = (user) => {
// let m = helperDB.model('user', {
// id: user.id,
// level: (user.get && user.get('level')) || 1,
// institute_id: (user.get && user.get('institute_id')) || null,
// password: (user.get && user.get('password')) || null,
// })
// let token = jwt.createUserToken(m)
// client.headers.authorization = `Bearer ${token}`
}
// Simple wrappers to wrap into promises
client.getAsync = (path) =>
new Promise((resolve, reject) => {
if (path.slice(0, 4) === 'http') {
return client.get(path, callback(resolve, reject))
}
client.get(prefix + path, callback(resolve, reject))
})
// Simple wrappers to wrap into promises
client.saveFileAsync = (path, destination) =>
new Promise((resolve, reject) => {
client.saveFile(prefix + path, destination, callback(resolve, reject, true))
})
client.postAsync = (path, data) =>
new Promise((resolve, reject) => {
client.post(prefix + path, data, callback(resolve, reject))
})
client.putAsync = (path, data) =>
new Promise((resolve, reject) => {
client.put(prefix + path, data, callback(resolve, reject))
})
client.deleteAsync = (path) =>
new Promise((resolve, reject) => {
client.del(prefix + path, callback(resolve, reject))
})
client.sendFileAsync = (path, files, data) =>
new Promise((resolve, reject) => {
client.sendFile(prefix + path, files, data || {}, callback(resolve, reject))
})
return client
}
*/

View File

@ -278,6 +278,10 @@ t.describe('Media (API)', () => {
resize: { resize: {
width: 300, width: 300,
}, },
blur: 0.5,
flatten: {r:0,g:0,b:0},
trim: 1,
extend: { top: 10, left: 10, bottom: 10, right: 10, background: {r:0,g:0,b:0} },
jpeg: { jpeg: {
quality: 80, quality: 80,
mozjpeg: true, mozjpeg: true,
@ -324,8 +328,8 @@ t.describe('Media (API)', () => {
assert.strictEqual(img.format, 'png') assert.strictEqual(img.format, 'png')
img = await sharp(resolve(`../../public/${data.test1.path}`)).metadata() img = await sharp(resolve(`../../public/${data.test1.path}`)).metadata()
assert.strictEqual(img.width, 300) assert.strictEqual(img.width, 320)
assert.strictEqual(img.height, 350) assert.strictEqual(img.height, 413)
assert.strictEqual(img.format, 'jpeg') assert.strictEqual(img.format, 'jpeg')
img = await sharp(resolve(`../../public/${data.test2.path}`)).metadata() img = await sharp(resolve(`../../public/${data.test2.path}`)).metadata()

View File

@ -285,6 +285,10 @@ t.describe('#resize', function() {
const stubUpload = stub() const stubUpload = stub()
const stubSharp = stub() const stubSharp = stub()
const stubSharpResize = stub() const stubSharpResize = stub()
const stubSharpBlur = stub()
const stubSharpTrim = stub()
const stubSharpExtend = stub()
const stubSharpFlatten = stub()
const stubSharpRotate = stub() const stubSharpRotate = stub()
const stubSharpToFile = stub() const stubSharpToFile = stub()
const stubSharpJpeg = stub() const stubSharpJpeg = stub()
@ -311,6 +315,10 @@ t.describe('#resize', function() {
jpeg: stubSharpJpeg, jpeg: stubSharpJpeg,
png: stubSharpPng, png: stubSharpPng,
resize: stubSharpResize, resize: stubSharpResize,
trim: stubSharpTrim,
extend: stubSharpExtend,
flatten: stubSharpFlatten,
blur: stubSharpBlur,
rotate: stubSharpRotate, rotate: stubSharpRotate,
toBuffer: stubSharpToBuffer, toBuffer: stubSharpToBuffer,
} }
@ -318,17 +326,11 @@ t.describe('#resize', function() {
stubStat.resolves({ size: 0 }) stubStat.resolves({ size: 0 })
stubSharp.reset() stubSharp.reset()
stubSharp.returns(def) stubSharp.returns(def)
stubSharpToFile.reset() for (let key in def) {
def[key].reset()
def[key].returns(def)
}
stubSharpToFile.resolves() stubSharpToFile.resolves()
stubSharpJpeg.reset()
stubSharpJpeg.returns(def)
stubSharpResize.reset()
stubSharpResize.returns(def)
stubSharpRotate.reset()
stubSharpRotate.returns(def)
stubSharpPng.reset()
stubSharpPng.returns(def)
stubSharpToBuffer.reset()
stubSharpToBuffer.resolves() stubSharpToBuffer.resolves()
routes.siteCache.clear() routes.siteCache.clear()
} }
@ -381,53 +383,73 @@ t.describe('#resize', function() {
assert.strictEqual(ctx.body[assertKey].path, `/${assertSite}/${assertFilename}_${assertKey}.jpg`) assert.strictEqual(ctx.body[assertKey].path, `/${assertSite}/${assertFilename}_${assertKey}.jpg`)
}) })
t.test('should call sharp correctly if items and resize are specified', async function() { let validOperations = [
reset() 'resize',
const assertKey = 'herpderp' 'blur',
const assertPng = { a: 1 } 'trim',
const assertResize = { a: 1 } 'extend',
const assertFilename = 'asdfsafd' 'flatten',
const assertSite = 'mario' ]
stubVerifyToken.resolves(assertSite)
stubUpload.resolves({ filename: assertFilename + '.png' })
let called = 0 let operationStubMap = {
stubStat.returnWith(function() { resize: stubSharpResize,
called += 10 blur: stubSharpBlur,
return { size: called } trim: stubSharpTrim,
extend: stubSharpExtend,
flatten: stubSharpFlatten,
}
validOperations.forEach(function(operation) {
t.test(`should call sharp correctly if items and ${operation} are specified`, async function() {
reset()
const assertKey = 'herpderp'
const assertPng = { a: 1 }
const assertPayload = { a: 1 }
const assertFilename = 'asdfsafd'
const assertSite = 'mario'
stubVerifyToken.resolves(assertSite)
stubUpload.resolves({ filename: assertFilename + '.png' })
let called = 0
stubStat.returnWith(function() {
called += 10
return { size: called }
})
let ctx = createContext({ req: { body: {
[assertKey]: {
format: 'png',
}
}}})
ctx.req.body[assertKey].png = assertPng
ctx.req.body[assertKey][operation] = assertPayload
await routes.resize(ctx)
assert.ok(stubSharp.called)
assert.match(stubSharp.firstCall[0], new RegExp(`\/${assertSite}\/${assertFilename}\.png`))
assert.ok(stubSharpRotate.called)
assert.ok(stubSharpPng.called)
assert.strictEqual(stubSharpPng.firstCall[0], assertPng)
assert.ok(stubSharpToFile.called)
assert.match(stubSharpToFile.firstCall[0], new RegExp(`\/${assertSite}\/${assertFilename}_${assertKey}\.png`))
assert.notOk(stubSharpJpeg.called)
assert.ok(operationStubMap[operation].called)
assert.strictEqual(operationStubMap[operation].firstCall[0], assertPayload)
assert.strictEqual(ctx.body.filename, assertFilename + '.png')
assert.strictEqual(ctx.body.path, `/${assertSite}/${assertFilename}.png`)
assert.strictEqual(ctx.body[assertKey].filename, `${assertFilename}_${assertKey}\.png`)
assert.strictEqual(ctx.body[assertKey].path, `/${assertSite}/${assertFilename}_${assertKey}.png`)
let filesFromCache = routes.filesCacheGet(assertSite)
assert.strictEqual(filesFromCache.length, 2)
assert.strictEqual(filesFromCache[0].filename, `${assertFilename}_${assertKey}\.png`)
assert.strictEqual(filesFromCache[0].size, 20)
assert.strictEqual(filesFromCache[1].filename, assertFilename + '.png')
assert.strictEqual(filesFromCache[1].size, 10)
}) })
let ctx = createContext({ req: { body: {
[assertKey]: {
format: 'png',
}
}}})
ctx.req.body[assertKey].png = assertPng
ctx.req.body[assertKey].resize = assertResize
await routes.resize(ctx)
assert.ok(stubSharp.called)
assert.match(stubSharp.firstCall[0], new RegExp(`\/${assertSite}\/${assertFilename}\.png`))
assert.ok(stubSharpRotate.called)
assert.ok(stubSharpPng.called)
assert.strictEqual(stubSharpPng.firstCall[0], assertPng)
assert.ok(stubSharpToFile.called)
assert.match(stubSharpToFile.firstCall[0], new RegExp(`\/${assertSite}\/${assertFilename}_${assertKey}\.png`))
assert.notOk(stubSharpJpeg.called)
assert.ok(stubSharpResize.called)
assert.strictEqual(stubSharpResize.firstCall[0], assertResize)
assert.strictEqual(ctx.body.filename, assertFilename + '.png')
assert.strictEqual(ctx.body.path, `/${assertSite}/${assertFilename}.png`)
assert.strictEqual(ctx.body[assertKey].filename, `${assertFilename}_${assertKey}\.png`)
assert.strictEqual(ctx.body[assertKey].path, `/${assertSite}/${assertFilename}_${assertKey}.png`)
let filesFromCache = routes.filesCacheGet(assertSite)
assert.strictEqual(filesFromCache.length, 2)
assert.strictEqual(filesFromCache[0].filename, `${assertFilename}_${assertKey}\.png`)
assert.strictEqual(filesFromCache[0].size, 20)
assert.strictEqual(filesFromCache[1].filename, assertFilename + '.png')
assert.strictEqual(filesFromCache[1].size, 10)
}) })
t.test('should notify which item failed if one fails', async function() { t.test('should notify which item failed if one fails', async function() {

View File

@ -323,58 +323,128 @@ t.describe('#verifyBody()', function() {
}) })
}) })
t.test('should allow empty value or object in resize', function() { let validObjectOperations = [
let ctx = createContext({ req: { body: { 'resize',
item: { 'extend',
format: 'test', 'flatten',
test: {}, ]
}
} } })
let tests = [ validObjectOperations.forEach(function(operation) {
[undefined, 'undefined'], t.test(`should allow empty value or object in ${operation}`, function() {
[null, 'null'], let ctx = createContext({ req: { body: {
[{}, 'object'], item: {
] format: 'test',
test: {},
tests.forEach(function (check) { }
ctx.req.body.item.resize = check[0] } } })
assert.doesNotThrow(function() { let tests = [
verifyBody(ctx) [undefined, 'undefined'],
}, `should not throw with ${check[1]} in resize`) [null, 'null'],
[{}, 'object'],
]
tests.forEach(function (check) {
ctx.req.body.item[operation] = check[0]
assert.doesNotThrow(function() {
verifyBody(ctx)
}, `should not throw with ${check[1]} in ${operation}`)
})
})
t.test(`should fail if ${operation} if specified is invalid`, function() {
let ctx = createContext({ req: { body: {
item: {
format: 'test',
test: {},
}
} } })
let tests = [
['', 'emptystring'],
['asdf', 'string'],
[0, 'emptynumber'],
[123, 'number'],
[[], 'array'],
]
tests.forEach(function (check) {
ctx.req.body.item[operation] = check[0]
assert.throws(function() { verifyBody(ctx) }, function(err) {
assert.ok(err instanceof HttpError)
assert.ok(err instanceof Error)
assert.strictEqual(err.status, 422)
assert.match(err.message, /body/i)
assert.match(err.message, /item/i)
assert.match(err.message, new RegExp(operation))
assert.match(err.message, /valid/i)
return true
}, `should fail if body item ${operation} is ${check[1]}`)
})
}) })
}) })
t.test('should fail if resize if specified is invalid', function() { let validNumberOperations = [
let ctx = createContext({ req: { body: { 'blur',
item: { 'trim',
format: 'test', ]
test: {},
}
} } })
let tests = [ validNumberOperations.forEach(function(operation) {
['', 'emptystring'], t.test(`should allow empty value or number in ${operation}`, function() {
['asdf', 'string'], let ctx = createContext({ req: { body: {
[0, 'emptynumber'], item: {
[123, 'number'], format: 'test',
[[], 'array'], test: {},
] }
} } })
tests.forEach(function (check) {
ctx.req.body.item.resize = check[0] let tests = [
[undefined, 'undefined'],
assert.throws(function() { verifyBody(ctx) }, function(err) { [null, 'null'],
assert.ok(err instanceof HttpError) [0, 'number'],
assert.ok(err instanceof Error) [0.5, 'positive number'],
assert.strictEqual(err.status, 422) ]
assert.match(err.message, /body/i)
assert.match(err.message, /item/i) tests.forEach(function (check) {
assert.match(err.message, /resize/i) ctx.req.body.item[operation] = check[0]
assert.match(err.message, /valid/i)
return true assert.doesNotThrow(function() {
}, `should fail if body item resize is ${check[1]}`) verifyBody(ctx)
}, `should not throw with ${check[1]} in ${operation}`)
})
})
t.test(`should fail if ${operation} if specified is invalid`, function() {
let ctx = createContext({ req: { body: {
item: {
format: 'test',
test: {},
}
} } })
let tests = [
['', 'emptystring'],
['asdf', 'string'],
[{}, 'object'],
[[], 'array'],
]
tests.forEach(function (check) {
ctx.req.body.item[operation] = check[0]
assert.throws(function() { verifyBody(ctx) }, function(err) {
assert.ok(err instanceof HttpError)
assert.ok(err instanceof Error)
assert.strictEqual(err.status, 422)
assert.match(err.message, /body/i)
assert.match(err.message, /item/i)
assert.match(err.message, new RegExp(operation))
assert.match(err.message, /valid/i)
return true
}, `should fail if body item ${operation} is ${check[1]}`)
})
}) })
}) })
}) })