2
1
Fork 0
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:
Nazar Gargol 2019-02-25 16:51:32 +07:00 committed by Naz Gargol
parent 4f9e687f62
commit f558b58c89
11 changed files with 157 additions and 47 deletions

View file

@ -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
}]
};
}
};

View 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);
}
});
}
};

View file

@ -21,5 +21,9 @@ module.exports = {
get users() {
return require('./users');
},
get images() {
return require('./images');
}
};

View file

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

View file

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

View file

@ -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);

View file

@ -30,8 +30,8 @@
"clientNotFound": "Client not found"
},
"actions": {
"upload": {
"image": "upload image"
"images": {
"upload": "upload image"
}
}
}

View file

@ -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));

View file

@ -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});

View file

@ -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();
});
});

View file

@ -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) {