Skip to content

Commit f985ca8

Browse files
authored
Merge pull request #911 from solid/feature/check-quota
Code to check quota on backend
2 parents 6b2be22 + a5a9617 commit f985ca8

File tree

13 files changed

+2031
-1772
lines changed

13 files changed

+2031
-1772
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@prefix dct: <http://purl.org/dc/terms/>.
2+
@prefix pim: <http://www.w3.org/ns/pim/space#>.
3+
@prefix solid: <http://www.w3.org/ns/solid/terms#>.
4+
@prefix unit: <http://www.w3.invalid/ns#>.
5+
6+
<>
7+
a pim:ConfigurationFile;
8+
9+
dct:description "Administrative settings for the POD that the user can only read." .
10+
11+
</>
12+
solid:storageQuota "25000000" .
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@prefix acl: <http://www.w3.org/ns/auth/acl#>.
2+
@prefix foaf: <http://xmlns.com/foaf/0.1/>.
3+
4+
<#owner>
5+
a acl:Authorization;
6+
7+
acl:agent
8+
<{{webId}}>;
9+
10+
acl:accessTo <./serverSide.ttl>;
11+
12+
acl:mode acl:Read .
13+

lib/debug.js

+1
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ exports.container = debug('solid:container')
1313
exports.accounts = debug('solid:accounts')
1414
exports.email = debug('solid:email')
1515
exports.ldp = debug('solid:ldp')
16+
exports.fs = debug('solid:fs')

lib/handlers/patch.js

+17-8
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const utils = require('../utils.js')
1010
const error = require('../http-error')
1111
const $rdf = require('rdflib')
1212
const crypto = require('crypto')
13+
const overQuota = require('../utils').overQuota
1314

1415
const DEFAULT_TARGET_TYPE = 'text/turtle'
1516

@@ -53,7 +54,7 @@ function patchHandler (req, res, next) {
5354
])
5455
// Patch the graph and write it back to the file
5556
.then(([graph, patchObject]) => applyPatch(patchObject, graph, target))
56-
.then(graph => writeGraph(graph, target))
57+
.then(graph => writeGraph(graph, target, root, ldp.serverUri))
5758
// Send the result to the client
5859
.then(result => { res.send(result) })
5960
.then(next, next)
@@ -137,19 +138,27 @@ function applyPatch (patchObject, graph, target) {
137138
}
138139

