0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added thumbnail upload support to Media API

refs https://github.com/TryGhost/Toolbox/issues/95

- Each media file quires a thumbnail and these changes provide a capability to upload them along with media files.
- The thumbnail file is always required and has to be the format of already supported image formats
- The thumbnail should be uploaded as a part of "thumbnail" attachment in the request
- The regression tests added with this changeset will be claened up and moved to unit-tests (this is a dirty-but-working version!)
- The thumbnail always gets a name of the uploaded media file and keeps it's own extension.
- The thumbnails is accessible under the url present in the "thumbnail_url" reponse field
This commit is contained in:
Naz 2021-11-03 17:38:59 +04:00
parent 6e53527666
commit 091240db48
6 changed files with 133 additions and 9 deletions

View file

@ -5,8 +5,14 @@ module.exports = {
upload: { upload: {
statusCode: 201, statusCode: 201,
permissions: false, permissions: false,
query(frame) { async query(frame) {
return storage.getStorage('media').save(frame.file); const file = await storage.getStorage('media').save(frame.files.file[0]);
const thumbnail = await storage.getStorage('media').save(frame.files.thumbnail[0]);
return {
filePath: file,
thumbnailPath: thumbnail
};
} }
} }
}; };

View file

@ -1,8 +1,8 @@
const config = require('../../../../../../shared/config'); const config = require('../../../../../../shared/config');
const STATIC_VIDEO_URL_PREFIX = require('@tryghost/constants'); const {STATIC_MEDIA_URL_PREFIX} = require('@tryghost/constants');
function getURL(urlPath) { function getURL(urlPath) {
const media = new RegExp('^' + config.getSubdir() + '/' + STATIC_VIDEO_URL_PREFIX); const media = new RegExp('^' + config.getSubdir() + '/' + STATIC_MEDIA_URL_PREFIX);
const absolute = media.test(urlPath) ? true : false; const absolute = media.test(urlPath) ? true : false;
if (absolute) { if (absolute) {
@ -16,10 +16,11 @@ function getURL(urlPath) {
} }
module.exports = { module.exports = {
upload(path, apiConfig, frame) { upload({filePath, thumbnailPath}, apiConfig, frame) {
return frame.response = { return frame.response = {
media: [{ media: [{
url: getURL(path), url: getURL(filePath),
thumbnail_url: getURL(thumbnailPath),
ref: frame.data.ref || null ref: frame.data.ref || null
}] }]
}; };

View file

@ -239,8 +239,8 @@ module.exports = function apiRoutes() {
// ## media // ## media
router.post('/media/upload', router.post('/media/upload',
mw.authAdminApi, mw.authAdminApi,
apiMw.upload.single('file'), apiMw.upload.media('file', 'thumbnail'),
apiMw.upload.validation({type: 'media'}), apiMw.upload.mediaValidation({type: 'media'}),
http(api.media.upload) http(api.media.upload)
); );

View file

@ -35,6 +35,10 @@ const messages = {
media: { media: {
missingFile: 'Please select a media file.', missingFile: 'Please select a media file.',
invalidFile: 'Please select a valid media file.' invalidFile: 'Please select a valid media file.'
},
thumbnail: {
missingFile: 'Please select a thumbnail.',
invalidFile: 'Please select a valid thumbnail.'
} }
}; };
@ -73,6 +77,43 @@ const single = name => (req, res, next) => {
}); });
}; };
const media = (fileName, thumbName) => (req, res, next) => {
const mediaUpload = upload.fields([{
name: fileName,
maxCount: 1
}, {
name: thumbName,
maxCount: 1
}]);
mediaUpload(req, res, (err) => {
if (err) {
return next(err);
}
if (enabledClear) {
const deleteFiles = () => {
res.removeListener('finish', deleteFiles);
res.removeListener('close', deleteFiles);
if (!req.disableUploadClear) {
if (req.files.file) {
return req.files.file.forEach(deleteSingleFile);
}
if (req.files.thumbnail) {
return req.files.thumbnail.forEach(deleteSingleFile);
}
}
};
if (!req.disableUploadClear) {
res.on('finish', deleteFiles);
res.on('close', deleteFiles);
}
}
next();
});
};
const checkFileExists = (fileData) => { const checkFileExists = (fileData) => {
return !!(fileData.mimetype && fileData.path); return !!(fileData.mimetype && fileData.path);
}; };
@ -123,9 +164,65 @@ const validation = function ({type}) {
}; };
}; };
/**
*
* @param {Object} options
* @param {String} options.type - type of the file
* @returns {Function}
*/
const mediaValidation = function ({type}) {
return function mediaUploadValidation(req, res, next) {
const extensions = (config.get('uploads')[type] && config.get('uploads')[type].extensions) || [];
const contentTypes = (config.get('uploads')[type] && config.get('uploads')[type].contentTypes) || [];
const thumbnailExtensions = (config.get('uploads').thumbnails && config.get('uploads').thumbnails.extensions) || [];
const thumbnailContentTypes = (config.get('uploads').thumbnails && config.get('uploads').thumbnails.contentTypes) || [];
const {file: [file] = []} = req.files;
if (!file || !checkFileExists(file)) {
return next(new errors.ValidationError({
message: tpl(messages[type].missingFile)
}));
}
req.file = file;
req.file.name = req.file.originalname;
req.file.type = req.file.mimetype;
req.file.ext = path.extname(req.file.name).toLowerCase();
const {thumbnail: [thumbnailFile] = []} = req.files;
if (!thumbnailFile || !checkFileExists(thumbnailFile)) {
return next(new errors.ValidationError({
message: tpl(messages.thumbnail.missingFile)
}));
}
req.thumbnail = thumbnailFile;
req.thumbnail.ext = path.extname(thumbnailFile.originalname).toLowerCase();
req.thumbnail.name = path.basename(req.file.name, path.extname(req.file.name)) + req.thumbnail.ext;
req.thumbnail.type = req.thumbnail.mimetype;
if (!checkFileIsValid(req.file, contentTypes, extensions)) {
return next(new errors.UnsupportedMediaTypeError({
message: tpl(messages[type].invalidFile, {extensions: extensions})
}));
}
if (!checkFileIsValid(req.thumbnail, thumbnailContentTypes, thumbnailExtensions)) {
return next(new errors.UnsupportedMediaTypeError({
message: tpl(messages.thumbnail.invalidFile, {extensions: thumbnailExtensions})
}));
}
next();
};
};
module.exports = { module.exports = {
single, single,
validation media,
validation,
mediaValidation
}; };
// Exports for testing only // Exports for testing only

