restore
Version:
Simple remoteStorage server
396 lines (334 loc) • 12.6 kB
JavaScript
;
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;