139140
// Writes the RDF graph to the given resource
140-
function writeGraph (graph, resource) {
141+
function writeGraph (graph, resource, root, serverUri) {
141142
debug('PATCH -- Writing patched file')
142143
return new Promise((resolve, reject) => {
143144
const resourceSym = graph.sym(resource.uri)
144145
const serialized = $rdf.serialize(resourceSym, graph, resource.uri, resource.contentType)
145146

146-
fs.writeFile(resource.file, serialized, {encoding: 'utf8'}, function (err) {
147-
if (err) {
148-
return reject(error(500, `Failed to write file after patch: ${err}`))
147+
// First check if we are above quota
148+
overQuota(root, serverUri).then((isOverQuota) => {
149+
if (isOverQuota) {
150+
return reject(error(413,
151+
'User has exceeded their storage quota'))
149152
}
150-
debug('PATCH -- applied successfully')
151-
resolve('Patch applied successfully.\n')
152-
})
153+
154+
fs.writeFile(resource.file, serialized, {encoding: 'utf8'}, function (err) {
155+
if (err) {
156+
return reject(error(500, `Failed to write file after patch: ${err}`))
157+
}
158+
debug('PATCH -- applied successfully')
159+
resolve('Patch applied successfully.\n')
160+
})
161+
}).catch(() => reject(error(500, 'Error finding user quota')))
153162
})
154163
}
155164

lib/ldp.js

+26-16
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const utils = require('./utils')
1010
const error = require('./http-error')
1111
const stringToStream = require('./utils').stringToStream
1212
const serialize = require('./utils').serialize
13+
const overQuota = require('./utils').overQuota
1314
const extend = require('extend')
1415
const rimraf = require('rimraf')
1516
const ldpContainer = require('./ldp-container')
@@ -266,24 +267,33 @@ class LDP {
266267
return callback(error(409,
267268
'PUT not supported on containers, use POST instead'))
268269
}
269-
// First, create the enclosing directory, if necessary
270-
const dirName = path.dirname(filePath)
271-
mkdirp(dirName, (err) => {
272-
if (err) {
273-
debug.handlers('PUT -- Error creating directory: ' + err)
274-
return callback(error(err,
275-
'Failed to create the path to the new resource'))
270+
271+
// First check if we are above quota
272+
overQuota(root, this.serverUri).then((isOverQuota) => {
273+
if (isOverQuota) {
274+
return callback(error(413,
275+
'User has exceeded their storage quota'))
276276
}
277-
// Directory created, now write the file
278-
const file = stream.pipe(fs.createWriteStream(filePath))
279-
file.on('error', function () {
280-
callback(error(500, 'Error writing data'))
281-
})
282-
file.on('finish', function () {
283-
debug.handlers('PUT -- Wrote data to: ' + filePath)
284-
callback(null)
277+
278+
// Second, create the enclosing directory, if necessary
279+
const dirName = path.dirname(filePath)
280+
mkdirp(dirName, (err) => {
281+
if (err) {
282+
debug.handlers('PUT -- Error creating directory: ' + err)
283+
return callback(error(err,
284+
'Failed to create the path to the new resource'))
285+
}
286+
// Directory created, now write the file
287+
const file = stream.pipe(fs.createWriteStream(filePath))
288+
file.on('error', function () {
289+
callback(error(500, 'Error writing data'))
290+
})
291+
file.on('finish', function () {
292+
debug.handlers('PUT -- Wrote data to: ' + filePath)
293+
callback(null)
294+
})
285295
})
286-
})
296+
}).catch(() => callback(error(500, 'Error finding user quota')))
287297
}
288298

289299
exists (hostname, path, callback) {

lib/utils.js

+63
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ module.exports.debrack = debrack
1313
module.exports.stripLineEndings = stripLineEndings
1414
module.exports.fullUrlForReq = fullUrlForReq
1515
module.exports.routeResolvedFile = routeResolvedFile
16+
module.exports.getQuota = getQuota
17+
module.exports.overQuota = overQuota
1618

1719
const fs = require('fs-extra')
1820
const path = require('path')
21+
const util = require('util')
1922
const $rdf = require('rdflib')
2023
const from = require('from2')
2124
const url = require('url')
25+
const debug = require('./debug').fs
26+
const getSize = require('get-folder-size')
27+
var ns = require('solid-namespace')($rdf)
2228

2329
/**
2430
* Returns a fully qualified URL from an Express.js Request object.
@@ -239,3 +245,60 @@ function routeResolvedFile (router, path, file, appendFileName = true) {
239245
const fullFile = require.resolve(file)
240246
router.get(fullPath, (req, res) => res.sendFile(fullFile))
241247
}
248+
249+
/**
250+
* Returns the number of bytes that the user owning the requested POD
251+
* may store or Infinity if no limit
252+
*/
253+
254+
async function getQuota (root, serverUri) {
255+
const filename = path.join(root, 'settings/serverSide.ttl')
256+
var prefs
257+
try {
258+
prefs = await _asyncReadfile(filename)
259+
} catch (error) {
260+
debug('Setting no quota. While reading serverSide.ttl, got ' + error)
261+
return Infinity
262+
}
263+
var graph = $rdf.graph()
264+
const storageUri = serverUri + '/'
265+
try {
266+
$rdf.parse(prefs, graph, storageUri, 'text/turtle')
267+
} catch (error) {
268+
throw new Error('Failed to parse serverSide.ttl, got ' + error)
269+
}
270+
return Number(graph.anyValue($rdf.sym(storageUri), ns.solid('storageQuota'))) || Infinity
271+
}
272+
273+
/**
274+
* Returns true of the user has already exceeded their quota, i.e. it
275+
* will check if new requests should be rejected, which means they
276+
* could PUT a large file and get away with it.
277+
*/
278+
279+
async function overQuota (root, serverUri) {
280+
let quota = await getQuota(root, serverUri)
281+
if (quota === Infinity) {
282+
return false
283+
}
284+
// TODO: cache this value?
285+
var size = await actualSize(root)
286+
return (size > quota)
287+
}
288+
289+
/**
290+
* Returns the number of bytes that is occupied by the actual files in
291+
* the file system. IMPORTANT NOTE: Since it traverses the directory
292+
* to find the actual file sizes, this does a costly operation, but
293+
* neglible for the small quotas we currently allow. If the quotas
294+
* grow bigger, this will significantly reduce write performance, and
295+
* so it needs to be rewritten.
296+
*/
297+
298+
function actualSize (root) {
299+
return util.promisify(getSize)(root)
300+
}
301+
302+
function _asyncReadfile (filename) {
303+
return util.promisify(fs.readFile)(filename, 'utf-8')
304+
}

0 commit comments

Comments
 (0)