View file

@ -34,6 +34,10 @@
"extensions": [".mp4",".webm", ".ogv"], "extensions": [".mp4",".webm", ".ogv"],
"contentTypes": ["video/mp4", "video/webm", "video/ogg"] "contentTypes": ["video/mp4", "video/webm", "video/ogg"]
}, },
"thumbnails": {
"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"]
},
"icons": { "icons": {
"extensions": [".png", ".ico"], "extensions": [".png", ".ico"],
"contentTypes": ["image/png", "image/x-icon", "image/vnd.microsoft.icon"] "contentTypes": ["image/png", "image/x-icon", "image/vnd.microsoft.icon"]

View file

@ -30,12 +30,15 @@ describe('Media API', function () {
.field('purpose', 'video') .field('purpose', 'video')
.field('ref', 'https://ghost.org/sample_640x360.mp4') .field('ref', 'https://ghost.org/sample_640x360.mp4')
.attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample_640x360.mp4')) .attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample_640x360.mp4'))
.attach('thumbnail', path.join(__dirname, '/../../utils/fixtures/images/ghost-logo.png'))
.expect(201); .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].url.should.match(new RegExp(`${config.get('url')}/content/media/\\d+/\\d+/sample_640x360.mp4`));
res.body.media[0].thumbnail_url.should.match(new RegExp(`${config.get('url')}/content/media/\\d+/\\d+/sample_640x360.png`));
res.body.media[0].ref.should.equal('https://ghost.org/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'), '')); media.push(res.body.media[0].url.replace(config.get('url'), ''));
media.push(res.body.media[0].thumbnail_url.replace(config.get('url'), ''));
}); });
it('Can upload a WebM', async function () { it('Can upload a WebM', async function () {
@ -45,6 +48,7 @@ describe('Media API', function () {
.field('purpose', 'video') .field('purpose', 'video')
.field('ref', 'https://ghost.org/sample_640x360.webm') .field('ref', 'https://ghost.org/sample_640x360.webm')
.attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample_640x360.webm')) .attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample_640x360.webm'))
.attach('thumbnail', path.join(__dirname, '/../../utils/fixtures/images/ghost-logo.png'))
.expect(201); .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].url.should.match(new RegExp(`${config.get('url')}/content/media/\\d+/\\d+/sample_640x360.webm`));
@ -60,6 +64,7 @@ describe('Media API', function () {
.field('purpose', 'video') .field('purpose', 'video')
.field('ref', 'https://ghost.org/sample_640x360.ogv') .field('ref', 'https://ghost.org/sample_640x360.ogv')
.attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample_640x360.ogv')) .attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample_640x360.ogv'))
.attach('thumbnail', path.join(__dirname, '/../../utils/fixtures/images/ghost-logo.png'))
.expect(201); .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].url.should.match(new RegExp(`${config.get('url')}/content/media/\\d+/\\d+/sample_640x360.ogv`));
@ -73,8 +78,19 @@ describe('Media API', function () {
.set('Origin', config.get('url')) .set('Origin', config.get('url'))
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
.attach('file', path.join(__dirname, '/../../utils/fixtures/images/favicon_16x_single.ico')) .attach('file', path.join(__dirname, '/../../utils/fixtures/images/favicon_16x_single.ico'))
.attach('thumbnail', path.join(__dirname, '/../../utils/fixtures/images/ghost-logo.png'))
.expect(415); .expect(415);
res.body.errors[0].message.should.match(/select a valid media file/gi); res.body.errors[0].message.should.match(/select a valid media file/gi);
}); });
it('Rejects when thumbnail is not present', async function () {
const res = await request.post(localUtils.API.getApiQuery('media/upload'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.attach('file', path.join(__dirname, '/../../utils/fixtures/media/sample_640x360.ogv'))
.expect(422);
res.body.errors[0].message.should.match(/Please select a thumbnail./gi);
});
}); });