/* eslint-disable node/no-deprecated-api */ module.exports.pathBasename = pathBasename module.exports.hasSuffix = hasSuffix module.exports.serialize = serialize module.exports.translate = translate module.exports.stringToStream = stringToStream module.exports.debrack = debrack module.exports.stripLineEndings = stripLineEndings module.exports.fullUrlForReq = fullUrlForReq module.exports.routeResolvedFile = routeResolvedFile module.exports.getQuota = getQuota module.exports.overQuota = overQuota module.exports.getContentType = getContentType module.exports.parse = parse const fs = require('fs') const path = require('path') const util = require('util') const $rdf = require('rdflib') const from = require('from2') const url = require('url') const debug = require('./debug').fs const getSize = require('get-folder-size') const ns = require('solid-namespace')($rdf) /** * Returns a fully qualified URL from an Express.js Request object. * (It's insane that Express does not provide this natively.) * * Usage: * * ``` * console.log(util.fullUrlForReq(req)) * // -> https://example.com/path/to/resource?q1=v1 * ``` * * @param req {IncomingRequest} * * @return {string} */ function fullUrlForReq (req) { const fullUrl = url.format({ protocol: req.protocol, host: req.get('host'), pathname: url.resolve(req.baseUrl, req.path), query: req.query }) return fullUrl } /** * Removes the `<` and `>` brackets around a string and returns it. * Used by the `allow` handler in `verifyDelegator()` logic. * @method debrack * * @param s {string} * * @return {string} */ function debrack (s) { if (!s || s.length < 2) { return s } if (s[0] !== '<') { return s } if (s[s.length - 1] !== '>') { return s } return s.substring(1, s.length - 1) } async function parse (data, baseUri, contentType) { const graph = $rdf.graph() return new Promise((resolve, reject) => { try { return $rdf.parse(data, graph, baseUri, contentType, (err, str) => { if (err) { return reject(err) } resolve(str) }) } catch (err) { return reject(err) } }) } function pathBasename (fullpath) { let bname = '' if (fullpath) { bname = (fullpath.lastIndexOf('/') === fullpath.length - 1) ? '' : path.basename(fullpath) } return bname } function hasSuffix (path, suffixes) { for (const i in suffixes) { if (path.indexOf(suffixes[i], path.length - suffixes[i].length) !== -1) { return true } } return false } function serialize (graph, baseUri, contentType) { return new Promise((resolve, reject) => { try { // target, kb, base, contentType, callback $rdf.serialize(null, graph, baseUri, contentType, function (err, result) { if (err) { return reject(err) } if (result === undefined) { return reject(new Error('Error serializing the graph to ' + contentType)) } resolve(result) }) } catch (err) { reject(err) } }) } function translate (stream, baseUri, from, to) { return new Promise((resolve, reject) => { let data = '' stream .on('data', function (chunk) { data += chunk }) .on('end', function () { const graph = $rdf.graph() $rdf.parse(data, graph, baseUri, from, function (err) { if (err) return reject(err) resolve(serialize(graph, baseUri, to)) }) }) }) } function stringToStream (string) { return from(function (size, next) { // if there's no more content // left in the string, close the stream. if (!string || string.length <= 0) { return next(null, null) } // Pull in a new chunk of text, // removing it from the string. const chunk = string.slice(0, size) string = string.slice(size) // Emit "chunk" from the stream. next(null, chunk) }) } /** * Removes line endings from a given string. Used for WebID TLS Certificate * generation. * * @param obj {string} * * @return {string} */ function stripLineEndings (obj) { if (!obj) { return obj } return obj.replace(/(\r\n|\n|\r)/gm, '') } /** * Adds a route that serves a static file from another Node module */ function routeResolvedFile (router, path, file, appendFileName = true) { const fullPath = appendFileName ? path + file.match(/[^/]+$/) : path const fullFile = require.resolve(file) router.get(fullPath, (req, res) => res.sendFile(fullFile)) } /** * Returns the number of bytes that the user owning the requested POD * may store or Infinity if no limit */ async function getQuota (root, serverUri) { const filename = path.join(root, 'settings/serverSide.ttl') let prefs try { prefs = await _asyncReadfile(filename) } catch (error) { debug('Setting no quota. While reading serverSide.ttl, got ' + error) return Infinity } const graph = $rdf.graph() const storageUri = serverUri.endsWith('/') ? serverUri : serverUri + '/' try { $rdf.parse(prefs, graph, storageUri, 'text/turtle') } catch (error) { throw new Error('Failed to parse serverSide.ttl, got ' + error) } return Number(graph.anyValue($rdf.sym(storageUri), ns.solid('storageQuota'))) || Infinity } /** * Returns true of the user has already exceeded their quota, i.e. it * will check if new requests should be rejected, which means they * could PUT a large file and get away with it. */ async function overQuota (root, serverUri) { const quota = await getQuota(root, serverUri) if (quota === Infinity) { return false } // TODO: cache this value? const size = await actualSize(root) return (size > quota) } /** * Returns the number of bytes that is occupied by the actual files in * the file system. IMPORTANT NOTE: Since it traverses the directory * to find the actual file sizes, this does a costly operation, but * neglible for the small quotas we currently allow. If the quotas * grow bigger, this will significantly reduce write performance, and * so it needs to be rewritten. */ function actualSize (root) { return util.promisify(getSize)(root) } function _asyncReadfile (filename) { return util.promisify(fs.readFile)(filename, 'utf-8') } /** * Get the content type from a headers object * @param headers An Express or Fetch API headers object * @return {string} A content type string */ function getContentType (headers) { const value = headers.get ? headers.get('content-type') : headers['content-type'] return value ? value.replace(/;.*/, '') : '' }