Skip to content

Commit 6c95b56

Browse files
authored
Merge pull request #1116 from solid/feature/lock-patch
Lock file during PATCH
2 parents 567b042 + e9c8db3 commit 6c95b56

File tree

4 files changed

+696
-1151
lines changed

4 files changed

+696
-1151
lines changed

lib/handlers/patch.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const $rdf = require('rdflib')
1010
const crypto = require('crypto')
1111
const overQuota = require('../utils').overQuota
1212
const getContentType = require('../utils').getContentType
13+
const { lock } = require('proper-lockfile')
1314

1415
// Patch parsers by request body content type
1516
const PATCH_PARSERS = {
@@ -23,6 +24,7 @@ const DEFAULT_FOR_NEW_CONTENT_TYPE = 'text/turtle'
2324
async function patchHandler (req, res, next) {
2425
debug(`PATCH -- ${req.originalUrl}`)
2526
res.header('MS-Author-Via', 'SPARQL')
27+
let releaseLock
2628
try {
2729
// Obtain details of the target resource
2830
const ldp = req.app.locals.ldp
@@ -50,22 +52,24 @@ async function patchHandler (req, res, next) {
5052
throw error(415, `Unsupported patch content type: ${patch.contentType}`)
5153
}
5254

53-
// Parse the target graph and the patch document,
54-
// and verify permission for performing this specific patch
55-
const [graph, patchObject] = await Promise.all([
56-
readGraph(resource),
57-
parsePatch(url, patch.uri, patch.text)
58-
.then(patchObject => checkPermission(req, patchObject))
59-
])
55+
// Parse the patch document and verify permissions
56+
const patchObject = await parsePatch(url, patch.uri, patch.text)
57+
await checkPermission(req, patchObject)
6058

6159
// Patch the graph and write it back to the file
60+
releaseLock = await lock(path, { retries: 10, realpath: false })
61+
const graph = await readGraph(resource)
6262
await applyPatch(patchObject, graph, url)
6363
const result = await writeGraph(graph, resource, ldp.resourceMapper.rootPath, ldp.serverUri)
6464

6565
// Send the result to the client
6666
res.send(result)
6767
} catch (err) {
6868
return next(err)
69+
} finally {
70+
if (releaseLock) {
71+
await releaseLock()
72+
}
6973
}
7074
return next()
7175
}

lib/ldp.js

Lines changed: 81 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const path = require('path')
1+
const { join, dirname } = require('path')
22
const url = require('url')
33
const fs = require('fs')
44
const $rdf = require('rdflib')
@@ -17,6 +17,7 @@ const parse = require('./utils').parse
1717
const fetch = require('node-fetch')
1818
const { promisify } = require('util')
1919
const URI = require('urijs')
20+
const { lock } = require('proper-lockfile')
2021

2122
const RDF_MIME_TYPES = new Set([
2223
'text/turtle', // .ttl
@@ -75,20 +76,18 @@ class LDP {
7576
})
7677
}
7778

78-
createReadStream (filename, start, end) {
79-
if (start && end) {
80-
return fs.createReadStream(filename, {'start': start, 'end': end})
81-
} else {
82-
return fs.createReadStream(filename)
83-
}
84-
}
85-
8679
async readResource (url) {
80+
let releaseLock
8781
try {
8882
const { path } = await this.resourceMapper.mapUrlToFile({ url })
89-
return await promisify(fs.readFile)(path, {'encoding': 'utf8'})
83+
releaseLock = await lock(path, { retries: 10 })
84+
return await promisify(fs.readFile)(path, {encoding: 'utf8'})
9085
} catch (err) {
9186
throw error(err.status, err.message)
87+
} finally {
88+
if (releaseLock) {
89+
await releaseLock()
90+
}
9291
}
9392
}
9493

@@ -99,7 +98,7 @@ class LDP {
9998
return this.readResource(url + this.suffixMeta)
10099
}
101100

102-
async listContainer (filename, reqUri, containerData, hostname) {
101+
async listContainer (container, reqUri, containerData, hostname) {
103102
const resourceGraph = $rdf.graph()
104103

105104
try {
@@ -111,14 +110,14 @@ class LDP {
111110

112111
try {
113112
// add container stats
114-
await ldpContainer.addContainerStats(this, reqUri, filename, resourceGraph)
113+
await ldpContainer.addContainerStats(this, reqUri, container, resourceGraph)
115114
// read directory
116-
const files = await ldpContainer.readdir(filename)
115+
const files = await ldpContainer.readdir(container)
117116
// iterate through all the files
118117
await Promise.all(files.map(async file => {
119118
const { url: fileUri } = await this.resourceMapper.mapFileToUrl(
120-
{ path: path.join(filename, file), hostname })
121-
return await ldpContainer.addFile(this, resourceGraph, reqUri, fileUri, filename, file)
119+
{ path: join(container, file), hostname })
120+
return await ldpContainer.addFile(this, resourceGraph, reqUri, fileUri, container, file)
122121
}))
123122
} catch (err) {
124123
throw error(500, "Can't list container")
@@ -154,7 +153,7 @@ class LDP {
154153
let originalPath = resourcePath
155154
if (container) {
156155
// Create directory by an LDP PUT to the container's .meta resource
157-
resourcePath = path.join(originalPath, ldp.suffixMeta)
156+
resourcePath = join(originalPath, ldp.suffixMeta)
158157
if (originalPath && !originalPath.endsWith('/')) {
159158
originalPath += '/'
160159
}
@@ -226,8 +225,8 @@ class LDP {
226225
}
227226

228227
// Second, create the enclosing directory, if necessary
229-
const { path: filePath } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true })
230-
const dirName = path.dirname(filePath)
228+
const { path } = await this.resourceMapper.mapUrlToFile({ url, contentType, createIfNotExists: true })
229+
const dirName = dirname(path)
231230
try {
232231
await promisify(mkdirp)(dirName)
233232
} catch (err) {
@@ -237,16 +236,24 @@ class LDP {
237236
}
238237

239238
// Directory created, now write the file
240-
return await new Promise((resolve, reject) => {
241-
const file = stream.pipe(fs.createWriteStream(filePath))
242-
file.on('error', function () {
243-
reject(error(500, 'Error writing data'))
244-
})
245-
file.on('finish', function () {
246-
debug.handlers('PUT -- Wrote data to: ' + filePath)
247-
resolve()
239+
let releaseLock
240+
try {
241+
releaseLock = await lock(path, { retries: 10, realpath: false })
242+
return await new Promise((resolve, reject) => {
243+
const file = stream.pipe(fs.createWriteStream(path))
244+
file.on('error', function () {
245+
reject(error(500, 'Error writing data'))
246+
})
247+
file.on('finish', function () {
248+
debug.handlers('PUT -- Wrote data to: ' + path)
249+
resolve()
250+
})
248251
})
249-
})
252+
} finally {
253+
if (releaseLock) {
254+
await releaseLock()
255+
}
256+
}
250257
}
251258

252259
async exists (hostname, path, searchIndex = true) {
@@ -320,82 +327,86 @@ class LDP {
320327
}
321328

322329
async get (options, searchIndex = true) {
323-
let filename, contentType, stats
330+
let path, contentType, stats
324331
try {
325-
({ path: filename, contentType } = await this.resourceMapper.mapUrlToFile({ url: options, searchIndex }))
326-
stats = await this.stat(filename)
332+
({ path, contentType } = await this.resourceMapper.mapUrlToFile({ url: options, searchIndex }))
333+
stats = await this.stat(path)
327334
} catch (err) {
328335
throw error(404, 'Can\'t find file requested: ' + options)
329336
}
330337

331338
// Just return, since resource exists
332339
if (!options.includeBody) {
333-
return { 'stream': stats, 'contentType': contentType, 'container': stats.isDirectory() }
340+
return { stream: stats, contentType, container: stats.isDirectory() }
334341
}
335342

336343
// Found a container
337344
if (stats.isDirectory()) {
338345
const { url: absContainerUri } = await this.resourceMapper
339-
.mapFileToUrl({ path: filename, hostname: options.hostname })
346+
.mapFileToUrl({ path, hostname: options.hostname })
340347
const metaFile = await this.readContainerMeta(absContainerUri)
341348
.catch(() => '') // Default to an empty meta file if it is missing
342349
let data
343350
try {
344-
data = await this.listContainer(filename, absContainerUri, metaFile, options.hostname)
351+
data = await this.listContainer(path, absContainerUri, metaFile, options.hostname)
345352
} catch (err) {
346353
debug.handlers('GET container -- Read error:' + err.message)
347354
throw err
348355
}
349356
const stream = stringToStream(data)
350357
// TODO 'text/turtle' is fixed, should be contentType instead
351358
// This forces one more translation turtle -> desired
352-
return { 'stream': stream, 'contentType': 'text/turtle', 'container': true }
359+
return { stream, contentType: 'text/turtle', container: true }
353360
} else {
354-
let stream
355-
let chunksize
356-
let contentRange
361+
let chunksize, contentRange, start, end
357362
if (options.range) {
358-
const total = fs.statSync(filename).size
363+
const total = fs.statSync(path).size
359364
const parts = options.range.replace(/bytes=/, '').split('-')
360365
const partialstart = parts[0]
361366
const partialend = parts[1]
362-
const start = parseInt(partialstart, 10)
363-
const end = partialend ? parseInt(partialend, 10) : total - 1
367+
start = parseInt(partialstart, 10)
368+
end = partialend ? parseInt(partialend, 10) : total - 1
364369
chunksize = (end - start) + 1
365370
contentRange = 'bytes ' + start + '-' + end + '/' + total
366-
stream = this.createReadStream(filename, start, end)
367-
} else {
368-
stream = this.createReadStream(filename)
369371
}
370-
return new Promise((resolve, reject) => {
371-
stream
372-
.on('error', function (err) {
373-
debug.handlers(`GET -- error reading ${filename}: ${err.message}`)
374-
return reject(error(err, "Can't read file " + err))
375-
})
376-
.on('open', function () {
377-
debug.handlers(`GET -- Reading ${filename}`)
378-
return resolve({ stream, contentType, container: false, contentRange, chunksize })
379-
})
380-
})
372+
let releaseLock
373+
try {
374+
releaseLock = await lock(path, { retries: 10 })
375+
return await new Promise((resolve, reject) => {
376+
const stream = fs.createReadStream(path, start && end && {start, end})
377+
stream
378+
.on('error', function (err) {
379+
debug.handlers(`GET -- error reading ${path}: ${err.message}`)
380+
return reject(error(err, "Can't read file " + err))
381+
})
382+
.on('open', function () {
383+
debug.handlers(`GET -- Reading ${path}`)
384+
return resolve({ stream, contentType, container: false, contentRange, chunksize })
385+
})
386+
})
387+
} finally {
388+
if (releaseLock) {
389+
await releaseLock()
390+
}
391+
}
381392
}
382393
}
383394

384395
async delete (url) {
385396
// First check if the path points to a valid file
386-
let filePath, stats
397+
let path, stats
387398
try {
388-
({ path: filePath } = await this.resourceMapper.mapUrlToFile({ url }))
389-
stats = await this.stat(filePath)
399+
({ path } = await this.resourceMapper.mapUrlToFile({ url }))
400+
stats = await this.stat(path)
390401
} catch (err) {
391402
throw error(404, "Can't find " + err)
392403
}
393404

394405
// If so, delete the directory or file
395406
if (stats.isDirectory()) {
396-
return this.deleteContainer(filePath)
407+
return this.deleteContainer(path)
397408
} else {
398-
return this.deleteResource(filePath)
409+
return this.deleteResource(path)
399410
}
400411
}
401412

@@ -425,28 +436,34 @@ class LDP {
425436
}
426437
}
427438

428-
async deleteResource (filename) {
439+
async deleteResource (path) {
440+
let releaseLock
429441
try {
430-
return promisify(fs.unlink)(filename)
442+
releaseLock = await lock(path, { retries: 10 })
443+
return await promisify(fs.unlink)(path)
431444
} catch (err) {
432445
debug.container('DELETE -- unlink() error: ' + err)
433446
throw error(err, 'Failed to delete resource')
447+
} finally {
448+
if (releaseLock) {
449+
await releaseLock()
450+
}
434451
}
435452
}
436453

437454
getAvailablePath (host, containerURI, { slug = uuid.v1(), extension }) {
438-
const filename = slug + extension
455+
const path = slug + extension
439456
function ensureNotExists (self, newPath) {
440457
// Verify whether the new path already exists
441458
return self.exists(host, newPath).then(
442459
// If it does, generate another one
443460
() => ensureNotExists(self, URI.joinPaths(containerURI,
444-
`${uuid.v1().split('-')[0]}-${filename}`).toString()),
461+
`${uuid.v1().split('-')[0]}-${path}`).toString()),
445462
// If not, we found an appropriate path
446463
() => newPath
447464
)
448465
}
449-
return ensureNotExists(this, URI.joinPaths(containerURI, filename).toString())
466+
return ensureNotExists(this, URI.joinPaths(containerURI, path).toString())
450467
}
451468

452469
getTrustedOrigins (req) {

0 commit comments

Comments
 (0)