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: {
|
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
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
|
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue