diff --git a/core/server/api/v2/index.js b/core/server/api/v2/index.js index 60ad9a0389..920439c06b 100644 --- a/core/server/api/v2/index.js +++ b/core/server/api/v2/index.js @@ -49,5 +49,9 @@ module.exports = { get subscribers() { return shared.pipeline(require('./subscribers'), localUtils); + }, + + get upload() { + return shared.pipeline(require('./upload'), localUtils); } }; diff --git a/core/server/api/v2/upload.js b/core/server/api/v2/upload.js new file mode 100644 index 0000000000..f9c3231e24 --- /dev/null +++ b/core/server/api/v2/upload.js @@ -0,0 +1,31 @@ +const fs = require('fs-extra'); +const storage = require('../../adapters/storage'); + +module.exports = { + docName: 'upload', + image: { + statusCode: 201, + permissions: false, + query(frame) { + const store = storage.getStorage(); + + if (frame.files) { + return Promise.map(frame.files, (file) => { + return store + .save(file) + .finally(() => { + // Remove uploaded file from tmp location + return fs.unlink(file.path); + }); + }).then((paths) => { + return paths[0]; + }); + } + + return store.save(frame.file).finally(() => { + // Remove uploaded file from tmp location + return fs.unlink(frame.file.path); + }); + } + } +}; diff --git a/core/server/api/v2/utils/serializers/output/index.js b/core/server/api/v2/utils/serializers/output/index.js index 1966b54de9..99cac5ec85 100644 --- a/core/server/api/v2/utils/serializers/output/index.js +++ b/core/server/api/v2/utils/serializers/output/index.js @@ -37,5 +37,9 @@ module.exports = { get subscribers() { return require('./subscribers'); + }, + + get upload() { + return require('./upload'); } }; diff --git a/core/server/api/v2/utils/serializers/output/upload.js b/core/server/api/v2/utils/serializers/output/upload.js new file mode 100644 index 0000000000..ddbb7dc8e9 --- /dev/null +++ b/core/server/api/v2/utils/serializers/output/upload.js @@ -0,0 +1,9 @@ +const debug = require('ghost-ignition').debug('api:v2:utils:serializers:output:upload'); + +module.exports = { + image(models, apiConfig, frame) { + debug('image'); + + return frame.response = models; + } +}; diff --git a/core/server/web/api/v2/admin/routes.js b/core/server/web/api/v2/admin/routes.js index 947b511357..cf3cf27f2a 100644 --- a/core/server/web/api/v2/admin/routes.js +++ b/core/server/web/api/v2/admin/routes.js @@ -171,7 +171,7 @@ module.exports = function apiRoutes() { upload.single('uploadimage'), shared.middlewares.validation.upload({type: 'images'}), shared.middlewares.image.normalize, - api.http(api.uploads.add) + apiv2.http(apiv2.upload.image) ); router.post('/uploads/profile-image', @@ -180,7 +180,7 @@ module.exports = function apiRoutes() { shared.middlewares.validation.upload({type: 'images'}), shared.middlewares.validation.profileImage, shared.middlewares.image.normalize, - api.http(api.uploads.add) + apiv2.http(apiv2.upload.image) ); router.post('/db/backup', mw.authenticateClient('Ghost Backup'), api.http(api.db.backupContent)); @@ -190,7 +190,7 @@ module.exports = function apiRoutes() { upload.single('uploadimage'), shared.middlewares.validation.upload({type: 'icons'}), shared.middlewares.validation.blogIcon(), - api.http(api.uploads.add) + apiv2.http(apiv2.upload.image) ); // ## Invites diff --git a/core/test/functional/api/v2/admin/upload_spec.js b/core/test/functional/api/v2/admin/upload_spec.js new file mode 100644 index 0000000000..96161dd4ac --- /dev/null +++ b/core/test/functional/api/v2/admin/upload_spec.js @@ -0,0 +1,159 @@ +const path = require('path'); +const fs = require('fs-extra'); +const should = require('should'); +const supertest = require('supertest'); +const localUtils = require('./utils'); +const testUtils = require('../../../../utils'); +const config = require('../../../../../../core/server/config'); + +const ghost = testUtils.startGhost; + +describe('Upload API', function () { + const images = []; + let request; + + before(function () { + return ghost() + .then(function () { + request = supertest.agent(config.get('url')); + }) + .then(function () { + return localUtils.doAuth(request); + }); + }); + + after(function () { + images.forEach(function (image) { + fs.removeSync(config.get('paths').appRoot + image); + }); + }); + + describe('success cases', function () { + it('valid png', function (done) { + request.post(localUtils.API.getApiQuery('uploads')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .attach('uploadimage', path.join(__dirname, '/../../../../utils/fixtures/images/ghost-logo.png')) + .expect(201) + .end(function (err, res) { + if (err) { + return done(err); + } + + images.push(res.body); + done(); + }); + }); + + it('valid jpg', function (done) { + request.post(localUtils.API.getApiQuery('uploads')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .attach('uploadimage', path.join(__dirname, '/../../../../utils/fixtures/images/ghosticon.jpg')) + .expect(201) + .end(function (err, res) { + if (err) { + return done(err); + } + + images.push(res.body); + done(); + }); + }); + + it('valid gif', function (done) { + request.post(localUtils.API.getApiQuery('uploads')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .attach('uploadimage', path.join(__dirname, '/../../../../utils/fixtures/images/loadingcat.gif')) + .expect(201) + .end(function (err, res) { + if (err) { + return done(err); + } + + images.push(res.body); + done(); + }); + }); + + it('valid profile image', function (done) { + request.post(localUtils.API.getApiQuery('uploads/profile-image')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .attach('uploadimage', path.join(__dirname, '/../../../../utils/fixtures/images/loadingcat_square.gif')) + .expect(201) + .end(function (err, res) { + if (err) { + return done(err); + } + + images.push(res.body); + done(); + }); + }); + }); + + describe('error cases', function () { + it('import should fail without file', function (done) { + request.post(localUtils.API.getApiQuery('uploads')) + .set('Origin', config.get('url')) + .set('Accept', 'application/json') + .expect('Content-Type', /json/) + .expect(403) + .end(function (err) { + if (err) { + return done(err); + } + + done(); + }); + }); + + it('import should fail with unsupported file', function (done) { + request.post(localUtils.API.getApiQuery('uploads')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .attach('uploadimage', path.join(__dirname, '/../../../../utils/fixtures/csv/single-column-with-header.csv')) + .expect(415) + .end(function (err) { + if (err) { + return done(err); + } + + done(); + }); + }); + + it('incorrect extension', function (done) { + request.post(localUtils.API.getApiQuery('uploads')) + .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')) + .expect(415) + .end(function (err) { + if (err) { + return done(err); + } + + done(); + }); + }); + + it('import should fail if profile image is not square', function (done) { + request.post(localUtils.API.getApiQuery('uploads/profile-image')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .attach('uploadimage', path.join(__dirname, '/../../../../utils/fixtures/images/favicon_not_square.png')) + .expect(422) + .end(function (err) { + if (err) { + return done(err); + } + + done(); + }); + }); + }); +});