Adds License and LicenseUrl to AssetInfo and Publish/Edit Forms #935
8 changed files with 148 additions and 61 deletions
|
@ -47,6 +47,7 @@ For a closed, custom-hosted and branded example, check out https://lbry.theantim
|
||||||
- `./lbrynet account_balance` gets your balance (initially 0.0)
|
- `./lbrynet account_balance` gets your balance (initially 0.0)
|
||||||
- `./lbrynet address_list` gets addresses you can use to recieve LBC
|
- `./lbrynet address_list` gets addresses you can use to recieve LBC
|
||||||
- [FFmpeg](https://www.ffmpeg.org/download.html)
|
- [FFmpeg](https://www.ffmpeg.org/download.html)
|
||||||
|
- [ImageMagick](https://packages.ubuntu.com/xenial/graphics/imagemagick)
|
||||||
- Spee.ch (below)
|
- Spee.ch (below)
|
||||||
- pm2 (optional) process manager such as pm2 to run speech server.js
|
- pm2 (optional) process manager such as pm2 to run speech server.js
|
||||||
- http proxy server e.g. caddy, nginx, or traefik, to forward 80/443 to speech port 3000
|
- http proxy server e.g. caddy, nginx, or traefik, to forward 80/443 to speech port 3000
|
||||||
|
@ -260,9 +261,11 @@ Spee.ch has a few types of URL formats that return different assets from the LBR
|
||||||
- retrieve the controlling `LBRY` claim:
|
- retrieve the controlling `LBRY` claim:
|
||||||
- https://spee.ch/`claim`
|
- https://spee.ch/`claim`
|
||||||
- https://spee.ch/`claim`.`ext` (serve)
|
- https://spee.ch/`claim`.`ext` (serve)
|
||||||
|
- https://spee.ch/`claim`.`ext`&`querystring` (serve transformed)
|
||||||
- retrieve a specific `LBRY` claim:
|
- retrieve a specific `LBRY` claim:
|
||||||
- https://spee.ch/`claim_id`/`claim`
|
- https://spee.ch/`claim_id`/`claim`
|
||||||
- https://spee.ch/`claim_id`/`claim`.`ext` (serve)
|
- https://spee.ch/`claim_id`/`claim`.`ext` (serve)
|
||||||
|
- https://spee.ch/`claim_id`/`claim`.`ext`&`querystring` (serve transformed)
|
||||||
- retrieve all contents for the controlling `LBRY` channel
|
- retrieve all contents for the controlling `LBRY` channel
|
||||||
- https://spee.ch/`@channel`
|
- https://spee.ch/`@channel`
|
||||||
- a specific `LBRY` channel
|
- a specific `LBRY` channel
|
||||||
|
@ -270,9 +273,15 @@ Spee.ch has a few types of URL formats that return different assets from the LBR
|
||||||
- retrieve a specific claim within the controlling `LBRY` channel
|
- retrieve a specific claim within the controlling `LBRY` channel
|
||||||
- https://spee.ch/`@channel`/`claim`
|
- https://spee.ch/`@channel`/`claim`
|
||||||
- https://spee.ch/`@channel`/`claim`.`ext` (serve)
|
- https://spee.ch/`@channel`/`claim`.`ext` (serve)
|
||||||
|
- https://spee.ch/`@channel`/`claim`.`ext`&`querystring` (serve)
|
||||||
- retrieve a specific claim within a specific `LBRY` channel
|
- retrieve a specific claim within a specific `LBRY` channel
|
||||||
- https://spee.ch/`@channel`:`channel_id`/`claim`
|
- https://spee.ch/`@channel`:`channel_id`/`claim`
|
||||||
- https://spee.ch/`@channel`:`channel_id`/`claim`.`ext` (serve)
|
- https://spee.ch/`@channel`:`channel_id`/`claim`.`ext` (serve)
|
||||||
|
- https://spee.ch/`@channel`:`channel_id`/`claim`.`ext`&`querystring` (serve)
|
||||||
|
- `querystring` can include the following transformation values separated by `&`
|
||||||
|
- h=`number` (defines height)
|
||||||
|
- w=`number` (defines width)
|
||||||
|
- t=`crop` or `stretch` (defines transformation - missing implies constrained proportions)
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
|
|
0
changelog.md
Normal file
0
changelog.md
Normal file
|
@ -49,6 +49,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serving": {
|
"serving": {
|
||||||
|
"dynamicFileSizing": {
|
||||||
|
"enabled": true,
|
||||||
|
"maxDimension": 2000
|
||||||
|
},
|
||||||
"markdownSettings": {
|
"markdownSettings": {
|
||||||
"skipHtmlMain": true,
|
"skipHtmlMain": true,
|
||||||
"escapeHtmlMain": true,
|
"escapeHtmlMain": true,
|
||||||
|
@ -83,10 +87,7 @@
|
||||||
"code",
|
"code",
|
||||||
"html",
|
"html",
|
||||||
"parsedHtml"
|
"parsedHtml"
|
||||||
],
|
]
|
||||||
"disallowedTypesMain": [],
|
|
||||||
"disallowedTypesDescriptions": ["image", "html"],
|
|
||||||
"disallowedTypesExample": ["image", "html"]
|
|
||||||
},
|
},
|
||||||
"customFileExtensions": {
|
"customFileExtensions": {
|
||||||
"application/x-troff-man": "man",
|
"application/x-troff-man": "man",
|
||||||
|
|
|
@ -26,7 +26,6 @@ PUBLISHING:
|
||||||
|
|
||||||
"primaryClaimAddress": null, - generally supplied by your lbrynet sdk
|
"primaryClaimAddress": null, - generally supplied by your lbrynet sdk
|
||||||
"uploadDirectory": "/home/lbry/Uploads", - lbrynet sdk will know your uploads are here
|
"uploadDirectory": "/home/lbry/Uploads", - lbrynet sdk will know your uploads are here
|
||||||
"lbrynetHome": "/home/lbry",
|
|
||||||
"thumbnailChannel": null, - when publishing non-image content, thumbnails will go here.
|
"thumbnailChannel": null, - when publishing non-image content, thumbnails will go here.
|
||||||
"thumbnailChannelId": null,
|
"thumbnailChannelId": null,
|
||||||
"additionalClaimAddresses": [],
|
"additionalClaimAddresses": [],
|
||||||
|
@ -50,48 +49,52 @@ PUBLISHING:
|
||||||
"application/octet-stream": 50000000
|
"application/octet-stream": 50000000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SERVING:
|
|
||||||
|
|
||||||
"markdownSettings": {
|
SERVING:
|
||||||
"skipHtmlMain": true, - false: render html, in a somewhat unpredictable way~
|
|
||||||
"escapeHtmlMain": true, - true: rather than render html, escape it and print it visibly
|
"dynamicFileSizing": {
|
||||||
"skipHtmlDescriptions": true, - as above, for descriptions
|
"enabled": false, - if you choose to allow your instance to serve transform images
|
||||||
"escapeHtmlDescriptions": true, - as above, for descriptions
|
"maxDimension": 2000 - the maximum size you allow transform to scale
|
||||||
"allowedTypesMain": [], - markdown rendered as main content
|
},
|
||||||
"allowedTypesDescriptions": [], - markdown rendered in description in content details
|
"markdownSettings": {
|
||||||
"allowedTypesExample": [ - here are examples of allowed types
|
"skipHtmlMain": true, - false: render html, in a somewhat unpredictable way~
|
||||||
"see react-markdown docs", `https://github.com/rexxars/react-markdown`
|
"escapeHtmlMain": true, - true: rather than render html, escape it and print it visibly
|
||||||
"root",
|
"skipHtmlDescriptions": true, - as above, for descriptions
|
||||||
"text",
|
"escapeHtmlDescriptions": true, - as above, for descriptions
|
||||||
"break",
|
"allowedTypesMain": [], - markdown rendered as main content
|
||||||
"paragraph",
|
"allowedTypesDescriptions": [], - markdown rendered in description in content details
|
||||||
"emphasis",
|
"allowedTypesExample": [ - here are examples of allowed types
|
||||||
"strong",
|
"see react-markdown docs", `https://github.com/rexxars/react-markdown`
|
||||||
"thematicBreak",
|
"root",
|
||||||
"blockquote",
|
"text",
|
||||||
"delete",
|
"break",
|
||||||
"link",
|
"paragraph",
|
||||||
"image", - you may not have a lot of control over how these are rendered
|
"emphasis",
|
||||||
"linkReference",
|
"strong",
|
||||||
"imageReference",
|
"thematicBreak",
|
||||||
"table",
|
"blockquote",
|
||||||
"tableHead",
|
"delete",
|
||||||
"tableBody",
|
"link",
|
||||||
"tableRow",
|
"image", - you may not have a lot of control over how these are rendered
|
||||||
"tableCell",
|
"linkReference",
|
||||||
"list",
|
"imageReference",
|
||||||
"listItem",
|
"table",
|
||||||
"heading",
|
"tableHead",
|
||||||
"inlineCode",
|
"tableBody",
|
||||||
"code",
|
"tableRow",
|
||||||
"html", - potentially DANGEROUS, intended for `serveOnlyApproved = true` environments, includes iframes, divs.
|
"tableCell",
|
||||||
"parsedHtml"
|
"list",
|
||||||
],
|
"listItem",
|
||||||
},
|
"heading",
|
||||||
"customFileExtensions": { - suggest a file extension for experimental content types you may be publishing
|
"inlineCode",
|
||||||
"application/example-type": "example"
|
"code",
|
||||||
}
|
"html", - potentially DANGEROUS, intended for `serveOnlyApproved = true` environments, includes iframes, divs.
|
||||||
|
"parsedHtml"
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"customFileExtensions": { - suggest a file extension for experimental content types you may be publishing
|
||||||
|
"application/example-type": "example"
|
||||||
|
}
|
||||||
|
|
||||||
STARTUP:
|
STARTUP:
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
const { getClaim } = require('../../../../lbrynet');
|
const { getClaim } = require('../../../../lbrynet');
|
||||||
const { createFileRecordDataAfterGet } = require('../../../../models/utils/createFileRecordData.js');
|
const {
|
||||||
|
createFileRecordDataAfterGet,
|
||||||
|
} = require('../../../../models/utils/createFileRecordData.js');
|
||||||
const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
|
const { handleErrorResponse } = require('../../../utils/errorHandlers.js');
|
||||||
const getClaimData = require('server/utils/getClaimData');
|
const getClaimData = require('server/utils/getClaimData');
|
||||||
const chainquery = require('chainquery').default;
|
const chainquery = require('chainquery').default;
|
||||||
const db = require('../../../../models');
|
const db = require('../../../../models');
|
||||||
const waitOn = require('wait-on');
|
|
||||||
const logger = require('winston');
|
const logger = require('winston');
|
||||||
|
const awaitFileSize = require('server/utils/awaitFileSize');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
|
@ -36,11 +38,11 @@ const claimGet = async ({ ip, originalUrl, params }, res) => {
|
||||||
if (!claimData) {
|
if (!claimData) {
|
||||||
throw new Error('claim/get: getClaimData failed to get file blobs');
|
throw new Error('claim/get: getClaimData failed to get file blobs');
|
||||||
}
|
}
|
||||||
await waitOn({
|
let fileReady = await awaitFileSize(lbrynetResult.download_path, 2000000, 10000, 250);
|
||||||
resources: [ lbrynetResult.download_path ],
|
|
||||||
timeout : 10000, // 10 seconds
|
if (fileReady !== 'ready') {
|
||||||
window : 500,
|
throw new Error('claim/get: failed to get file after 10 seconds');
|
||||||
});
|
}
|
||||||
const fileData = await createFileRecordDataAfterGet(claimData, lbrynetResult);
|
const fileData = await createFileRecordDataAfterGet(claimData, lbrynetResult);
|
||||||
if (!fileData) {
|
if (!fileData) {
|
||||||
throw new Error('claim/get: createFileRecordDataAfterGet failed to create file in time');
|
throw new Error('claim/get: createFileRecordDataAfterGet failed to create file in time');
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
const logger = require('winston');
|
const logger = require('winston');
|
||||||
const transformImage = require('./transformImage');
|
const transformImage = require('./transformImage');
|
||||||
|
|
||||||
|
const isValidQueryObject = require('server/utils/isValidQueryObj');
|
||||||
|
const {
|
||||||
|
serving: { dynamicFileSizing },
|
||||||
|
} = require('@config/siteConfig');
|
||||||
|
const { enabled: dynamicEnabled } = dynamicFileSizing;
|
||||||
|
|
||||||
const serveFile = async ({ filePath, fileType }, res, originalUrl) => {
|
const serveFile = async ({ filePath, fileType }, res, originalUrl) => {
|
||||||
const queryObject = {};
|
const queryObject = {};
|
||||||
// TODO: replace quick/dirty try catch with better practice
|
// TODO: replace quick/dirty try catch with better practice
|
||||||
|
@ -22,7 +28,10 @@ const serveFile = async ({ filePath, fileType }, res, originalUrl) => {
|
||||||
|
|
||||||
let mediaType = fileType ? fileType.substr(0, fileType.indexOf('/')) : '';
|
let mediaType = fileType ? fileType.substr(0, fileType.indexOf('/')) : '';
|
||||||
const transform =
|
const transform =
|
||||||
mediaType === 'image' && queryObject.hasOwnProperty('h') && queryObject.hasOwnProperty('w');
|
mediaType === 'image' &&
|
||||||
|
queryObject.hasOwnProperty('h') &&
|
||||||
|
queryObject.hasOwnProperty('w') &&
|
||||||
|
dynamicEnabled;
|
||||||
|
|
||||||
const sendFileOptions = {
|
const sendFileOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -33,14 +42,26 @@ const serveFile = async ({ filePath, fileType }, res, originalUrl) => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
logger.debug(`fileOptions for ${filePath}:`, sendFileOptions);
|
logger.debug(`fileOptions for ${filePath}:`, sendFileOptions);
|
||||||
if (transform) {
|
try {
|
||||||
logger.debug(`transforming and sending file`);
|
if (transform) {
|
||||||
|
if (!isValidQueryObject(queryObject)) {
|
||||||
|
logger.debug(`Unacceptable querystring`, { queryObject });
|
||||||
|
res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Querystring may not have dimensions greater than 2000',
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
logger.debug(`transforming and sending file`);
|
||||||
|
|
||||||
let xformed = await transformImage(filePath, queryObject);
|
let xformed = await transformImage(filePath, queryObject);
|
||||||
res.status(200).set(sendFileOptions.headers);
|
res.status(200).set(sendFileOptions.headers);
|
||||||
res.end(xformed, 'binary');
|
res.end(xformed, 'binary');
|
||||||
} else {
|
} else {
|
||||||
res.status(200).sendFile(filePath, sendFileOptions);
|
res.status(200).sendFile(filePath, sendFileOptions);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
27
server/utils/awaitFileSize.js
Normal file
27
server/utils/awaitFileSize.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
const fs = require('fs');
|
||||||
|
const { promisify } = require('util');
|
||||||
|
|
||||||
|
const fsstat = promisify(fs.stat);
|
||||||
|
const awaitFileSize = (path, sizeInBytes, timeout, interval) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let totalTime = 0;
|
||||||
|
let timer = setInterval(() => {
|
||||||
|
totalTime = totalTime + interval;
|
||||||
|
fsstat(path)
|
||||||
|
.then(stats => {
|
||||||
|
if (stats.size > sizeInBytes) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve('ready');
|
||||||
|
}
|
||||||
|
if (totalTime > timeout) {
|
||||||
|
const error = new Error('File did not arrive in time');
|
||||||
|
error.name = 'FILE_NOT_ARRIVED';
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch();
|
||||||
|
}, interval);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = awaitFileSize;
|
24
server/utils/isValidQueryObj.js
Normal file
24
server/utils/isValidQueryObj.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
const {
|
||||||
|
serving: { dynamicFileSizing },
|
||||||
|
} = require('@config/siteConfig');
|
||||||
|
const { maxDimension } = dynamicFileSizing;
|
||||||
|
|
||||||
|
const isValidQueryObj = queryObj => {
|
||||||
|
let {
|
||||||
|
h: cHeight = null,
|
||||||
|
w: cWidth = null,
|
||||||
|
t: transform = null,
|
||||||
|
x: xOrigin = null,
|
||||||
|
y: yOrigin = null,
|
||||||
|
} = queryObj;
|
||||||
|
|
||||||
|
return (
|
||||||
|
((cHeight <= maxDimension && cHeight > 0) || cHeight === null) &&
|
||||||
|
((cWidth <= maxDimension && cWidth > 0) || cWidth === null) &&
|
||||||
|
(transform === null || transform === 'crop' || transform === 'stretch') &&
|
||||||
|
((xOrigin <= maxDimension && xOrigin >= 0) || xOrigin === null) &&
|
||||||
|
((yOrigin <= maxDimension && yOrigin >= 0) || yOrigin === null)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = isValidQueryObj;
|
Loading…
Reference in a new issue