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:
parent
6e53527666
commit
091240db48
6 changed files with 133 additions and 9 deletions
|
@ -5,8 +5,14 @@ module.exports = {
|
|||
upload: {
|
||||
statusCode: 201,
|
||||
permissions: false,
|
||||
query(frame) {
|
||||
return storage.getStorage('media').save(frame.file);
|
||||
async query(frame) {
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
const config = require('../../../../../../shared/config');
|
||||
const STATIC_VIDEO_URL_PREFIX = require('@tryghost/constants');
|
||||
const {STATIC_MEDIA_URL_PREFIX} = require('@tryghost/constants');
|
||||
|
||||
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;
|
||||
|
||||
if (absolute) {
|
||||
|
@ -16,10 +16,11 @@ function getURL(urlPath) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
upload(path, apiConfig, frame) {
|
||||
upload({filePath, thumbnailPath}, apiConfig, frame) {
|
||||
return frame.response = {
|
||||
media: [{
|
||||
url: getURL(path),
|
||||
url: getURL(filePath),
|
||||
thumbnail_url: getURL(thumbnailPath),
|
||||
ref: frame.data.ref || null
|
||||
}]
|
||||
};
|
||||
|
|
|
@ -239,8 +239,8 @@ module.exports = function apiRoutes() {
|
|||
// ## media
|
||||
router.post('/media/upload',
|
||||
mw.authAdminApi,
|
||||
apiMw.upload.single('file'),
|
||||
apiMw.upload.validation({type: 'media'}),
|
||||
apiMw.upload.media('file', 'thumbnail'),
|
||||
apiMw.upload.mediaValidation({type: 'media'}),
|
||||
http(api.media.upload)
|
||||
);
|
||||
|
||||
|
|
|
@ -35,6 +35,10 @@ const messages = {
|
|||
media: {
|
||||
missingFile: 'Please select a 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) => {
|
||||
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 = {
|
||||
single,
|
||||
validation
|
||||
media,
|
||||
validation,
|
||||
mediaValidation
|
||||
};
|
||||
|
||||
// Exports for testing only
|
||||
|
|
|
@ -34,6 +34,10 @@
|
|||
"extensions": [".mp4",".webm", ".ogv"],
|
||||
"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": {
|
||||
"extensions": [".png", ".ico"],
|
||||
"contentTypes": ["image/png", "image/x-icon", "image/vnd.microsoft.icon"]
|
||||
|
|
|
@ -30,12 +30,15 @@ describe('Media API', function () {
|
|||
.field('purpose', 'video')
|
||||
.field('ref', 'https://ghost.org/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);
|
||||
|
||||
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');
|
||||
|
||||
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 () {
|
||||
|
@ -45,6 +48,7 @@ describe('Media API', function () {
|
|||
.field('purpose', 'video')
|
||||
.field('ref', 'https://ghost.org/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);
|
||||
|
||||
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('ref', 'https://ghost.org/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);
|
||||
|
||||
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'))
|
||||
.expect('Content-Type', /json/)
|
||||
.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);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue