diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..ef5f69b32 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.swp +node_modules/ diff --git a/Makefile b/Makefile index 1cbfb9959..bc93fd357 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ run-for-test: NODE_PATH=.:../tabulator-firefox/content/js/rdf/dist/ node server.js \ - -v --uriBase http://localhost:3000/test/ --fileBase `pwd`/test/ + -v --uriBase http://localhost:3000/test/ -p 3000 --fileBase `pwd`/test/ diff --git a/acl.js b/acl.js new file mode 100644 index 000000000..7b0de4f3e --- /dev/null +++ b/acl.js @@ -0,0 +1,78 @@ +var acl = require('acl'); +var fs = require('fs'); +var path = require('path'); + +var file = require('./fileStore.js'); +var options = require('./options.js'); + +var permissionExtension = '.permissions'; +var aclEnabled = false; + +module.exports.readPermissions = function() { + var permissionFile = options.fileBase + permissionExtension; + fs.readFile(permissionFile, readPermissionsCallback); + + function readPermissionsCallback(err, rawPermissions) { + if (err) { + return; + } else { + var jsonPermissions; + try { + jsonPermissions = JSON.parse(rawPermissions); + } catch (parseErr) { + return; + } + if ('roles' in jsonPermissions && 'users' in jsonPermissions) { + try { + acl = new acl(new acl.memoryBackend()); + for (var rIndex = 0; rIndex < jsonPermissions.roles.length; rIndex++) { + acl.allow(jsonPermissions.roles[rIndex][0], + jsonPermissions.roles[rIndex][1], + jsonPermissions.roles[rIndex][2]); + } + for (var uIndex = 0; uIndex < jsonPermissions.users.length; uIndex++) { + acl.addUserRoles(jsonPermissions.users[uIndex][0], + jsonPermissions.users[uIndex][1]); + } + aclEnabled = true; + return; + } catch (permissionErr) { + return; + } + } else { + return; + } + } + } +}; + +module.exports.isAllowed = function(userId, resource, permissions, callback) { + if (aclEnabled) { + acl.isAllowed(userId, resource, permissions, callback); + } else { + callback(undefined, true); + } +}; + +module.exports.aclHandler = function(req, res, next) { + //TODO authentication + var filename = file.uriToFilename(req.path); + var resource = path.relative(options.fileBase, filename); + + //TODO complete this line + // acl.isAllowed(userId, resource, req.method); + next(); + + function aclCallback(err, allowed) { + if (err) { + return res.sendStatus(500); + } else { + if (allowed) { + next(); + } else { + return res.sendStatus(403); + } + } + + } +}; diff --git a/container.js b/container.js new file mode 100644 index 000000000..7ec6a04a3 --- /dev/null +++ b/container.js @@ -0,0 +1,228 @@ +var fs = require('fs'); +var $rdf = require('rdflib'); +var path = require('path'); +var uuid = require('node-uuid'); + +var logging = require('./logging.js'); +var metadata = require('./metadata.js'); +var options = require('./options.js'); + +var rdfVocab = require('./vocab/rdf.js'); +var ldpVocab = require('./vocab/ldp.js'); + +var addUriTriple = function(kb, s, o, p) { + kb.add(kb.sym(s), kb.sym(o), kb.sym(p)); +}; + +var usedURIs = {}; + +module.exports.createRootContainer = function() { + if (!metadata.hasMetadata(options.fileBase)) { + logging.log("Creating root metadata"); + var rootMetadata = new metadata.Metadata(); + rootMetadata.filename = options.fileBase; + rootMetadata.isResource = true; + rootMetadata.isContainer = true; + rootMetadata.isSourceResource = true; + rootMetadata.isBasicContainer = true; + metadata.writeMetadata(options.fileBase, rootMetadata, + writeCallback); + } + //TODO handle case when .container file does not exist + + function writeCallback(err) { + logging.log(options.pathStart); + if (err) { + process.exit(1); + } else if (!metadata.hasContainerMetadata(options.fileBase)) { + var rootContainer = $rdf.graph(); + addUriTriple(rootContainer, options.pathStart, rdfVocab.type, + ldpVocab.Resource); + addUriTriple(rootContainer, options.pathStart, rdfVocab.type, + ldpVocab.RDFSource); + addUriTriple(rootContainer, options.pathStart, rdfVocab.type, + ldpVocab.Container); + addUriTriple(rootContainer, options.pathStart, rdfVocab.type, + ldpVocab.BasicContainer); + rootContainer.add(rootContainer.sym(options.pathStart), + rootContainer.sym('http://purl.org/dc/terms/title'), + '"Root Container"'); + var serializedContainer = $rdf.serialize(undefined, rootContainer, + options.pathStart, 'text/turtle'); + logging.log("Root container: ", serializedContainer); + metadata.writeContainerMetadata(options.fileBase, + serializedContainer, function(err) { + if (err) { + //TODO handle error + logging.log("Could not write root container"); + } else { + logging.log("Wrote root container"); + } + }); + } + } +}; + +module.exports.createNewContainer = function(container, type, callback) { + fs.mkdir(container, function(err) { + if (err) { + this.releaseResourceUri(container); + callback(err); + } else { + var containerMetadata = new metadata.Metadata(); + containerMetadata.filename = container; + containerMetadata.isResource = true; + containerMetadata.isContainer = true; + containerMetadata.isSourceResource = true; + if (type === ldpVocab.BasicContainer) + containerMetadata.isBasicContainer = true; + if (type === ldpVocab.DirectContainer) + containerMetadata.isDirectContainer = true; + metadata.writeMetadata(options.fileBase, rootMetadata, writeCallback); + } + }); + + function writeCallback(err) { + if (err) { + this.releaseResourceUri(container); + callback(err); + } else { + var newContainer = $rdf.graph(); + addUriTriple(newContainer, options.pathStart, rdfVocab.type, + ldpVocab.Resource); + addUriTriple(newContainer, options.pathStart, rdfVocab.type, + ldpVocab.RDFSource); + addUriTriple(newContainer, options.pathStart, rdfVocab.type, + ldpVocab.Container); + if (type === ldpVocab.BasicContainer) + addUriTriple(newContainer, options.pathStart, rdfVocab.type, + ldpVocab.BasicContainer); + else if (type === ldpVocab.DirectContainer) + addUriTriple(newContainer, options.pathStart, rdfVocab.type, + ldpVocab.DirectContainer); + + newContainer.add(newContainer.sym(options.pathStart), + newContainer.sym('http://purl.org/dc/terms/title'), + container); + var serializedContainer = $rdf.serialize(undefined, newContainer, + container, 'text/turtle'); + metadata.writeContainerMetadata(container, serializedContainer, + function(err) { + this.releaseResourceUri(container); + if (err) { + callback(err); + } else { + logging.log("Wrote new container"); + callback(err); + } + }); + } + } + +}; + +module.exports.createResourceUri = function(containerURI, slug) { + var newPath; + if (slug) { + newPath = path.join(containerURI, slug); + } else { + newPath = path.join(containerURI, uuid.v1()); + } + if (!(fs.existsSync(newPath) || containerURI in usedURIs)) { + usedURIs[newPath] = true; + } else { + return null; + } + return newPath; +}; + +module.exports.releaseResourceUri = function(uri) { + delete usedURIs[uri]; +}; + +module.exports.verify = function(containerGraph, type) { + //TODO work on this method + var results = containerGraph.each(undefined, "a", type); + if (results.length === 1) { + return true; + } else { + return false; + } +}; + +module.exports.verifyDirectContainer = function(containerGraph) { + +}; + +module.exports.createNewResource = function(containerPath, containerGraph, + resourcePath, resourceGraph, resourceMetadata, callback) { + var containerURI = path.relative(options.fileBase, containerPath); + var resourceURI = path.relative(options.fileBase, resourcePath); + //TODO replace url with resource url + var rawResource = $rdf.serialize(undefined, + resourceGraph, options.baseUri + resourceURI, 'text/turtle'); + logging.log("Writing new resource to ", resourcePath); + logging.log(rawResource); + fs.writeFile(resourcePath, rawResource, writeResourceCallback); + + function writeResourceCallback(err) { + if (err) { + container.releaseResourceUri(resoucePath); + callback(err); + } else { + addUriTriple(containerGraph, containerURI, ldpVocab.contains, + resourceURI); + var rawContainer = $rdf.serialize(undefined, containerGraph, + options.uriBase, 'text/turtle'); + metadata.writeContainerMetadata(containerPath, rawContainer, + writeContainerCallback); + } + } + + function writeContainerCallback(err) { + if (err) { + module.exports.releaseResourceUri(resourcePath); + return callback(err); + } else { + metadata.writeMetadata(resourcePath, resourceMetadata, + writeMetadataCallback); + } + } + + function writeMetadataCallback(err) { + module.exports.releaseResourceUri(resourcePath); + return callback(err); + } +}; + +module.exports.createNewContainer = function(containerPath, containerGraph, + containerMetadata, callback) { + fs.mkdir(containerPath, mkdirCallback); + + function mkdirCallback(err) { + if (err) { + module.exports.releaseResourceUri(containerPath); + return callback(err); + } else { + var rawContainer = $rdf.serialize(undefined, containerGraph, + options.uriBase, 'text/turtle'); + metadata.writeContainerMetadata(containerPath, rawContainer, + writeContainerCallback); + } + } + + function writeContainerCallback(err) { + if (err) { + module.exports.releaseResourceUri(containerPath); + return callback(err); + } else { + metadata.writeMetadata(containerPath, containerMetadata, + writeMetadataCallback); + } + } + + function writeMetadataCallback(err) { + module.exports.releaseResourceUri(containerPath); + return callback(err); + } +}; diff --git a/fileStore.js b/fileStore.js new file mode 100644 index 000000000..b2acdc9ed --- /dev/null +++ b/fileStore.js @@ -0,0 +1,14 @@ +var path = require('path'); + +var options = require('./options.js'); +var logging = require('./logging.js'); + +module.exports.uriToFilename = function(uri) { + //if (uri.slice(0, options.pathStart.length) !== options.pathStart) { + //throw "Path '" + uri + "'not starting with base '" + options.pathStart + "'."; + //} + //var filename = options.fileBase + uri.slice(options.pathStart.length); + var filename = path.join(options.fileBase, uri); + logging.log(' -- filename ' +filename); + return filename; +}; diff --git a/handlers/delete.js b/handlers/delete.js new file mode 100644 index 000000000..0918dea38 --- /dev/null +++ b/handlers/delete.js @@ -0,0 +1,47 @@ +var fs = require('fs'); +var rimraf = require('rimraf'); + +var file = require('../fileStore.js'); +var logging = require('../logging.js'); +var metadata = require('../metadata.js'); + +module.exports.handler = function(req, res) { + logging.log('DELETE -- ' + req.path); + // res.header('MS-Author-Via' , 'SPARQL' ); + var filename = file.uriToFilename(req.path); + fs.stat(filename, function(err, stats) { + if (err) { + logging.log(" ### DELETE unlink() error: " + err); + return res.status(404).send("Can't delete file: " + err); + } else if (stats.isDirectory()) { + if (filename.charAt(filename.length - 1) !== '/') + filename += '/'; + metadata.deleteContainerMetadata(filename, containerCallback); + } else { + fs.unlink(filename, fileCallback); + } + }); + + function fileCallback(err) { + if (err) { + logging.log(" ### DELETE unlink() error: " + err); + return res.status(404).send("Can't delete file: " + err); // @@ best + } else { + //TODO remove file from container + metadata.deleteMetadata(filename, function(err) {}); + logging.log(" -- delete Ok " + req.text.length); + res.sendStatus(200); + } + } + + function containerCallback(err) { + if (err) { + logging.log("DELETE unlink() error: " + err); + return res.status(404).send("Can't delete container: " + err); + } else { + metadata.deleteMetadata(filename, function(err) {}); + logging.log(" -- delete Ok " + req.text.length); + res.sendStatus(200); + } + } +}; diff --git a/handlers/get.js b/handlers/get.js new file mode 100644 index 000000000..2705318ac --- /dev/null +++ b/handlers/get.js @@ -0,0 +1,84 @@ +var mime = require('mime'); +var fs = require('fs'); + +var header = require('../header.js'); +var metadata = require('../metadata.js'); +var options = require('../options.js'); +var logging = require('../logging.js'); +var file = require('../fileStore.js'); + +module.exports.handler = function(req, res){ + get(req, res, true); +}; + +module.exports.headHandler = function(req, res) { + logging.log("HEAD ssdss"); + get(req, res, false); +}; + +var get = function(req, res, includeBody) { + // Add request to subscription service + if (('' +req.path).slice(- options.changesSuffix.length) === + options.changesSuffix) { + logging.log("Subscribed to ", req.path); + return subscription.subscribeToChanges(req, res); + } + // Set headers + res.header('MS-Author-Via' , 'SPARQL' ); + // Note not yet in + // http://www.iana.org/assignments/link-relations/link-relations.xhtml + header.addLink(res, req.path + options.changesSuffix, 'changes' ); + if (includeBody) + logging.log('GET -- ' +req.path); + else + logging.log('HEAD -- ' +req.path); + var filename = file.uriToFilename(req.path); + fs.stat(filename, function(err, stats) { + if (err) { + logging.log(' -- read error ' + err); + res.status(404).send("Can't read file: "+ err); + } else if (stats.isDirectory()) { + if (includeBody) { + metadata.readContainerMetadata(filename, containerHandler); + } + else { + res.status(200).send(); + res.end(); + } + } else { + if (includeBody) + fs.readFile(filename, fileHandler); + else { + res.status(200).send(); + res.end(); + } + } + }); + + var fileHandler = function(err, data) { + if (err) { + logging.log(' -- read error ' + err); + res.status(404).send("Can't read file: "+ err); + } else { + logging.log(' -- read Ok ' + data.length); + ct = mime.lookup(filename); + res.set('content-type', ct); + logging.log(' -- content-type ' + ct); + res.send(data); + } + }; + + var containerHandler = function(err, rawContainer) { + if (err) { + res.status(404).send("Not a container"); + } else { + var containerGraph; + try { + // TODO: parse rawContainer to check is a valid graph + res.status(200).send(rawContainer); + } catch (parseErr) { + res.status(404).send("Not a valid container"); + } + } + }; +}; diff --git a/handlers/patch.js b/handlers/patch.js new file mode 100644 index 000000000..8b1c938dd --- /dev/null +++ b/handlers/patch.js @@ -0,0 +1,88 @@ +var mime = require('mime'); +var fs = require('fs'); +var $rdf = require('rdflib') + +var subscription = require('../subscription.js') +var options = require('../options.js') +var logging = require('../logging.js') + +module.exports.handler = function(req, res) { + logging.log('\nPOST ' +req.path); + logging.log(' text length: ' + (req.text ? req.text.length : 'undefined2')); + res.header('MS-Author-Via' , 'SPARQL' ); + var filename = uriToFilename(req.path); + patchType = req.get('content-type'); + fileType = mime.lookup(filename); + patchContentType = req.get('content-type'); + patchContentType = patchContentType.split(';')[0].trim(); // Ignore parameters + targetContentType = mime.lookup(filename); + var targetURI = options.prePathSlash + req.path; + var patchURI = targetURI ; // @@@ beware the triples from the patch ending up in the same place + logging.log("Patch Content-type " + patchContentType + " patching target " + targetContentType + " <" + targetURI + '>'); + var targetKB = $rdf.graph(); + var patchKB = $rdf.graph(); // Keep the patch in a sep KB as its URI is the same ! + + var fail = function(status, message) { + logging.log("FAIL "+status+ " " + message); + return res.status(status).send('
\n'+ message+ '\n\n'); + } + switch(patchContentType) { + case 'application/sparql-update': + try { // Must parse relative to document's base address but patch doc should get diff URI + logging.log("parsing patch ..."); + var patch = $rdf.sparqlUpdateParser(req.text, patchKB, patchURI); + } catch(e) { + return res.status(400).send("Patch format syntax error:\n" + e + '\n'); + } + logging.log("reading target file ..."); + fs.readFile(filename, 'utf8', function (err,data) { + if (err) { + return res.status(404).send("Patch: Original file read error:" + + err + '\n'); + } + logging.log("File read OK "+data.length); + try { + logging.log("parsing target file ..."); + $rdf.parse(data, targetKB, targetURI, targetContentType); + logging.log("Target parsed OK "); + + } catch(e) { + return res.status(500).send("Patch: Target " + + targetContentType + " file syntax error:" + e); + } + + var target = patchKB.sym(targetURI); + + var writeFileBack = function() { + logging.log("Accumulated namespaces:" + targetKB.namespaces); + logging.log("Writeback URI base " + targetURI); + var data = $rdf.serialize(target, targetKB, targetURI, + targetContentType); + fs.writeFile(filename, data, 'utf8', function(err){ + if (err) { + return fail(500, + "Failed to write file back after patch: " + err); + } else { + logging.log("Patch applied OK"); + res.send("Patch applied OK\n"); + return subscription.publishDelta(req, res, + patchKB, targetURI); + }; + }); // end write done + }; + targetKB.applyPatch(patch, target, function(err){ + if (err) { + return fail(409, err); // HTTP inconsistency error -- very important. + } else { + writeFileBack(); + }; + }); + + }); // end read file done + break; + + default: + return fail(400, "Sorry unknowm patch content type: " + patchContentType) + }; // switch content-type +}; // postOrPatch + diff --git a/handlers/post.js b/handlers/post.js new file mode 100644 index 000000000..8780101be --- /dev/null +++ b/handlers/post.js @@ -0,0 +1,149 @@ +var path = require('path'); +var $rdf = require('rdflib'); + +var container = require('../container.js'); +var file = require('../fileStore.js'); +var header = require('../header.js'); +var logging = require('../logging.js'); +var metadata = require('../metadata.js'); + +var ldpVocab = require('../vocab/ldp.js'); +var rdfVocab = require('../vocab/rdf.js'); + +module.exports.handler = function(req, res) { + var containerPath = file.uriToFilename(req.path); + if (metadata.isMetadataFile(containerPath)) + return res.status(404).send(); + metadata.readMetadata(containerPath, metadataCallback); + + function metadataCallback(err, rawMetadata) { + if (err) { + res.sendStatus(404); + return; + } else { + var fileMetadata = metadata.parseMetadata(rawMetadata); + if (fileMetadata.isContainer) { + // Containers are always directories so it's safe to add a final slash + // if it does not exist already + if (containerPath.charAt(containerPath.length - 1) !== '/') + containerPath += '/'; + metadata.readContainerMetadata(containerPath, containerCallback); + } else { + res.set('Allow', 'GET,HEAD,PUT,DELETE').sendStatus(405); + return; + } + } + } + + function containerCallback(err, rawContainer) { + if (err) { + res.sendStatus(404); + return; + } + var contentType = ""; + if (req.is('text/turtle')) + contentType = 'text/turtle'; + else if (req.is('text/n3')) + contentType = 'text/n3'; + else if (req.is('application/rdf+xml')) + contentType = 'application/rdf+xml'; + else { + res.sendStatus(415); + return; + } + + var slug = req.get('Slug'); + var resourcePath = container.createResourceUri(containerPath, slug); + var resourceGraph = $rdf.graph(); + var containerGraph = $rdf.graph(); + + if (resourcePath === null) { + container.releaseResourceUri(resourcePath); + logging.log("URI already exists or in use"); + res.sendStatus(400); + return; + } + + try { + var resourceURI = path.resourceURI = path.relative(options.fileBase, + resourcePath); + logging.log(options.uriBase + resourceURI); + $rdf.parse(rawContainer, containerGraph, + options.uriBase + resourceURI, 'text/turtle'); + } catch (parseErr) { + logging.log(parseErr); + logging.log("Could not parse container:\n", rawContainer); + container.releaseResourceUri(resourcePath); + res.sendStatus(500); + return; + } + + try { + $rdf.parse(req.text, resourceGraph, options.uriBase, contentType); + } catch (parseErr) { + logging.log(req.text); + container.releaseResourceUri(resourcePath); + res.sendStatus(400); + return; + } + + var resourceMetadata = header.parseMetadataFromHeader(req.get('Link')); + header.addLinks(res, resourceMetadata); + var resourceType = ""; + + if (resourceMetadata.isBasicContainer) { + resourceMetadata.isContainer = true; + resourceType = ldpVocab.BasicContainer; + resourcePath += '/'; + } else if (resourceMetadata.isDirectContainer) { + resourceMetadata.isContainer = true; + resourceType = ldpVocab.DirectContainer; + } else { + resourceMetadata.isContainer = false; + resourceType = ldpVocab.Resource; + } + + resourceMetadata.isResource = true; + //TODO figure out if how to determine if RDFSource is true or false + resourceMetadata.isSourceResource = true; + + //TODO work on input verification. For now assume input is correct + //if (!container.verify(newResource, resourceType)) { + //container.releaseResourceUri(baseUri); + //res.send(400); + //return; + //} + + if (resourceMetadata.isBasicContainer) { + container.createNewContainer(resourcePath, resourceGraph, + resourceMetadata, function(err) { + if (err) { + res.sendStatus(500); + } else { + var resourceURI = path.relative(options.fileBase, + resourcePath); + res.set('Location', options.uriBase + resourceURI); + res.sendStatus(201); + } + return; + }); + } else if (!resourceMetadata.isContainer) { + container.createNewResource(containerPath, containerGraph, resourcePath, + resourceGraph, resourceMetadata, function(err) { + if (err) { + logging.log("Error creating resource:", err); + res.sendStatus(500); + } else { + var resourceURI = path.relative(options.fileBase, + resourcePath); + res.set('Location', options.uriBase + resourceURI); + res.sendStatus(201); + } + return; + }); + } else { + res.sendStatus(400); + return; + } + } +}; diff --git a/handlers/put.js b/handlers/put.js new file mode 100644 index 000000000..66c7036d0 --- /dev/null +++ b/handlers/put.js @@ -0,0 +1,38 @@ +var mime = require('mime'); +var fs = require('fs'); +var $rdf = require('rdflib'); + +var file = require('../fileStore.js'); +var header = require('../header.js'); +var metadata = require('../metadata.js'); +var options = require('../options.js'); +var logging = require('../logging.js'); + +module.exports.handler = function(req, res){ + logging.log('PUT ' + req.path); + logging.log('text length:' + (req.text ? req.text.length : 'undefined')); + res.header('MS-Author-Via' , 'SPARQL' ); + + var fileMetadata = header.parseMetadataFromHeader(req.get('Link')); + var filename = file.uriToFilename(req.path); + var ct1 = req.get('content-type'); + var ct2 = mime.lookup(filename); + + if (ct1 && ct2 && (ct1 !== ct2)) { + res.status(415).send("Content type mismatch with path file.extenstion"); + } + if (!ct2) { + res.status(415).send("Sorry, Filename must have extension for content type"); + } + + fs.writeFile(filename, req.text, function(err) { + if (err) { + logging.log(" ### Write error: " + err); + return res.status(500).send("Can't write file: "+ err); + } else { + logging.log(" -- write Ok " + req.text.length); + metadata.writeMetadata(filename, fileMetadata, function(err) {}); + res.send(); + } + }); // file write +}; diff --git a/header.js b/header.js new file mode 100644 index 000000000..672fc81c5 --- /dev/null +++ b/header.js @@ -0,0 +1,54 @@ +var li = require('li'); + +var logging = require('./logging.js'); +var metadata = require('./metadata.js'); +var ldpVocab = require('./vocab/ldp.js'); + +module.exports.addLink = function(res, value, rel) { + var oldLink = res.get('Link'); + if (oldLink === undefined) + res.set('Link', '<' + value + '>; rel=\'' + rel + '\''); + else + res.set('Link', oldLink + ', ' + '<' + value + + '>; rel=\'' + rel + '\''); +}; + +module.exports.addLinks = function(res, fileMetadata) { + if (fileMetadata.isResource) + module.exports.addLink(res, ldpVocab.Resource, 'type'); + if (fileMetadata.isSourceResource) + module.exports.addLink(res, ldpVocab.RDFSource, 'type'); + if (fileMetadata.isContainer) + module.exports.addLink(res, ldpVocab.Container, 'type'); + if (fileMetadata.isBasicContainer) + module.exports.addLink(res, ldpVocab.BasicContainer, 'type'); + if (fileMetadata.isDirectContainer) + module.exports.addLink(res, ldpVocab.DirectContainer, 'type'); +}; + +module.exports.parseMetadataFromHeader = function(linkHeader) { + var fileMetadata = new metadata.Metadata(); + if (linkHeader === undefined) + return fileMetadata; + var links = linkHeader.split(','); + for (var linkIndex in links) { + var link = links[linkIndex]; + var parsedLinks = li.parse(link); + console.log(parsedLinks, link); + for (var rel in parsedLinks) { + if (rel === 'type') { + if (parsedLinks[rel] === ldpVocab.Resource) + fileMetadata.isResource = true; + else if (parsedLinks[rel] === ldpVocab.RDFSource) + fileMetadata.isSourceResource = true; + else if (parsedLinks[rel] === ldpVocab.Container) + fileMetadata.isContainer = true; + else if (parsedLinks[rel] === ldpVocab.BasicContainer) + fileMetadata.isBasicContainer = true; + else if (parsedLinks[rel] === ldpVocab.DirectContainer) + fileMetadata.isDirectContainer = true; + } + } + } + return fileMetadata; +}; diff --git a/logging.js b/logging.js new file mode 100644 index 000000000..bc85ce4d0 --- /dev/null +++ b/logging.js @@ -0,0 +1,6 @@ +options = require('./options.js') + +module.exports.log = function() { + if (options.verbose) console.log.apply(console, arguments); +} + diff --git a/metadata.js b/metadata.js new file mode 100644 index 000000000..220cfdac8 --- /dev/null +++ b/metadata.js @@ -0,0 +1,118 @@ +// Metadata + +var fs = require('fs'); +var path = require('path'); + +var file = require('./fileStore.js'); +var header = require('./header.js'); +var logging = require('./logging.js'); +var ldpVocab = require('./vocab/ldp.js'); + +var metadataExtension = ".metadata"; +var containerExtension = ".container"; + +module.exports.Metadata = function() { + this.filename = ""; + this.isResource = false; + this.isSourceResource = false; + this.isContainer = false; + this.isBasicContainer = false; + this.isDirectContainer = false; +}; + +module.exports.isMetadataFile = function(filename) { + if (path.extname(filename) === metadataExtension || + path.extname(filename) === containerExtension) + return true; + return false; +}; + +module.exports.hasMetadata = function(filename) { + return fs.existsSync(filename + metadataExtension); +}; + +module.exports.parseMetadata = function(rawMetadata) { + var getProperty = function(object, property) { + if (object[property] !== undefined) return object[property]; + else throw new ReferenceError('Property does not exists'); + + }; + + try { + var jsonMetadata = JSON.parse(rawMetadata); + var fileMetadata = new module.exports.Metadata(); + fileMetadata.filename = getProperty(jsonMetadata, 'filename'); + fileMetadata.isResource = getProperty(jsonMetadata, 'isResource'); + fileMetadata.isSourceResource = getProperty(jsonMetadata, + 'isSourceResource'); + fileMetadata.isContainer = getProperty(jsonMetadata, + 'isContainer'); + fileMetadata.isBasicContainer = getProperty(jsonMetadata, + 'isBasicContainer'); + fileMetadata.isDirectContainer = getProperty(jsonMetadata, + 'isDirectContainer'); + return fileMetadata; + } catch (err) { + logging.log("Could not parse metadata from source"); + return Error("Invalid metadata"); + } +}; + +module.exports.writeMetadata = function(filename, metadata, callback) { + var rawMetadata = JSON.stringify(metadata); + fs.writeFile(filename + metadataExtension, rawMetadata, { + 'encoding': 'utf8' + }, callback); +}; + +module.exports.readMetadata = function(filename, callback) { + fs.readFile(filename + metadataExtension, { + 'encoding': 'utf8' + }, callback); +}; + +module.exports.deleteMetadata = function(filename, callback) { + fs.unlink(filename + metadataExtension, callback); +}; + +module.exports.hasContainerMetadata = function(directory) { + return fs.existsSync(directory + containerExtension); +}; + +module.exports.writeContainerMetadata = function(directory, container, callback) { + fs.writeFile(directory + containerExtension, container, callback); +}; + +module.exports.readContainerMetadata = function(directory, callback) { + fs.readFile(directory + containerExtension, { + 'encoding': 'utf8' + }, callback); +}; + +module.exports.deleteContainerMetadata = function(directory, callback) { + fs.unlink(directory + containerExtension, callback); +}; + +module.exports.linksHandler = function(req, res, next) { + var filename = file.uriToFilename(req.url); + filename = path.join(filename, req.path); + if (module.exports.isMetadataFile(filename)) { + res.send(404); + return; + } + var fileMetadata = new module.exports.Metadata(); + module.exports.readMetadata(filename, function(err, rawMetadata) { + if (err) { + fileMetadata.isResource = true; + } else { + try { + fileMetadata = module.exports.parseMetadata(rawMetadata); + } catch (parseErr) { + res.send(500); + return next(parseErr); + } + } + header.addLinks(res, fileMetadata); + next(); + }); +}; diff --git a/options.js b/options.js new file mode 100644 index 000000000..5893c6aae --- /dev/null +++ b/options.js @@ -0,0 +1,41 @@ +//Server options + +var logging = require('./logging.js'); +var regexp = require('node-regexp'); + +module.exports.aclSuffix = ""; +module.exports.uriBase = ""; +module.exports.fileBase = ""; +module.exports.address = ""; +module.exports.port = 3000; +module.exports.verbose = false; +module.exports.changesSuffix = ""; +module.exports.ssl = false; +module.exports.cors = false; +module.exports.pathStart = ""; +module.exports.prePathSlash = ""; +module.exports.pathFilter = ""; + +module.exports.init = function(argv) { + this.aclSuffix = argv.aclSuffix || process.env.ACLSUFFIX || ",acl"; + this.uriBase = argv.uriBase || process.env.URIBASE || + 'http://localhost:3000' + process.cwd() + '/test/'; + this.fileBase = argv.fileBase || process.env.FILEBASE || + process.cwd() + '/test/'; + this.address = argv.a || '0.0.0.0'; + this.port = parseInt(argv.p || process.env.PORT || 3000); + this.verbose = argv.v; + this.changesSuffix = argv.changesSuffix || ',changes'; + this.ssl = argv.S; + this.cors = argv.cors; + logging.log(" uriBase: " + this.uriBase); + this.pathStart = '/' + + this.uriBase.split('//')[1].split('/').slice(1).join('/'); + this.prePathSlash = this.uriBase.split('/') + .slice(0, 3).join('/'); + logging.log("URI pathStart: " + this.pathStart); + this.pathFilter = regexp().start(this.pathStart) + .toRegExp(); + logging.log("URI path filter regexp: " + this.pathFilter); + logging.log("Verbose: " + this.verbose); +}; diff --git a/package.json b/package.json index 822f9c8fe..e87f185d3 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,38 @@ { - "name": "ldp-httpd", - "description": "Read-write linked data web server based on express and rdflib.js", - "version": "0.1.0", - "private": false, - "author": { "name": "Tim Berners-Lee", "email": "timbl@w3.org" }, - "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/linkeddata/node-ldp-httpd" - }, - "homepage": "http://github.com/linkeddata/node-ldp-httpd", - "bugs": "http://github.com/linkeddata/node-ldp-httpd/issues", - "dependencies": { - "express": "3.x", - "mime": "1.x", - "node-regexp": "", - "optimist": "0.6", - "raw-body": "1.x", - "rdflib": "git://github.com/linkeddata/rdflib.js.git", - "response-time": "2.x" - }, - "files": [ "server.js", "test", "Makefile"], - "main": "server.js", - "scripts": { - "start": "node server.js" - } + "name": "ldp-httpd", + "description": "Read-write linked data web server based on express and rdflib.js", + "version": "0.1.0", + "private": false, + "author": { + "name": "Tim Berners-Lee", + "email": "timbl@w3.org" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/linkeddata/node-ldp-httpd" + }, + "homepage": "http://github.com/linkeddata/node-ldp-httpd", + "bugs": "http://github.com/linkeddata/node-ldp-httpd/issues", + "dependencies": { + "express": "4.x", + "mime": "1.x", + "node-regexp": "", + "optimist": "0.6", + "raw-body": "1.x", + "rdflib": "0.1.0", + "response-time": "2.x", + "li": "1.x", + "node-uuid": "1.x", + "acl": "0.4.x" + }, + "files": [ + "server.js", + "test", + "Makefile" + ], + "main": "server.js", + "scripts": { + "start": "node server.js" + } } diff --git a/server.js b/server.js index 33464d0d5..976211827 100644 --- a/server.js +++ b/server.js @@ -1,50 +1,37 @@ // Read-Write Web: Linked Data Server -// - var express = require('express'); // See http://expressjs.com/guide.html var app = express(); - -var mime = require('mime'); -var fs = require('fs'); -var $rdf = require('rdflib.js') var responseTime = require('response-time'); // Add X-Response-Time headers -var path = require('path'); -var regexp = require('node-regexp'); - - - - -// Debugging: -// See http://stackoverflow.com/questions/1911015/how-to-debug-node-js-applications -// iff debug -//var agent = require('webkit-devtools-agent') -//agent.start() - -/* Install to your application, npm install webkit-devtools-agent -Include in your application, agent = require('webkit-devtools-agent') -Activate the agent: kill -SIGUSR2