Merge pull request #928 from jessopb/xformFileOnServe

implements querystring image transformation
This commit is contained in:
jessopb 2019-02-21 17:56:59 -05:00 committed by GitHub
commit ed9d037155
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 555 additions and 399 deletions

802
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -46,6 +46,7 @@
"express-http-context": "^1.2.0", "express-http-context": "^1.2.0",
"generate-password": "^1.4.1", "generate-password": "^1.4.1",
"get-video-dimensions": "^1.0.0", "get-video-dimensions": "^1.0.0",
"gm": "^1.23.1",
"helmet": "^3.15.0", "helmet": "^3.15.0",
"image-size": "^0.6.3", "image-size": "^0.6.3",
"inquirer": "^5.2.0", "inquirer": "^5.2.0",

View file

@ -15,9 +15,20 @@ const BLOCKED_CLAIM = 'BLOCKED_CLAIM';
const NO_FILE = 'NO_FILE'; const NO_FILE = 'NO_FILE';
const CONTENT_UNAVAILABLE = 'CONTENT_UNAVAILABLE'; const CONTENT_UNAVAILABLE = 'CONTENT_UNAVAILABLE';
const { publishing: { serveOnlyApproved, approvedChannels } } = require('@config/siteConfig'); const {
publishing: { serveOnlyApproved, approvedChannels },
} = require('@config/siteConfig');
const getClaimIdAndServeAsset = (channelName, channelClaimId, claimName, claimId, originalUrl, ip, res, headers) => { const getClaimIdAndServeAsset = (
channelName,
channelClaimId,
claimName,
claimId,
originalUrl,
ip,
res,
headers
) => {
getClaimId(channelName, channelClaimId, claimName, claimId) getClaimId(channelName, channelClaimId, claimName, claimId)
.then(fullClaimId => { .then(fullClaimId => {
claimId = fullClaimId; claimId = fullClaimId;
@ -39,19 +50,27 @@ const getClaimIdAndServeAsset = (channelName, channelClaimId, claimName, claimId
.then(claim => { .then(claim => {
let claimDataValues = claim.dataValues; let claimDataValues = claim.dataValues;
if (serveOnlyApproved && !isApprovedChannel({ longId: claimDataValues.publisher_id || claimDataValues.certificateId }, approvedChannels)) { if (
serveOnlyApproved &&
!isApprovedChannel(
{ longId: claimDataValues.publisher_id || claimDataValues.certificateId },
approvedChannels
)
) {
throw new Error(CONTENT_UNAVAILABLE); throw new Error(CONTENT_UNAVAILABLE);
} }
let outpoint = claimDataValues.outpoint || `${claimDataValues.transaction_hash_id}:${claimDataValues.vout}`; let outpoint =
claimDataValues.outpoint ||
`${claimDataValues.transaction_hash_id}:${claimDataValues.vout}`;
logger.debug('Outpoint:', outpoint); logger.debug('Outpoint:', outpoint);
return db.Blocked.isNotBlocked(outpoint).then(() => { return db.Blocked.isNotBlocked(outpoint).then(() => {
// If content was found, is approved, and not blocked - log a view. // If content was found, is approved, and not blocked - log a view.
if (headers && headers['user-agent'] && /LBRY/.test(headers['user-agent']) === false) { if (headers && headers['user-agent'] && /LBRY/.test(headers['user-agent']) === false) {
db.Views.create({ db.Views.create({
time : Date.now(), time: Date.now(),
isChannel : false, isChannel: false,
claimId : claimDataValues.claim_id || claimDataValues.claimId, claimId: claimDataValues.claim_id || claimDataValues.claimId,
publisherId: claimDataValues.publisher_id || claimDataValues.certificateId, publisherId: claimDataValues.publisher_id || claimDataValues.certificateId,
ip, ip,
}); });
@ -70,7 +89,7 @@ const getClaimIdAndServeAsset = (channelName, channelClaimId, claimName, claimId
if (!fileRecord) { if (!fileRecord) {
throw NO_FILE; throw NO_FILE;
} }
serveFile(fileRecord.dataValues, res); serveFile(fileRecord.dataValues, res, originalUrl);
}) })
.catch(error => { .catch(error => {
if (error === NO_CLAIM) { if (error === NO_CLAIM) {
@ -98,7 +117,8 @@ const getClaimIdAndServeAsset = (channelName, channelClaimId, claimName, claimId
logger.debug('claim was blocked'); logger.debug('claim was blocked');
return res.status(451).json({ return res.status(451).json({
success: false, success: false,
message: 'In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications. For more details, see https://lbry.io/faq/dmca', message:
'In response to a complaint we received under the US Digital Millennium Copyright Act, we have blocked access to this content from our applications. For more details, see https://lbry.io/faq/dmca',
}); });
} }
if (error === NO_FILE) { if (error === NO_FILE) {

View file

@ -1,19 +1,46 @@
const logger = require('winston'); const logger = require('winston');
const transformImage = require('./transformImage');
const serveFile = async ({ filePath, fileType }, res, originalUrl) => {
const queryObject = {};
// TODO: replace quick/dirty try catch with better practice
try {
originalUrl
.split('?')[1]
.split('&')
.map(pair => {
if (pair.includes('=')) {
let parr = pair.split('=');
queryObject[parr[0]] = parr[1];
} else queryObject[pair] = true;
});
} catch (e) {}
const serveFile = ({ filePath, fileType }, res) => {
if (!fileType) { if (!fileType) {
logger.error(`no fileType provided for ${filePath}`); logger.error(`no fileType provided for ${filePath}`);
} }
let mediaType = fileType ? fileType.substr(0, fileType.indexOf('/')) : '';
const transform =
mediaType === 'image' && queryObject.hasOwnProperty('h') && queryObject.hasOwnProperty('w');
const sendFileOptions = { const sendFileOptions = {
headers: { headers: {
'X-Content-Type-Options' : 'nosniff', 'X-Content-Type-Options': 'nosniff',
'Content-Type' : fileType, 'Content-Type': fileType,
'Access-Control-Allow-Origin' : '*', 'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept', 'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept',
}, },
}; };
logger.debug(`fileOptions for ${filePath}:`, sendFileOptions); logger.debug(`fileOptions for ${filePath}:`, sendFileOptions);
res.status(200).sendFile(filePath, sendFileOptions); if (transform) {
logger.debug(`transforming and sending file`);
let xformed = await transformImage(filePath, queryObject);
res.status(200).set(sendFileOptions.headers);
res.end(xformed, 'binary');
} else {
res.status(200).sendFile(filePath, sendFileOptions);
}
}; };
module.exports = serveFile; module.exports = serveFile;

View file

@ -0,0 +1,76 @@
const gm = require('gm');
const logger = require('winston');
const imageMagick = gm.subClass({ imageMagick: true });
const { getImageHeightAndWidth } = require('../../../utils/imageProcessing');
module.exports = function transformImage(path, queryObj) {
return new Promise((resolve, reject) => {
let { h: cHeight = null } = queryObj;
let { w: cWidth = null } = queryObj;
let { t: transform = null } = queryObj;
let { x: xOrigin = null } = queryObj;
let { y: yOrigin = null } = queryObj;
let oHeight,
oWidth = null;
try {
getImageHeightAndWidth(path).then(hwarr => {
oHeight = hwarr[0];
oWidth = hwarr[1];
// conditional logic here
if (transform === 'crop') {
resolve(_cropCenter(path, cWidth, cHeight, oWidth, oHeight));
} else if (transform === 'stretch') {
imageMagick(path)
.resize(cWidth, cHeight, '!')
.toBuffer(null, (err, buf) => {
resolve(buf);
});
} else {
// resize scaled
imageMagick(path)
.resize(cWidth, cHeight)
.toBuffer(null, (err, buf) => {
resolve(buf);
});
}
});
} catch (e) {
logger.error(e);
reject(e);
}
});
};
function _cropCenter(path, cropWidth, cropHeight, originalWidth, originalHeight) {
let oAspect = originalWidth / originalHeight;
let cAspect = cropWidth / cropHeight;
let resizeX,
resizeY,
xpoint,
ypoint = null;
if (oAspect >= cAspect) {
// if crop is narrower aspect than original
resizeY = cropHeight;
xpoint = (oAspect * cropHeight) / 2 - cropWidth / 2;
ypoint = 0;
} else {
// if crop is wider aspect than original
resizeX = cropWidth;
xpoint = 0;
ypoint = cropWidth / oAspect / 2 - cropHeight / 2;
}
return new Promise((resolve, reject) => {
try {
imageMagick(path)
.resize(resizeX, resizeY)
.crop(cropWidth, cropHeight, xpoint, ypoint)
.toBuffer(null, (err, buf) => {
resolve(buf);
});
} catch (e) {
logger.error(e);
reject(e);
}
});
}