Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 55 additions & 83 deletions lib/handlers/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ const DEFAULT_FOR_NEW_CONTENT_TYPE = 'text/turtle'

// Handles a PATCH request
async function patchHandler (req, res, next) {
debug('@@ Patch ' + req.originalUrl)
debug(`PATCH -- ${req.originalUrl}`)
res.header('MS-Author-Via', 'SPARQL')
try {
// Obtain details of the target resource
const ldp = req.app.locals.ldp
const serverUri = ldp.serverUri // @@ ??? timbl
debug('@@ serverUri' + serverUri)
const graph = $rdf.graph()
let path, contentType
try {
// First check if the file already exists
Expand All @@ -50,20 +54,61 @@ async function patchHandler (req, res, next) {
throw 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))
])
const patchObject = await parsePatch(url, patch.uri, patch.text)
try {
checkPermission(req, patchObject)
} catch (err) {
return res.status(403).send('Unauthorized patch')
}

const root = ldp.resourceMapper.getBasePath(req.hostname)

const isOverQuota = await overQuota(root, serverUri)
if (isOverQuota) {
return res.status(413).send('Patch: User storage quote exceeded')
}
// ###########################################################
/* THIS SECTION MUST NOT GIVE UP CONTROL
*/
// Patch the graph and write it back to the file
await applyPatch(patchObject, graph, url)
const result = await writeGraph(graph, resource, ldp.resourceMapper.rootPath, ldp.serverUri)
debug('Patch: Sync block starts ####################')
var fileContents

try {
fileContents = fs.readFileSync(resource.path, { encoding: 'utf8' })
} catch (err) {
if (err.code === 'ENOENT') { // Solid you CAN patch an non-existent file into existence
fileContents = ''
debug('Patch: Patching non-existent file into existence')
// Fail on all other errors
} else {
debug('Patch: Target file read error! ' + err)
return res.status(500).send('Patch: Original file read error ' + err)
}
}
debug('PATCH -- Read target file OK (%d bytes)', fileContents.length)

graph.applyPatch(patchObject, graph.sym(resource.url), (err) => {
if (err) {
const message = err.message || err // returns string at the moment
debug(`PATCH -- FAILED. Returning 409. Message: '${message}'`)
return res.status(409).send('Conflict: Patch application failed for some reason')
}
const resourceSym = graph.sym(resource.url)
const serialized = $rdf.serialize(resourceSym, graph, resource.url, resource.contentType)
debug('PATCH: Serialized bytes ' + serialized.length)
try {
fs.writeFileSync(resource.path, serialized, { encoding: 'utf8' })
} catch (err) {
debug('PATCH Sync block end in error:' + err)
return res.status(500).send(`Failed to write file after patch: ${err}`)
}
debug('PATCH -- applied successfully ##############')
res.status(200).send('Patch applied successfully') // String tested in test
})
// ########################################################### SYNC BLOCK END

// Send the result to the client
res.send(result)
} catch (err) {
return next(err)
}
Expand All @@ -77,39 +122,6 @@ function handler (req, res, next) {

const readEntity = bodyParser.text({ type: () => true })

// Reads the RDF graph in the given resource
function readGraph (resource) {
// Read the resource's file
return new Promise((resolve, reject) =>
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)
if (err.code === 'ENOENT') {
fileContents = ''
// Fail on all other errors
} else {
return reject(error(500, `Original file read error: ${err}`))
}
}
debug('PATCH -- Read target file (%d bytes)', fileContents.length)
resolve(fileContents)
})
)
// Parse the resource's file contents
.then((fileContents) => {
const graph = $rdf.graph()
debug('PATCH -- Reading %s with content type %s', resource.url, resource.contentType)
try {
$rdf.parse(fileContents, graph, resource.url, resource.contentType)
} catch (err) {
throw error(500, `Patch: Target ${resource.contentType} file syntax error: ${err}`)
}
debug('PATCH -- Parsed target file')
return graph
})
}

// Verifies whether the user is allowed to perform the patch on the target
async function checkPermission (request, patchObject) {
// If no ACL object was passed down, assume permissions are okay.
Expand Down Expand Up @@ -143,46 +155,6 @@ async function checkPermission (request, patchObject) {
return Promise.resolve(patchObject)
}

// Applies the patch to the RDF graph
function applyPatch (patchObject, graph, url) {
debug('PATCH -- Applying patch')
return new Promise((resolve, reject) =>
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}'`)
return reject(error(409, `The patch could not be applied. ${message}`))
}
resolve(graph)
})
)
}

// Writes the RDF graph to the given resource
function writeGraph (graph, resource, root, serverUri) {
debug('PATCH -- Writing patched file')
return new Promise((resolve, reject) => {
const resourceSym = graph.sym(resource.url)
const serialized = $rdf.serialize(resourceSym, graph, resource.url, resource.contentType)

// First check if we are above quota
overQuota(root, serverUri).then((isOverQuota) => {
if (isOverQuota) {
return reject(error(413,
'User has exceeded their storage quota'))
}

fs.writeFile(resource.path, serialized, { encoding: 'utf8' }, function (err) {
if (err) {
return reject(error(500, `Failed to write file after patch: ${err}`))
}
debug('PATCH -- applied successfully')
resolve('Patch applied successfully.\n')
})
}).catch(() => reject(error(500, 'Error finding user quota')))
})
}

// Creates a hash of the given text
function hash (text) {
return crypto.createHash('md5').update(text).digest('hex')
Expand Down
Loading