mirror of
https://github.com/TryGhost/Ghost.git
synced 2023-12-13 21:00:40 +01:00
Combined /images* endpoints into /images/upload
- refs #10438 - /images/upload now accepts all the image uploads and distinguishes their purpuse using new `purpose` form data field
This commit is contained in:
parent
4f9e687f62
commit
f558b58c89
11 changed files with 157 additions and 47 deletions
|
@ -6,7 +6,10 @@ module.exports = {
|
|||
debug('upload');
|
||||
|
||||
return frame.response = {
|
||||
url: mapper.mapImage(path)
|
||||
images: [{
|
||||
url: mapper.mapImage(path),
|
||||
ref: frame.data.ref || null
|
||||
}]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
81
core/server/api/v2/utils/validators/input/images.js
Normal file
81
core/server/api/v2/utils/validators/input/images.js
Normal file
|
@ -0,0 +1,81 @@
|
|||
const jsonSchema = require('../utils/json-schema');
|
||||
const config = require('../../../../../config');
|
||||
const common = require('../../../../../lib/common');
|
||||
const imageLib = require('../../../../../lib/image');
|
||||
|
||||
const profileImage = (frame) => {
|
||||
return imageLib.imageSize.getImageSizeFromPath(frame.file.path).then((response) => {
|
||||
// save the image dimensions in new property for file
|
||||
frame.file.dimensions = response;
|
||||
|
||||
// CASE: file needs to be a square
|
||||
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.images.isNotSquare')
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const icon = (frame) => {
|
||||
const iconExtensions = (config.get('uploads').icons && config.get('uploads').icons.extensions) || [];
|
||||
|
||||
const validIconFileSize = (size) => {
|
||||
return (size / 1024) <= 100;
|
||||
};
|
||||
|
||||
// CASE: file should not be larger than 100kb
|
||||
if (!validIconFileSize(frame.file.size)) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.icons.invalidFile', {extensions: iconExtensions})
|
||||
}));
|
||||
}
|
||||
|
||||
return imageLib.blogIcon.getIconDimensions(frame.file.path).then((response) => {
|
||||
// save the image dimensions in new property for file
|
||||
frame.file.dimensions = response;
|
||||
|
||||
// CASE: file needs to be a square
|
||||
if (frame.file.dimensions.width !== frame.file.dimensions.height) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.icons.invalidFile', {extensions: iconExtensions})
|
||||
}));
|
||||
}
|
||||
|
||||
// CASE: icon needs to be bigger than or equal to 60px
|
||||
// .ico files can contain multiple sizes, we need at least a minimum of 60px (16px is ok, as long as 60px are present as well)
|
||||
if (frame.file.dimensions.width < 60) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.icons.invalidFile', {extensions: iconExtensions})
|
||||
}));
|
||||
}
|
||||
|
||||
// CASE: icon needs to be smaller than or equal to 1000px
|
||||
if (frame.file.dimensions.width > 1000) {
|
||||
return Promise.reject(new common.errors.ValidationError({
|
||||
message: common.i18n.t('errors.api.icons.invalidFile', {extensions: iconExtensions})
|
||||
}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
upload(apiConfig, frame) {
|
||||
return Promise.resolve()
|
||||
.then(() => {
|
||||
const schema = require('./schemas/images-upload');
|
||||
const definitions = require('./schemas/images');
|
||||
return jsonSchema.validate(schema, definitions, frame.data);
|
||||
})
|
||||
.then(() => {
|
||||
if (frame.data.purpose === 'profile_image') {
|
||||
return profileImage(frame);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (frame.data.purpose === 'icon') {
|
||||
return icon(frame);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
|
@ -21,5 +21,9 @@ module.exports = {
|
|||
|
||||
get users() {
|
||||
return require('./users');
|
||||
},
|
||||
|
||||
get images() {
|
||||
return require('./images');
|
||||
}
|
||||
};
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "images.upload",
|
||||
"title": "images.upload",
|
||||
"description": "Schema for images.upload",
|
||||
"$ref": "images#/definitions/image"
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "images",
|
||||
"title": "images",
|
||||
"description": "Base images definitions",
|
||||
"definitions": {
|
||||
"image": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"purpose": {
|
||||
"type": "string",
|
||||
"enum": ["image", "profile_image", "icon"],
|
||||
"default": "image"
|
||||
},
|
||||
"ref": {
|
||||
"type": ["string", "null"],
|
||||
"maxLength": 2000
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,7 +5,8 @@ const common = require('../../../../../lib/common');
|
|||
|
||||
const validate = (schema, definitions, data) => {
|
||||
const ajv = new Ajv({
|
||||
allErrors: true
|
||||
allErrors: true,
|
||||
useDefaults: true
|
||||
});
|
||||
|
||||
stripKeyword(ajv);
|
||||
|
|
|
@ -30,8 +30,8 @@
|
|||
"clientNotFound": "Client not found"
|
||||
},
|
||||
"actions": {
|
||||
"upload": {
|
||||
"image": "upload image"
|
||||
"images": {
|
||||
"upload": "upload image"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -192,31 +192,14 @@ module.exports = function apiRoutes() {
|
|||
router.get('/authentication/setup', api.http(api.authentication.isSetup));
|
||||
|
||||
// ## Images
|
||||
router.post('/images',
|
||||
router.post('/images/upload',
|
||||
mw.authAdminApi,
|
||||
upload.single('uploadimage'),
|
||||
upload.single('file'),
|
||||
shared.middlewares.validation.upload({type: 'images'}),
|
||||
shared.middlewares.image.normalize,
|
||||
http(apiv2.images.upload)
|
||||
);
|
||||
|
||||
router.post('/images/profile-image',
|
||||
mw.authAdminApi,
|
||||
upload.single('uploadimage'),
|
||||
shared.middlewares.validation.upload({type: 'images'}),
|
||||
shared.middlewares.validation.profileImage,
|
||||
shared.middlewares.image.normalize,
|
||||
http(apiv2.images.upload)
|
||||
);
|
||||
|
||||
router.post('/images/icon',
|
||||
mw.authAdminApi,
|
||||
upload.single('uploadimage'),
|
||||
shared.middlewares.validation.upload({type: 'icons'}),
|
||||
shared.middlewares.validation.blogIcon(),
|
||||
http(apiv2.images.upload)
|
||||
);
|
||||
|
||||
// ## Invites
|
||||
router.get('/invites', mw.authAdminApi, http(apiv2.invites.browse));
|
||||
router.get('/invites/:id', mw.authAdminApi, http(apiv2.invites.read));
|
||||
|
|
|
@ -108,6 +108,8 @@ _private.prepareUserMessage = (err, res) => {
|
|||
if (action) {
|
||||
if (err.context) {
|
||||
userError.context = `${err.message} ${err.context}`;
|
||||
} else {
|
||||
userError.context = err.message;
|
||||
}
|
||||
|
||||
userError.message = common.i18n.t(`errors.api.userMessages.${err.name}`, {action: action});
|
||||
|
|
|
@ -29,73 +29,76 @@ describe('Images API', function () {
|
|||
});
|
||||
|
||||
it('Can upload a png', function (done) {
|
||||
request.post(localUtils.API.getApiQuery('images'))
|
||||
request.post(localUtils.API.getApiQuery('images/upload'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/ghost-logo.png'))
|
||||
.field('purpose', 'image')
|
||||
.field('ref', 'https://ghost.org/ghost-logo.png')
|
||||
.attach('file', path.join(__dirname, '/../../../utils/fixtures/images/ghost-logo.png'))
|
||||
.expect(201)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
res.body.url.should.match(new RegExp(`${config.get('url')}/content/images/\\d+/\\d+/ghost-logo.png`));
|
||||
|
||||
images.push(res.body.url.replace(config.get('url'), ''));
|
||||
res.body.images[0].url.should.match(new RegExp(`${config.get('url')}/content/images/\\d+/\\d+/ghost-logo.png`));
|
||||
res.body.images[0].ref.should.equal('https://ghost.org/ghost-logo.png');
|
||||
images.push(res.body.images[0].url.replace(config.get('url'), ''));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Can upload a jpg', function (done) {
|
||||
request.post(localUtils.API.getApiQuery('images'))
|
||||
request.post(localUtils.API.getApiQuery('images/upload'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/ghosticon.jpg'))
|
||||
.attach('file', path.join(__dirname, '/../../../utils/fixtures/images/ghosticon.jpg'))
|
||||
.expect(201)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
res.body.url.should.match(new RegExp(`${config.get('url')}/content/images/\\d+/\\d+/ghosticon.jpg`));
|
||||
res.body.images[0].url.should.match(new RegExp(`${config.get('url')}/content/images/\\d+/\\d+/ghosticon.jpg`));
|
||||
should(res.body.images[0].ref).equal(null);
|
||||
|
||||
images.push(res.body.url.replace(config.get('url'), ''));
|
||||
images.push(res.body.images[0].url.replace(config.get('url'), ''));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Can upload a gif', function (done) {
|
||||
request.post(localUtils.API.getApiQuery('images'))
|
||||
request.post(localUtils.API.getApiQuery('images/upload'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/loadingcat.gif'))
|
||||
.attach('file', path.join(__dirname, '/../../../utils/fixtures/images/loadingcat.gif'))
|
||||
.expect(201)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
res.body.url.should.match(new RegExp(`${config.get('url')}/content/images/\\d+/\\d+/loadingcat.gif`));
|
||||
res.body.images[0].url.should.match(new RegExp(`${config.get('url')}/content/images/\\d+/\\d+/loadingcat.gif`));
|
||||
|
||||
images.push(res.body.url.replace(config.get('url'), ''));
|
||||
images.push(res.body.images[0].url.replace(config.get('url'), ''));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('Can upload a square profile image', function (done) {
|
||||
request.post(localUtils.API.getApiQuery('images/profile-image'))
|
||||
request.post(localUtils.API.getApiQuery('images/upload'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.attach('uploadimage', path.join(__dirname, '/../../../utils/fixtures/images/loadingcat_square.gif'))
|
||||
.attach('file', path.join(__dirname, '/../../../utils/fixtures/images/loadingcat_square.gif'))
|
||||
.expect(201)
|
||||
.end(function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
res.body.url.should.match(new RegExp(`${config.get('url')}/content/images/\\d+/\\d+/loadingcat_square.gif`));
|
||||
res.body.images[0].url.should.match(new RegExp(`${config.get('url')}/content/images/\\d+/\\d+/loadingcat_square.gif`));
|
||||
|
||||
images.push(res.body.url.replace(config.get('url'), ''));
|
||||
images.push(res.body.images[0].url.replace(config.get('url'), ''));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('Images API', function () {
|
|||
|
||||
it('Can\'t import fail without file', function () {
|
||||
return request
|
||||
.post(localUtils.API.getApiQuery('images'))
|
||||
.post(localUtils.API.getApiQuery('images/upload'))
|
||||
.set('Origin', config.get('url'))
|
||||
.set('Accept', 'application/json')
|
||||
.expect('Content-Type', /json/)
|
||||
|
@ -38,10 +38,10 @@ describe('Images API', function () {
|
|||
});
|
||||
|
||||
it('Can\'t import with unsupported file', function (done) {
|
||||
request.post(localUtils.API.getApiQuery('images'))
|
||||
request.post(localUtils.API.getApiQuery('images/upload'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.attach('uploadimage', path.join(__dirname, '/../../../../utils/fixtures/csv/single-column-with-header.csv'))
|
||||
.attach('file', path.join(__dirname, '/../../../../utils/fixtures/csv/single-column-with-header.csv'))
|
||||
.expect(415)
|
||||
.end(function (err) {
|
||||
if (err) {
|
||||
|
@ -53,11 +53,11 @@ describe('Images API', function () {
|
|||
});
|
||||
|
||||
it('Can\'t upload incorrect extension', function (done) {
|
||||
request.post(localUtils.API.getApiQuery('images'))
|
||||
request.post(localUtils.API.getApiQuery('images/upload'))
|
||||
.set('Origin', config.get('url'))
|
||||
.set('content-type', 'image/png')
|
||||
.expect('Content-Type', /json/)
|
||||
.attach('uploadimage', path.join(__dirname, '/../../../../utils/fixtures/images/ghost-logo.pngx'))
|
||||
.attach('file', path.join(__dirname, '/../../../../utils/fixtures/images/ghost-logo.pngx'))
|
||||
.expect(415)
|
||||
.end(function (err) {
|
||||
if (err) {
|
||||
|
@ -69,10 +69,11 @@ describe('Images API', function () {
|
|||
});
|
||||
|
||||
it('Can\'t import if profile image is not square', function (done) {
|
||||
request.post(localUtils.API.getApiQuery('images/profile-image'))
|
||||
request.post(localUtils.API.getApiQuery('images/upload'))
|
||||
.set('Origin', config.get('url'))
|
||||
.expect('Content-Type', /json/)
|
||||
.attach('uploadimage', path.join(__dirname, '/../../../../utils/fixtures/images/favicon_not_square.png'))
|
||||
.field('purpose', 'profile_image')
|
||||
.attach('file', path.join(__dirname, '/../../../../utils/fixtures/images/favicon_not_square.png'))
|
||||
.expect(422)
|
||||
.end(function (err) {
|
||||
if (err) {
|
||||
|
|
Loading…
Reference in a new issue