2020-04-22 09:37:02 +03:00
|
|
|
|
const path = require('path');
|
2018-12-12 16:02:09 +03:00
|
|
|
|
const os = require('os');
|
|
|
|
|
const multer = require('multer');
|
|
|
|
|
const fs = require('fs-extra');
|
2020-04-22 09:37:02 +03:00
|
|
|
|
const errors = require('@tryghost/errors');
|
2020-05-27 20:47:53 +03:00
|
|
|
|
const config = require('../../../../shared/config');
|
2021-10-04 14:19:18 +03:00
|
|
|
|
const tpl = require('@tryghost/tpl');
|
2021-06-15 17:36:27 +03:00
|
|
|
|
const logging = require('@tryghost/logging');
|
2018-12-12 16:02:09 +03:00
|
|
|
|
|
2021-10-04 14:19:18 +03:00
|
|
|
|
const messages = {
|
|
|
|
|
db: {
|
|
|
|
|
missingFile: 'Please select a database file to import.',
|
|
|
|
|
invalidFile: 'Unsupported file. Please try any of the following formats: {extensions}'
|
|
|
|
|
},
|
|
|
|
|
redirects: {
|
|
|
|
|
missingFile: 'Please select a JSON file.',
|
|
|
|
|
invalidFile: 'Please select a valid JSON file to import.'
|
|
|
|
|
},
|
|
|
|
|
routes: {
|
|
|
|
|
missingFile: 'Please select a YAML file.',
|
|
|
|
|
invalidFile: 'Please select a valid YAML file to import.'
|
|
|
|
|
},
|
|
|
|
|
themes: {
|
|
|
|
|
missingFile: 'Please select a theme.',
|
|
|
|
|
invalidFile: 'Please select a valid zip file.'
|
|
|
|
|
},
|
|
|
|
|
images: {
|
|
|
|
|
missingFile: 'Please select an image.',
|
|
|
|
|
invalidFile: 'Please select a valid image.'
|
|
|
|
|
},
|
|
|
|
|
icons: {
|
|
|
|
|
missingFile: 'Please select an icon.',
|
|
|
|
|
invalidFile: 'Icon must be a square .ico or .png file between 60px – 1,000px, under 100kb.'
|
2021-10-26 14:57:16 +03:00
|
|
|
|
},
|
|
|
|
|
media: {
|
|
|
|
|
missingFile: 'Please select a media file.',
|
|
|
|
|
invalidFile: 'Please select a valid media file.'
|
2021-11-03 16:38:59 +03:00
|
|
|
|
},
|
|
|
|
|
thumbnail: {
|
|
|
|
|
missingFile: 'Please select a thumbnail.',
|
|
|
|
|
invalidFile: 'Please select a valid thumbnail.'
|
2021-10-04 14:19:18 +03:00
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2021-10-25 14:06:25 +03:00
|
|
|
|
const enabledClear = config.get('uploadClear') || true;
|
|
|
|
|
const upload = multer({dest: os.tmpdir()});
|
2018-12-12 16:02:09 +03:00
|
|
|
|
|
2020-04-09 21:40:00 +03:00
|
|
|
|
const deleteSingleFile = file => fs.unlink(file.path).catch(err => logging.error(err));
|
2018-12-12 16:02:09 +03:00
|
|
|
|
|
|
|
|
|
const single = name => (req, res, next) => {
|
2021-10-25 14:06:25 +03:00
|
|
|
|
const singleUpload = upload.single(name);
|
|
|
|
|
|
2018-12-12 16:02:09 +03:00
|
|
|
|
singleUpload(req, res, (err) => {
|
|
|
|
|
if (err) {
|
|
|
|
|
return next(err);
|
|
|
|
|
}
|
2021-10-25 14:06:25 +03:00
|
|
|
|
if (enabledClear) {
|
2018-12-12 16:02:09 +03:00
|
|
|
|
const deleteFiles = () => {
|
|
|
|
|
res.removeListener('finish', deleteFiles);
|
|
|
|
|
res.removeListener('close', deleteFiles);
|
|
|
|
|
if (!req.disableUploadClear) {
|
|
|
|
|
if (req.files) {
|
|
|
|
|
return req.files.forEach(deleteSingleFile);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (req.file) {
|
|
|
|
|
return deleteSingleFile(req.file);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if (!req.disableUploadClear) {
|
|
|
|
|
res.on('finish', deleteFiles);
|
|
|
|
|
res.on('close', deleteFiles);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
next();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2021-11-03 16:38:59 +03:00
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-22 09:37:02 +03:00
|
|
|
|
const checkFileExists = (fileData) => {
|
|
|
|
|
return !!(fileData.mimetype && fileData.path);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const checkFileIsValid = (fileData, types, extensions) => {
|
|
|
|
|
const type = fileData.mimetype;
|
|
|
|
|
|
|
|
|
|
if (types.includes(type) && extensions.includes(fileData.ext)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
2021-10-26 14:57:16 +03:00
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @param {Object} options
|
|
|
|
|
* @param {String} options.type - type of the file
|
|
|
|
|
* @returns {Function}
|
|
|
|
|
*/
|
|
|
|
|
const validation = function ({type}) {
|
2020-04-22 09:37:02 +03:00
|
|
|
|
// if we finish the data/importer logic, we forward the request to the specified importer
|
|
|
|
|
return function uploadValidation(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) || [];
|
|
|
|
|
|
|
|
|
|
req.file = req.file || {};
|
|
|
|
|
req.file.name = req.file.originalname;
|
|
|
|
|
req.file.type = req.file.mimetype;
|
|
|
|
|
|
|
|
|
|
// Check if a file was provided
|
|
|
|
|
if (!checkFileExists(req.file)) {
|
|
|
|
|
return next(new errors.ValidationError({
|
2021-10-04 14:19:18 +03:00
|
|
|
|
message: tpl(messages[type].missingFile)
|
2020-04-22 09:37:02 +03:00
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req.file.ext = path.extname(req.file.name).toLowerCase();
|
|
|
|
|
|
|
|
|
|
// Check if the file is valid
|
|
|
|
|
if (!checkFileIsValid(req.file, contentTypes, extensions)) {
|
|
|
|
|
return next(new errors.UnsupportedMediaTypeError({
|
2021-10-04 14:19:18 +03:00
|
|
|
|
message: tpl(messages[type].invalidFile, {extensions: extensions})
|
2020-04-22 09:37:02 +03:00
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next();
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2021-11-03 16:38:59 +03:00
|
|
|
|
/**
|
|
|
|
|
*
|
|
|
|
|
* @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();
|
2021-11-04 18:03:45 +03:00
|
|
|
|
req.thumbnail.name = `${path.basename(req.file.name, path.extname(req.file.name))}_thumb${req.thumbnail.ext}`;
|
2021-11-03 16:38:59 +03:00
|
|
|
|
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();
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-22 09:37:02 +03:00
|
|
|
|
module.exports = {
|
|
|
|
|
single,
|
2021-11-03 16:38:59 +03:00
|
|
|
|
media,
|
|
|
|
|
validation,
|
|
|
|
|
mediaValidation
|
2020-04-22 09:37:02 +03:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Exports for testing only
|
|
|
|
|
module.exports._test = {
|
|
|
|
|
checkFileExists,
|
|
|
|
|
checkFileIsValid
|
|
|
|
|
};
|