From 0c01a48d04163de469290d4bb92d4b5243d6c307 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Mon, 20 Aug 2018 19:17:15 -0400 Subject: [PATCH 01/42] Add resourcMapper field to LDP. --- lib/create-app.js | 7 +++++++ lib/resource-mapper.js | 7 ++++++- test/integration/ldp-test.js | 10 +++++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/create-app.js b/lib/create-app.js index db448d20b..29e224faf 100644 --- a/lib/create-app.js +++ b/lib/create-app.js @@ -23,6 +23,7 @@ const options = require('./handlers/options') const debug = require('./debug').authentication const path = require('path') const { routeResolvedFile } = require('./utils') +const LegacyResourceMapper = require('./legacy-resource-mapper') const corsSettings = cors({ methods: [ @@ -41,6 +42,12 @@ function createApp (argv = {}) { argv.host = SolidHost.from({ port: argv.port, serverUri: argv.serverUri }) + argv.resourceMapper = new LegacyResourceMapper({ + rootUrl: argv.serverUri, + rootPath: argv.root, + includeHost: argv.multiuser + }) + const configPath = config.initConfigDir(argv) argv.templates = config.initTemplateDirs(configPath) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index 3725711a8..18aee3818 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -8,7 +8,12 @@ const readdir = promisify(fs.readdir) // following the principles of the “sweet spot” discussed in // https://www.w3.org/DesignIssues/HTTPFilenameMapping.html class ResourceMapper { - constructor ({ rootUrl, rootPath, includeHost, defaultContentType = 'application/octet-stream' }) { + constructor ({ + rootUrl = 'https://localhost:8443/', + rootPath = '/tmp/', + includeHost = false, + defaultContentType = 'application/octet-stream' + }) { this._rootUrl = this._removeTrailingSlash(rootUrl) this._rootPath = this._removeTrailingSlash(rootPath) this._includeHost = includeHost diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index e6195156a..09e5105b2 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -4,6 +4,7 @@ var ns = require('solid-namespace')($rdf) var LDP = require('../../lib/ldp') var path = require('path') var stringToStream = require('../../lib/utils').stringToStream +var LegacyResourceMapper = require('../../lib/legacy-resource-mapper') // Helper functions for the FS var rm = require('./../utils').rm @@ -13,8 +14,15 @@ var read = require('./../utils').read var fs = require('fs') describe('LDP', function () { + var root = path.join(__dirname, '..') + + var resourceMapper = new LegacyResourceMapper({ + rootPath: root + }) + var ldp = new LDP({ - root: path.join(__dirname, '..'), + root, + resourceMapper, webid: false }) From 546462c106503f0ebe8a76fb6555ea325e29adcd Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Mon, 20 Aug 2018 19:28:22 -0400 Subject: [PATCH 02/42] Use ResourceMapper in ldp#graph. --- lib/ldp.js | 40 ++++++++++------------------------------ lib/utils.js | 13 ++----------- 2 files changed, 12 insertions(+), 41 deletions(-) diff --git a/lib/ldp.js b/lib/ldp.js index cade52701..24eda81af 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -349,42 +349,22 @@ class LDP { * @return {Promise} */ getGraph (uri, contentType = DEFAULT_CONTENT_TYPE) { - const parsedUri = url.parse(uri) - const path = parsedUri.pathname - const hostname = parsedUri.hostname + const { path } = url.parse(uri) + return this.graph(path, uri, contentType) + } + async graph (url, baseUri, contentType = 'text/turtle') { + const { path } = await this.resourceMapper.mapUrlToFile({ url }) return new Promise((resolve, reject) => { - this.graph(hostname, path, uri, contentType, (error, graph) => { - if (error) { return reject(error) } - - resolve(graph) + this.readFile(path, (err, body) => { + if (err) return reject(err) + const graph = $rdf.graph() + $rdf.parse(body, graph, baseUri, contentType, + err => err ? reject(err) : resolve(graph)) }) }) } - graph (host, reqPath, baseUri, contentType, callback) { - const ldp = this - - // overloading - if (typeof contentType === 'function') { - callback = contentType - contentType = 'text/turtle' - } - - if (typeof baseUri === 'function') { - callback = baseUri - baseUri = undefined - } - - const root = ldp.multiuser ? ldp.root + host + '/' : ldp.root - const filename = utils.uriToFilename(reqPath, root) - - ldp.readFile(filename, (err, body) => { - if (err) return callback(err) - parse(body, baseUri, contentType, callback) - }) - } - get (options, callback) { let host let reqPath diff --git a/lib/utils.js b/lib/utils.js index 8797ac421..55fbd30a8 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -4,7 +4,6 @@ module.exports.getBaseUri = getBaseUri module.exports.pathBasename = pathBasename module.exports.getFullUri = getFullUri module.exports.hasSuffix = hasSuffix -module.exports.parse = parse module.exports.serialize = serialize module.exports.translate = translate module.exports.stringToStream = stringToStream @@ -138,15 +137,6 @@ function hasSuffix (path, suffixes) { return false } -function parse (data, baseUri, contentType, callback) { - const graph = $rdf.graph() - try { - return $rdf.parse(data, graph, baseUri, contentType, callback) - } catch (err) { - return callback(err) - } -} - function serialize (graph, baseUri, contentType, callback) { try { // target, kb, base, contentType, callback @@ -183,7 +173,8 @@ function translate (stream, baseUri, from, to, callback) { data += chunk }) .on('end', function () { - parse(data, baseUri, from, function (err, graph) { + const graph = $rdf.graph() + $rdf.parse(data, graph, baseUri, from, function (err) { if (err) return callback(err) serialize(graph, baseUri, to, function (err, data) { if (err) return callback(err) From a3bb625d477149d0b9b7faea23f2114674dc9229 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Mon, 20 Aug 2018 20:46:57 -0400 Subject: [PATCH 03/42] Use ResourceMapper in ldp#readContainerMeta. --- lib/ldp.js | 28 +++++++++++++--------------- test/integration/ldp-test.js | 19 +++++++------------ 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/lib/ldp.js b/lib/ldp.js index 24eda81af..5f55fe899 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -118,19 +118,18 @@ class LDP { }) } - readContainerMeta (directory, callback) { - const ldp = this - - if (directory[ directory.length - 1 ] !== '/') { - directory += '/' + async readContainerMeta (url, callback) { + if (url[ url.length - 1 ] !== '/') { + url += '/' } + url += this.suffixMeta - ldp.readFile(directory + ldp.suffixMeta, function (err, data) { - if (err) { - return callback(error(err, "Can't read meta file")) - } - - return callback(null, data) + const { path } = await this.resourceMapper.mapUrlToFile({ url }) + return new Promise((resolve, reject) => { + this.readFile(path, function (err, data) { + if (err) return reject(error(err, "Can't read meta file")) + resolve(data) + }) }) } @@ -397,10 +396,9 @@ class LDP { // Found a container if (stats.isDirectory()) { - return ldp.readContainerMeta(filename, function (err, metaFile) { - if (err) { - metaFile = '' - } + return ldp.readContainerMeta(reqPath) + .catch(() => '') + .then(metaFile => { const absContainerUri = baseUri + reqPath ldp.listContainer(filename, absContainerUri, baseUri, metaFile, contentType, function (err, data) { diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index 09e5105b2..44238e384 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -46,33 +46,28 @@ describe('LDP', function () { }) }) - describe('readContainerMeta', function () { - it('should return 404 if .meta is not found', function (done) { - ldp.readContainerMeta('resources/', function (err) { + describe('readContainerMeta', () => { + it('should return 404 if .meta is not found', () => { + return ldp.readContainerMeta('/resources/').catch(err => { assert.equal(err.status, 404) - done() }) }) - it('should return content if metaFile exists', function (done) { + it('should return content if metaFile exists', () => { // file can be empty as well write('This function just reads this, does not parse it', '.meta') - ldp.readContainerMeta(path.join(__dirname, '../resources/'), function (err, metaFile) { + return ldp.readContainerMeta('/resources/').then(metaFile => { rm('.meta') - assert.notOk(err) assert.equal(metaFile, 'This function just reads this, does not parse it') - done() }) }) - it('should work also if trailing `/` is not passed', function (done) { + it('should work also if trailing `/` is not passed', () => { // file can be empty as well write('This function just reads this, does not parse it', '.meta') - ldp.readContainerMeta(path.join(__dirname, '../resources'), function (err, metaFile) { + return ldp.readContainerMeta('/resources').then(metaFile => { rm('.meta') - assert.notOk(err) assert.equal(metaFile, 'This function just reads this, does not parse it') - done() }) }) }) From 805a0da1fc670cb127bb5e66888c086fed7c6376 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Mon, 20 Aug 2018 21:27:32 -0400 Subject: [PATCH 04/42] Use ResourceMapper in ldp#readResource. --- lib/handlers/allow.js | 16 +------------ lib/ldp-container.js | 25 ++++---------------- lib/ldp.js | 45 ++++++++++++++---------------------- test/integration/ldp-test.js | 13 ++++------- 4 files changed, 28 insertions(+), 71 deletions(-) diff --git a/lib/handlers/allow.js b/lib/handlers/allow.js index f4c2e5f48..b2172aae4 100644 --- a/lib/handlers/allow.js +++ b/lib/handlers/allow.js @@ -1,7 +1,6 @@ module.exports = allow const ACL = require('../acl-checker') -const $rdf = require('rdflib') const utils = require('../utils') const debug = require('../debug.js').ACL const LegacyResourceMapper = require('../legacy-resource-mapper') @@ -71,19 +70,6 @@ function allow (mode) { */ function fetchFromLdp (mapper, ldp) { return function fetch (url, callback) { - // Convert the URL into a filename - mapper.mapUrlToFile({ url }) - // Read the file from disk - .then(({ path }) => new Promise((resolve, reject) => { - ldp.readFile(path, (e, c) => e ? reject(e) : resolve(c)) - })) - // Parse the file as Turtle - .then(body => { - const graph = $rdf.graph() - $rdf.parse(body, graph, url, 'text/turtle') - return graph - }) - // Return the ACL graph - .then(graph => callback(null, graph), callback) + ldp.getGraph(url).then(g => callback(null, g), callback) } } diff --git a/lib/ldp-container.js b/lib/ldp-container.js index c290879af..eb67dfa82 100644 --- a/lib/ldp-container.js +++ b/lib/ldp-container.js @@ -1,7 +1,6 @@ module.exports.addContainerStats = addContainerStats module.exports.addFile = addFile module.exports.addStats = addStats -module.exports.getMetadataGraph = getMetadataGraph module.exports.readdir = readdir const $rdf = require('rdflib') @@ -61,7 +60,7 @@ function addFile (ldp, resourceGraph, containerUri, reqUri, uri, container, file // Set up a metaFile path // Earlier code used a .ttl file as its own meta file, which // caused massive data files to parsed as part of deirectory listings just looking for type triples - const metaFile = container + file + ldp.suffixMeta + const metaFile = containerUri + file + ldp.suffixMeta getMetadataGraph(ldp, metaFile, memberUri, function (err, metadataGraph) { if (err) { @@ -160,25 +159,11 @@ function getMetadataGraph (ldp, metaFile, fileBaseUri, callback) { } if (metaStats && metaStats.isFile()) { - ldp.readFile(metaFile, function (err, rawMetadata) { - if (err) { - return callback(err) - } - - const metadataGraph = $rdf.graph() - try { - $rdf.parse( - rawMetadata, - metadataGraph, - fileBaseUri, - 'text/turtle') - } catch (dirErr) { - return callback(error(err, 'Can\'t parse container metadata')) - } - return callback(null, metadataGraph) - }) + ldp.getGraph(metaFile) + .then(graph => callback(null, graph), + err => callback(error(err, 'Can\'t parse container metadata'))) } else { - return callback(null, $rdf.graph()) + callback(null, $rdf.graph()) } }) } diff --git a/lib/ldp.js b/lib/ldp.js index 5f55fe899..abeba5181 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -106,31 +106,24 @@ class LDP { } } - readFile (filename, callback) { - fs.readFile( - filename, - { 'encoding': 'utf8' }, - function (err, data) { - if (err) { - return callback(error(err, "Can't read file")) - } - return callback(null, data) - }) + async readResource (url) { + const { path } = await this.resourceMapper.mapUrlToFile({ url }) + return new Promise((resolve, reject) => { + fs.readFile( + path, + { 'encoding': 'utf8' }, + (err, data) => { + if (err) return reject(error(err, "Can't read file")) + resolve(data) + }) + }) } async readContainerMeta (url, callback) { if (url[ url.length - 1 ] !== '/') { url += '/' } - url += this.suffixMeta - - const { path } = await this.resourceMapper.mapUrlToFile({ url }) - return new Promise((resolve, reject) => { - this.readFile(path, function (err, data) { - if (err) return reject(error(err, "Can't read meta file")) - resolve(data) - }) - }) + return this.readResource(url + this.suffixMeta) } listContainer (filename, reqUri, uri, containerData, contentType, callback) { @@ -348,19 +341,15 @@ class LDP { * @return {Promise} */ getGraph (uri, contentType = DEFAULT_CONTENT_TYPE) { - const { path } = url.parse(uri) - return this.graph(path, uri, contentType) + return this.graph(uri, uri, contentType) } async graph (url, baseUri, contentType = 'text/turtle') { - const { path } = await this.resourceMapper.mapUrlToFile({ url }) + const body = await this.readResource(url) return new Promise((resolve, reject) => { - this.readFile(path, (err, body) => { - if (err) return reject(err) - const graph = $rdf.graph() - $rdf.parse(body, graph, baseUri, contentType, - err => err ? reject(err) : resolve(graph)) - }) + const graph = $rdf.graph() + $rdf.parse(body, graph, baseUri, contentType, + err => err ? reject(err) : resolve(graph)) }) } diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index 44238e384..281f162d6 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -26,22 +26,19 @@ describe('LDP', function () { webid: false }) - describe('readFile', function () { - it('return 404 if file does not exist', function (done) { - ldp.readFile('resources/unexistent.ttl', function (err) { + describe('readResource', function () { + it('return 404 if file does not exist', () => { + return ldp.readResource('/resources/unexistent.ttl').catch(err => { assert.equal(err.status, 404) - done() }) }) - it('return file if file exists', function (done) { + it('return file if file exists', () => { // file can be empty as well write('hello world', 'fileExists.txt') - ldp.readFile(path.join(__dirname, '../resources/fileExists.txt'), function (err, file) { + return ldp.readResource('/resources/fileExists.txt').then(file => { rm('fileExists.txt') - assert.notOk(err) assert.equal(file, 'hello world') - done() }) }) }) From 4198144a0f6dfee8acdd8bc8e30fc005f998ffa3 Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Thu, 23 Aug 2018 18:15:33 -0400 Subject: [PATCH 05/42] Switch stat to promises. --- lib/handlers/index.js | 6 ++---- lib/ldp-container.js | 41 +++++++++++++++-------------------------- lib/ldp.js | 33 ++++++++++++++------------------- 3 files changed, 31 insertions(+), 49 deletions(-) diff --git a/lib/handlers/index.js b/lib/handlers/index.js index 50b975c93..c9f4f2ac7 100644 --- a/lib/handlers/index.js +++ b/lib/handlers/index.js @@ -12,9 +12,7 @@ function handler (req, res, next) { const requestedType = negotiator.mediaType() const filename = utils.reqToPath(req) - ldp.stat(filename, function (err, stats) { - if (err) return next() - + ldp.stat(filename).then(stats => { if (!stats.isDirectory()) { return next() } @@ -37,5 +35,5 @@ function handler (req, res, next) { debug('Found an index for current path') return next() }) - }) + }, () => next()) } diff --git a/lib/ldp-container.js b/lib/ldp-container.js index eb67dfa82..58fe0323a 100644 --- a/lib/ldp-container.js +++ b/lib/ldp-container.js @@ -12,21 +12,19 @@ const mime = require('mime-types') const path = require('path') function addContainerStats (ldp, reqUri, filename, resourceGraph, next) { - ldp.stat(filename, function (err, containerStats) { - if (!err) { - addStats(resourceGraph, reqUri, containerStats) - resourceGraph.add( - resourceGraph.sym(reqUri), - ns.rdf('type'), - ns.ldp('BasicContainer')) + ldp.stat(filename).then(containerStats => { + addStats(resourceGraph, reqUri, containerStats) + resourceGraph.add( + resourceGraph.sym(reqUri), + ns.rdf('type'), + ns.ldp('BasicContainer')) - resourceGraph.add( - resourceGraph.sym(reqUri), - ns.rdf('type'), - ns.ldp('Container')) - } + resourceGraph.add( + resourceGraph.sym(reqUri), + ns.rdf('type'), + ns.ldp('Container')) next() - }) + }, () => next()) } function addFile (ldp, resourceGraph, containerUri, reqUri, uri, container, file, callback) { @@ -38,12 +36,7 @@ function addFile (ldp, resourceGraph, containerUri, reqUri, uri, container, file const filePath = path.join(container, file) // Get file stats - ldp.stat(filePath, function (err, stats) { - if (err) { - // File does not exist, skip - return callback(null) - } - + ldp.stat(filePath).then(stats => { let memberUri = reqUri + (stats.isDirectory() ? '/' : '') // var fileSubject = file + (stats.isDirectory() ? '/' : '') // var fileBaseUri = utils.filenameToBaseUri(fileSubject, uri, root) @@ -110,7 +103,7 @@ function addFile (ldp, resourceGraph, containerUri, reqUri, uri, container, file return callback(null) }) - }) + }, () => callback(null)) } function addStats (resourceGraph, reqUri, stats) { @@ -153,11 +146,7 @@ function readdir (filename, callback) { } function getMetadataGraph (ldp, metaFile, fileBaseUri, callback) { - ldp.stat(metaFile, function (err, metaStats) { - if (err) { - return callback(err) - } - + ldp.stat(metaFile).then(metaStats => { if (metaStats && metaStats.isFile()) { ldp.getGraph(metaFile) .then(graph => callback(null, graph), @@ -165,5 +154,5 @@ function getMetadataGraph (ldp, metaFile, fileBaseUri, callback) { } else { callback(null, $rdf.graph()) } - }) + }, callback) } diff --git a/lib/ldp.js b/lib/ldp.js index abeba5181..4143fd5e2 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -89,12 +89,12 @@ class LDP { return this } - stat (file, callback) { - fs.stat(file, function (err, stats) { - if (err) { - return callback(error(err, "Can't read metadata")) - } - return callback(null, stats) + async stat (file, callback) { + return new Promise((resolve, reject) => { + fs.stat(file, (err, stats) => { + if (err) return reject(error(err, "Can't read metadata")) + resolve(stats) + }) }) } @@ -372,12 +372,7 @@ class LDP { const root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' const filename = utils.uriToFilename(reqPath, root) - ldp.stat(filename, function (err, stats) { - // File does not exist - if (err) { - return callback(error(err, 'Can\'t find file requested: ' + filename)) - } - + ldp.stat(filename).then(stats => { // Just return, since resource exists if (!includeBody) { return callback(null, {'stream': stats, 'contentType': contentType, 'container': stats.isDirectory()}) @@ -432,24 +427,24 @@ class LDP { return callback(null, {'stream': stream, 'contentType': contentType, 'container': false, 'contentRange': contentRange, 'chunksize': chunksize}) }) } - }) + }, + err => callback(error(err, 'Can\'t find file requested: ' + filename)) + ) } delete (host, resourcePath, callback) { const ldp = this const root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' const filename = utils.uriToFilename(resourcePath, root) - ldp.stat(filename, function (err, stats) { - if (err) { - return callback(error(404, "Can't find " + err)) - } - + ldp.stat(filename).then(stats => { if (stats.isDirectory()) { return ldp.deleteContainer(filename, callback) } else { return ldp.deleteResource(filename, callback) } - }) + }, + err => callback(error(404, "Can't find " + err)) + ) } deleteContainer (directory, callback) { From 646086e6c8117d65e74e6c30d803f6982c84966a Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Thu, 23 Aug 2018 19:18:21 -0400 Subject: [PATCH 06/42] Support Express request objects in ResourceMapper. --- lib/resource-mapper.js | 17 ++++++++++++++++- test/unit/resource-mapper-test.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index 18aee3818..a16cf901e 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -81,7 +81,7 @@ class ResourceMapper { // Determine the full file path corresponding to a URL _getFullPath (url) { - const { pathname, hostname } = typeof url === 'string' ? URL.parse(url) : url + const { pathname, hostname } = this._parseUrl(url) const fullPath = decodeURIComponent(`${this.getBasePath(hostname)}${pathname}`) if (fullPath.indexOf('/..') >= 0) { throw new Error('Disallowed /.. segment in URL') @@ -89,6 +89,21 @@ class ResourceMapper { return fullPath } + // Parses a URL into a hostname and pathname + _parseUrl (url) { + // URL specified as string + if (typeof url === 'string') { + return URL.parse(url) + } + // URL specified as Express request object + if (!url.pathname && url.path) { + const { hostname, path } = url + return { hostname, pathname: path.replace(/[?#].*/, '') } + } + // URL specified as object + return url + } + // Gets the expected content type based on the extension of the path _getContentTypeByExtension (path) { const extension = /\.([^/.]+)$/.exec(path) diff --git a/test/unit/resource-mapper-test.js b/test/unit/resource-mapper-test.js index 9575a9541..a7efd76e1 100644 --- a/test/unit/resource-mapper-test.js +++ b/test/unit/resource-mapper-test.js @@ -306,6 +306,34 @@ describe('ResourceMapper', () => { contentType: 'text/html' }) + itMapsUrl(mapper, 'a URL with a host specified as a URL object', + { + url: { + hostname: 'example.org', + path: '/space/foo.html' + }, + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}example.org/space/foo.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL with a host specified as an Express request object', + { + url: { + hostname: 'example.org', + pathname: '/space/foo.html' + }, + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}example.org/space/foo.html`, + contentType: 'text/html' + }) + itMapsUrl(mapper, 'a URL with a host with a port', { url: 'http://example.org:3000/space/foo.html', From b720eac040f318655aa130bce499f06abdac93aa Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 14 Nov 2018 10:34:30 +0100 Subject: [PATCH 07/42] Promisify util#serialize and util#translate --- lib/handlers/get.js | 17 +++++----- lib/ldp.js | 35 +++++++++----------- lib/utils.js | 79 ++++++++++++++++++++++----------------------- 3 files changed, 63 insertions(+), 68 deletions(-) diff --git a/lib/handlers/get.js b/lib/handlers/get.js index d26bc96c6..6c4f33d12 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -131,16 +131,17 @@ function handler (req, res, next) { } // Translate from the contentType found to the possibleRDFType desired - translate(stream, baseUri, contentType, possibleRDFType, function (err, data) { - if (err) { + translate(stream, baseUri, contentType, possibleRDFType) + .then((data) => { + debug(req.originalUrl + ' translating ' + contentType + ' -> ' + possibleRDFType) + res.setHeader('Content-Type', possibleRDFType) + res.send(data) + return next() + }) + .catch((err) => { debug('error translating: ' + req.originalUrl + ' ' + contentType + ' -> ' + possibleRDFType + ' -- ' + 500 + ' ' + err.message) return next(error(500, 'Error translating between RDF formats')) - } - debug(req.originalUrl + ' translating ' + contentType + ' -> ' + possibleRDFType) - res.setHeader('Content-Type', possibleRDFType) - res.send(data) - return next() - }) + }) }) } diff --git a/lib/ldp.js b/lib/ldp.js index 4143fd5e2..d613e316b 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -162,18 +162,13 @@ class LDP { )) }) .catch(() => { throw error(500, "Can't list container") }) - .then(() => new Promise((resolve, reject) => { - // TODO 'text/turtle' is fixed, should be contentType instead - // This forces one more translation turtle -> desired - serialize(resourceGraph, reqUri, 'text/turtle', function (err, result) { - if (err) { - debug.handlers('GET -- Error serializing container: ' + err) - reject(error(500, "Can't serialize container")) - } else { - resolve(result) - } - }) - })) + // TODO 'text/turtle' is fixed, should be contentType instead + // This forces one more translation turtle -> desired + .then(() => serialize(resourceGraph, reqUri, 'text/turtle') + .catch((err) => { + debug.handlers('GET -- Error serializing container: ' + err) + return Promise.reject(error(500, "Can't serialize container")) + })) .then(result => callback(null, result), callback) } @@ -234,17 +229,17 @@ class LDP { let hostname = parsedUri.hostname let path = parsedUri.pathname - serialize(graph, uri, contentType, (error, content) => { - if (error) { return reject(error) } + serialize(graph, uri, contentType) + .then((content) => { + let stream = stringToStream(content) - let stream = stringToStream(content) + this.put(hostname, path, stream, (error) => { + if (error) { return reject(error) } - this.put(hostname, path, stream, (error) => { - if (error) { return reject(error) } - - resolve(graph) + resolve(graph) + }) }) - }) + .catch(reject) }) } diff --git a/lib/utils.js b/lib/utils.js index 55fbd30a8..86fe04c6a 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -137,51 +137,50 @@ function hasSuffix (path, suffixes) { return false } -function serialize (graph, baseUri, contentType, callback) { - try { - // target, kb, base, contentType, callback - $rdf.serialize(null, graph, baseUri, contentType, function (err, result) { - if (err) { - console.log(err) - return callback(err) - } - if (result === undefined) { - return callback(new Error('Error serializing the graph to ' + - contentType)) - } - - return callback(null, result) - }) - } catch (err) { - console.log(err) - callback(err) - } +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, callback) { - // Handle Turtle Accept header - if (to === 'text/turtle' || - to === 'text/n3' || - to === 'application/turtle' || - to === 'application/n3') { - to = 'text/turtle' - } +function translate (stream, baseUri, from, to) { + return new Promise((resolve, reject) => { + // Handle Turtle Accept header + if (to === 'text/turtle' || + to === 'text/n3' || + to === 'application/turtle' || + to === 'application/n3') { + to = 'text/turtle' + } - 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 callback(err) - serialize(graph, baseUri, to, function (err, data) { - if (err) return callback(err) - callback(null, data) + 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) { From ab5143b9c36f8ccb865532aca488c04669b4a068 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 14 Nov 2018 11:08:48 +0100 Subject: [PATCH 08/42] Use ResourceMapper in ldp#put --- lib/handlers/post.js | 18 ++++---- lib/handlers/put.js | 18 ++++---- lib/ldp.js | 81 ++++++++++++++++-------------------- test/integration/ldp-test.js | 14 +++---- 4 files changed, 58 insertions(+), 73 deletions(-) diff --git a/lib/handlers/post.js b/lib/handlers/post.js index 5480c6bba..898adbcdc 100644 --- a/lib/handlers/post.js +++ b/lib/handlers/post.js @@ -54,17 +54,15 @@ function handler (req, res, next) { debug('receving multiple files') const busboy = new Busboy({ headers: req.headers }) - busboy.on('file', function (fieldname, file, filename, encoding, mimetype) { + busboy.on('file', async function (fieldname, file, filename, encoding, mimetype) { debug('One file received via multipart: ' + filename) - ldp.put( - req.hostname, - path.join(containerPath, filename), - file, - function (err) { - if (err) { - busboy.emit('error', err) - } - }) + const { url: putUrl } = await ldp.resourceMapper.mapFileToUrl( + { path: ldp.resourceMapper._rootPath + path.join(containerPath, filename), hostname: req.hostname }) + try { + await ldp.put(putUrl, file) + } catch (err) { + busboy.emit('error', err) + } }) busboy.on('error', function (err) { debug('Error receiving the file: ' + err.message) diff --git a/lib/handlers/put.js b/lib/handlers/put.js index 1dd2f550d..7d6536251 100644 --- a/lib/handlers/put.js +++ b/lib/handlers/put.js @@ -7,16 +7,16 @@ function handler (req, res, next) { debug(req.originalUrl) res.header('MS-Author-Via', 'SPARQL') - ldp.put(req.hostname, req.path, req, function (err) { - if (err) { + ldp.put(req, req) + .then(() => { + debug('succeded putting the file') + + res.sendStatus(201) + return next() + }) + .catch(err => { debug('error putting the file:' + err.message) err.message = 'Can\'t write file: ' + err.message return next(err) - } - - debug('succeded putting the file') - - res.sendStatus(201) - return next() - }) + }) } diff --git a/lib/ldp.js b/lib/ldp.js index d613e316b..ab31faa07 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -188,21 +188,23 @@ class LDP { extension = '' } // TODO: possibly package this in ldp.post - ldp.getAvailablePath(host, containerPath, { slug, extension }).then(resourcePath => { - debug.handlers('POST -- Will create at: ' + resourcePath) - let originalPath = resourcePath - if (container) { - // Create directory by an LDP PUT to the container's .meta resource - resourcePath = path.join(originalPath, ldp.suffixMeta) - if (originalPath && !originalPath.endsWith('/')) { - originalPath += '/' + ldp.getAvailablePath(host, containerPath, { slug, extension }) + .then(async resourcePath => { + debug.handlers('POST -- Will create at: ' + resourcePath) + let originalPath = resourcePath + if (container) { + // Create directory by an LDP PUT to the container's .meta resource + resourcePath = path.join(originalPath, ldp.suffixMeta) + if (originalPath && !originalPath.endsWith('/')) { + originalPath += '/' + } } - } - ldp.put(host, resourcePath, stream, function (err) { - if (err) callback(err) + const { url: putUrl } = await this.resourceMapper.mapFileToUrl( + { path: this.resourceMapper._rootPath + resourcePath, hostname: host }) + await ldp.put(putUrl, stream) callback(null, originalPath) }) - }) + .catch((err) => callback(err)) } /** @@ -223,52 +225,41 @@ class LDP { * * @return {Promise} */ - putGraph (graph, uri, contentType = DEFAULT_CONTENT_TYPE) { - return new Promise((resolve, reject) => { - let parsedUri = url.parse(uri) - let hostname = parsedUri.hostname - let path = parsedUri.pathname - - serialize(graph, uri, contentType) - .then((content) => { - let stream = stringToStream(content) - - this.put(hostname, path, stream, (error) => { - if (error) { return reject(error) } - - resolve(graph) - }) - }) - .catch(reject) - }) + async putGraph (graph, uri, contentType = DEFAULT_CONTENT_TYPE) { + const { path } = url.parse(uri) + const content = await serialize(graph, uri, contentType) + let stream = stringToStream(content) + return await this.put(path, stream) } - put (host, resourcePath, stream, callback) { - const ldp = this - const root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' - const filePath = utils.uriToFilename(resourcePath, root, host) + async put (url, stream) { + const { path: filePath } = await this.resourceMapper.mapUrlToFile({ url }) // PUT requests not supported on containers. Use POST instead if (filePath.endsWith('/')) { - return callback(error(409, - 'PUT not supported on containers, use POST instead')) + throw error(409, + 'PUT not supported on containers, use POST instead') } + // First, create the enclosing directory, if necessary const dirName = path.dirname(filePath) - mkdirp(dirName, (err) => { - if (err) { - debug.handlers('PUT -- Error creating directory: ' + err) - return callback(error(err, - 'Failed to create the path to the new resource')) - } - // Directory created, now write the file + try { + await promisify(mkdirp)(dirName) + } catch (err) { + debug.handlers('PUT -- Error creating directory: ' + err) + throw error(err, + 'Failed to create the path to the new resource') + } + + // Directory created, now write the file + return new Promise((resolve, reject) => { const file = stream.pipe(fs.createWriteStream(filePath)) file.on('error', function () { - callback(error(500, 'Error writing data')) + reject(error(500, 'Error writing data')) }) file.on('finish', function () { debug.handlers('PUT -- Wrote data to: ' + filePath) - callback(null) + resolve(null) }) }) } diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index 281f162d6..31e9a9322 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -115,22 +115,19 @@ describe('LDP', function () { }) describe('put', function () { - it('should write a file in an existing dir', function (done) { + it('should write a file in an existing dir', () => { var stream = stringToStream('hello world') - ldp.put('localhost', '/resources/testPut.txt', stream, function (err) { - assert.notOk(err) + return ldp.put('/resources/testPut.txt', stream).then(() => { var found = read('testPut.txt') rm('testPut.txt') assert.equal(found, 'hello world') - done() }) }) - it('should fail if a trailing `/` is passed', function (done) { + it('should fail if a trailing `/` is passed', () => { var stream = stringToStream('hello world') - ldp.put('localhost', '/resources/', stream, function (err) { + return ldp.put('/resources/', stream).catch(err => { assert.equal(err.status, 409) - done() }) }) }) @@ -138,8 +135,7 @@ describe('LDP', function () { describe('delete', function () { it('should delete a file in an existing dir', function (done) { var stream = stringToStream('hello world') - ldp.put('localhost', '/resources/testPut.txt', stream, function (err) { - assert.notOk(err) + ldp.put('/resources/testPut.txt', stream).then(() => { fs.stat(ldp.root + '/resources/testPut.txt', function (err) { if (err) { return done(err) From 04a18c9fa9392bbf37ca4bb764698b883eb24a81 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 14 Nov 2018 13:42:55 +0100 Subject: [PATCH 09/42] Use ResourceMapper in ldp#delete --- lib/handlers/delete.js | 17 ++++---- lib/ldp.js | 84 ++++++++++++++++++------------------ test/integration/ldp-test.js | 76 ++++++++++++++++++++++++++------ 3 files changed, 113 insertions(+), 64 deletions(-) diff --git a/lib/handlers/delete.js b/lib/handlers/delete.js index 40ced1b84..c6df97774 100644 --- a/lib/handlers/delete.js +++ b/lib/handlers/delete.js @@ -6,13 +6,14 @@ function handler (req, res, next) { debug('DELETE -- Request on' + req.originalUrl) const ldp = req.app.locals.ldp - ldp.delete(req.hostname, req.path, function (err) { - if (err) { + ldp.delete(req) + .then(() => { + debug('DELETE -- Ok.') + res.sendStatus(200) + next() + }) + .catch(err => { debug('DELETE -- Failed to delete: ' + err) - return next(err) - } - debug('DELETE -- Ok.') - res.sendStatus(200) - return next() - }) + next(err) + }) } diff --git a/lib/ldp.js b/lib/ldp.js index ab31faa07..eb98d9946 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -418,58 +418,58 @@ class LDP { ) } - delete (host, resourcePath, callback) { - const ldp = this - const root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' - const filename = utils.uriToFilename(resourcePath, root) - ldp.stat(filename).then(stats => { - if (stats.isDirectory()) { - return ldp.deleteContainer(filename, callback) - } else { - return ldp.deleteResource(filename, callback) - } - }, - err => callback(error(404, "Can't find " + err)) - ) + async delete (url) { + const { path } = await this.resourceMapper.mapUrlToFile({ url }) + + // First check if the path points to a valid file + let stats + try { + stats = await this.stat(path) + } catch (err) { + throw error(404, "Can't find " + err) + } + + // If so, delete the directory or file + if (stats.isDirectory()) { + return this.deleteContainer(path) + } else { + return this.deleteResource(path) + } } - deleteContainer (directory, callback) { - const self = this + async deleteContainer (directory) { if (directory[ directory.length - 1 ] !== '/') { directory += '/' } - let countValid = 0 - fs.readdir(directory, function (err, list) { - if (err) return callback(error(404, 'The container does not exist')) - - if (list.indexOf(self.suffixMeta) > -1) { - countValid++ - } - - if (list.indexOf(self.suffixAcl) > -1) { - countValid++ - } + // Ensure the container exists + let list + try { + list = await promisify(fs.readdir)(directory) + } catch (err) { + throw error(404, 'The container does not exist') + } - if (list.length !== countValid) { - return callback(error(409, 'Container is not empty')) - } + // Ensure the container is empty (we ignore .meta and .acl) + if (list.some(file => file !== this.suffixMeta && file !== this.suffixAcl)) { + throw error(409, 'Container is not empty') + } - return rimraf(directory, function (err) { - if (err) return callback(error(err, 'Failed to delete the container')) - return callback(null) - }) - }) + // Delete the directory recursively + try { + await promisify(rimraf)(directory) + } catch (err) { + throw error(err, 'Failed to delete the container') + } } - deleteResource (filename, callback) { - return fs.unlink(filename, function (err, data) { - if (err) { - debug.container('DELETE -- unlink() error: ' + err) - return callback(error(err, 'Failed to delete resource')) - } - return callback(null, data) - }) + async deleteResource (filename) { + try { + return promisify(fs.unlink)(filename) + } catch (err) { + debug.container('DELETE -- unlink() error: ' + err) + throw error(err, 'Failed to delete resource') + } } getAvailablePath (host, containerURI, { slug = uuid.v1(), extension }) { diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index 31e9a9322..179b3bf54 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -1,4 +1,6 @@ -var assert = require('chai').assert +var chai = require('chai') +var assert = chai.assert +chai.use(require('chai-as-promised')) var $rdf = require('rdflib') var ns = require('solid-namespace')($rdf) var LDP = require('../../lib/ldp') @@ -133,21 +135,67 @@ describe('LDP', function () { }) describe('delete', function () { - it('should delete a file in an existing dir', function (done) { + it('should error when deleting a non-existing file', () => { + return assert.isRejected(ldp.delete('/resources/testPut.txt')) + }) + + it('should delete a file in an existing dir', async () => { + // First create a dummy file var stream = stringToStream('hello world') - ldp.put('/resources/testPut.txt', stream).then(() => { - fs.stat(ldp.root + '/resources/testPut.txt', function (err) { - if (err) { - return done(err) - } - ldp.delete('localhost', '/resources/testPut.txt', function (err) { - if (err) done(err) - fs.stat(ldp.root + '/resources/testPut.txt', function (err) { - return done(err ? null : new Error('file still exists')) - }) - }) - }) + await ldp.put('/resources/testPut.txt', stream) + // Make sure it exists + fs.stat(ldp.root + '/resources/testPut.txt', function (err) { + if (err) { + throw err + } + }) + + // Now delete the dummy file + await ldp.delete('/resources/testPut.txt') + // Make sure it does not exist anymore + fs.stat(ldp.root + '/resources/testPut.txt', function (err, s) { + if (!err) { + throw new Error('file still exists') + } + }) + }) + + it('should fail to delete a non-empty folder', async () => { + // First create a dummy file + var stream = stringToStream('hello world') + await ldp.put('/resources/dummy/testPutBlocking.txt', stream) + // Make sure it exists + fs.stat(ldp.root + '/resources/dummy/testPutBlocking.txt', function (err) { + if (err) { + throw err + } }) + + // Now try to delete its folder + return assert.isRejected(ldp.delete('/resources/dummy/')) + }) + + it('should fail to delete nested non-empty folders', async () => { + // First create a dummy file + var stream = stringToStream('hello world') + await ldp.put('/resources/dummy/dummy2/testPutBlocking.txt', stream) + // Make sure it exists + fs.stat(ldp.root + '/resources/dummy/dummy2/testPutBlocking.txt', function (err) { + if (err) { + throw err + } + }) + + // Now try to delete its parent folder + return assert.isRejected(ldp.delete('/resources/dummy/')) + }) + + after(async function () { + // Clean up after delete tests + await ldp.delete('/resources/dummy/testPutBlocking.txt') + await ldp.delete('/resources/dummy/dummy2/testPutBlocking.txt') + await ldp.delete('/resources/dummy/dummy2/') + await ldp.delete('/resources/dummy/') }) }) describe('listContainer', function () { From ae95dee8096770662cd6aea382ba1d4413168651 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 14 Nov 2018 14:30:47 +0100 Subject: [PATCH 10/42] Use ResourceMapper in patch handler --- lib/handlers/patch.js | 89 +++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/lib/handlers/patch.js b/lib/handlers/patch.js index a5a320bcb..744c47166 100644 --- a/lib/handlers/patch.js +++ b/lib/handlers/patch.js @@ -3,16 +3,12 @@ module.exports = handler const bodyParser = require('body-parser') -const mime = require('mime-types') const fs = require('fs') const debug = require('../debug').handlers -const utils = require('../utils.js') const error = require('../http-error') const $rdf = require('rdflib') const crypto = require('crypto') -const DEFAULT_TARGET_TYPE = 'text/turtle' - // Patch parsers by request body content type const PATCH_PARSERS = { 'application/sparql-update': require('./patch/sparql-update-parser.js'), @@ -20,43 +16,46 @@ const PATCH_PARSERS = { } // Handles a PATCH request -function patchHandler (req, res, next) { +async function patchHandler (req, res, next) { debug(`PATCH -- ${req.originalUrl}`) res.header('MS-Author-Via', 'SPARQL') + try { + // Obtain details of the target resource + const ldp = req.app.locals.ldp + const { path, contentType } = await ldp.resourceMapper.mapUrlToFile({ url: req }) + const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname }) + const resource = { path, contentType, url } + debug('PATCH -- Target <%s> (%s)', url, contentType) - // Obtain details of the target resource - const ldp = req.app.locals.ldp - const root = !ldp.multiuser ? ldp.root : `${ldp.root}${req.hostname}/` - const target = {} - target.file = utils.uriToFilename(req.path, root) - target.uri = utils.getBaseUri(req) + req.originalUrl - target.contentType = mime.lookup(target.file) || DEFAULT_TARGET_TYPE - debug('PATCH -- Target <%s> (%s)', target.uri, target.contentType) + // Obtain details of the patch document + const patch = {} + patch.text = req.body ? req.body.toString() : '' + patch.uri = `${url}#patch-${hash(patch.text)}` + patch.contentType = (req.get('content-type') || '').match(/^[^;\s]*/)[0] + debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType) + const parsePatch = PATCH_PARSERS[patch.contentType] + if (!parsePatch) { + throw error(415, `Unsupported patch content type: ${patch.contentType}`) + } - // Obtain details of the patch document - const patch = {} - patch.text = req.body ? req.body.toString() : '' - patch.uri = `${target.uri}#patch-${hash(patch.text)}` - patch.contentType = (req.get('content-type') || '').match(/^[^;\s]*/)[0] - debug('PATCH -- Received patch (%d bytes, %s)', patch.text.length, patch.contentType) - const parsePatch = PATCH_PARSERS[patch.contentType] - if (!parsePatch) { - return next(error(415, `Unsupported patch content type: ${patch.contentType}`)) - } + // Parse the target graph and the patch document, + // and verify permission for performing this specific patch + const [graph, patchObject] = await Promise.all([ + readGraph(resource), + parsePatch(url, patch.uri, patch.text) + .then(patchObject => checkPermission(req, patchObject)) + ]) - // Parse the target graph and the patch document, - // and verify permission for performing this specific patch - Promise.all([ - readGraph(target), - parsePatch(target.uri, patch.uri, patch.text) - .then(patchObject => checkPermission(target, req, patchObject)) - ]) - // Patch the graph and write it back to the file - .then(([graph, patchObject]) => applyPatch(patchObject, graph, target)) - .then(graph => writeGraph(graph, target)) - // Send the result to the client - .then(result => { res.send(result) }) - .then(next, next) + // Patch the graph and write it back to the file + await applyPatch(patchObject, graph, url) + const result = await writeGraph(graph, resource) + + // Send the result to the client + res.send(result) + } catch (err) { + return next(err) + } + return next() } // Reads the request body and calls the actual patch handler @@ -69,7 +68,7 @@ const readEntity = bodyParser.text({ type: () => true }) function readGraph (resource) { // Read the resource's file return new Promise((resolve, reject) => - fs.readFile(resource.file, {encoding: 'utf8'}, function (err, fileContents) { + fs.readFile(resource.path, {encoding: 'utf8'}, function (err, fileContents) { if (err) { // If the file does not exist, assume empty contents // (it will be created after a successful patch) @@ -87,9 +86,9 @@ function readGraph (resource) { // Parse the resource's file contents .then((fileContents) => { const graph = $rdf.graph() - debug('PATCH -- Reading %s with content type %s', resource.uri, resource.contentType) + debug('PATCH -- Reading %s with content type %s', resource.url, resource.contentType) try { - $rdf.parse(fileContents, graph, resource.uri, resource.contentType) + $rdf.parse(fileContents, graph, resource.url, resource.contentType) } catch (err) { throw error(500, `Patch: Target ${resource.contentType} file syntax error: ${err}`) } @@ -99,7 +98,7 @@ function readGraph (resource) { } // Verifies whether the user is allowed to perform the patch on the target -function checkPermission (target, request, patchObject) { +function checkPermission (request, patchObject) { // If no ACL object was passed down, assume permissions are okay. if (!request.acl) return Promise.resolve(patchObject) // At this point, we already assume append access, @@ -122,10 +121,10 @@ function checkPermission (target, request, patchObject) { } // Applies the patch to the RDF graph -function applyPatch (patchObject, graph, target) { +function applyPatch (patchObject, graph, url) { debug('PATCH -- Applying patch') return new Promise((resolve, reject) => - graph.applyPatch(patchObject, graph.sym(target.uri), (err) => { + graph.applyPatch(patchObject, graph.sym(url), (err) => { if (err) { const message = err.message || err // returns string at the moment debug(`PATCH -- FAILED. Returning 409. Message: '${message}'`) @@ -140,10 +139,10 @@ function applyPatch (patchObject, graph, target) { function writeGraph (graph, resource) { debug('PATCH -- Writing patched file') return new Promise((resolve, reject) => { - const resourceSym = graph.sym(resource.uri) - const serialized = $rdf.serialize(resourceSym, graph, resource.uri, resource.contentType) + const resourceSym = graph.sym(resource.url) + const serialized = $rdf.serialize(resourceSym, graph, resource.url, resource.contentType) - fs.writeFile(resource.file, serialized, {encoding: 'utf8'}, function (err) { + fs.writeFile(resource.path, serialized, {encoding: 'utf8'}, function (err) { if (err) { return reject(error(500, `Failed to write file after patch: ${err}`)) } From 13a29ec20d4fdaf2080949f839ab5a6a349ee29e Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 11:09:04 +0100 Subject: [PATCH 11/42] Use ResourceMapper in ldp#get --- lib/handlers/get.js | 164 +++++++++++++++--------------- lib/ldp-container.js | 191 ++++++++++++++++++----------------- lib/ldp.js | 182 ++++++++++++++------------------- lib/resource-mapper.js | 2 +- test/integration/ldp-test.js | 110 ++++++++++---------- 5 files changed, 313 insertions(+), 336 deletions(-) diff --git a/lib/handlers/get.js b/lib/handlers/get.js index 6c4f33d12..a155b9a71 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -19,7 +19,7 @@ const error = require('../http-error') const RDFs = require('../ldp').RDF_MIME_TYPES const LegacyResourceMapper = require('../legacy-resource-mapper') -function handler (req, res, next) { +async function handler (req, res, next) { const ldp = req.app.locals.ldp const includeBody = req.method === 'GET' const negotiator = new Negotiator(req) @@ -42,107 +42,105 @@ function handler (req, res, next) { const options = { 'hostname': req.hostname, 'path': path, - 'baseUri': baseUri, 'includeBody': includeBody, 'possibleRDFType': possibleRDFType, 'range': req.headers.range } - ldp.get(options, function (err, ret) { + + let ret + try { + ret = await ldp.get(options) + } catch (err) { // use globHandler if magic is detected - if (err && err.status === 404 && glob.hasMagic(path)) { + if (err.status === 404 && glob.hasMagic(path)) { debug('forwarding to glob request') return globHandler(req, res, next) - } - - // Handle error - if (err) { + } else { debug(req.method + ' -- Error: ' + err.status + ' ' + err.message) return next(err) } + } - let stream - let contentType - let container - let contentRange - let chunksize - - if (ret) { - stream = ret.stream - contentType = ret.contentType - container = ret.container - contentRange = ret.contentRange - chunksize = ret.chunksize - } - - // Till here it must exist - if (!includeBody) { - debug('HEAD only') - const rootUrl = utils.getBaseUri(req) - const mapper = new LegacyResourceMapper({ - rootUrl, - rootPath: ldp.root, - includeHost: ldp.multiuser - }) - return mapper.mapFileToUrl({ path }).then(mappedFile => { - contentType = mappedFile.contentType - res.setHeader('Content-Type', contentType) - res.status(200).send('OK') - return next() - }) - } + let stream + let contentType + let container + let contentRange + let chunksize + + if (ret) { + stream = ret.stream + contentType = ret.contentType + container = ret.container + contentRange = ret.contentRange + chunksize = ret.chunksize + } - // Handle dataBrowser - if (requestedType && requestedType.includes('text/html')) { - let mimeTypeByExt = mime.lookup(_path.basename(path)) - let isHtmlResource = mimeTypeByExt && mimeTypeByExt.includes('html') - let useDataBrowser = ldp.dataBrowserPath && ( - container || - RDFs.includes(contentType) && !isHtmlResource && !ldp.suppressDataBrowser) - - if (useDataBrowser) { - res.set('Content-Type', 'text/html') - const defaultDataBrowser = _path.join(__dirname, '../../static/databrowser.html') - const dataBrowserPath = ldp.dataBrowserPath === 'default' ? defaultDataBrowser : ldp.dataBrowserPath - debug(' sending data browser file: ' + dataBrowserPath) - res.sendFile(dataBrowserPath) - return - } else if (stream) { - res.setHeader('Content-Type', contentType) - return stream.pipe(res) - } - } + // Till here it must exist + if (!includeBody) { + debug('HEAD only') + const rootUrl = utils.getBaseUri(req) + const mapper = new LegacyResourceMapper({ + rootUrl, + rootPath: ldp.root, + includeHost: ldp.multiuser + }) + const mappedFile = await mapper.mapFileToUrl({ path }) + contentType = mappedFile.contentType + res.setHeader('Content-Type', contentType) + res.status(200).send('OK') + return next() + } - // If request accepts the content-type we found - if (stream && negotiator.mediaType([contentType])) { + // Handle dataBrowser + if (requestedType && requestedType.includes('text/html')) { + let mimeTypeByExt = mime.lookup(_path.basename(path)) + let isHtmlResource = mimeTypeByExt && mimeTypeByExt.includes('html') + let useDataBrowser = ldp.dataBrowserPath && ( + container || + RDFs.includes(contentType) && !isHtmlResource && !ldp.suppressDataBrowser) + + if (useDataBrowser) { + res.set('Content-Type', 'text/html') + const defaultDataBrowser = _path.join(__dirname, '../../static/databrowser.html') + const dataBrowserPath = ldp.dataBrowserPath === 'default' ? defaultDataBrowser : ldp.dataBrowserPath + debug(' sending data browser file: ' + dataBrowserPath) + res.sendFile(dataBrowserPath) + return + } else if (stream) { res.setHeader('Content-Type', contentType) - if (contentRange) { - const headers = { 'Content-Range': contentRange, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize } - res.writeHead(206, headers) - return stream.pipe(res) - } else { - return stream.pipe(res) - } + return stream.pipe(res) } + } - // If it is not in our RDFs we can't even translate, - // Sorry, we can't help - if (!possibleRDFType) { - return next(error(406, 'Cannot serve requested type: ' + contentType)) + // If request accepts the content-type we found + if (stream && negotiator.mediaType([contentType])) { + res.setHeader('Content-Type', contentType) + if (contentRange) { + const headers = { 'Content-Range': contentRange, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize } + res.writeHead(206, headers) + return stream.pipe(res) + } else { + return stream.pipe(res) } + } + + // If it is not in our RDFs we can't even translate, + // Sorry, we can't help + if (!possibleRDFType) { + return next(error(406, 'Cannot serve requested type: ' + contentType)) + } + try { // Translate from the contentType found to the possibleRDFType desired - translate(stream, baseUri, contentType, possibleRDFType) - .then((data) => { - debug(req.originalUrl + ' translating ' + contentType + ' -> ' + possibleRDFType) - res.setHeader('Content-Type', possibleRDFType) - res.send(data) - return next() - }) - .catch((err) => { - debug('error translating: ' + req.originalUrl + ' ' + contentType + ' -> ' + possibleRDFType + ' -- ' + 500 + ' ' + err.message) - return next(error(500, 'Error translating between RDF formats')) - }) - }) + const data = await translate(stream, baseUri, contentType, possibleRDFType) + debug(req.originalUrl + ' translating ' + contentType + ' -> ' + possibleRDFType) + res.setHeader('Content-Type', possibleRDFType) + res.send(data) + return next() + } catch (err) { + debug('error translating: ' + req.originalUrl + ' ' + contentType + ' -> ' + possibleRDFType + ' -- ' + 500 + ' ' + err.message) + return next(error(500, 'Error translating between RDF formats')) + } } function globHandler (req, res, next) { diff --git a/lib/ldp-container.js b/lib/ldp-container.js index 58fe0323a..4a9ad967e 100644 --- a/lib/ldp-container.js +++ b/lib/ldp-container.js @@ -11,99 +11,99 @@ const ns = require('solid-namespace')($rdf) const mime = require('mime-types') const path = require('path') -function addContainerStats (ldp, reqUri, filename, resourceGraph, next) { - ldp.stat(filename).then(containerStats => { - addStats(resourceGraph, reqUri, containerStats) - resourceGraph.add( - resourceGraph.sym(reqUri), - ns.rdf('type'), - ns.ldp('BasicContainer')) - - resourceGraph.add( - resourceGraph.sym(reqUri), - ns.rdf('type'), - ns.ldp('Container')) - next() - }, () => next()) +async function addContainerStats (ldp, reqUri, filename, resourceGraph) { + const containerStats = await ldp.stat(filename) + addStats(resourceGraph, reqUri, containerStats) + resourceGraph.add( + resourceGraph.sym(reqUri), + ns.rdf('type'), + ns.ldp('BasicContainer')) + resourceGraph.add( + resourceGraph.sym(reqUri), + ns.rdf('type'), + ns.ldp('Container')) } -function addFile (ldp, resourceGraph, containerUri, reqUri, uri, container, file, callback) { +async function addFile (ldp, resourceGraph, containerUri, reqUri, uri, container, file) { // Skip .meta and .acl if (file.endsWith(ldp.suffixMeta) || file.endsWith(ldp.suffixAcl)) { - return callback(null) + return null } const filePath = path.join(container, file) // Get file stats - ldp.stat(filePath).then(stats => { - let memberUri = reqUri + (stats.isDirectory() ? '/' : '') - // var fileSubject = file + (stats.isDirectory() ? '/' : '') - // var fileBaseUri = utils.filenameToBaseUri(fileSubject, uri, root) - - // Add fileStats to resource Graph - addStats(resourceGraph, memberUri, stats) - - // Add to `contains` list - resourceGraph.add( - resourceGraph.sym(containerUri), - ns.ldp('contains'), - resourceGraph.sym(memberUri)) + let stats + try { + stats = await ldp.stat(filePath) + } catch (e) { + return null + } + let memberUri = reqUri + (stats.isDirectory() ? '/' : '') - // Set up a metaFile path - // Earlier code used a .ttl file as its own meta file, which - // caused massive data files to parsed as part of deirectory listings just looking for type triples - const metaFile = containerUri + file + ldp.suffixMeta + // Add fileStats to resource Graph + addStats(resourceGraph, memberUri, stats) - getMetadataGraph(ldp, metaFile, memberUri, function (err, metadataGraph) { - if (err) { - metadataGraph = $rdf.graph() - } + // Add to `contains` list + resourceGraph.add( + resourceGraph.sym(containerUri), + ns.ldp('contains'), + resourceGraph.sym(memberUri)) + + // Set up a metaFile path + // Earlier code used a .ttl file as its own meta file, which + // caused massive data files to parsed as part of deirectory listings just looking for type triples + const metaFile = containerUri + file + ldp.suffixMeta + + let metadataGraph + try { + metadataGraph = await getMetadataGraph(ldp, metaFile, memberUri) + } catch (err) { + metadataGraph = $rdf.graph() + } - // Add Container or BasicContainer types - if (stats.isDirectory()) { - resourceGraph.add( - metadataGraph.sym(memberUri), - ns.rdf('type'), - ns.ldp('BasicContainer')) + // Add Container or BasicContainer types + if (stats.isDirectory()) { + resourceGraph.add( + metadataGraph.sym(memberUri), + ns.rdf('type'), + ns.ldp('BasicContainer')) + resourceGraph.add( + metadataGraph.sym(memberUri), + ns.rdf('type'), + ns.ldp('Container')) + } + // Add generic LDP type + resourceGraph.add( + metadataGraph.sym(memberUri), + ns.rdf('type'), + ns.ldp('Resource')) + + // Add type from metadataGraph + metadataGraph + .statementsMatching( + metadataGraph.sym(memberUri), + ns.rdf('type'), + undefined) + .forEach(function (typeStatement) { + // If the current is a file and its type is BasicContainer, + // This is not possible, so do not infer its type! + if ( + ( + typeStatement.object.uri !== ns.ldp('BasicContainer').uri && + typeStatement.object.uri !== ns.ldp('Container').uri + ) || + !stats.isFile() + ) { resourceGraph.add( - metadataGraph.sym(memberUri), - ns.rdf('type'), - ns.ldp('Container')) + resourceGraph.sym(reqUri), + typeStatement.predicate, + typeStatement.object) } - // Add generic LDP type - resourceGraph.add( - metadataGraph.sym(memberUri), - ns.rdf('type'), - ns.ldp('Resource')) - - // Add type from metadataGraph - metadataGraph - .statementsMatching( - metadataGraph.sym(memberUri), - ns.rdf('type'), - undefined) - .forEach(function (typeStatement) { - // If the current is a file and its type is BasicContainer, - // This is not possible, so do not infer its type! - if ( - ( - typeStatement.object.uri !== ns.ldp('BasicContainer').uri && - typeStatement.object.uri !== ns.ldp('Container').uri - ) || - !stats.isFile() - ) { - resourceGraph.add( - resourceGraph.sym(reqUri), - typeStatement.predicate, - typeStatement.object) - } - }) - - return callback(null) }) - }, () => callback(null)) + + return null } function addStats (resourceGraph, reqUri, stats) { @@ -132,27 +132,30 @@ function addStats (resourceGraph, reqUri, stats) { } } -function readdir (filename, callback) { +function readdir (filename) { debug.handlers('GET -- Reading directory') - fs.readdir(filename, function (err, files) { - if (err) { - debug.handlers('GET -- Error reading files: ' + err) - return callback(error(err, 'Can\'t read container')) - } + return new Promise((resolve, reject) => { + fs.readdir(filename, function (err, files) { + if (err) { + debug.handlers('GET -- Error reading files: ' + err) + return reject(error(err, 'Can\'t read container')) + } - debug.handlers('Files in directory: ' + files.toString().slice(0, 100)) - return callback(null, files) + debug.handlers('Files in directory: ' + files.toString().slice(0, 100)) + return resolve(files) + }) }) } -function getMetadataGraph (ldp, metaFile, fileBaseUri, callback) { - ldp.stat(metaFile).then(metaStats => { - if (metaStats && metaStats.isFile()) { - ldp.getGraph(metaFile) - .then(graph => callback(null, graph), - err => callback(error(err, 'Can\'t parse container metadata'))) - } else { - callback(null, $rdf.graph()) +async function getMetadataGraph (ldp, metaFile) { + const metaStats = await ldp.stat(metaFile) + if (metaStats && metaStats.isFile()) { + try { + return await ldp.getGraph(metaFile) + } catch (err) { + throw error(err, 'Can\'t parse container metadata') } - }, callback) + } else { + return $rdf.graph() + } } diff --git a/lib/ldp.js b/lib/ldp.js index eb98d9946..8636b8c7b 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -89,10 +89,10 @@ class LDP { return this } - async stat (file, callback) { + async stat (file) { return new Promise((resolve, reject) => { fs.stat(file, (err, stats) => { - if (err) return reject(error(err, "Can't read metadata")) + if (err) return reject(error(err, "Can't read metadata of " + file)) resolve(stats) }) }) @@ -119,57 +119,45 @@ class LDP { }) } - async readContainerMeta (url, callback) { + async readContainerMeta (url) { if (url[ url.length - 1 ] !== '/') { url += '/' } return this.readResource(url + this.suffixMeta) } - listContainer (filename, reqUri, uri, containerData, contentType, callback) { - const ldp = this - // var host = url.parse(uri).hostname - // var root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' - - // var baseUri = utils.filenameToBaseUri(filename, uri, root) + async listContainer (filename, reqUri, uri, containerData, contentType) { const resourceGraph = $rdf.graph() try { $rdf.parse(containerData, resourceGraph, reqUri, 'text/turtle') } catch (err) { debug.handlers('GET -- Error parsing data: ' + err) - return callback(error(500, "Can't parse container")) + throw error(500, "Can't parse container") + } + + try { + // add container stats + await ldpContainer.addContainerStats(this, reqUri, filename, resourceGraph) + // read directory + const files = await ldpContainer.readdir(filename) + // iterate through all the files + await Promise.all(files.map(file => { + const fileUri = url.resolve(reqUri, encodeURIComponent(file)) + return ldpContainer.addFile(this, resourceGraph, reqUri, fileUri, uri, filename, file) + })) + } catch (err) { + throw error(500, "Can't list container") } - // add container stats - new Promise((resolve, reject) => - ldpContainer.addContainerStats(ldp, reqUri, filename, resourceGraph, - err => err ? reject(err) : resolve()) - ) - // read directory - .then(() => new Promise((resolve, reject) => - ldpContainer.readdir(filename, - (err, files) => err ? reject(err) : resolve(files)) - )) - // iterate through all the files - .then(files => { - return Promise.all(files.map(file => - new Promise((resolve, reject) => { - const fileUri = url.resolve(reqUri, encodeURIComponent(file)) - ldpContainer.addFile(ldp, resourceGraph, reqUri, fileUri, uri, - filename, file, err => err ? reject(err) : resolve()) - }) - )) - }) - .catch(() => { throw error(500, "Can't list container") }) // TODO 'text/turtle' is fixed, should be contentType instead // This forces one more translation turtle -> desired - .then(() => serialize(resourceGraph, reqUri, 'text/turtle') - .catch((err) => { - debug.handlers('GET -- Error serializing container: ' + err) - return Promise.reject(error(500, "Can't serialize container")) - })) - .then(result => callback(null, result), callback) + try { + return await serialize(resourceGraph, reqUri, 'text/turtle') + } catch (err) { + debug.handlers('GET -- Error serializing container: ' + err) + throw error(500, "Can't serialize container") + } } post (host, containerPath, stream, { container, slug, extension }, callback) { @@ -267,11 +255,9 @@ class LDP { exists (hostname, path, callback) { const options = { hostname, path, includeBody: false } if (callback) { - return this.get(options, callback) + return this.get(options).then((data) => callback(null, data), callback) } else { - return new Promise((resolve, reject) => { - this.get(options, err => err ? reject(err) : resolve(true)) - }) + return this.get(options).then(() => true) } } @@ -339,83 +325,71 @@ class LDP { }) } - get (options, callback) { - let host - let reqPath - let baseUri - let includeBody - let contentType - let range - if (options) { - host = options.hostname - reqPath = options.path - baseUri = options.baseUri - includeBody = options.includeBody - contentType = options.possibleRDFType - range = options.range + async get (options) { + const { path: filename, contentType } = await this.resourceMapper.mapUrlToFile({ url: options }) + const baseUri = this.resourceMapper.getBaseUrl(options.hostname) + + let stats + try { + stats = await this.stat(filename) + } catch (err) { + throw error(err, 'Can\'t find file requested: ' + filename) + } + // Just return, since resource exists + if (!options.includeBody) { + return { 'stream': stats, 'contentType': contentType, 'container': stats.isDirectory() } } - const ldp = this - const root = !ldp.multiuser ? ldp.root : ldp.root + host + '/' - const filename = utils.uriToFilename(reqPath, root) - ldp.stat(filename).then(stats => { - // Just return, since resource exists - if (!includeBody) { - return callback(null, {'stream': stats, 'contentType': contentType, 'container': stats.isDirectory()}) + // Found a container + if (stats.isDirectory()) { + const { url: absContainerUri } = await this.resourceMapper + .mapFileToUrl({ path: filename, hostname: options.hostname }) + const metaFile = await this.readContainerMeta(absContainerUri) + .catch(() => '') // Default to an empty meta file if it is missing + let data + try { + data = await this.listContainer(filename, absContainerUri, baseUri, metaFile, contentType) + } catch (err) { + debug.handlers('GET container -- Read error:' + err.message) + throw err } - - // Found a container - if (stats.isDirectory()) { - return ldp.readContainerMeta(reqPath) - .catch(() => '') - .then(metaFile => { - const absContainerUri = baseUri + reqPath - ldp.listContainer(filename, absContainerUri, baseUri, metaFile, contentType, - function (err, data) { - if (err) { - debug.handlers('GET container -- Read error:' + err.message) - return callback(err) - } - const stream = stringToStream(data) - // TODO 'text/turtle' is fixed, should be contentType instead - // This forces one more translation turtle -> desired - return callback(null, {'stream': stream, 'contentType': 'text/turtle', 'container': true}) - }) - }) + const stream = stringToStream(data) + // TODO 'text/turtle' is fixed, should be contentType instead + // This forces one more translation turtle -> desired + return { 'stream': stream, 'contentType': 'text/turtle', 'container': true } + } else { + let stream + let chunksize + let contentRange + if (options.range) { + const total = fs.statSync(filename).size + const parts = options.range.replace(/bytes=/, '').split('-') + const partialstart = parts[0] + const partialend = parts[1] + const start = parseInt(partialstart, 10) + const end = partialend ? parseInt(partialend, 10) : total - 1 + chunksize = (end - start) + 1 + contentRange = 'bytes ' + start + '-' + end + '/' + total + stream = this.createReadStream(filename, start, end) } else { - let stream - let chunksize - let contentRange - if (range) { - const total = fs.statSync(filename).size - const parts = range.replace(/bytes=/, '').split('-') - const partialstart = parts[0] - const partialend = parts[1] - const start = parseInt(partialstart, 10) - const end = partialend ? parseInt(partialend, 10) : total - 1 - chunksize = (end - start) + 1 - contentRange = 'bytes ' + start + '-' + end + '/' + total - stream = ldp.createReadStream(filename, start, end) - } else { - stream = ldp.createReadStream(filename) - } + stream = this.createReadStream(filename) + } + return new Promise((resolve, reject) => { stream .on('error', function (err) { debug.handlers(`GET -- error reading ${filename}: ${err.message}`) - return callback(error(err, "Can't read file " + err)) + return reject(error(err, "Can't read file " + err)) }) .on('open', function () { debug.handlers(`GET -- Reading ${filename}`) let contentType = mime.lookup(filename) || DEFAULT_CONTENT_TYPE - if (utils.hasSuffix(filename, ldp.turtleExtensions)) { + if (utils.hasSuffix(filename, this.turtleExtensions)) { contentType = 'text/turtle' } - return callback(null, {'stream': stream, 'contentType': contentType, 'container': false, 'contentRange': contentRange, 'chunksize': chunksize}) + return resolve({ stream, contentType, 'container': false, contentRange, chunksize }) }) - } - }, - err => callback(error(err, 'Can\'t find file requested: ' + filename)) - ) + }) + } } async delete (url) { diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index a16cf901e..a03c7211f 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -10,7 +10,7 @@ const readdir = promisify(fs.readdir) class ResourceMapper { constructor ({ rootUrl = 'https://localhost:8443/', - rootPath = '/tmp/', + rootPath = process.cwd(), includeHost = false, defaultContentType = 'application/octet-stream' }) { diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index 179b3bf54..2f4e4b443 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -237,7 +237,7 @@ describe('LDP', function () { }) }) */ - it('should not inherit type of BasicContainer/Container if type is File', function (done) { + it('should not inherit type of BasicContainer/Container if type is File', () => { write('@prefix dcterms: .' + '@prefix o: .' + '<> a ;' + @@ -250,62 +250,64 @@ describe('LDP', function () { ' dcterms:title "This is a container" ;' + ' o:limit 500000.00 .', 'sampleContainer/basicContainerFile.ttl') - ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { - if (err) done(err) - var graph = $rdf.graph() - $rdf.parse( - data, - graph, - 'https://server.tld/sampleContainer', - 'text/turtle') - - var basicContainerStatements = graph - .each( - $rdf.sym('https://server.tld/basicContainerFile.ttl'), - ns.rdf('type'), - undefined - ) - .map(d => { return d.uri }) - - let expectedStatements = [ - 'http://www.w3.org/ns/iana/media-types/text/turtle#Resource', - 'http://www.w3.org/ns/ldp#Resource' - ] - assert.deepEqual(basicContainerStatements.sort(), expectedStatements) - - var containerStatements = graph - .each( - $rdf.sym('https://server.tld/containerFile.ttl'), - ns.rdf('type'), - undefined - ) - .map(d => { return d.uri }) - - assert.deepEqual(containerStatements.sort(), expectedStatements) - - rm('sampleContainer/containerFile.ttl') - rm('sampleContainer/basicContainerFile.ttl') - done() - }) + return ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle') + .then(data => { + var graph = $rdf.graph() + $rdf.parse( + data, + graph, + 'https://server.tld/sampleContainer', + 'text/turtle') + + var basicContainerStatements = graph + .each( + $rdf.sym('https://server.tld/basicContainerFile.ttl'), + ns.rdf('type'), + undefined + ) + .map(d => { return d.uri }) + + let expectedStatements = [ + 'http://www.w3.org/ns/iana/media-types/text/turtle#Resource', + 'http://www.w3.org/ns/ldp#Resource' + ] + assert.deepEqual(basicContainerStatements.sort(), expectedStatements) + + var containerStatements = graph + .each( + $rdf.sym('https://server.tld/containerFile.ttl'), + ns.rdf('type'), + undefined + ) + .map(d => { return d.uri }) + + assert.deepEqual(containerStatements.sort(), expectedStatements) + + rm('sampleContainer/containerFile.ttl') + rm('sampleContainer/basicContainerFile.ttl') + }) }) - it('should ldp:contains the same files in dir', function (done) { - ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle', function (err, data) { - if (err) done(err) - fs.readdir(path.join(__dirname, '../resources/sampleContainer/'), function (err, expectedFiles) { - const graph = $rdf.graph() - $rdf.parse(data, graph, 'https://server.tld/sampleContainer/', 'text/turtle') - const statements = graph.match(null, ns.ldp('contains'), null) - const files = statements - .map(s => s.object.value.replace(/.*\//, '')) - .map(decodeURIComponent) - - files.sort() - expectedFiles.sort() - assert.deepEqual(files, expectedFiles) - done(err) + it('should ldp:contains the same files in dir', () => { + ldp.listContainer(path.join(__dirname, '../resources/sampleContainer/'), 'https://server.tld/resources/sampleContainer/', 'https://server.tld', '', 'text/turtle') + .then(data => { + fs.readdir(path.join(__dirname, '../resources/sampleContainer/'), function (err, expectedFiles) { + if (err) { + return Promise.reject(err) + } + + const graph = $rdf.graph() + $rdf.parse(data, graph, 'https://server.tld/sampleContainer/', 'text/turtle') + const statements = graph.match(null, ns.ldp('contains'), null) + const files = statements + .map(s => s.object.value.replace(/.*\//, '')) + .map(decodeURIComponent) + + files.sort() + expectedFiles.sort() + assert.deepEqual(files, expectedFiles) + }) }) - }) }) }) }) From 0135c236cf276e331bcb97049bc668fa1f7d3349 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 11:15:37 +0100 Subject: [PATCH 12/42] Promisify ldp#post --- lib/handlers/post.js | 10 ++++------ lib/ldp.js | 36 ++++++++++++++++-------------------- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/lib/handlers/post.js b/lib/handlers/post.js index 898adbcdc..e2c665e76 100644 --- a/lib/handlers/post.js +++ b/lib/handlers/post.js @@ -86,16 +86,14 @@ function handler (req, res, next) { const extension = mimeType in extensions ? `.${extensions[mimeType][0]}` : '' ldp.post(req.hostname, containerPath, req, - { slug, extension, container: links.isBasicContainer }, - function (err, resourcePath) { - if (err) { - return next(err) - } + { slug, extension, container: links.isBasicContainer }).then( + resourcePath => { debug('File stored in ' + resourcePath) header.addLinks(res, links) res.set('Location', resourcePath) res.sendStatus(201) next() - }) + }, + err => next(err)) } } diff --git a/lib/ldp.js b/lib/ldp.js index 8636b8c7b..371adefdf 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -160,15 +160,14 @@ class LDP { } } - post (host, containerPath, stream, { container, slug, extension }, callback) { + async post (host, containerPath, stream, { container, slug, extension }) { const ldp = this debug.handlers('POST -- On parent: ' + containerPath) // prepare slug if (slug) { slug = decodeURIComponent(slug) if (slug.match(/\/|\||:/)) { - callback(error(400, 'The name of new file POSTed may not contain : | or /')) - return + throw error(400, 'The name of new file POSTed may not contain : | or /') } } // Containers should not receive an extension @@ -176,23 +175,20 @@ class LDP { extension = '' } // TODO: possibly package this in ldp.post - ldp.getAvailablePath(host, containerPath, { slug, extension }) - .then(async resourcePath => { - debug.handlers('POST -- Will create at: ' + resourcePath) - let originalPath = resourcePath - if (container) { - // Create directory by an LDP PUT to the container's .meta resource - resourcePath = path.join(originalPath, ldp.suffixMeta) - if (originalPath && !originalPath.endsWith('/')) { - originalPath += '/' - } - } - const { url: putUrl } = await this.resourceMapper.mapFileToUrl( - { path: this.resourceMapper._rootPath + resourcePath, hostname: host }) - await ldp.put(putUrl, stream) - callback(null, originalPath) - }) - .catch((err) => callback(err)) + let resourcePath = await ldp.getAvailablePath(host, containerPath, { slug, extension }) + debug.handlers('POST -- Will create at: ' + resourcePath) + let originalPath = resourcePath + if (container) { + // Create directory by an LDP PUT to the container's .meta resource + resourcePath = path.join(originalPath, ldp.suffixMeta) + if (originalPath && !originalPath.endsWith('/')) { + originalPath += '/' + } + } + const { url: putUrl } = await this.resourceMapper.mapFileToUrl( + { path: this.resourceMapper._rootPath + resourcePath, hostname: host }) + await ldp.put(putUrl, stream) + return originalPath } /** From 43eaa0b789696a970476ffaf30cea3107f7798e5 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 11:34:49 +0100 Subject: [PATCH 13/42] Promisify ldp#exists --- lib/handlers/allow.js | 61 +++++++++++++++++++---------------- lib/handlers/index.js | 21 ++++++------ lib/handlers/post.js | 37 +++++++++++---------- lib/ldp.js | 9 ++---- lib/models/account-manager.js | 16 ++++----- 5 files changed, 70 insertions(+), 74 deletions(-) diff --git a/lib/handlers/allow.js b/lib/handlers/allow.js index b2172aae4..9b71ebac1 100644 --- a/lib/handlers/allow.js +++ b/lib/handlers/allow.js @@ -6,7 +6,7 @@ const debug = require('../debug.js').ACL const LegacyResourceMapper = require('../legacy-resource-mapper') function allow (mode) { - return function allowHandler (req, res, next) { + return async function allowHandler (req, res, next) { const ldp = req.app.locals.ldp || {} if (!ldp.webid) { return next() @@ -27,36 +27,41 @@ function allow (mode) { : req.path // Check whether the resource exists - ldp.exists(req.hostname, reqPath, (err, ret) => { - // Ensure directories always end in a slash - const stat = err ? null : ret.stream - if (!reqPath.endsWith('/') && stat && stat.isDirectory()) { - reqPath += '/' - } + let stat + try { + const ret = await ldp.exists(req.hostname, reqPath) + stat = ret.stream + } catch (err) { + stat = null + } - // Obtain and store the ACL of the requested resource - req.acl = new ACL(rootUrl + reqPath, { - origin: req.get('origin'), - host: req.protocol + '://' + req.get('host'), - fetch: fetchFromLdp(mapper, ldp), - fetchGraph: (uri, options) => { - // first try loading from local fs - return ldp.getGraph(uri, options.contentType) - // failing that, fetch remote graph - .catch(() => ldp.fetchGraph(uri, options)) - }, - suffix: ldp.suffixAcl, - strictOrigin: ldp.strictOrigin - }) + // Ensure directories always end in a slash + if (!reqPath.endsWith('/') && stat && stat.isDirectory()) { + reqPath += '/' + } - // Ensure the user has the required permission - const userId = req.session.userId - req.acl.can(userId, mode) - .then(() => next(), err => { - debug(`${mode} access denied to ${userId || '(none)'}`) - next(err) - }) + // Obtain and store the ACL of the requested resource + req.acl = new ACL(rootUrl + reqPath, { + origin: req.get('origin'), + host: req.protocol + '://' + req.get('host'), + fetch: fetchFromLdp(mapper, ldp), + fetchGraph: (uri, options) => { + // first try loading from local fs + return ldp.getGraph(uri, options.contentType) + // failing that, fetch remote graph + .catch(() => ldp.fetchGraph(uri, options)) + }, + suffix: ldp.suffixAcl, + strictOrigin: ldp.strictOrigin }) + + // Ensure the user has the required permission + const userId = req.session.userId + req.acl.can(userId, mode) + .then(() => next(), err => { + debug(`${mode} access denied to ${userId || '(none)'}`) + next(err) + }) } } diff --git a/lib/handlers/index.js b/lib/handlers/index.js index c9f4f2ac7..236b780a8 100644 --- a/lib/handlers/index.js +++ b/lib/handlers/index.js @@ -5,14 +5,15 @@ const debug = require('debug')('solid:index') const utils = require('../utils') const Negotiator = require('negotiator') -function handler (req, res, next) { +async function handler (req, res, next) { const indexFile = 'index.html' const ldp = req.app.locals.ldp const negotiator = new Negotiator(req) const requestedType = negotiator.mediaType() const filename = utils.reqToPath(req) - ldp.stat(filename).then(stats => { + try { + const stats = await ldp.stat(filename) if (!stats.isDirectory()) { return next() } @@ -27,13 +28,11 @@ function handler (req, res, next) { debug('Looking for index in ' + req.path) // Check if file exists in first place - ldp.exists(req.hostname, path.join(req.path, indexFile), function (err) { - if (err) { - return next() - } - res.locals.path = path.join(req.path, indexFile) - debug('Found an index for current path') - return next() - }) - }, () => next()) + await ldp.exists(req.hostname, path.join(req.path, indexFile)) + res.locals.path = path.join(req.path, indexFile) + debug('Found an index for current path') + return next() + } catch (e) { + next() + } } diff --git a/lib/handlers/post.js b/lib/handlers/post.js index e2c665e76..3d0ab7cba 100644 --- a/lib/handlers/post.js +++ b/lib/handlers/post.js @@ -8,7 +8,7 @@ const patch = require('./patch') const error = require('../http-error') const { extensions } = require('mime-types') -function handler (req, res, next) { +async function handler (req, res, next) { const ldp = req.app.locals.ldp const contentType = req.get('content-type') debug('content-type is ', contentType) @@ -26,29 +26,28 @@ function handler (req, res, next) { } // Check if container exists - ldp.exists(req.hostname, containerPath, function (err, ret) { - if (err) { - return next(error(err, 'Container not valid')) - } - - let stats + let stats + try { + const ret = await ldp.exists(req.hostname, containerPath) if (ret) { stats = ret.stream } + } catch (err) { + return next(error(err, 'Container not valid')) + } - // Check if container is a directory - if (stats && !stats.isDirectory()) { - debug('Path is not a container, 405!') - return next(error(405, 'Requested resource is not a container')) - } + // Check if container is a directory + if (stats && !stats.isDirectory()) { + debug('Path is not a container, 405!') + return next(error(405, 'Requested resource is not a container')) + } - // Dispatch to the right handler - if (req.is('multipart/form-data')) { - multi() - } else { - one() - } - }) + // Dispatch to the right handler + if (req.is('multipart/form-data')) { + multi() + } else { + one() + } function multi () { debug('receving multiple files') diff --git a/lib/ldp.js b/lib/ldp.js index 371adefdf..27b020dff 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -248,13 +248,10 @@ class LDP { }) } - exists (hostname, path, callback) { + async exists (hostname, path) { const options = { hostname, path, includeBody: false } - if (callback) { - return this.get(options).then((data) => callback(null, data), callback) - } else { - return this.get(options).then(() => true) - } + await this.get(options) + return true } /** diff --git a/lib/models/account-manager.js b/lib/models/account-manager.js index cb0c70359..e358d5f00 100644 --- a/lib/models/account-manager.js +++ b/lib/models/account-manager.js @@ -107,16 +107,12 @@ class AccountManager { * * @return {Promise} */ - accountUriExists (accountUri, accountResource = '/') { - return new Promise((resolve, reject) => { - this.store.exists(accountUri, accountResource, (err, result) => { - if (err && err.status === 404) { - return resolve(false) - } - - resolve(!!result) - }) - }) + async accountUriExists (accountUri, accountResource = '/') { + try { + return await this.store.exists(accountUri, accountResource) + } catch (err) { + return false + } } /** From c81f9e39292412ac42c880feb9ab2ebe6bbb1d0c Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 11:55:32 +0100 Subject: [PATCH 14/42] Simplify promise-based handlers using async-await syntax --- lib/handlers/delete.js | 21 +++++++------- lib/handlers/patch/n3-patch-parser.js | 33 +++++++++++----------- lib/handlers/patch/sparql-update-parser.js | 16 +++++------ lib/handlers/put.js | 23 ++++++++------- 4 files changed, 45 insertions(+), 48 deletions(-) diff --git a/lib/handlers/delete.js b/lib/handlers/delete.js index c6df97774..7454a3c13 100644 --- a/lib/handlers/delete.js +++ b/lib/handlers/delete.js @@ -2,18 +2,17 @@ module.exports = handler const debug = require('../debug').handlers -function handler (req, res, next) { +async function handler (req, res, next) { debug('DELETE -- Request on' + req.originalUrl) const ldp = req.app.locals.ldp - ldp.delete(req) - .then(() => { - debug('DELETE -- Ok.') - res.sendStatus(200) - next() - }) - .catch(err => { - debug('DELETE -- Failed to delete: ' + err) - next(err) - }) + try { + await ldp.delete(req) + debug('DELETE -- Ok.') + res.sendStatus(200) + next() + } catch (err) { + debug('DELETE -- Failed to delete: ' + err) + next(err) + } } diff --git a/lib/handlers/patch/n3-patch-parser.js b/lib/handlers/patch/n3-patch-parser.js index 91df7c5b6..52abde252 100644 --- a/lib/handlers/patch/n3-patch-parser.js +++ b/lib/handlers/patch/n3-patch-parser.js @@ -9,34 +9,35 @@ const PATCH_NS = 'http://www.w3.org/ns/solid/terms#' const PREFIXES = `PREFIX solid: <${PATCH_NS}>\n` // Parses the given N3 patch document -function parsePatchDocument (targetURI, patchURI, patchText) { +async function parsePatchDocument (targetURI, patchURI, patchText) { // Parse the N3 document into triples - return new Promise((resolve, reject) => { - const patchGraph = $rdf.graph() + const patchGraph = $rdf.graph() + try { $rdf.parse(patchText, patchGraph, patchURI, 'text/n3') - resolve(patchGraph) - }) - .catch(err => { throw error(400, `Patch document syntax error: ${err}`) }) + } catch (err) { + throw error(400, `Patch document syntax error: ${err}`) + } // Query the N3 document for insertions and deletions - .then(patchGraph => queryForFirstResult(patchGraph, `${PREFIXES} + let firstResult; + try { + firstResult = await queryForFirstResult(patchGraph, `${PREFIXES} SELECT ?insert ?delete ?where WHERE { ?patch solid:patches <${targetURI}>. OPTIONAL { ?patch solid:inserts ?insert. } OPTIONAL { ?patch solid:deletes ?delete. } OPTIONAL { ?patch solid:where ?where. } }`) - .catch(err => { throw error(400, `No patch for ${targetURI} found.`, err) }) - ) + } catch (err) { + throw error(400, `No patch for ${targetURI} found.`, err) + } // Return the insertions and deletions as an rdflib patch document - .then(result => { - const {'?insert': insert, '?delete': deleted, '?where': where} = result - if (!insert && !deleted) { - throw error(400, 'Patch should at least contain inserts or deletes.') - } - return {insert, delete: deleted, where} - }) + const {'?insert': insert, '?delete': deleted, '?where': where} = firstResult + if (!insert && !deleted) { + throw error(400, 'Patch should at least contain inserts or deletes.') + } + return {insert, delete: deleted, where} } // Queries the store with the given SPARQL query and returns the first result diff --git a/lib/handlers/patch/sparql-update-parser.js b/lib/handlers/patch/sparql-update-parser.js index 365e9c82d..8e442c9ed 100644 --- a/lib/handlers/patch/sparql-update-parser.js +++ b/lib/handlers/patch/sparql-update-parser.js @@ -6,13 +6,11 @@ const $rdf = require('rdflib') const error = require('../../http-error') // Parses the given SPARQL UPDATE document -function parsePatchDocument (targetURI, patchURI, patchText) { - return new Promise((resolve, reject) => { - const baseURI = patchURI.replace(/#.*/, '') - try { - resolve($rdf.sparqlUpdateParser(patchText, $rdf.graph(), baseURI)) - } catch (err) { - reject(error(400, `Patch document syntax error: ${err}`)) - } - }) +async function parsePatchDocument (targetURI, patchURI, patchText) { + const baseURI = patchURI.replace(/#.*/, '') + try { + return $rdf.sparqlUpdateParser(patchText, $rdf.graph(), baseURI) + } catch (err) { + throw error(400, `Patch document syntax error: ${err}`) + } } diff --git a/lib/handlers/put.js b/lib/handlers/put.js index 7d6536251..7b1c2467f 100644 --- a/lib/handlers/put.js +++ b/lib/handlers/put.js @@ -2,21 +2,20 @@ module.exports = handler const debug = require('debug')('solid:put') -function handler (req, res, next) { +async function handler (req, res, next) { const ldp = req.app.locals.ldp debug(req.originalUrl) res.header('MS-Author-Via', 'SPARQL') - ldp.put(req, req) - .then(() => { - debug('succeded putting the file') + try { + await ldp.put(req, req) + debug('succeded putting the file') - res.sendStatus(201) - return next() - }) - .catch(err => { - debug('error putting the file:' + err.message) - err.message = 'Can\'t write file: ' + err.message - return next(err) - }) + res.sendStatus(201) + return next() + } catch (err) { + debug('error putting the file:' + err.message) + err.message = 'Can\'t write file: ' + err.message + return next(err) + } } From 0303cab27e7e9b9dd0924778d2fe93888f8e8abc Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 12:06:41 +0100 Subject: [PATCH 15/42] Use ResourceMapper in header#linksHandler --- lib/header.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/header.js b/lib/header.js index 4ed1f143d..2fe2dc402 100644 --- a/lib/header.js +++ b/lib/header.js @@ -41,12 +41,10 @@ function addLinks (res, fileMetadata) { } } -function linksHandler (req, res, next) { +async function linksHandler (req, res, next) { const ldp = req.app.locals.ldp - const root = !ldp.multiuser ? ldp.root : ldp.root + req.hostname + '/' - let filename = utils.uriToFilename(req.url, root) + const { path: filename } = await ldp.resourceMapper.mapUrlToFile(req) - filename = path.join(filename, req.path) if (path.extname(filename) === ldp.suffixMeta) { debug.metadata('Trying to access metadata file as regular file.') From f957b2ebbd8a60d2713cdae3fe905dceaf0da939 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 12:08:49 +0100 Subject: [PATCH 16/42] Use ResourceMapper in get#globHandler --- lib/handlers/get.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/lib/handlers/get.js b/lib/handlers/get.js index a155b9a71..362d4f292 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -5,7 +5,6 @@ const glob = require('glob') const _path = require('path') const $rdf = require('rdflib') const Negotiator = require('negotiator') -const url = require('url') const mime = require('mime-types') const debug = require('debug')('solid:get') @@ -143,12 +142,10 @@ async function handler (req, res, next) { } } -function globHandler (req, res, next) { +async function globHandler (req, res, next) { const ldp = req.app.locals.ldp - const root = !ldp.multiuser ? ldp.root : ldp.root + req.hostname + '/' - const filename = utils.uriToFilename(req.path, root) - const uri = utils.getFullUri(req) - const requestUri = url.resolve(uri, req.path) + const { path: filename } = await ldp.resourceMapper.mapUrlToFile({ url: req }) + const requestUri = (await ldp.resourceMapper.mapFileToUrl({ path: filename, hostname: req.hostname })).url const globOptions = { noext: true, @@ -165,25 +162,23 @@ function globHandler (req, res, next) { // Matches found const globGraph = $rdf.graph() - let reqOrigin = utils.getBaseUri(req) - debugGlob('found matches ' + matches) - Promise.all(matches.map(match => new Promise((resolve, reject) => { - const baseUri = reqOrigin + '/' + match.substr(root.length) + Promise.all(matches.map(match => new Promise(async (resolve, reject) => { + const baseUri = await ldp.resourceMapper.mapFileToUrl({ path: match, hostname: req.hostname }) fs.readFile(match, {encoding: 'utf8'}, function (err, fileData) { if (err) { debugGlob('error ' + err) return resolve() } // Files should have the .ttl extension or be extensionless (also Turtle) - if (!/\.ttl$|\/[^.]+$/.test(match)) { + if (baseUri.contentType !== 'text/turtle') { return resolve() } // The agent should have Read access to the file hasReadPermissions(match, req, res, function (allowed) { if (allowed) { try { - $rdf.parse(fileData, globGraph, baseUri, 'text/turtle') + $rdf.parse(fileData, globGraph, baseUri.url, 'text/turtle') } catch (parseErr) { debugGlob(`error parsing ${match}: ${parseErr}`) } From f768c6d997d3f41a004ad3878eef8916d9450845 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 13:10:26 +0100 Subject: [PATCH 17/42] Use ResourceMapper in copy handler --- lib/handlers/copy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/handlers/copy.js b/lib/handlers/copy.js index 7ff3cca18..a3de08471 100644 --- a/lib/handlers/copy.js +++ b/lib/handlers/copy.js @@ -14,7 +14,7 @@ const url = require('url') * "Save an external resource to Solid" type apps. * @method handler */ -function handler (req, res, next) { +async function handler (req, res, next) { const copyFrom = req.header('Source') if (!copyFrom) { return next(error(400, 'Source header required')) @@ -23,7 +23,7 @@ function handler (req, res, next) { const serverRoot = utils.getBaseUri(req) const copyFromUrl = fromExternal ? copyFrom : serverRoot + copyFrom const copyTo = res.locals.path || req.path - const copyToPath = utils.reqToPath(req) + const { path: copyToPath } = await req.app.locals.ldp.resourceMapper.mapUrlToFile({ url: req }) ldpCopy(copyToPath, copyFromUrl, function (err) { if (err) { let statusCode = err.statusCode || 500 From 5cc2d195cafa6857caefbc21c0517c4b4733a09a Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 13:10:44 +0100 Subject: [PATCH 18/42] Use ResourceMapper in root handler --- lib/handlers/index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/handlers/index.js b/lib/handlers/index.js index 236b780a8..ba80e832f 100644 --- a/lib/handlers/index.js +++ b/lib/handlers/index.js @@ -2,7 +2,6 @@ module.exports = handler const path = require('path') const debug = require('debug')('solid:index') -const utils = require('../utils') const Negotiator = require('negotiator') async function handler (req, res, next) { @@ -10,7 +9,7 @@ async function handler (req, res, next) { const ldp = req.app.locals.ldp const negotiator = new Negotiator(req) const requestedType = negotiator.mediaType() - const filename = utils.reqToPath(req) + const { path: filename } = await req.app.locals.ldp.resourceMapper.mapUrlToFile({ url: req }) try { const stats = await ldp.stat(filename) From 796473a02dfe8bf37315ecc61dd5edf794a4ce64 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 13:13:08 +0100 Subject: [PATCH 19/42] Remove utils#uriToFilename All usages have been refactored to use the ResourceMapper instead. This also removes the related reqToPath and uriToRelativeFilename functions. --- lib/utils.js | 33 --------------------------------- test/unit/utils-test.js | 12 ------------ 2 files changed, 45 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 86fe04c6a..76af535c1 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,5 +1,3 @@ -module.exports.uriToFilename = uriToFilename -module.exports.uriToRelativeFilename = uriToRelativeFilename module.exports.getBaseUri = getBaseUri module.exports.pathBasename = pathBasename module.exports.getFullUri = getFullUri @@ -7,13 +5,11 @@ module.exports.hasSuffix = hasSuffix module.exports.serialize = serialize module.exports.translate = translate module.exports.stringToStream = stringToStream -module.exports.reqToPath = reqToPath module.exports.debrack = debrack module.exports.stripLineEndings = stripLineEndings module.exports.fullUrlForReq = fullUrlForReq module.exports.routeResolvedFile = routeResolvedFile -const fs = require('fs-extra') const path = require('path') const $rdf = require('rdflib') const from = require('from2') @@ -67,29 +63,6 @@ function debrack (s) { return s.substring(1, s.length - 1) } -function uriToFilename (uri, base) { - const decoded = uri.split('/').map(decodeURIComponent).join('/') - let filename = path.join(base, decoded) - // Make sure filename ends with '/' if filename exists and is a directory. - // TODO this sync operation can be avoided and can be left - // to do, to other components, see `ldp.get` - try { - const fileStats = fs.statSync(filename) - if (fileStats.isDirectory() && !filename.endsWith('/')) { - filename += '/' - } else if (fileStats.isFile() && filename.endsWith('/')) { - filename = filename.substr(0, filename.length - 1) - } - } catch (err) {} - return filename -} - -function uriToRelativeFilename (uri, base) { - const filename = uriToFilename(uri, base) - const relative = path.relative(base, filename) - return relative -} - function getBaseUri (req) { // Obtain the protocol from the configured server URI // (in case the server is running behind a reverse proxy) @@ -215,12 +188,6 @@ function stripLineEndings (obj) { return obj.replace(/(\r\n|\n|\r)/gm, '') } -function reqToPath (req) { - const ldp = req.app.locals.ldp - const root = ldp.multiuser ? ldp.root + req.hostname + '/' : ldp.root - return uriToFilename(req.path, root) -} - /** * Adds a route that serves a static file from another Node module */ diff --git a/test/unit/utils-test.js b/test/unit/utils-test.js index 7d5d3fc96..3d7213bf0 100644 --- a/test/unit/utils-test.js +++ b/test/unit/utils-test.js @@ -21,18 +21,6 @@ describe('Utility functions', function () { }) }) - describe('uriToFilename', function () { - it('should decode hex-encoded space', function () { - assert.equal(utils.uriToFilename('uri%20', 'base/'), 'base/uri ') - }) - it('should decode hex-encoded at sign', function () { - assert.equal(utils.uriToFilename('film%4011', 'base/'), 'base/film@11') - }) - it('should decode hex-encoded single quote', function () { - assert.equal(utils.uriToFilename('quote%27', 'base/'), 'base/quote\'') - }) - }) - describe('stripLineEndings()', () => { it('should pass through falsy string arguments', () => { assert.equal(utils.stripLineEndings(''), '') From 252a227902ec577f710cd7c002ac18c62738cca3 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 13:36:49 +0100 Subject: [PATCH 20/42] Use ResourceMapper instead of utils#getBaseUri --- lib/handlers/allow.js | 11 ++--------- lib/handlers/copy.js | 6 +++--- lib/handlers/get.js | 11 ++--------- test/integration/http-copy-test.js | 4 ++-- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/lib/handlers/allow.js b/lib/handlers/allow.js index 9b71ebac1..be61e1f77 100644 --- a/lib/handlers/allow.js +++ b/lib/handlers/allow.js @@ -1,9 +1,7 @@ module.exports = allow const ACL = require('../acl-checker') -const utils = require('../utils') const debug = require('../debug.js').ACL -const LegacyResourceMapper = require('../legacy-resource-mapper') function allow (mode) { return async function allowHandler (req, res, next) { @@ -13,12 +11,7 @@ function allow (mode) { } // Set up URL to filesystem mapping - const rootUrl = utils.getBaseUri(req) - const mapper = new LegacyResourceMapper({ - rootUrl, - rootPath: ldp.root, - includeHost: ldp.multiuser - }) + const rootUrl = ldp.resourceMapper.getBaseUrl(req.hostname) // Determine the actual path of the request // (This is used as an ugly hack to check the ACL status of other resources.) @@ -44,7 +37,7 @@ function allow (mode) { req.acl = new ACL(rootUrl + reqPath, { origin: req.get('origin'), host: req.protocol + '://' + req.get('host'), - fetch: fetchFromLdp(mapper, ldp), + fetch: fetchFromLdp(ldp.resourceMapper, ldp), fetchGraph: (uri, options) => { // first try loading from local fs return ldp.getGraph(uri, options.contentType) diff --git a/lib/handlers/copy.js b/lib/handlers/copy.js index a3de08471..f069fff2d 100644 --- a/lib/handlers/copy.js +++ b/lib/handlers/copy.js @@ -3,7 +3,6 @@ module.exports = handler const debug = require('../debug') const error = require('../http-error') const ldpCopy = require('../ldp-copy') -const utils = require('../utils') const url = require('url') /** @@ -20,10 +19,11 @@ async function handler (req, res, next) { return next(error(400, 'Source header required')) } const fromExternal = !!url.parse(copyFrom).hostname - const serverRoot = utils.getBaseUri(req) + const ldp = req.app.locals.ldp + const serverRoot = ldp.resourceMapper.getBaseUrl(req.hostname) const copyFromUrl = fromExternal ? copyFrom : serverRoot + copyFrom const copyTo = res.locals.path || req.path - const { path: copyToPath } = await req.app.locals.ldp.resourceMapper.mapUrlToFile({ url: req }) + const { path: copyToPath } = await ldp.resourceMapper.mapUrlToFile({ url: req }) ldpCopy(copyToPath, copyFromUrl, function (err) { if (err) { let statusCode = err.statusCode || 500 diff --git a/lib/handlers/get.js b/lib/handlers/get.js index 362d4f292..dcbd3115b 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -16,7 +16,6 @@ const translate = require('../utils.js').translate const error = require('../http-error') const RDFs = require('../ldp').RDF_MIME_TYPES -const LegacyResourceMapper = require('../legacy-resource-mapper') async function handler (req, res, next) { const ldp = req.app.locals.ldp @@ -33,7 +32,7 @@ async function handler (req, res, next) { // Set live updates if (ldp.live) { - res.header('Updates-Via', utils.getBaseUri(req).replace(/^http/, 'ws')) + res.header('Updates-Via', ldp.resourceMapper.getBaseUrl(req.hostname).replace(/^http/, 'ws')) } debug(req.originalUrl + ' on ' + req.hostname) @@ -77,13 +76,7 @@ async function handler (req, res, next) { // Till here it must exist if (!includeBody) { debug('HEAD only') - const rootUrl = utils.getBaseUri(req) - const mapper = new LegacyResourceMapper({ - rootUrl, - rootPath: ldp.root, - includeHost: ldp.multiuser - }) - const mappedFile = await mapper.mapFileToUrl({ path }) + const mappedFile = await ldp.resourceMapper.mapFileToUrl({ path }) contentType = mappedFile.contentType res.setHeader('Content-Type', contentType) res.status(200).send('OK') diff --git a/test/integration/http-copy-test.js b/test/integration/http-copy-test.js index 56d660a38..a90a03852 100644 --- a/test/integration/http-copy-test.js +++ b/test/integration/http-copy-test.js @@ -8,7 +8,7 @@ var rm = require('./../utils').rm var solidServer = require('../../index') describe('HTTP COPY API', function () { - var address = 'https://localhost:3456' + var address = 'https://localhost:8443' var ldpHttpsServer var ldp = solidServer.createServer({ @@ -19,7 +19,7 @@ describe('HTTP COPY API', function () { }) before(function (done) { - ldpHttpsServer = ldp.listen(3456, done) + ldpHttpsServer = ldp.listen(8443, done) }) after(function () { From 6769d23664c3493c2498512bd9ef60dd193c7c97 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 14:15:27 +0100 Subject: [PATCH 21/42] Use ResourceMapper in handlers/get#hasReadPermissions --- lib/handlers/get.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/handlers/get.js b/lib/handlers/get.js index dcbd3115b..6863d850a 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -200,7 +200,7 @@ function hasReadPermissions (file, req, res, callback) { return callback(true) } - const root = ldp.multiuser ? ldp.root + req.hostname + '/' : ldp.root + const root = ldp.resourceMapper.getBasePath(req.hostname) const relativePath = '/' + _path.relative(root, file) res.locals.path = relativePath allow('Read')(req, res, err => callback(!err)) From 7857a29ce177b75759c677729416ebad8c6099be Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 14:32:09 +0100 Subject: [PATCH 22/42] Pass content type to ResourceMapper when putting resources --- lib/ldp.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/ldp.js b/lib/ldp.js index 27b020dff..a5f903add 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -185,9 +185,9 @@ class LDP { originalPath += '/' } } - const { url: putUrl } = await this.resourceMapper.mapFileToUrl( + const { url: putUrl, contentType } = await this.resourceMapper.mapFileToUrl( { path: this.resourceMapper._rootPath + resourcePath, hostname: host }) - await ldp.put(putUrl, stream) + await ldp.put(putUrl, stream, contentType) return originalPath } @@ -213,11 +213,11 @@ class LDP { const { path } = url.parse(uri) const content = await serialize(graph, uri, contentType) let stream = stringToStream(content) - return await this.put(path, stream) + return await this.put(path, stream, contentType) } - async put (url, stream) { - const { path: filePath } = await this.resourceMapper.mapUrlToFile({ url }) + async put (url, stream, contentType) { + const { path: filePath } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true }) // PUT requests not supported on containers. Use POST instead if (filePath.endsWith('/')) { @@ -309,7 +309,7 @@ class LDP { return this.graph(uri, uri, contentType) } - async graph (url, baseUri, contentType = 'text/turtle') { + async graph (url, baseUri, contentType = DEFAULT_CONTENT_TYPE) { const body = await this.readResource(url) return new Promise((resolve, reject) => { const graph = $rdf.graph() From a6d1052e33d0fcb42a181e4c718e655d01c484f6 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 15:04:37 +0100 Subject: [PATCH 23/42] Remove ldp.root in favor of ResourceMapper --- lib/ldp.js | 10 +-------- lib/models/account-manager.js | 12 +--------- test/integration/account-manager-test.js | 28 +++++++++++++++++++----- test/integration/ldp-test.js | 9 ++++---- test/integration/params-test.js | 4 ++-- test/unit/account-manager-test.js | 25 ++++++++++++++++----- test/unit/user-accounts-api-test.js | 7 +++++- 7 files changed, 56 insertions(+), 39 deletions(-) diff --git a/lib/ldp.js b/lib/ldp.js index a5f903add..6bddb0af9 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -36,14 +36,6 @@ class LDP { constructor (argv = {}) { extend(this, argv) - // Setting root - if (!this.root) { - this.root = process.cwd() - } - if (!this.root.endsWith('/')) { - this.root += '/' - } - // Suffixes if (!this.suffixAcl) { this.suffixAcl = '.acl' @@ -79,7 +71,7 @@ class LDP { debug.settings('Config path: ' + this.configPath) debug.settings('Suffix Acl: ' + this.suffixAcl) debug.settings('Suffix Meta: ' + this.suffixMeta) - debug.settings('Filesystem Root: ' + this.root) + debug.settings('Filesystem Root: ' + (this.resourceMapper ? this.resourceMapper._rootPath : 'none')) debug.settings('Allow WebID authentication: ' + !!this.webid) debug.settings('Live-updates: ' + !!this.live) debug.settings('Multi-user: ' + !!this.multiuser) diff --git a/lib/models/account-manager.js b/lib/models/account-manager.js index e358d5f00..ba56c0fe0 100644 --- a/lib/models/account-manager.js +++ b/lib/models/account-manager.js @@ -131,17 +131,7 @@ class AccountManager { * @return {string} */ accountDirFor (accountName) { - let accountDir - - if (this.multiuser) { - let uri = this.accountUriFor(accountName) - let hostname = url.parse(uri).hostname - accountDir = path.join(this.store.root, hostname) - } else { - // single user mode - accountDir = this.store.root - } - return accountDir + return this.store.resourceMapper.getBasePath(url.parse(this.accountUriFor(accountName)).hostname) } /** diff --git a/test/integration/account-manager-test.js b/test/integration/account-manager-test.js index 215e21339..c8042e549 100644 --- a/test/integration/account-manager-test.js +++ b/test/integration/account-manager-test.js @@ -9,6 +9,7 @@ chai.should() const LDP = require('../../lib/ldp') const SolidHost = require('../../lib/models/solid-host') const AccountManager = require('../../lib/models/account-manager') +const LegacyResourceMapper = require('../../lib/legacy-resource-mapper') const testAccountsDir = path.join(__dirname, '../resources/accounts') const accountTemplatePath = path.join(__dirname, '../../default-templates/new-account') @@ -29,7 +30,10 @@ describe('AccountManager', () => { describe('in multi user mode', () => { let multiuser = true - let store = new LDP({ root: testAccountsDir, multiuser }) + let resourceMapper = new LegacyResourceMapper({ + includeHost: multiuser + }) + let store = new LDP({ multiuser, resourceMapper }) let options = { multiuser, store, host } let accountManager = AccountManager.from(options) @@ -54,9 +58,13 @@ describe('AccountManager', () => { let multiuser = false it('resolves to true if root .acl exists in root storage', () => { + let resourceMapper = new LegacyResourceMapper({ + includeHost: multiuser, + rootPath: path.join(testAccountsDir, 'tim.localhost') + }) let store = new LDP({ - root: path.join(testAccountsDir, 'tim.localhost'), - multiuser + multiuser, + resourceMapper }) let options = { multiuser, store, host } let accountManager = AccountManager.from(options) @@ -68,9 +76,13 @@ describe('AccountManager', () => { }) it('resolves to false if root .acl does not exist in root storage', () => { + let resourceMapper = new LegacyResourceMapper({ + includeHost: multiuser, + rootPath: testAccountsDir + }) let store = new LDP({ - root: testAccountsDir, - multiuser + multiuser, + resourceMapper }) let options = { multiuser, store, host } let accountManager = AccountManager.from(options) @@ -86,7 +98,11 @@ describe('AccountManager', () => { describe('createAccountFor()', () => { it('should create an account directory', () => { let multiuser = true - let store = new LDP({ root: testAccountsDir, multiuser }) + let resourceMapper = new LegacyResourceMapper({ + includeHost: multiuser, + rootPath: testAccountsDir + }) + let store = new LDP({ multiuser, resourceMapper }) let options = { host, multiuser, store, accountTemplatePath } let accountManager = AccountManager.from(options) diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index 2f4e4b443..bbf0011e6 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -23,7 +23,6 @@ describe('LDP', function () { }) var ldp = new LDP({ - root, resourceMapper, webid: false }) @@ -144,7 +143,7 @@ describe('LDP', function () { var stream = stringToStream('hello world') await ldp.put('/resources/testPut.txt', stream) // Make sure it exists - fs.stat(ldp.root + '/resources/testPut.txt', function (err) { + fs.stat(ldp.resourceMapper._rootPath + '/resources/testPut.txt', function (err) { if (err) { throw err } @@ -153,7 +152,7 @@ describe('LDP', function () { // Now delete the dummy file await ldp.delete('/resources/testPut.txt') // Make sure it does not exist anymore - fs.stat(ldp.root + '/resources/testPut.txt', function (err, s) { + fs.stat(ldp.resourceMapper._rootPath + '/resources/testPut.txt', function (err, s) { if (!err) { throw new Error('file still exists') } @@ -165,7 +164,7 @@ describe('LDP', function () { var stream = stringToStream('hello world') await ldp.put('/resources/dummy/testPutBlocking.txt', stream) // Make sure it exists - fs.stat(ldp.root + '/resources/dummy/testPutBlocking.txt', function (err) { + fs.stat(ldp.resourceMapper._rootPath + '/resources/dummy/testPutBlocking.txt', function (err) { if (err) { throw err } @@ -180,7 +179,7 @@ describe('LDP', function () { var stream = stringToStream('hello world') await ldp.put('/resources/dummy/dummy2/testPutBlocking.txt', stream) // Make sure it exists - fs.stat(ldp.root + '/resources/dummy/dummy2/testPutBlocking.txt', function (err) { + fs.stat(ldp.resourceMapper._rootPath + '/resources/dummy/dummy2/testPutBlocking.txt', function (err) { if (err) { throw err } diff --git a/test/integration/params-test.js b/test/integration/params-test.js index f80ae2674..06b918f16 100644 --- a/test/integration/params-test.js +++ b/test/integration/params-test.js @@ -31,7 +31,7 @@ describe('LDNODE params', function () { var server = supertest(ldp) it('should fallback on current working directory', function () { - assert.equal(ldp.locals.ldp.root, process.cwd() + '/') + assert.equal(ldp.locals.ldp.resourceMapper._rootPath, process.cwd()) }) it('should find resource in correct path', function (done) { @@ -56,7 +56,7 @@ describe('LDNODE params', function () { var server = supertest(ldp) it('should fallback on current working directory', function () { - assert.equal(ldp.locals.ldp.root, './test/resources/') + assert.equal(ldp.locals.ldp.resourceMapper._rootPath, './test/resources') }) it('should find resource in correct path', function (done) { diff --git a/test/unit/account-manager-test.js b/test/unit/account-manager-test.js index 6d69bd391..d9e79528f 100644 --- a/test/unit/account-manager-test.js +++ b/test/unit/account-manager-test.js @@ -17,6 +17,7 @@ const AccountManager = require('../../lib/models/account-manager') const UserAccount = require('../../lib/models/user-account') const TokenService = require('../../lib/services/token-service') const WebIdTlsCertificate = require('../../lib/models/webid-tls-certificate') +const LegacyResourceMapper = require('../../lib/legacy-resource-mapper') const testAccountsDir = path.join(__dirname, '../resources/accounts') @@ -102,17 +103,25 @@ describe('AccountManager', () => { describe('accountDirFor()', () => { it('should match the solid root dir config, in single user mode', () => { let multiuser = false - let store = new LDP({ root: testAccountsDir, multiuser }) + let resourceMapper = new LegacyResourceMapper({ + includeHost: multiuser, + rootPath: testAccountsDir + }) + let store = new LDP({ multiuser, resourceMapper }) let options = { multiuser, store, host } let accountManager = AccountManager.from(options) let accountDir = accountManager.accountDirFor('alice') - expect(accountDir).to.equal(store.root) + expect(accountDir).to.equal(store.resourceMapper._rootPath) }) it('should compose the account dir in multi user mode', () => { let multiuser = true - let store = new LDP({ root: testAccountsDir, multiuser }) + let resourceMapper = new LegacyResourceMapper({ + includeHost: multiuser, + rootPath: testAccountsDir + }) + let store = new LDP({ multiuser, resourceMapper }) let host = SolidHost.from({ serverUri: 'https://localhost' }) let options = { multiuser, store, host } let accountManager = AccountManager.from(options) @@ -302,7 +311,10 @@ describe('AccountManager', () => { describe('rootAclFor()', () => { it('should return the server root .acl in single user mode', () => { - let store = new LDP({ suffixAcl: '.acl', multiuser: false }) + let resourceMapper = new LegacyResourceMapper({ + includeHost: false + }) + let store = new LDP({ suffixAcl: '.acl', multiuser: false, resourceMapper }) let options = { host, multiuser: false, store } let accountManager = AccountManager.from(options) @@ -314,7 +326,10 @@ describe('AccountManager', () => { }) it('should return the profile root .acl in multi user mode', () => { - let store = new LDP({ suffixAcl: '.acl', multiuser: true }) + let resourceMapper = new LegacyResourceMapper({ + includeHost: true + }) + let store = new LDP({ suffixAcl: '.acl', multiuser: true, resourceMapper }) let options = { host, multiuser: true, store } let accountManager = AccountManager.from(options) diff --git a/test/unit/user-accounts-api-test.js b/test/unit/user-accounts-api-test.js index f420dd002..df1655a55 100644 --- a/test/unit/user-accounts-api-test.js +++ b/test/unit/user-accounts-api-test.js @@ -13,6 +13,7 @@ const LDP = require('../../lib/ldp') const SolidHost = require('../../lib/models/solid-host') const AccountManager = require('../../lib/models/account-manager') const testAccountsDir = path.join(__dirname, '..', 'resources', 'accounts') +var LegacyResourceMapper = require('../../lib/legacy-resource-mapper') const api = require('../../lib/api/accounts/user-accounts') @@ -26,7 +27,11 @@ describe('api/accounts/user-accounts', () => { describe('newCertificate()', () => { describe('in multi user mode', () => { let multiuser = true - let store = new LDP({ root: testAccountsDir, multiuser }) + let resourceMapper = new LegacyResourceMapper({ + includeHost: multiuser, + rootPath: testAccountsDir + }) + let store = new LDP({ multiuser, resourceMapper }) it('should throw a 400 error if spkac param is missing', done => { let options = { host, store, multiuser, authMethod: 'oidc' } From 3f1d9ae76e5725c530d79fbd1149728dacce4691 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 15:09:15 +0100 Subject: [PATCH 24/42] Use ResourceMapper in ensureWelcomePage --- lib/server-config.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/server-config.js b/lib/server-config.js index 78df260f2..b0c3a82e2 100644 --- a/lib/server-config.js +++ b/lib/server-config.js @@ -40,9 +40,8 @@ function ensureDirCopyExists (fromDir, toDir) { * @param argv {Object} Express.js app object */ async function ensureWelcomePage (argv) { - const { multiuser, templates, server, host } = argv - const rootDir = path.resolve(argv.root) - const serverRootDir = multiuser ? path.join(rootDir, argv.host.hostname) : rootDir + const { resourceMapper, templates, server, host } = argv + const serverRootDir = resourceMapper.getBasePath(host.hostname) const existingIndexPage = path.join(serverRootDir, 'index.html') const packageData = require('../package.json') From 2d35a0e04051118ee114785fe8474c5078b324ed Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Mon, 19 Nov 2018 15:56:54 +0100 Subject: [PATCH 25/42] Move calls from utils#getFullUri to ResourceMapper --- lib/capability-discovery.js | 3 +-- lib/handlers/get.js | 3 +-- lib/handlers/options.js | 3 +-- lib/header.js | 2 +- lib/resource-mapper.js | 9 +++++++-- lib/utils.js | 19 ------------------- 6 files changed, 11 insertions(+), 28 deletions(-) diff --git a/lib/capability-discovery.js b/lib/capability-discovery.js index 314ae8a3f..50f9e5bfa 100644 --- a/lib/capability-discovery.js +++ b/lib/capability-discovery.js @@ -3,7 +3,6 @@ * @module capability-discovery */ const express = require('express') -const util = require('./utils') const serviceConfigDefaults = { 'api': { @@ -47,7 +46,7 @@ function capabilityDiscovery () { function serviceCapabilityDocument (serviceConfig) { return (req, res) => { // Add the server root url - serviceConfig.root = util.getFullUri(req) // TODO make sure we align with the rest + serviceConfig.root = req.app.locals.ldp.resourceMapper.getBaseUrl(req.hostname, req.path) // Add the 'apps' urls section serviceConfig.apps = req.app.locals.appUrls res.json(serviceConfig) diff --git a/lib/handlers/get.js b/lib/handlers/get.js index 6863d850a..1575e670f 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -11,7 +11,6 @@ const debug = require('debug')('solid:get') const debugGlob = require('debug')('solid:glob') const allow = require('./allow') -const utils = require('../utils.js') const translate = require('../utils.js').translate const error = require('../http-error') @@ -21,7 +20,7 @@ async function handler (req, res, next) { const ldp = req.app.locals.ldp const includeBody = req.method === 'GET' const negotiator = new Negotiator(req) - const baseUri = utils.getFullUri(req) + const baseUri = ldp.resourceMapper.getBaseUrl(req.hostname, req.path) const path = res.locals.path || req.path const requestedType = negotiator.mediaType() let possibleRDFType = negotiator.mediaType(RDFs) diff --git a/lib/handlers/options.js b/lib/handlers/options.js index b9b8a4099..43ca01b38 100644 --- a/lib/handlers/options.js +++ b/lib/handlers/options.js @@ -1,5 +1,4 @@ const addLink = require('../header').addLink -const utils = require('../utils') const url = require('url') module.exports = handler @@ -23,7 +22,7 @@ function linkAuthProvider (req, res) { } function linkServiceEndpoint (req, res) { - let serviceEndpoint = url.resolve(utils.getFullUri(req), '.well-known/solid') + let serviceEndpoint = url.resolve(req.app.locals.ldp.resourceMapper.getBaseUrl(req.hostname, req.path), '.well-known/solid') addLink(res, serviceEndpoint, 'service') } diff --git a/lib/header.js b/lib/header.js index 2fe2dc402..36dcf5f42 100644 --- a/lib/header.js +++ b/lib/header.js @@ -103,7 +103,7 @@ function addPermissions (req, res, next) { if (!acl) return next() // Turn permissions for the public and the user into a header - const resource = utils.getFullUri(req) + const resource = req.app.locals.ldp.resourceMapper.getBaseUrl(req.hostname, req.path) Promise.all([ getPermissionsFor(acl, null, resource), getPermissionsFor(acl, session.userId, resource) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index a03c7211f..92b65592f 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -74,9 +74,14 @@ class ResourceMapper { } // Gets the base URL for the given hostname - getBaseUrl (hostname) { - return !this._includeHost ? this._rootUrl + // Optionally, a pathname may be passed that will be appended to the baseUrl. + getBaseUrl (hostname, pathname) { + let baseUrl = !this._includeHost ? this._rootUrl : `${this._protocol}//${hostname}${this._port}${this._rootUrl}` + if (pathname) { + baseUrl += pathname + } + return baseUrl } // Determine the full file path corresponding to a URL diff --git a/lib/utils.js b/lib/utils.js index 76af535c1..49bf881a3 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,5 @@ module.exports.getBaseUri = getBaseUri module.exports.pathBasename = pathBasename -module.exports.getFullUri = getFullUri module.exports.hasSuffix = hasSuffix module.exports.serialize = serialize module.exports.translate = translate @@ -73,24 +72,6 @@ function getBaseUri (req) { return `${protocol || req.protocol}://${req.get('host')}` } -/** - * Composes and returns the fully-qualified URI for the request, to be used - * as a base URI for RDF parsing or serialization. For example, if a request - * is to `Host: example.com`, `GET /files/` using the `https:` protocol, - * then: - * - * ``` - * getFullUri(req) // -> 'https://example.com/files/' - * ``` - * - * @param req {IncomingMessage} - * - * @return {string} - */ -function getFullUri (req) { - return getBaseUri(req) + url.resolve(req.baseUrl, req.path) -} - function pathBasename (fullpath) { let bname = '' if (fullpath) { From 8344096dcc34392d1a10794421da9d924464c455 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Tue, 20 Nov 2018 08:26:44 +0100 Subject: [PATCH 26/42] Remove deprecated utils#getBaseUri --- lib/utils.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 49bf881a3..1a9b56a8c 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,3 @@ -module.exports.getBaseUri = getBaseUri module.exports.pathBasename = pathBasename module.exports.hasSuffix = hasSuffix module.exports.serialize = serialize @@ -62,16 +61,6 @@ function debrack (s) { return s.substring(1, s.length - 1) } -function getBaseUri (req) { - // Obtain the protocol from the configured server URI - // (in case the server is running behind a reverse proxy) - const locals = req.app.locals - const serverUri = locals.host.serverUri - const protocol = serverUri.replace(/:.*/, '') - - return `${protocol || req.protocol}://${req.get('host')}` -} - function pathBasename (fullpath) { let bname = '' if (fullpath) { From bd4cf23323bd6c38fb0dbdba901e4130a69101f6 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Tue, 20 Nov 2018 09:15:21 +0100 Subject: [PATCH 27/42] Fix standard linting issues --- lib/handlers/patch/n3-patch-parser.js | 2 +- lib/models/account-manager.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/handlers/patch/n3-patch-parser.js b/lib/handlers/patch/n3-patch-parser.js index 52abde252..6f297b6cb 100644 --- a/lib/handlers/patch/n3-patch-parser.js +++ b/lib/handlers/patch/n3-patch-parser.js @@ -19,7 +19,7 @@ async function parsePatchDocument (targetURI, patchURI, patchText) { } // Query the N3 document for insertions and deletions - let firstResult; + let firstResult try { firstResult = await queryForFirstResult(patchGraph, `${PREFIXES} SELECT ?insert ?delete ?where WHERE { diff --git a/lib/models/account-manager.js b/lib/models/account-manager.js index ba56c0fe0..f8c98d68d 100644 --- a/lib/models/account-manager.js +++ b/lib/models/account-manager.js @@ -1,7 +1,6 @@ 'use strict' const url = require('url') -const path = require('path') const rdf = require('rdflib') const ns = require('solid-namespace')(rdf) From beb080ffabbff920b062d7f0584b40bdec774177 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Tue, 20 Nov 2018 12:49:19 +0100 Subject: [PATCH 28/42] Make ResourceMapper interpret URLs ending with '/' as index pages Essentially, URLs ending with a '/' will internally be translated to paths such as 'index.html', 'index.ttl', ... depending on the content type --- lib/resource-mapper.js | 23 +++++++++++--- test/unit/resource-mapper-test.js | 50 ++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index 92b65592f..e3806838d 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -12,13 +12,15 @@ class ResourceMapper { rootUrl = 'https://localhost:8443/', rootPath = process.cwd(), includeHost = false, - defaultContentType = 'application/octet-stream' + defaultContentType = 'application/octet-stream', + indexName = 'index' }) { this._rootUrl = this._removeTrailingSlash(rootUrl) this._rootPath = this._removeTrailingSlash(rootPath) this._includeHost = includeHost this._readdir = readdir this._defaultContentType = defaultContentType + this._indexName = indexName // If the host needs to be replaced on every call, pre-split the root URL if (includeHost) { @@ -30,16 +32,28 @@ class ResourceMapper { } // Maps the request for a given resource and representation format to a server file + // When the URL ends with a '/', then files with the prefix 'index.' will be matched, + // such as 'index.html' and 'index.ttl'. async mapUrlToFile ({ url, contentType, createIfNotExists }) { - const fullPath = this._getFullPath(url) + let fullPath = this._getFullPath(url) + let isIndex = fullPath.endsWith('/') let path + // Append index filename if the URL ends with a '/' + if (isIndex) { + fullPath += this._indexName + } + // Create the path for a new file if (createIfNotExists) { path = fullPath // If the extension is not correct for the content type, append the correct extension if (this._getContentTypeByExtension(path) !== contentType) { - path += contentType in extensions ? `$.${extensions[contentType][0]}` : '$.unknown' + // Append a '$', unless we map for the index + if (!isIndex) { + path += '$' + } + path += contentType in extensions ? `.${extensions[contentType][0]}` : '.unknown' } // Determine the path of an existing file } else { @@ -49,7 +63,8 @@ class ResourceMapper { const files = await this._readdir(folder) // Find a file with the same name (minus the dollar extension) - const match = files.find(f => this._removeDollarExtension(f) === filename) + const match = files.find(f => this._removeDollarExtension(f) === filename || + (isIndex && f.startsWith(this._indexName + '.'))) if (!match) { throw new Error('File not found') } diff --git a/test/unit/resource-mapper-test.js b/test/unit/resource-mapper-test.js index a7efd76e1..ee7e147a9 100644 --- a/test/unit/resource-mapper-test.js +++ b/test/unit/resource-mapper-test.js @@ -187,6 +187,54 @@ describe('ResourceMapper', () => { contentType: 'text/html' }) + itMapsUrl(mapper, 'a URL ending with a slash to an index file when index.html is available', + { + url: 'http://localhost/space/' + }, + [ + `${rootPath}space/index.html`, + `${rootPath}space/index$.ttl` + ], + { + path: `${rootPath}space/index.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL ending with a slash to an index file when index$.html is available', + { + url: 'http://localhost/space/' + }, + [ + `${rootPath}space/index$.html`, + `${rootPath}space/index$.ttl` + ], + { + path: `${rootPath}space/index$.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL ending with a slash to an index file for text/html when index.html not is available', + { + url: 'http://localhost/space/', + contentType: 'text/html', + createIfNotExists: true + }, + { + path: `${rootPath}space/index.html`, + contentType: 'text/html' + }) + + itMapsUrl(mapper, 'a URL ending with a slash to an index file for text/turtle when index.ttl not is available', + { + url: 'http://localhost/space/', + contentType: 'text/turtle', + createIfNotExists: true + }, + { + path: `${rootPath}space/index.ttl`, + contentType: 'text/turtle' + }) + // Security cases itMapsUrl(mapper, 'a URL with an unknown content type', @@ -443,7 +491,7 @@ function mapsUrl (it, mapper, label, options, files, expected) { // Mock filesystem function mockReaddir () { mapper._readdir = async (path) => { - expect(path).to.equal(`${rootPath}space/`) + expect(path.startsWith(`${rootPath}space/`)).to.equal(true) return files.map(f => f.replace(/.*\//, '')) } } From f0736a991bf97725e41b5a0fcce111206abbd6ff Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 10:26:17 +0100 Subject: [PATCH 29/42] Improve error message when file is not found in ResourceMapper --- lib/resource-mapper.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index e3806838d..05ccea46c 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -66,7 +66,7 @@ class ResourceMapper { const match = files.find(f => this._removeDollarExtension(f) === filename || (isIndex && f.startsWith(this._indexName + '.'))) if (!match) { - throw new Error('File not found') + throw new Error(`File not found: ${fullPath}`) } path = `${folder}${match}` contentType = this._getContentTypeByExtension(match) From 5d8dd788375b2aab5cecb4d07701fcf8314264d0 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 10:43:12 +0100 Subject: [PATCH 30/42] Map URLs to folders when no index is available --- lib/resource-mapper.js | 11 +++++++++-- test/unit/resource-mapper-test.js | 9 +++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index 05ccea46c..660430d99 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -63,10 +63,17 @@ class ResourceMapper { const files = await this._readdir(folder) // Find a file with the same name (minus the dollar extension) - const match = files.find(f => this._removeDollarExtension(f) === filename || + let match = files.find(f => this._removeDollarExtension(f) === filename || (isIndex && f.startsWith(this._indexName + '.'))) if (!match) { - throw new Error(`File not found: ${fullPath}`) + // Error if no match was found, + // unless the URL ends with a '/', + // in that case we fallback to the folder itself. + if (isIndex) { + match = ''; + } else { + throw new Error(`File not found: ${fullPath}`) + } } path = `${folder}${match}` contentType = this._getContentTypeByExtension(match) diff --git a/test/unit/resource-mapper-test.js b/test/unit/resource-mapper-test.js index ee7e147a9..5c67b28b9 100644 --- a/test/unit/resource-mapper-test.js +++ b/test/unit/resource-mapper-test.js @@ -213,6 +213,15 @@ describe('ResourceMapper', () => { contentType: 'text/html' }) + itMapsUrl(mapper, 'a URL ending with a slash to a folder when no index is available', + { + url: 'http://localhost/space/' + }, + { + path: `${rootPath}space/`, + contentType: 'application/octet-stream' + }) + itMapsUrl(mapper, 'a URL ending with a slash to an index file for text/html when index.html not is available', { url: 'http://localhost/space/', From 5d30510b3431cfa22af4f8644dfc260990b5f7cd Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 10:58:31 +0100 Subject: [PATCH 31/42] Make text/turtle the default content type for mapping .acl files --- lib/resource-mapper.js | 6 ++-- test/unit/resource-mapper-test.js | 53 +++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index 660430d99..f8412804e 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -13,7 +13,8 @@ class ResourceMapper { rootPath = process.cwd(), includeHost = false, defaultContentType = 'application/octet-stream', - indexName = 'index' + indexName = 'index', + overrideTypes = { acl: 'text/turtle' } }) { this._rootUrl = this._removeTrailingSlash(rootUrl) this._rootPath = this._removeTrailingSlash(rootPath) @@ -21,6 +22,7 @@ class ResourceMapper { this._readdir = readdir this._defaultContentType = defaultContentType this._indexName = indexName + this._types = { ...types, ...overrideTypes } // If the host needs to be replaced on every call, pre-split the root URL if (includeHost) { @@ -134,7 +136,7 @@ class ResourceMapper { // Gets the expected content type based on the extension of the path _getContentTypeByExtension (path) { const extension = /\.([^/.]+)$/.exec(path) - return extension && types[extension[1].toLowerCase()] || this._defaultContentType + return extension && this._types[extension[1].toLowerCase()] || this._defaultContentType } // Removes a possible trailing slash from a path diff --git a/test/unit/resource-mapper-test.js b/test/unit/resource-mapper-test.js index 5c67b28b9..e0b3f471d 100644 --- a/test/unit/resource-mapper-test.js +++ b/test/unit/resource-mapper-test.js @@ -93,6 +93,28 @@ describe('ResourceMapper', () => { contentType: 'image/jpeg' }) + itMapsUrl(mapper, 'a URL with an overridden extension that matches the content type', + { + url: 'http://localhost/space/foo.acl', + contentType: 'text/turtle', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo.acl`, + contentType: 'text/turtle' + }) + + itMapsUrl(mapper, 'a URL with an alternative overridden extension that matches the content type', + { + url: 'http://localhost/space/foo.acl', + contentType: 'text/n3', + createIfNotExists: true + }, + { + path: `${rootPath}space/foo.acl$.n3`, + contentType: 'text/n3' + }) + // GET/HEAD/POST/DELETE/PATCH base cases itMapsUrl(mapper, 'a URL of a non-existing file', @@ -187,6 +209,30 @@ describe('ResourceMapper', () => { contentType: 'text/html' }) + itMapsUrl(mapper, 'a URL of an existing .acl file', + { + url: 'http://localhost/space/.acl' + }, + [ + `${rootPath}space/.acl` + ], + { + path: `${rootPath}space/.acl`, + contentType: 'text/turtle' + }) + + itMapsUrl(mapper, 'a URL of an existing .acl file with a different content type', + { + url: 'http://localhost/space/.acl' + }, + [ + `${rootPath}space/.acl$.n3` + ], + { + path: `${rootPath}space/.acl$.n3`, + contentType: 'text/n3' + }) + itMapsUrl(mapper, 'a URL ending with a slash to an index file when index.html is available', { url: 'http://localhost/space/' @@ -285,6 +331,13 @@ describe('ResourceMapper', () => { contentType: 'text/turtle' }) + itMapsFile(mapper, 'an ACL file', + { path: `${rootPath}space/.acl` }, + { + url: 'http://localhost/space/.acl', + contentType: 'text/turtle' + }) + itMapsFile(mapper, 'an unknown file type', { path: `${rootPath}space/foo.bar` }, { From 900616ff0b39f6b70204c25d316ca9b776fa548d Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 11:25:33 +0100 Subject: [PATCH 32/42] Require Content-Type in POST request --- lib/handlers/post.js | 2 +- lib/handlers/put.js | 2 +- lib/ldp.js | 12 +++++++++--- test/integration/acl-oidc-test.js | 3 ++- test/integration/acl-tls-test.js | 3 ++- test/integration/ldp-test.js | 24 +++++++++++++++++++----- 6 files changed, 34 insertions(+), 12 deletions(-) diff --git a/lib/handlers/post.js b/lib/handlers/post.js index 3d0ab7cba..f136314ae 100644 --- a/lib/handlers/post.js +++ b/lib/handlers/post.js @@ -58,7 +58,7 @@ async function handler (req, res, next) { const { url: putUrl } = await ldp.resourceMapper.mapFileToUrl( { path: ldp.resourceMapper._rootPath + path.join(containerPath, filename), hostname: req.hostname }) try { - await ldp.put(putUrl, file) + await ldp.put(putUrl, file, mimetype) } catch (err) { busboy.emit('error', err) } diff --git a/lib/handlers/put.js b/lib/handlers/put.js index 7b1c2467f..17f036187 100644 --- a/lib/handlers/put.js +++ b/lib/handlers/put.js @@ -8,7 +8,7 @@ async function handler (req, res, next) { res.header('MS-Author-Via', 'SPARQL') try { - await ldp.put(req, req) + await ldp.put(req, req, req.headers['content-type']) debug('succeded putting the file') res.sendStatus(201) diff --git a/lib/ldp.js b/lib/ldp.js index 6bddb0af9..37540d430 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -209,14 +209,20 @@ class LDP { } async put (url, stream, contentType) { - const { path: filePath } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true }) - // PUT requests not supported on containers. Use POST instead - if (filePath.endsWith('/')) { + if ((url.url || url).endsWith('/')) { throw error(409, 'PUT not supported on containers, use POST instead') } + // PUT without content type is forbidden + if (!contentType) { + throw error(415, + 'PUT request require a valid content type via the Content-Type header') + } + + const { path: filePath } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true }) + // First, create the enclosing directory, if necessary const dirName = path.dirname(filePath) try { diff --git a/test/integration/acl-oidc-test.js b/test/integration/acl-oidc-test.js index 336fe2a70..df6d205ec 100644 --- a/test/integration/acl-oidc-test.js +++ b/test/integration/acl-oidc-test.js @@ -96,7 +96,8 @@ describe('ACL with WebID+OIDC over HTTP', function () { const options = { url: timAccountUri + path, headers: { - accept: 'text/turtle' + 'accept': 'text/turtle', + 'content-type': 'text/plain' } } if (user) { diff --git a/test/integration/acl-tls-test.js b/test/integration/acl-tls-test.js index aedae7289..4dd38fc41 100644 --- a/test/integration/acl-tls-test.js +++ b/test/integration/acl-tls-test.js @@ -77,7 +77,8 @@ describe('ACL with WebID+TLS', function () { var options = { url: address + path, headers: { - accept: 'text/turtle' + accept: 'text/turtle', + 'content-type': 'text/plain' } } if (user) { diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index bbf0011e6..9dba86558 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -118,7 +118,7 @@ describe('LDP', function () { describe('put', function () { it('should write a file in an existing dir', () => { var stream = stringToStream('hello world') - return ldp.put('/resources/testPut.txt', stream).then(() => { + return ldp.put('/resources/testPut.txt', stream, 'text/plain').then(() => { var found = read('testPut.txt') rm('testPut.txt') assert.equal(found, 'hello world') @@ -127,10 +127,24 @@ describe('LDP', function () { it('should fail if a trailing `/` is passed', () => { var stream = stringToStream('hello world') - return ldp.put('/resources/', stream).catch(err => { + return ldp.put('/resources/', stream, 'text/plain').catch(err => { assert.equal(err.status, 409) }) }) + + it('should fail if a trailing `/` is passed without content type', () => { + var stream = stringToStream('hello world') + return ldp.put('/resources/', stream, null).catch(err => { + assert.equal(err.status, 409) + }) + }) + + it('should fail if no content type is passed', () => { + var stream = stringToStream('hello world') + return ldp.put('/resources/testPut.txt', stream, null).catch(err => { + assert.equal(err.status, 415) + }) + }) }) describe('delete', function () { @@ -141,7 +155,7 @@ describe('LDP', function () { it('should delete a file in an existing dir', async () => { // First create a dummy file var stream = stringToStream('hello world') - await ldp.put('/resources/testPut.txt', stream) + await ldp.put('/resources/testPut.txt', stream, 'text/plain') // Make sure it exists fs.stat(ldp.resourceMapper._rootPath + '/resources/testPut.txt', function (err) { if (err) { @@ -162,7 +176,7 @@ describe('LDP', function () { it('should fail to delete a non-empty folder', async () => { // First create a dummy file var stream = stringToStream('hello world') - await ldp.put('/resources/dummy/testPutBlocking.txt', stream) + await ldp.put('/resources/dummy/testPutBlocking.txt', stream, 'text/plain') // Make sure it exists fs.stat(ldp.resourceMapper._rootPath + '/resources/dummy/testPutBlocking.txt', function (err) { if (err) { @@ -177,7 +191,7 @@ describe('LDP', function () { it('should fail to delete nested non-empty folders', async () => { // First create a dummy file var stream = stringToStream('hello world') - await ldp.put('/resources/dummy/dummy2/testPutBlocking.txt', stream) + await ldp.put('/resources/dummy/dummy2/testPutBlocking.txt', stream, 'text/plain') // Make sure it exists fs.stat(ldp.resourceMapper._rootPath + '/resources/dummy/dummy2/testPutBlocking.txt', function (err) { if (err) { From 9d0f26adb8db568c70fdf67a61cebb6ed8fd2409 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 13:37:32 +0100 Subject: [PATCH 33/42] Inherit content type from source in COPY handler --- lib/handlers/copy.js | 25 +++++++++---------- lib/ldp-copy.js | 59 ++++++++++++++++++++++++-------------------- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/lib/handlers/copy.js b/lib/handlers/copy.js index f069fff2d..1a79f94ad 100644 --- a/lib/handlers/copy.js +++ b/lib/handlers/copy.js @@ -22,17 +22,16 @@ async function handler (req, res, next) { const ldp = req.app.locals.ldp const serverRoot = ldp.resourceMapper.getBaseUrl(req.hostname) const copyFromUrl = fromExternal ? copyFrom : serverRoot + copyFrom - const copyTo = res.locals.path || req.path - const { path: copyToPath } = await ldp.resourceMapper.mapUrlToFile({ url: req }) - ldpCopy(copyToPath, copyFromUrl, function (err) { - if (err) { - let statusCode = err.statusCode || 500 - let errorMessage = err.statusMessage || err.message - debug.handlers('Error with COPY request:' + errorMessage) - return next(error(statusCode, errorMessage)) - } - res.set('Location', copyTo) - res.sendStatus(201) - next() - }) + const copyToUrl = res.locals.path || req.path + try { + await ldpCopy(ldp.resourceMapper, copyToUrl, copyFromUrl) + } catch (err) { + let statusCode = err.statusCode || 500 + let errorMessage = err.statusMessage || err.message + debug.handlers('Error with COPY request:' + errorMessage) + return next(error(statusCode, errorMessage)) + } + res.set('Location', copyToUrl) + res.sendStatus(201) + next() } diff --git a/lib/ldp-copy.js b/lib/ldp-copy.js index a9e1ccf19..25b7521b9 100644 --- a/lib/ldp-copy.js +++ b/lib/ldp-copy.js @@ -3,6 +3,7 @@ module.exports = copy const debug = require('./debug') const fs = require('fs') const mkdirp = require('fs-extra').mkdirp +const error = require('./http-error') const path = require('path') const http = require('http') const https = require('https') @@ -21,47 +22,51 @@ function cleanupFileStream (stream) { /** * Performs an LDP Copy operation, imports a remote resource to a local path. - * @param copyToPath {String} Local path to copy the resource into + * @param resourceMapper {ResourceMapper} A resource mapper instance. + * @param copyToUri {Object} The location (in the current domain) to copy to. * @param copyFromUri {String} Location of remote resource to copy from - * @param callback {Function} Node error callback + * @return A promise resolving when the copy operation is finished */ -function copy (copyToPath, copyFromUri, callback) { - mkdirp(path.dirname(copyToPath), function (err) { - if (err) { - debug.handlers('COPY -- Error creating destination directory: ' + err) - return callback( - new Error('Failed to create the path to the destination resource: ' + - err)) - } - const destinationStream = fs.createWriteStream(copyToPath) - .on('error', function (err) { - cleanupFileStream(this) - return callback(new Error('Error writing data: ' + err)) - }) - .on('finish', function () { - // Success - debug.handlers('COPY -- Wrote data to: ' + copyToPath) - callback() - }) +function copy (resourceMapper, copyToUri, copyFromUri) { + return new Promise((resolve, reject) => { const request = /^https:/.test(copyFromUri) ? https : http request.get(copyFromUri) .on('error', function (err) { debug.handlers('COPY -- Error requesting source file: ' + err) this.end() - cleanupFileStream(destinationStream) - return callback(new Error('Error writing data: ' + err)) + return reject(new Error('Error writing data: ' + err)) }) .on('response', function (response) { if (response.statusCode !== 200) { - debug.handlers('COPY -- HTTP error reading source file: ' + - response.statusMessage) + debug.handlers('COPY -- HTTP error reading source file: ' + response.statusMessage) this.end() - cleanupFileStream(destinationStream) let error = new Error('Error reading source file: ' + response.statusMessage) error.statusCode = response.statusCode - return callback(error) + return reject(error) } - response.pipe(destinationStream) + // Grab the content type from the source + const contentType = response.headers['content-type'] + resourceMapper.mapUrlToFile({ url: copyToUri, createIfNotExists: true, contentType }) + .then(({ path: copyToPath }) => { + mkdirp(path.dirname(copyToPath), function (err) { + if (err) { + debug.handlers('COPY -- Error creating destination directory: ' + err) + return reject(new Error('Failed to create the path to the destination resource: ' + err)) + } + const destinationStream = fs.createWriteStream(copyToPath) + .on('error', function (err) { + cleanupFileStream(this) + return reject(new Error('Error writing data: ' + err)) + }) + .on('finish', function () { + // Success + debug.handlers('COPY -- Wrote data to: ' + copyToPath) + resolve() + }) + response.pipe(destinationStream) + }) + }) + .catch(() => reject(error(500, 'Could not find target file to copy'))) }) }) } From 2f6cb7f57dfbc6364130d1f6c3479af3ce0caf07 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 15:42:11 +0100 Subject: [PATCH 34/42] Transition to new ResourceMapper --- lib/create-app.js | 4 +- lib/legacy-resource-mapper.js | 21 -- test/unit/legacy-resource-mapper-test.js | 246 ----------------------- 3 files changed, 2 insertions(+), 269 deletions(-) delete mode 100644 lib/legacy-resource-mapper.js delete mode 100644 test/unit/legacy-resource-mapper-test.js diff --git a/lib/create-app.js b/lib/create-app.js index 29e224faf..f857397d0 100644 --- a/lib/create-app.js +++ b/lib/create-app.js @@ -23,7 +23,7 @@ const options = require('./handlers/options') const debug = require('./debug').authentication const path = require('path') const { routeResolvedFile } = require('./utils') -const LegacyResourceMapper = require('./legacy-resource-mapper') +const ResourceMapper = require('./resource-mapper') const corsSettings = cors({ methods: [ @@ -42,7 +42,7 @@ function createApp (argv = {}) { argv.host = SolidHost.from({ port: argv.port, serverUri: argv.serverUri }) - argv.resourceMapper = new LegacyResourceMapper({ + argv.resourceMapper = new ResourceMapper({ rootUrl: argv.serverUri, rootPath: argv.root, includeHost: argv.multiuser diff --git a/lib/legacy-resource-mapper.js b/lib/legacy-resource-mapper.js deleted file mode 100644 index de9d44a2b..000000000 --- a/lib/legacy-resource-mapper.js +++ /dev/null @@ -1,21 +0,0 @@ -const ResourceMapper = require('./resource-mapper') - -// A LegacyResourceMapper models the old mapping between HTTP URLs and server filenames, -// and is intended to be replaced by ResourceMapper -class LegacyResourceMapper extends ResourceMapper { - constructor (options) { - super(Object.assign({ defaultContentType: 'text/turtle' }, options)) - } - - // Maps the request for a given resource and representation format to a server file - async mapUrlToFile ({ url }) { - return { path: this._getFullPath(url), contentType: this._getContentTypeByExtension(url) } - } - - // Preserve dollars in paths - _removeDollarExtension (path) { - return path - } -} - -module.exports = LegacyResourceMapper diff --git a/test/unit/legacy-resource-mapper-test.js b/test/unit/legacy-resource-mapper-test.js deleted file mode 100644 index f9efec22c..000000000 --- a/test/unit/legacy-resource-mapper-test.js +++ /dev/null @@ -1,246 +0,0 @@ -const LegacyResourceMapper = require('../../lib/legacy-resource-mapper') -const chai = require('chai') -const { expect } = chai -chai.use(require('chai-as-promised')) - -const rootUrl = 'http://localhost/' -const rootPath = '/var/www/folder/' - -const itMapsUrl = asserter(mapsUrl) -const itMapsFile = asserter(mapsFile) - -describe('LegacyResourceMapper', () => { - describe('A LegacyResourceMapper instance for a single-host setup', () => { - const mapper = new LegacyResourceMapper({ rootUrl, rootPath }) - - // adapted PUT base cases from https://www.w3.org/DesignIssues/HTTPFilenameMapping.html - - itMapsUrl(mapper, 'a URL with an extension that matches the content type', - { - url: 'http://localhost/space/foo.html', - contentType: 'text/html', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo.html`, - contentType: 'text/html' - }) - - // Additional PUT cases - - itMapsUrl(mapper, 'a URL without content type', - { - url: 'http://localhost/space/foo.html', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo.html`, - contentType: 'text/html' - }) - - itMapsUrl(mapper, 'a URL with an alternative extension that matches the content type', - { - url: 'http://localhost/space/foo.jpeg', - contentType: 'image/jpeg', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo.jpeg`, - contentType: 'image/jpeg' - }) - - itMapsUrl(mapper, 'a URL with an uppercase extension that matches the content type', - { - url: 'http://localhost/space/foo.JPG', - contentType: 'image/jpeg', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo.JPG`, - contentType: 'image/jpeg' - }) - - itMapsUrl(mapper, 'a URL with a mixed-case extension that matches the content type', - { - url: 'http://localhost/space/foo.jPeG', - contentType: 'image/jpeg', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo.jPeG`, - contentType: 'image/jpeg' - }) - - // GET/HEAD/POST/DELETE/PATCH base cases - - itMapsUrl(mapper, 'a URL of an existing file with extension', - { - url: 'http://localhost/space/foo.html' - }, - { - path: `${rootPath}space/foo.html`, - contentType: 'text/html' - }) - - itMapsUrl(mapper, 'an extensionless URL of an existing file', - { - url: 'http://localhost/space/foo' - }, - { - path: `${rootPath}space/foo`, - contentType: 'text/turtle' - }) - - itMapsUrl(mapper, 'a URL of an existing file with encoded characters', - { - url: 'http://localhost/space/foo%20bar%20bar.html' - }, - { - path: `${rootPath}space/foo bar bar.html`, - contentType: 'text/html' - }) - - itMapsUrl(mapper, 'a URL of a new file with encoded characters', - { - url: 'http://localhost/space%2Ffoo%20bar%20bar.html', - contentType: 'text/html', - createIfNotExists: true - }, - { - path: `${rootPath}space/foo bar bar.html`, - contentType: 'text/html' - }) - - // Security cases - - itMapsUrl(mapper, 'a URL with a /.. path segment', - { - url: 'http://localhost/space/../bar' - }, - new Error('Disallowed /.. segment in URL')) - - itMapsUrl(mapper, 'a URL with an encoded /.. path segment', - { - url: 'http://localhost/space%2F..%2Fbar' - }, - new Error('Disallowed /.. segment in URL')) - - // File to URL mapping - - itMapsFile(mapper, 'an HTML file', - { path: `${rootPath}space/foo.html` }, - { - url: 'http://localhost/space/foo.html', - contentType: 'text/html' - }) - - itMapsFile(mapper, 'a Turtle file', - { path: `${rootPath}space/foo.ttl` }, - { - url: 'http://localhost/space/foo.ttl', - contentType: 'text/turtle' - }) - - itMapsFile(mapper, 'a file with an uppercase extension', - { path: `${rootPath}space/foo.HTML` }, - { - url: 'http://localhost/space/foo.HTML', - contentType: 'text/html' - }) - - itMapsFile(mapper, 'a file with a mixed-case extension', - { path: `${rootPath}space/foo.HtMl` }, - { - url: 'http://localhost/space/foo.HtMl', - contentType: 'text/html' - }) - - itMapsFile(mapper, 'a file with disallowed IRI characters', - { path: `${rootPath}space/foo bar bar.html` }, - { - url: 'http://localhost/space/foo%20bar%20bar.html', - contentType: 'text/html' - }) - }) - - describe('A LegacyResourceMapper instance for a multi-host setup', () => { - const mapper = new LegacyResourceMapper({ rootUrl, rootPath, includeHost: true }) - - itMapsUrl(mapper, 'a URL with a host', - { - url: 'http://example.org/space/foo.html', - contentType: 'text/html', - createIfNotExists: true - }, - { - path: `${rootPath}example.org/space/foo.html`, - contentType: 'text/html' - }) - - itMapsUrl(mapper, 'a URL with a host with a port', - { - url: 'http://example.org:3000/space/foo.html', - contentType: 'text/html', - createIfNotExists: true - }, - { - path: `${rootPath}example.org/space/foo.html`, - contentType: 'text/html' - }) - - itMapsFile(mapper, 'a file on a host', - { - path: `${rootPath}space/foo.html`, - hostname: 'example.org' - }, - { - url: 'http://example.org/space/foo.html', - contentType: 'text/html' - }) - }) - - describe('A LegacyResourceMapper instance for a multi-host setup with a subfolder root URL', () => { - const rootUrl = 'http://localhost/foo/bar/' - const mapper = new LegacyResourceMapper({ rootUrl, rootPath, includeHost: true }) - - itMapsFile(mapper, 'a file on a host', - { - path: `${rootPath}space/foo.html`, - hostname: 'example.org' - }, - { - url: 'http://example.org/foo/bar/space/foo.html', - contentType: 'text/html' - }) - }) -}) - -function asserter (assert) { - const f = (...args) => assert(it, ...args) - f.skip = (...args) => assert(it.skip, ...args) - f.only = (...args) => assert(it.only, ...args) - return f -} - -function mapsUrl (it, mapper, label, options, expected) { - // Set up positive test - if (!(expected instanceof Error)) { - it(`maps ${label}`, async () => { - const actual = await mapper.mapUrlToFile(options) - expect(actual).to.deep.equal(expected) - }) - // Set up error test - } else { - it(`does not map ${label}`, async () => { - const actual = mapper.mapUrlToFile(options) - await expect(actual).to.be.rejectedWith(expected.message) - }) - } -} - -function mapsFile (it, mapper, label, options, expected) { - it(`maps ${label}`, async () => { - const actual = await mapper.mapFileToUrl(options) - expect(actual).to.deep.equal(expected) - }) -} From e5073ffc4e6c4598418a6b3608e58c3c55ab18ae Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 15:44:30 +0100 Subject: [PATCH 35/42] Allow linksHandler to map resources that do (not) exist yet --- lib/header.js | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/header.js b/lib/header.js index 36dcf5f42..87a5accf3 100644 --- a/lib/header.js +++ b/lib/header.js @@ -43,7 +43,19 @@ function addLinks (res, fileMetadata) { async function linksHandler (req, res, next) { const ldp = req.app.locals.ldp - const { path: filename } = await ldp.resourceMapper.mapUrlToFile(req) + let filename + try { + // Hack: createIfNotExists is set to true for PUT or PATCH requests + // because the file does not exist yet at this point. + // But it will be created afterwards. + // This should be improved with the new server architecture. + ({ path: filename } = await ldp.resourceMapper + .mapUrlToFile({ url: req, createIfNotExists: req.method === 'PUT' || req.method === 'PATCH' })) + } catch (e) { + // Silently ignore errors here + // Later handlers will error as well, but they will be able to given a more concrete error message (like 400 or 404) + return next() + } if (path.extname(filename) === ldp.suffixMeta) { debug.metadata('Trying to access metadata file as regular file.') From d682fe0647b4d9907be100950bd74d2dd03b4109 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 15:45:21 +0100 Subject: [PATCH 36/42] Allow patch handler to map resources that do no exist yet --- lib/handlers/patch.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/handlers/patch.js b/lib/handlers/patch.js index 744c47166..68c0bdadf 100644 --- a/lib/handlers/patch.js +++ b/lib/handlers/patch.js @@ -15,6 +15,8 @@ const PATCH_PARSERS = { 'text/n3': require('./patch/n3-patch-parser.js') } +const DEFAULT_CONTENT_TYPE = 'text/turtle' + // Handles a PATCH request async function patchHandler (req, res, next) { debug(`PATCH -- ${req.originalUrl}`) @@ -22,7 +24,15 @@ async function patchHandler (req, res, next) { try { // Obtain details of the target resource const ldp = req.app.locals.ldp - const { path, contentType } = await ldp.resourceMapper.mapUrlToFile({ url: req }) + let path, contentType; + try { + // First check if the file already exists + ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile({ url: req })) + } catch (err) { + // If the file doesn't exist, request one to be created with the default content type + ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile( + { url: req, createIfNotExists: true, contentType: DEFAULT_CONTENT_TYPE })) + } const { url } = await ldp.resourceMapper.mapFileToUrl({ path, hostname: req.hostname }) const resource = { path, contentType, url } debug('PATCH -- Target <%s> (%s)', url, contentType) From 289ee0e4fe1b3fe953cc1250c7a7b1c27adc1eaa Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 15:54:22 +0100 Subject: [PATCH 37/42] Fix incorrect response content type being passed in HEAD --- lib/handlers/get.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/handlers/get.js b/lib/handlers/get.js index 1575e670f..022123764 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -75,9 +75,7 @@ async function handler (req, res, next) { // Till here it must exist if (!includeBody) { debug('HEAD only') - const mappedFile = await ldp.resourceMapper.mapFileToUrl({ path }) - contentType = mappedFile.contentType - res.setHeader('Content-Type', contentType) + res.setHeader('Content-Type', ret.contentType) res.status(200).send('OK') return next() } From ff0423e29432a367369cb626804efba8a2364863 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 15:55:16 +0100 Subject: [PATCH 38/42] Fix databrowser being used instead of index.html files --- lib/handlers/get.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/handlers/get.js b/lib/handlers/get.js index 022123764..c9c6ce3bd 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -82,7 +82,8 @@ async function handler (req, res, next) { // Handle dataBrowser if (requestedType && requestedType.includes('text/html')) { - let mimeTypeByExt = mime.lookup(_path.basename(path)) + const { path: filename } = await ldp.resourceMapper.mapUrlToFile({ url: options }) + let mimeTypeByExt = mime.lookup(_path.basename(filename)) let isHtmlResource = mimeTypeByExt && mimeTypeByExt.includes('html') let useDataBrowser = ldp.dataBrowserPath && ( container || From 58f937a315fb9afe5926927e33ffb3303536e8f6 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 15:55:43 +0100 Subject: [PATCH 39/42] Fix globbing crash with new ResourceMapper --- lib/handlers/get.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/handlers/get.js b/lib/handlers/get.js index c9c6ce3bd..15ecbf59a 100644 --- a/lib/handlers/get.js +++ b/lib/handlers/get.js @@ -135,7 +135,9 @@ async function handler (req, res, next) { async function globHandler (req, res, next) { const ldp = req.app.locals.ldp - const { path: filename } = await ldp.resourceMapper.mapUrlToFile({ url: req }) + // TODO: This is a hack, as globbing and resource mapping in combination is complex + // TODO: Proper support for this is not implemented, as globbing support will probably be removed in the future. + const filename = ldp.resourceMapper._getFullPath(req) const requestUri = (await ldp.resourceMapper.mapFileToUrl({ path: filename, hostname: req.hostname })).url const globOptions = { From 2fe6788e441a83e87d445595541b35aadd330ea4 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 15:56:55 +0100 Subject: [PATCH 40/42] Fix minor LDP issues with new ResourceMapper --- lib/handlers/index.js | 3 ++- lib/ldp.js | 45 ++++++++++++++++--------------------------- 2 files changed, 19 insertions(+), 29 deletions(-) diff --git a/lib/handlers/index.js b/lib/handlers/index.js index ba80e832f..817552d52 100644 --- a/lib/handlers/index.js +++ b/lib/handlers/index.js @@ -9,9 +9,10 @@ async function handler (req, res, next) { const ldp = req.app.locals.ldp const negotiator = new Negotiator(req) const requestedType = negotiator.mediaType() - const { path: filename } = await req.app.locals.ldp.resourceMapper.mapUrlToFile({ url: req }) try { + const { path: filename } = await req.app.locals.ldp.resourceMapper.mapUrlToFile({ url: req }) + const stats = await ldp.stat(filename) if (!stats.isDirectory()) { return next() diff --git a/lib/ldp.js b/lib/ldp.js index 37540d430..44d964a7f 100644 --- a/lib/ldp.js +++ b/lib/ldp.js @@ -1,4 +1,3 @@ -const mime = require('mime-types') const path = require('path') const url = require('url') const fs = require('fs') @@ -6,7 +5,6 @@ const $rdf = require('rdflib') const mkdirp = require('fs-extra').mkdirp const uuid = require('uuid') const debug = require('./debug') -const utils = require('./utils') const error = require('./http-error') const stringToStream = require('./utils').stringToStream const serialize = require('./utils').serialize @@ -99,16 +97,12 @@ class LDP { } async readResource (url) { - const { path } = await this.resourceMapper.mapUrlToFile({ url }) - return new Promise((resolve, reject) => { - fs.readFile( - path, - { 'encoding': 'utf8' }, - (err, data) => { - if (err) return reject(error(err, "Can't read file")) - resolve(data) - }) - }) + try { + const { path } = await this.resourceMapper.mapUrlToFile({ url }) + return await promisify(fs.readFile)(path, {'encoding': 'utf8'}) + } catch (err) { + throw error(404, "Can't read file") + } } async readContainerMeta (url) { @@ -234,7 +228,7 @@ class LDP { } // Directory created, now write the file - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const file = stream.pipe(fs.createWriteStream(filePath)) file.on('error', function () { reject(error(500, 'Error writing data')) @@ -317,15 +311,15 @@ class LDP { } async get (options) { - const { path: filename, contentType } = await this.resourceMapper.mapUrlToFile({ url: options }) - const baseUri = this.resourceMapper.getBaseUrl(options.hostname) - - let stats + let filename, contentType, stats try { + ({ path: filename, contentType } = await this.resourceMapper.mapUrlToFile({ url: options })) stats = await this.stat(filename) } catch (err) { - throw error(err, 'Can\'t find file requested: ' + filename) + throw error(404, 'Can\'t find file requested: ' + options) } + const baseUri = this.resourceMapper.getBaseUrl(options.hostname) + // Just return, since resource exists if (!options.includeBody) { return { 'stream': stats, 'contentType': contentType, 'container': stats.isDirectory() } @@ -373,10 +367,6 @@ class LDP { }) .on('open', function () { debug.handlers(`GET -- Reading ${filename}`) - let contentType = mime.lookup(filename) || DEFAULT_CONTENT_TYPE - if (utils.hasSuffix(filename, this.turtleExtensions)) { - contentType = 'text/turtle' - } return resolve({ stream, contentType, 'container': false, contentRange, chunksize }) }) }) @@ -384,21 +374,20 @@ class LDP { } async delete (url) { - const { path } = await this.resourceMapper.mapUrlToFile({ url }) - // First check if the path points to a valid file - let stats + let filePath, stats try { - stats = await this.stat(path) + ({ path: filePath } = await this.resourceMapper.mapUrlToFile({ url })) + stats = await this.stat(filePath) } catch (err) { throw error(404, "Can't find " + err) } // If so, delete the directory or file if (stats.isDirectory()) { - return this.deleteContainer(path) + return this.deleteContainer(filePath) } else { - return this.deleteResource(path) + return this.deleteResource(filePath) } } From d70d2d366b181312e8211a61052285b6c4db7edb Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 15:57:38 +0100 Subject: [PATCH 41/42] Update unit tests to new ResourceMapper --- .../integration/account-creation-oidc-test.js | 2 +- test/integration/account-manager-test.js | 10 +- test/integration/acl-oidc-test.js | 64 +++++++------ test/integration/acl-tls-test.js | 4 +- test/integration/formats-test.js | 9 +- test/integration/http-test.js | 91 ++++++++++++------- test/integration/ldp-test.js | 4 +- test/resources/sampleContainer/example4$.ttl | 7 ++ test/unit/account-manager-test.js | 10 +- test/unit/user-accounts-api-test.js | 4 +- 10 files changed, 128 insertions(+), 77 deletions(-) create mode 100644 test/resources/sampleContainer/example4$.ttl diff --git a/test/integration/account-creation-oidc-test.js b/test/integration/account-creation-oidc-test.js index 9632b93a8..98549f350 100644 --- a/test/integration/account-creation-oidc-test.js +++ b/test/integration/account-creation-oidc-test.js @@ -166,7 +166,7 @@ describe('AccountManager (OIDC account creation tests)', function () { } var graph = $rdf.graph() $rdf.parse( - data.text, + data.body.toString(), graph, 'https://nicola.' + host + '/.meta', 'text/turtle') diff --git a/test/integration/account-manager-test.js b/test/integration/account-manager-test.js index c8042e549..a41dfcc3c 100644 --- a/test/integration/account-manager-test.js +++ b/test/integration/account-manager-test.js @@ -9,7 +9,7 @@ chai.should() const LDP = require('../../lib/ldp') const SolidHost = require('../../lib/models/solid-host') const AccountManager = require('../../lib/models/account-manager') -const LegacyResourceMapper = require('../../lib/legacy-resource-mapper') +const ResourceMapper = require('../../lib/resource-mapper') const testAccountsDir = path.join(__dirname, '../resources/accounts') const accountTemplatePath = path.join(__dirname, '../../default-templates/new-account') @@ -30,7 +30,7 @@ describe('AccountManager', () => { describe('in multi user mode', () => { let multiuser = true - let resourceMapper = new LegacyResourceMapper({ + let resourceMapper = new ResourceMapper({ includeHost: multiuser }) let store = new LDP({ multiuser, resourceMapper }) @@ -58,7 +58,7 @@ describe('AccountManager', () => { let multiuser = false it('resolves to true if root .acl exists in root storage', () => { - let resourceMapper = new LegacyResourceMapper({ + let resourceMapper = new ResourceMapper({ includeHost: multiuser, rootPath: path.join(testAccountsDir, 'tim.localhost') }) @@ -76,7 +76,7 @@ describe('AccountManager', () => { }) it('resolves to false if root .acl does not exist in root storage', () => { - let resourceMapper = new LegacyResourceMapper({ + let resourceMapper = new ResourceMapper({ includeHost: multiuser, rootPath: testAccountsDir }) @@ -98,7 +98,7 @@ describe('AccountManager', () => { describe('createAccountFor()', () => { it('should create an account directory', () => { let multiuser = true - let resourceMapper = new LegacyResourceMapper({ + let resourceMapper = new ResourceMapper({ includeHost: multiuser, rootPath: testAccountsDir }) diff --git a/test/integration/acl-oidc-test.js b/test/integration/acl-oidc-test.js index df6d205ec..c8e9e1c31 100644 --- a/test/integration/acl-oidc-test.js +++ b/test/integration/acl-oidc-test.js @@ -92,12 +92,12 @@ describe('ACL with WebID+OIDC over HTTP', function () { const origin1 = 'http://example.org/' const origin2 = 'http://example.com/' - function createOptions (path, user) { + function createOptions (path, user, contentType = 'text/plain') { const options = { url: timAccountUri + path, headers: { 'accept': 'text/turtle', - 'content-type': 'text/plain' + 'content-type': contentType } } if (user) { @@ -220,7 +220,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it("should create test file's acl file", function (done) { - var options = createOptions('/write-acl/test-file.acl', 'user1') + var options = createOptions('/write-acl/test-file.acl', 'user1', 'text/turtle') options.body = '' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -254,7 +254,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) it('should PUT new ACL file', function (done) { - var options = createOptions('/origin/test-folder/.acl', 'user1') + var options = createOptions('/origin/test-folder/.acl', 'user1', 'text/turtle') options.body = '<#Owner> a ;\n' + ' ;\n' + ' <' + user1 + '>;\n' + @@ -362,7 +362,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user1 should be able to modify ACL file', function (done) { - var options = createOptions('/read-acl/.acl', 'user1') + var options = createOptions('/read-acl/.acl', 'user1', 'text/turtle') options.body = body request.put(options, function (error, response, body) { assert.equal(error, null) @@ -387,7 +387,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user2 should not be able to modify ACL file', function (done) { - var options = createOptions('/read-acl/.acl', 'user2') + var options = createOptions('/read-acl/.acl', 'user2', 'text/turtle') options.body = ' .' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -404,7 +404,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('agent should not be able to modify ACL file', function (done) { - var options = createOptions('/read-acl/.acl') + var options = createOptions('/read-acl/.acl', null, 'text/turtle') options.body = ' .' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -424,7 +424,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) - it('user1 should be able to PATCH a resource', function (done) { + it('user1 should be able to PATCH a new resource', function (done) { var options = createOptions('/append-inherited/test.ttl', 'user1') options.body = 'INSERT DATA { :test :hello 456 .}' options.headers['content-type'] = 'application/sparql-update' @@ -434,6 +434,16 @@ describe('ACL with WebID+OIDC over HTTP', function () { done() }) }) + it('user1 should be able to PATCH an existing resource', function (done) { + var options = createOptions('/append-inherited/test.ttl', 'user1') + options.body = 'INSERT DATA { :test :hello 789 .}' + options.headers['content-type'] = 'application/sparql-update' + request.patch(options, function (error, response, body) { + assert.equal(error, null) + assert.equal(response.statusCode, 200) + done() + }) + }) it('user1 should be able to access test file', function (done) { var options = createOptions('/append-acl/abc.ttl', 'user1') request.head(options, function (error, response, body) { @@ -444,7 +454,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) // TODO POST instead of PUT it('user1 should be able to modify test file', function (done) { - var options = createOptions('/append-acl/abc.ttl', 'user1') + var options = createOptions('/append-acl/abc.ttl', 'user1', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -453,7 +463,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it("user2 should not be able to access test file's ACL file", function (done) { - var options = createOptions('/append-acl/abc.ttl.acl', 'user2') + var options = createOptions('/append-acl/abc.ttl.acl', 'user2', 'text/turtle') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) @@ -461,7 +471,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user2 should not be able to access test file', function (done) { - var options = createOptions('/append-acl/abc.ttl', 'user2') + var options = createOptions('/append-acl/abc.ttl', 'user2', 'text/turtle') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 403) @@ -469,7 +479,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user2 (with append permission) cannot use PUT to append', function (done) { - var options = createOptions('/append-acl/abc.ttl', 'user2') + var options = createOptions('/append-acl/abc.ttl', 'user2', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -486,7 +496,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('agent (with append permissions) should not PUT', function (done) { - var options = createOptions('/append-acl/abc.ttl') + var options = createOptions('/append-acl/abc.ttl', null, 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -540,7 +550,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) it.skip('user2 should be able to write a file in the test directory', function (done) { - var options = createOptions('/group/test-folder/test.ttl', 'user2') + var options = createOptions('/group/test-folder/test.ttl', 'user2', 'text/turtle') options.body = '<#Dahut> a .\n' request.put(options, function (error, response, body) { @@ -550,7 +560,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it.skip('user1 should be able to get the file', function (done) { - var options = createOptions('/group/test-folder/test.ttl', 'user1') + var options = createOptions('/group/test-folder/test.ttl', 'user1', 'text/turtle') request.get(options, function (error, response, body) { assert.equal(error, null) @@ -560,7 +570,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) it.skip('user2 should not be able to write to the ACL', function (done) { - var options = createOptions('/group/test-folder/.acl', 'user2') + var options = createOptions('/group/test-folder/.acl', 'user2', 'text/turtle') options.body = '<#Dahut> a .\n' request.put(options, function (error, response, body) { @@ -570,7 +580,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it.skip('user1 should be able to delete the file', function (done) { - var options = createOptions('/group/test-folder/test.ttl', 'user1') + var options = createOptions('/group/test-folder/test.ttl', 'user1', 'text/turtle') request.delete(options, function (error, response, body) { assert.equal(error, null) @@ -610,7 +620,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { ' <' + user2 + '>;\n' + ' , .\n' it("user1 should be able to modify test file's ACL file", function (done) { - var options = createOptions('/append-acl/abc2.ttl.acl', 'user1') + var options = createOptions('/append-acl/abc2.ttl.acl', 'user1', 'text/turtle') options.body = body request.put(options, function (error, response, body) { assert.equal(error, null) @@ -619,7 +629,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it("user1 should be able to access test file's ACL file", function (done) { - var options = createOptions('/append-acl/abc2.ttl.acl', 'user1') + var options = createOptions('/append-acl/abc2.ttl.acl', 'user1', 'text/turtle') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -627,7 +637,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user1 should be able to access test file', function (done) { - var options = createOptions('/append-acl/abc2.ttl', 'user1') + var options = createOptions('/append-acl/abc2.ttl', 'user1', 'text/turtle') request.head(options, function (error, response, body) { assert.equal(error, null) assert.equal(response.statusCode, 200) @@ -635,7 +645,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user1 should be able to modify test file', function (done) { - var options = createOptions('/append-acl/abc2.ttl', 'user1') + var options = createOptions('/append-acl/abc2.ttl', 'user1', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -660,7 +670,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user2 should be able to modify test file', function (done) { - var options = createOptions('/append-acl/abc2.ttl', 'user2') + var options = createOptions('/append-acl/abc2.ttl', 'user2', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -677,7 +687,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('agent should not be able to modify test file', function (done) { - var options = createOptions('/append-acl/abc2.ttl') + var options = createOptions('/append-acl/abc2.ttl', null, 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -704,7 +714,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { ' ;\n' + ' .\n' it("user1 should be able to modify test directory's ACL file", function (done) { - var options = createOptions('/write-acl/default-for-new/.acl', 'user1') + var options = createOptions('/write-acl/default-for-new/.acl', 'user1', 'text/turtle') options.body = body request.put(options, function (error, response, body) { assert.equal(error, null) @@ -721,7 +731,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user1 should be able to create new test file', function (done) { - var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user1') + var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user1', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -754,7 +764,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('user2 should not be able to modify new test file', function (done) { - var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user2') + var options = createOptions('/write-acl/default-for-new/test-file.ttl', 'user2', 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) @@ -771,7 +781,7 @@ describe('ACL with WebID+OIDC over HTTP', function () { }) }) it('agent should not be able to modify new test file', function (done) { - var options = createOptions('/write-acl/default-for-new/test-file.ttl') + var options = createOptions('/write-acl/default-for-new/test-file.ttl', null, 'text/turtle') options.body = ' .\n' request.put(options, function (error, response, body) { assert.equal(error, null) diff --git a/test/integration/acl-tls-test.js b/test/integration/acl-tls-test.js index 4dd38fc41..6a89f7a49 100644 --- a/test/integration/acl-tls-test.js +++ b/test/integration/acl-tls-test.js @@ -272,7 +272,7 @@ describe('ACL with WebID+TLS', function () { }) it('should PUT new ACL file', function (done) { - var options = createOptions('/acl-tls/origin/test-folder/.acl', 'user1') + var options = createOptions('/acl-tls/origin/test-folder/.acl', 'user1', 'text/turtle') options.headers = { 'content-type': 'text/turtle' } @@ -370,7 +370,7 @@ describe('ACL with WebID+TLS', function () { }) it('should PUT new ACL file', function (done) { - var options = createOptions('/acl-tls/origin/test-folder/.acl', 'user1') + var options = createOptions('/acl-tls/origin/test-folder/.acl', 'user1', 'text/turtle') options.headers = { 'content-type': 'text/turtle' } diff --git a/test/integration/formats-test.js b/test/integration/formats-test.js index c82fcb4d1..994a415a3 100644 --- a/test/integration/formats-test.js +++ b/test/integration/formats-test.js @@ -95,8 +95,15 @@ describe('formats', function () { .expect(200, done) }) - it('should return turtle when listing container', function (done) { + it('should return turtle when listing container with an index page', function (done) { server.get('/sampleContainer/') + .set('accept', 'application/rdf+xml;q=0.4, application/xhtml+xml;q=0.3, text/xml;q=0.2, application/xml;q=0.2, text/html;q=0.3, text/plain;q=0.1, text/turtle;q=1.0, application/n3;q=1') + .expect('content-type', /text\/html/) + .expect(200, done) + }) + + it('should return turtle when listing container without an index page', function (done) { + server.get('/sampleContainer2/') .set('accept', 'application/rdf+xml;q=0.4, application/xhtml+xml;q=0.3, text/xml;q=0.2, application/xml;q=0.2, text/html;q=0.3, text/plain;q=0.1, text/turtle;q=1.0, application/n3;q=1') .expect('content-type', /text\/turtle/) .expect(200, done) diff --git a/test/integration/http-test.js b/test/integration/http-test.js index d850d1662..506f782b9 100644 --- a/test/integration/http-test.js +++ b/test/integration/http-test.js @@ -58,16 +58,18 @@ function createTestResource (resourceName) { describe('HTTP APIs', function () { var emptyResponse = function (res) { if (res.text) { - console.log('Not empty response') + throw new Error('Not empty response') } } var getLink = function (res, rel) { - var links = res.headers.link.split(',') - for (var i in links) { - var link = links[i] - var parsedLink = li.parse(link) - if (parsedLink[rel]) { - return parsedLink[rel] + if (res.headers.link) { + var links = res.headers.link.split(',') + for (var i in links) { + var link = links[i] + var parsedLink = li.parse(link) + if (parsedLink[rel]) { + return parsedLink[rel] + } } } return undefined @@ -77,10 +79,10 @@ describe('HTTP APIs', function () { var link = getLink(res, rel) if (link) { if (link !== value) { - console.log('Not same value:', value, '!=', link) + throw new Error('Not same value: ' + value + ' != ' + link) } } else { - console.log(rel, 'header does not exist:', link) + throw new Error('header does not exist: ' + rel + ' = ' + value) } } return handler @@ -137,13 +139,13 @@ describe('HTTP APIs', function () { }) it('should return 204 on success', function (done) { - server.options('/sampleContainer/example1.ttl') + server.options('/sampleContainer2/example1.ttl') .expect(204) .end(done) }) it('should have Access-Control-Allow-Origin', function (done) { - server.options('/sampleContainer/example1.ttl') + server.options('/sampleContainer2/example1.ttl') .set('Origin', 'http://example.com') .expect('Access-Control-Allow-Origin', 'http://example.com') .end(done) @@ -151,20 +153,26 @@ describe('HTTP APIs', function () { it('should have set acl and describedBy Links for resource', function (done) { - server.options('/sampleContainer/example1.ttl') + server.options('/sampleContainer2/example1.ttl') .expect(hasHeader('acl', 'example1.ttl' + suffixAcl)) .expect(hasHeader('describedBy', 'example1.ttl' + suffixMeta)) .end(done) }) it('should have set Link as resource', function (done) { - server.options('/sampleContainer/example1.ttl') + server.options('/sampleContainer2/example1.ttl') .expect('Link', /; rel="type"/) .end(done) }) - it('should have set Link as Container/BasicContainer', function (done) { + it('should have set Link as resource on a implicit index page', function (done) { server.options('/sampleContainer/') + .expect('Link', /; rel="type"/) + .end(done) + }) + + it('should have set Link as Container/BasicContainer', function (done) { + server.options('/sampleContainer2/') .set('Origin', 'http://example.com') .expect('Link', /; rel="type"/) .expect('Link', /; rel="type"/) @@ -172,14 +180,14 @@ describe('HTTP APIs', function () { }) it('should have set Accept-Post for containers', function (done) { - server.options('/sampleContainer/') + server.options('/sampleContainer2/') .set('Origin', 'http://example.com') .expect('Accept-Post', '*/*') .end(done) }) it('should have set acl and describedBy Links for container', function (done) { - server.options('/sampleContainer/') + server.options('/sampleContainer2/') .expect(hasHeader('acl', suffixAcl)) .expect(hasHeader('describedBy', suffixMeta)) .end(done) @@ -205,7 +213,7 @@ describe('HTTP APIs', function () { }) it('should have Access-Control-Allow-Origin as Origin on containers', function (done) { - server.get('/sampleContainer/') + server.get('/sampleContainer2/') .set('Origin', 'http://example.com') .expect('content-type', /text\/turtle/) .expect('Access-Control-Allow-Origin', 'http://example.com') @@ -213,40 +221,40 @@ describe('HTTP APIs', function () { }) it('should have Access-Control-Allow-Origin as Origin on resources', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .set('Origin', 'http://example.com') .expect('content-type', /text\/turtle/) .expect('Access-Control-Allow-Origin', 'http://example.com') .expect(200, done) }) it('should have set Link as resource', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .expect('content-type', /text\/turtle/) .expect('Link', /; rel="type"/) .expect(200, done) }) it('should have set Updates-Via to use WebSockets', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .expect('updates-via', /wss?:\/\//) .expect(200, done) }) it('should have set acl and describedBy Links for resource', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .expect('content-type', /text\/turtle/) .expect(hasHeader('acl', 'example1.ttl' + suffixAcl)) .expect(hasHeader('describedBy', 'example1.ttl' + suffixMeta)) .end(done) }) it('should have set Link as Container/BasicContainer', function (done) { - server.get('/sampleContainer/') + server.get('/sampleContainer2/') .expect('content-type', /text\/turtle/) .expect('Link', /; rel="type"/) .expect('Link', /; rel="type"/) .expect(200, done) }) it('should load skin (mashlib) if resource was requested as text/html', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .set('Accept', 'text/html') .expect('content-type', /text\/html/) .expect(function (res) { @@ -276,6 +284,19 @@ describe('HTTP APIs', function () { .end(done) }) + it('should NOT load data browser (mashlib) if directory has an index file', function (done) { + server.get('/sampleContainer/') + .set('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8') + .expect('content-type', /text\/html/) + .expect(200) + .expect((res) => { + if (res.text.includes('TabulatorOutline')) { + throw new Error('Loaded data browser though resource has an .html extension') + } + }) + .end(done) + }) + it('should show data browser if container was requested as text/html', function (done) { server.get('/sampleContainer2/') .set('Accept', 'text/html') @@ -332,7 +353,7 @@ describe('HTTP APIs', function () { }) it('should have set acl and describedBy Links for container', function (done) { - server.get('/sampleContainer/') + server.get('/sampleContainer2/') .expect(hasHeader('acl', suffixAcl)) .expect(hasHeader('describedBy', suffixMeta)) .expect('content-type', /text\/turtle/) @@ -370,12 +391,18 @@ describe('HTTP APIs', function () { describe('HEAD API', function () { it('should return content-type turtle by default', function (done) { server.head('/sampleContainer/blank') - .expect('Content-Type', 'text/turtle; charset=utf-8') + .expect('Content-Type', 'application/octet-stream; charset=utf-8') .end(done) }) it('should have set content-type for turtle files', function (done) { - server.head('/sampleContainer/example1.ttl') + server.head('/sampleContainer2/example1.ttl') + .expect('Content-Type', 'text/turtle; charset=utf-8') + .end(done) + }) + it('should have set content-type for implicit turtle files', + function (done) { + server.head('/sampleContainer/example4') .expect('Content-Type', 'text/turtle; charset=utf-8') .end(done) }) @@ -386,7 +413,7 @@ describe('HTTP APIs', function () { .end(done) }) it('should have Access-Control-Allow-Origin as Origin', function (done) { - server.head('/sampleContainer/example1.ttl') + server.head('/sampleContainer2/example1.ttl') .set('Origin', 'http://example.com') .expect('Access-Control-Allow-Origin', 'http://example.com') .expect(200, done) @@ -397,32 +424,32 @@ describe('HTTP APIs', function () { .expect(200, done) }) it('should have set Updates-Via to use WebSockets', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .expect('updates-via', /wss?:\/\//) .expect(200, done) }) it('should have set Link as Resource', function (done) { - server.head('/sampleContainer/example1.ttl') + server.head('/sampleContainer2/example1.ttl') .expect('Link', /; rel="type"/) .expect(200, done) }) it('should have set acl and describedBy Links for resource', function (done) { - server.get('/sampleContainer/example1.ttl') + server.get('/sampleContainer2/example1.ttl') .expect(hasHeader('acl', 'example1.ttl' + suffixAcl)) .expect(hasHeader('describedBy', 'example1.ttl' + suffixMeta)) .end(done) }) it('should have set Link as Container/BasicContainer', function (done) { - server.get('/sampleContainer/') + server.get('/sampleContainer2/') .expect('Link', /; rel="type"/) .expect('Link', /; rel="type"/) .expect(200, done) }) it('should have set acl and describedBy Links for container', function (done) { - server.get('/sampleContainer/') + server.get('/sampleContainer2/') .expect(hasHeader('acl', suffixAcl)) .expect(hasHeader('describedBy', suffixMeta)) .end(done) diff --git a/test/integration/ldp-test.js b/test/integration/ldp-test.js index 9dba86558..69e384524 100644 --- a/test/integration/ldp-test.js +++ b/test/integration/ldp-test.js @@ -6,7 +6,7 @@ var ns = require('solid-namespace')($rdf) var LDP = require('../../lib/ldp') var path = require('path') var stringToStream = require('../../lib/utils').stringToStream -var LegacyResourceMapper = require('../../lib/legacy-resource-mapper') +var ResourceMapper = require('../../lib/resource-mapper') // Helper functions for the FS var rm = require('./../utils').rm @@ -18,7 +18,7 @@ var fs = require('fs') describe('LDP', function () { var root = path.join(__dirname, '..') - var resourceMapper = new LegacyResourceMapper({ + var resourceMapper = new ResourceMapper({ rootPath: root }) diff --git a/test/resources/sampleContainer/example4$.ttl b/test/resources/sampleContainer/example4$.ttl new file mode 100644 index 000000000..fd650c9a2 --- /dev/null +++ b/test/resources/sampleContainer/example4$.ttl @@ -0,0 +1,7 @@ +@prefix : . + +:a :b "The first line\nThe second line\n more" . + +:a :b """The first line +The second line + more""" . diff --git a/test/unit/account-manager-test.js b/test/unit/account-manager-test.js index d9e79528f..7591be27a 100644 --- a/test/unit/account-manager-test.js +++ b/test/unit/account-manager-test.js @@ -17,7 +17,7 @@ const AccountManager = require('../../lib/models/account-manager') const UserAccount = require('../../lib/models/user-account') const TokenService = require('../../lib/services/token-service') const WebIdTlsCertificate = require('../../lib/models/webid-tls-certificate') -const LegacyResourceMapper = require('../../lib/legacy-resource-mapper') +const ResourceMapper = require('../../lib/resource-mapper') const testAccountsDir = path.join(__dirname, '../resources/accounts') @@ -103,7 +103,7 @@ describe('AccountManager', () => { describe('accountDirFor()', () => { it('should match the solid root dir config, in single user mode', () => { let multiuser = false - let resourceMapper = new LegacyResourceMapper({ + let resourceMapper = new ResourceMapper({ includeHost: multiuser, rootPath: testAccountsDir }) @@ -117,7 +117,7 @@ describe('AccountManager', () => { it('should compose the account dir in multi user mode', () => { let multiuser = true - let resourceMapper = new LegacyResourceMapper({ + let resourceMapper = new ResourceMapper({ includeHost: multiuser, rootPath: testAccountsDir }) @@ -311,7 +311,7 @@ describe('AccountManager', () => { describe('rootAclFor()', () => { it('should return the server root .acl in single user mode', () => { - let resourceMapper = new LegacyResourceMapper({ + let resourceMapper = new ResourceMapper({ includeHost: false }) let store = new LDP({ suffixAcl: '.acl', multiuser: false, resourceMapper }) @@ -326,7 +326,7 @@ describe('AccountManager', () => { }) it('should return the profile root .acl in multi user mode', () => { - let resourceMapper = new LegacyResourceMapper({ + let resourceMapper = new ResourceMapper({ includeHost: true }) let store = new LDP({ suffixAcl: '.acl', multiuser: true, resourceMapper }) diff --git a/test/unit/user-accounts-api-test.js b/test/unit/user-accounts-api-test.js index df1655a55..4da29b8d0 100644 --- a/test/unit/user-accounts-api-test.js +++ b/test/unit/user-accounts-api-test.js @@ -13,7 +13,7 @@ const LDP = require('../../lib/ldp') const SolidHost = require('../../lib/models/solid-host') const AccountManager = require('../../lib/models/account-manager') const testAccountsDir = path.join(__dirname, '..', 'resources', 'accounts') -var LegacyResourceMapper = require('../../lib/legacy-resource-mapper') +var ResourceMapper = require('../../lib/resource-mapper') const api = require('../../lib/api/accounts/user-accounts') @@ -27,7 +27,7 @@ describe('api/accounts/user-accounts', () => { describe('newCertificate()', () => { describe('in multi user mode', () => { let multiuser = true - let resourceMapper = new LegacyResourceMapper({ + let resourceMapper = new ResourceMapper({ includeHost: multiuser, rootPath: testAccountsDir }) From 96cb163f54b9e2871cf72637adb07919757c5fe4 Mon Sep 17 00:00:00 2001 From: Ruben Taelman Date: Wed, 21 Nov 2018 16:00:43 +0100 Subject: [PATCH 42/42] Fix linting errors due to ResourceMapper changes --- lib/handlers/patch.js | 2 +- lib/resource-mapper.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/handlers/patch.js b/lib/handlers/patch.js index 68c0bdadf..f2b2f8532 100644 --- a/lib/handlers/patch.js +++ b/lib/handlers/patch.js @@ -24,7 +24,7 @@ async function patchHandler (req, res, next) { try { // Obtain details of the target resource const ldp = req.app.locals.ldp - let path, contentType; + let path, contentType try { // First check if the file already exists ({ path, contentType } = await ldp.resourceMapper.mapUrlToFile({ url: req })) diff --git a/lib/resource-mapper.js b/lib/resource-mapper.js index f8412804e..ed7385fa0 100644 --- a/lib/resource-mapper.js +++ b/lib/resource-mapper.js @@ -72,7 +72,7 @@ class ResourceMapper { // unless the URL ends with a '/', // in that case we fallback to the folder itself. if (isIndex) { - match = ''; + match = '' } else { throw new Error(`File not found: ${fullPath}`) }