diff --git a/core/server/api/canary/index.js b/core/server/api/canary/index.js index 0300ac313a..25caa5038d 100644 --- a/core/server/api/canary/index.js +++ b/core/server/api/canary/index.js @@ -105,6 +105,10 @@ module.exports = { return shared.pipeline(require('./images'), localUtils); }, + get media() { + return shared.pipeline(require('./media'), localUtils); + }, + get tags() { return shared.pipeline(require('./tags'), localUtils); }, diff --git a/core/server/api/canary/media.js b/core/server/api/canary/media.js new file mode 100644 index 0000000000..fab7bde0d3 --- /dev/null +++ b/core/server/api/canary/media.js @@ -0,0 +1,12 @@ +const storage = require('../../adapters/storage'); + +module.exports = { + docName: 'media', + upload: { + statusCode: 201, + permissions: false, + query(frame) { + return storage.getStorage('media').save(frame.file); + } + } +}; diff --git a/core/server/api/canary/utils/serializers/output/index.js b/core/server/api/canary/utils/serializers/output/index.js index 5ec7fe2058..3950bde961 100644 --- a/core/server/api/canary/utils/serializers/output/index.js +++ b/core/server/api/canary/utils/serializers/output/index.js @@ -85,6 +85,10 @@ module.exports = { return require('./images'); }, + get media() { + return require('./media'); + }, + get tags() { return require('./tags'); }, diff --git a/core/server/api/canary/utils/serializers/output/media.js b/core/server/api/canary/utils/serializers/output/media.js new file mode 100644 index 0000000000..1753c68bac --- /dev/null +++ b/core/server/api/canary/utils/serializers/output/media.js @@ -0,0 +1,27 @@ +const config = require('../../../../../../shared/config'); + +function getURL(urlPath) { + const STATIC_VIDEO_URL_PREFIX = 'content/media'; + const imagePathRe = new RegExp('^' + config.getSubdir() + '/' + STATIC_VIDEO_URL_PREFIX); + const absolute = imagePathRe.test(urlPath) ? true : false; + + if (absolute) { + // Remove the sub-directory from the URL because ghostConfig will add it back. + urlPath = urlPath.replace(new RegExp('^' + config.getSubdir()), ''); + const baseUrl = config.getSiteUrl().replace(/\/$/, ''); + urlPath = baseUrl + urlPath; + } + + return urlPath; +} + +module.exports = { + upload(path, apiConfig, frame) { + return frame.response = { + media: [{ + url: getURL(path), + ref: frame.data.ref || null + }] + }; + } +}; diff --git a/core/server/web/api/canary/admin/routes.js b/core/server/web/api/canary/admin/routes.js index 253ea1d787..4654b4af0e 100644 --- a/core/server/web/api/canary/admin/routes.js +++ b/core/server/web/api/canary/admin/routes.js @@ -236,6 +236,14 @@ module.exports = function apiRoutes() { http(api.images.upload) ); + // ## media + router.post('/media/upload', + mw.authAdminApi, + apiMw.upload.single('file'), + apiMw.upload.validation({type: 'media'}), + http(api.media.upload) + ); + // ## Invites router.get('/invites', mw.authAdminApi, http(api.invites.browse)); router.get('/invites/:id', mw.authAdminApi, http(api.invites.read)); diff --git a/core/shared/config/overrides.json b/core/shared/config/overrides.json index 21b9d2fddf..cdd2d06391 100644 --- a/core/shared/config/overrides.json +++ b/core/shared/config/overrides.json @@ -30,6 +30,10 @@ "extensions": [".jpg", ".jpeg", ".gif", ".png", ".svg", ".svgz", ".ico", ".webp"], "contentTypes": ["image/jpeg", "image/png", "image/gif", "image/svg+xml", "image/x-icon", "image/vnd.microsoft.icon", "image/webp"] }, + "media": { + "extensions": [".mp4",".webm", ".ogv"], + "contentTypes": ["video/mp4", "video/webm", "video/ogg"] + }, "icons": { "extensions": [".png", ".ico"], "contentTypes": ["image/png", "image/x-icon", "image/vnd.microsoft.icon"] diff --git a/test/e2e-api/admin/media.test.js b/test/e2e-api/admin/media.test.js new file mode 100644 index 0000000000..27adad26e5 --- /dev/null +++ b/test/e2e-api/admin/media.test.js @@ -0,0 +1,70 @@ +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/shared/config'); + +describe('Media API', function () { + // NOTE: holds paths to media that need to be cleaned up after the tests are run + const media = []; + let request; + + before(async function () { + await testUtils.startGhost(); + request = supertest.agent(config.get('url')); + await localUtils.doAuth(request); + }); + + after(function () { + media.forEach(function (image) { + fs.removeSync(config.get('paths').appRoot + image); + }); + }); + + it('Can upload a MP4', async function () { + const res = await request.post(localUtils.API.getApiQuery('media/upload')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .field('purpose', 'video') + .field('ref', 'https://ghost.org/sample_640x360.mp4') + .attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample_640x360.mp4')) + .expect(201); + + res.body.media[0].url.should.match(new RegExp(`${config.get('url')}/content/media/\\d+/\\d+/sample_640x360.mp4`)); + res.body.media[0].ref.should.equal('https://ghost.org/sample_640x360.mp4'); + + media.push(res.body.media[0].url.replace(config.get('url'), '')); + }); + + it('Can upload a WebM', async function () { + const res = await request.post(localUtils.API.getApiQuery('media/upload')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .field('purpose', 'video') + .field('ref', 'https://ghost.org/sample_640x360.webm') + .attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample_640x360.webm')) + .expect(201); + + res.body.media[0].url.should.match(new RegExp(`${config.get('url')}/content/media/\\d+/\\d+/sample_640x360.webm`)); + res.body.media[0].ref.should.equal('https://ghost.org/sample_640x360.webm'); + + media.push(res.body.media[0].url.replace(config.get('url'), '')); + }); + + it('Can upload an Ogg', async function () { + const res = await request.post(localUtils.API.getApiQuery('media/upload')) + .set('Origin', config.get('url')) + .expect('Content-Type', /json/) + .field('purpose', 'video') + .field('ref', 'https://ghost.org/sample_640x360.ogv') + .attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample_640x360.ogv')) + .expect(201); + + res.body.media[0].url.should.match(new RegExp(`${config.get('url')}/content/media/\\d+/\\d+/sample_640x360.ogv`)); + res.body.media[0].ref.should.equal('https://ghost.org/sample_640x360.ogv'); + + media.push(res.body.media[0].url.replace(config.get('url'), '')); + }); +});