Merge pull request #928 from jessopb/xformFileOnServe
implements querystring image transformation
This commit is contained in:
commit
ed9d037155
5 changed files with 555 additions and 399 deletions
802
package-lock.json
generated
802
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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",
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
76
server/controllers/assets/utils/transformImage.js
Normal file
76
server/controllers/assets/utils/transformImage.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue