0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-01-06 22:40:14 -05:00

Implemented '/media/upload' API endpoint

refs https://linear.app/tryghost/issue/CORE-121/create-a-video-storage-adapter

- This is an experimental implementation of video file upload support
- Also the output serializer skipped use of url utils in favor of inline implementatoin - this should almost certainly be it's own package
This commit is contained in:
Naz 2021-10-25 14:53:22 +04:00 committed by naz
parent 4907b7bf1e
commit 4a551661d9
7 changed files with 129 additions and 0 deletions

View file

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

View file

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

View file

@ -85,6 +85,10 @@ module.exports = {
return require('./images');
},
get media() {
return require('./media');
},
get tags() {
return require('./tags');
},

View file

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

View file

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

View file

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

View file

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