UNPKG

restore

Version:
396 lines (334 loc) 12.6 kB
'use strict'; var async = require('async'), core = require('./core'), fs = require('fs'), lockfile = require('lockfile'), mkdirp = require('mkdirp'), path = require('path'), rename = require('./rename'), util = require('util'); var FileTree = function(options) { this._dir = path.resolve(options.path); this._renameLegacyFiles(); }; FileTree.prototype._lock = function(username, callback) { var lockPath = path.join(this._dir, username.substr(0,2), username, '.lock'), self = this; mkdirp(path.dirname(lockPath), function() { lockfile.lock(lockPath, {wait: 10000}, function(error) { if (error) return lockfile.unlock(lockPath, function() { self._lock(username, callback) }); callback(function() { lockfile.unlockSync(lockPath) }); }); }); }; FileTree.prototype.authPath = function(username) { return path.join(username.substr(0,2), username, 'auth.json'); }; FileTree.prototype.dataPath = function(username, pathname) { var query = core.parsePath(pathname).slice(1), filename = query.pop() || '', dir = query.map(function(q) { return q.replace(/\/$/, '~') }).join('/'); return path.join(username.substr(0,2), username, 'storage', dir, filename); }; FileTree.prototype.metaPath = function(username, pathname) { var query = core.parsePath(pathname).slice(1), filename = query.pop() || '', dir = query.map(function(q) { return q.replace(/\/$/, '~') }).join('/'); return path.join(username.substr(0,2), username, 'storage', dir, '.~' + filename); }; FileTree.prototype.dirname = function(username, pathname) { return path.dirname(path.join(this._dir, this.dataPath(username, pathname + '_'))); }; FileTree.prototype.childPaths = function(username, path, callback) { fs.readdir(this.dirname(username, path), function(error, entries) { callback(error ? [] : entries.sort()); }); }; FileTree.prototype.touch = function(dirname, modified, callback) { fs.stat(dirname, function(error, stat) { if (error) return callback(error); fs.utimes(dirname, stat.atime.getTime() / 1000, modified / 1000, callback); }); }; FileTree.prototype.createUser = function(params, callback) { var self = this, errors = core.validateUser(params); if (errors.length > 0) return callback(errors[0]); var userPath = this.authPath(params.username); this.writeFile(userPath, function(error, json, write) { if (json) return write(null, function() { callback(new Error('The username is already taken')); }); core.hashPassword(params.password, null, function(error, hash) { var data = {email: params.email, password: hash}; write(JSON.stringify(data, true, 2), callback); }); }); }; FileTree.prototype.authenticate = function(params, callback) { var username = params.username || ''; this.readFile(this.authPath(username), function(error, json) { if (error) return callback(new Error('Username not found')); var user = JSON.parse(json), key = user.password.key; core.hashPassword(params.password, user.password, function(error, hash) { if (hash.key === key) callback(null); else callback(new Error('Incorrect password')); }); }); }; FileTree.prototype.authorize = function(clientId, username, permissions, callback) { this.writeFile(this.authPath(username), function(error, json, write) { if (error) return callback(error); var user = JSON.parse(json), token = core.generateToken(), session, category; user.sessions = user.sessions || {}; session = user.sessions[token] = {clientId: clientId, permissions: {}}; for (var scope in permissions) { category = scope.replace(/^\/?/, '/').replace(/\/?$/, '/'); session.permissions[category] = {}; for (var i = 0, n = permissions[scope].length; i < n; i++) { session.permissions[category][permissions[scope][i]] = true; } } write(JSON.stringify(user, true, 2), function(error) { if (error) callback(error); else callback(null, token); }); }); }; FileTree.prototype.revokeAccess = function(username, token, callback) { callback = callback || function() {}; this.writeFile(this.authPath(username), function(error, json, write) { if (error) return callback(error); var user = JSON.parse(json); if (user.sessions) delete user.sessions[token]; write(JSON.stringify(user, true, 2), callback); }); }; FileTree.prototype.permissions = function(username, token, callback) { this.readFile(this.authPath(username), function(error, json) { if (error) return callback(null, {}); var data = JSON.parse(json).sessions; if (!data || !data[token]) return callback(null, {}); var permissions = data[token].permissions, output = {}; for (var category in permissions) output[category] = Object.keys(permissions[category]).sort(); return callback(null, output); }); }; FileTree.prototype.error = function(message, status) { var error = new Error(message); error.status = status; return error; }; FileTree.prototype._versionMatch = function(version, modified) { if (!version || !modified) return false; return version === modified; }; FileTree.prototype.get = function(username, path, version, callback) { var self = this, isdir = /\/$/.test(path), dataPath = this.dataPath(username, path), metaPath = this.metaPath(username, path); this._lock(username, function(release) { if (isdir) { fs.stat(self.dirname(username, path), function(error, stat) { var mtime = stat && new Date(stat.mtime.getTime()).getTime(); self.childPaths(username, path, function(entries) { if (entries.length === 0) { release(); return callback(null, null); } entries = entries.filter(function(e) { return !/^\.~/.test(e) }); async.map(entries, function(entry, callback) { self._getListing(username, path, entry, callback); }, function(error, listing) { release(); callback(null, {children: listing, modified: mtime}, self._versionMatch(version, mtime)); }); }); }); } else { self.readFile(dataPath, function(error, blob, modified) { self.readFile(metaPath, function(error, json, modified) { if (error) { release(); return callback(null, null); } var record = JSON.parse(json); record.modified = modified; record.value = blob; release(); callback(null, record, self._versionMatch(version, modified)); }); }); } }); }; FileTree.prototype._getListing = function(username, pathname, entry, callback) { var fullPath = path.join(this.dirname(username, pathname), entry); fs.stat(fullPath, function(error, stat) { callback(error, { name: entry.replace(/~$/, '/'), modified: new Date(stat.mtime.getTime()).getTime() }); }); }; FileTree.prototype.put = function(username, pathname, type, value, version, callback) { var self = this, query = core.parsePath(pathname), filename = query.pop(); this._lock(username, function(release) { var dataPath = path.join(self._dir, self.dataPath(username, pathname)), metaPath = path.join(self._dir, self.metaPath(username, pathname)); self.getCurrentState(dataPath, version, function(error, current) { if (error || !current) { release(); return callback(error, null, null, true); } async.waterfall([ function(next) { self.writeBlob(metaPath, JSON.stringify({length: value.length, type: type}, true, 2), next); }, function(exists, modified, next) { self.writeBlob(dataPath, value, next); }, function(exists, modified, next) { async.forEach(core.indexed(query), function(entry, done) { var q = entry.value, i = entry.index; self.touch(self.dirname(username, query.slice(0, i+1).join('')), modified, done); }, function() { next(null, exists, modified); }); } ], function(error, exists, modified) { release(); callback(error, !exists, modified); }); }); }); }; FileTree.prototype.delete = function(username, path, version, callback) { var self = this; this._lock(username, function(release) { self._delete(username, path, version, function(exists, modified, conflict) { if (!exists || conflict) { release(); return callback(null, exists, null, conflict); } self._removeParents(username, path, function() { release(); callback(null, true, modified); }); }); }); }; FileTree.prototype._delete = function(username, pathname, version, callback) { var dataPath = path.join(this._dir, this.dataPath(username, pathname)), metaPath = path.join(this._dir, this.metaPath(username, pathname)); this.getCurrentState(dataPath, version, function(error, current, modified) { if (error || !current) return callback(false, null, !current); fs.unlink(dataPath, function(error) { fs.unlink(metaPath, function(error) { callback(!error, modified); }); }); }); }; FileTree.prototype._removeParents = function(username, pathname, callback) { var self = this, query = core.parsePath(pathname), filename = query.pop(), parents = core.parents(pathname); async.forEachSeries(parents, function(parent, done) { var dirname = self.dirname(username, parent); self.childPaths(username, parent, function(entries) { if (entries.length === 0) { fs.rmdir(dirname, done); } else { var modified = new Date(); self.touch(dirname, modified.getTime(), done); } }); }, callback); }; FileTree.prototype.readFile = function(filename, callback) { var fullPath = path.join(this._dir, filename); fs.readFile(fullPath, function(error, content) { fs.stat(fullPath, function(error, stat) { var mtime = stat && new Date(stat.mtime.getTime()).getTime(); callback(error, error ? null : content, error ? null : mtime); }); }); }; FileTree.prototype.writeFile = function(filename, writer) { var fullPath = path.join(this._dir, filename), self = this; fs.stat(fullPath, function(error, stat) { fs.readFile(fullPath, function(error, content) { writer(error, error ? null : content.toString(), function(newContent, callback) { if (newContent === null) return callback(null, !!stat); self.writeBlob(fullPath, newContent, function(error) { callback(error, !!stat); }); }); }); }); }; FileTree.prototype.getCurrentState = function(fullPath, version, callback) { fs.stat(fullPath, function(error, stat) { var mtime = stat && new Date(stat.mtime.getTime()).getTime(); if (!version) return callback(null, true, mtime); if (version === '*') return callback(null, !mtime); if (error) return callback(null, false); callback(null, mtime === version, mtime); }); }; FileTree.prototype.writeBlob = function(fullPath, newContent, callback) { var tmpPath = fullPath + '.tmp'; mkdirp(path.dirname(fullPath), function(error) { fs.writeFile(tmpPath, newContent, function(error) { fs.stat(fullPath, function(error, exists) { fs.rename(tmpPath, fullPath, function(error) { fs.stat(fullPath, function(e, stat) { callback(error, !!exists, stat && new Date(stat.mtime.getTime()).getTime()); }); }); }); }); }); }; var REWRITE_PATTERNS = [ [/^\.([^~]*)\.(json|meta)$/, '.~$1'], [/^([^~]*)\.d$/, '$1~' ], [/^([^~]*)\.blob$/, '$1' ] ]; FileTree.prototype._renameLegacyFiles = function() { var self = this; fs.readdir(self._dir, function(error, entries) { if (error) return; entries.forEach(function(entry) { fs.readdir(path.join(self._dir, entry), function(error, users) { if (error) return; users.forEach(function(username) { self._lock(username, function(release) { var pathname = path.join(self._dir, entry, username, 'storage'); rename(pathname, REWRITE_PATTERNS, release); }); }); }); }); }); }; module.exports = FileTree;