level-filesystem
Version:
Full implementation of the fs module on top of leveldb
598 lines (479 loc) • 14.4 kB
JavaScript
var fwd = require('fwd-stream');
var sublevel = require('level-sublevel');
var blobs = require('level-blobs');
var peek = require('level-peek');
var once = require('once');
var octal = require('octal')
var errno = require('./errno');
var paths = require('./paths');
var watchers = require('./watchers');
var nextTick = function(cb, err, val) {
process.nextTick(function() {
cb(err, val);
});
};
var noop = function() {};
module.exports = function(db, opts) {
var fs = {};
db = sublevel(db);
var bl = blobs(db.sublevel('blobs'), opts);
var ps = paths(db.sublevel('stats'));
var links = db.sublevel('links');
var listeners = watchers();
var fds = [];
var now = Date.now();
var inc = function() {
return ++now;
};
fs.mkdir = function(key, mode, cb) {
if (typeof mode === 'function') return fs.mkdir(key, null, mode);
if (!mode) mode = octal(777);
if (!cb) cb = noop;
ps.follow(key, function(err, stat, key) {
if (err && err.code !== 'ENOENT') return cb(err);
if (stat) return cb(errno.EEXIST(key));
ps.put(key, {
type:'directory',
mode: mode,
size: 4096
}, listeners.cb(key, cb));
});
};
fs.rmdir = function(key, cb) {
if (!cb) cb = noop;
ps.follow(key, function(err, stat, key) {
if (err) return cb(err);
fs.readdir(key, function(err, files) {
if (err) return cb(err);
if (files.length) return cb(errno.ENOTEMPTY(key));
ps.del(key, listeners.cb(key, cb));
});
});
};
fs.readdir = function(key, cb) {
ps.follow(key, function(err, stat, key) {
if (err) return cb(err);
if (!stat) return cb(errno.ENOENT(key));
if (!stat.isDirectory()) return cb(errno.ENOTDIR(key));
ps.list(key, cb);
});
};
var stat = function(key, lookup, cb) {
lookup(key, function(err, stat, key) {
if (err) return cb(err);
if (!stat.isFile()) return cb(null, stat);
var blob = stat && stat.blob || key;
bl.size(blob, function(err, size) {
if (err) return cb(err);
stat.size = size;
cb(null, stat);
});
});
};
fs.stat = function(key, cb) {
stat(key, ps.follow, cb);
};
fs.lstat = function(key, cb) {
stat(key, ps.get, cb);
};
fs.exists = function(key, cb) {
ps.follow(key, function(err) {
cb(!err);
});
};
var chmod = function(key, lookup, mode, cb) {
if (!cb) cb = noop;
lookup(key, function(err, stat, key) {
if (err) return cb(err);
ps.update(key, {mode:mode}, listeners.cb(key, cb));
});
};
fs.chmod = function(key, mode, cb) {
chmod(key, ps.follow, mode, cb);
};
fs.lchmod = function(key, mode, cb) {
chmod(key, ps.get, mode, cb);
};
var chown = function(key, lookup, uid, gid, cb) {
if (!cb) cb = noop;
lookup(key, function(err, stat, key) {
if (err) return cb(err);
ps.update(key, {uid:uid, gid:gid}, listeners.cb(key, cb));
});
};
fs.chown = function(key, uid, gid, cb) {
chown(key, ps.follow, uid, gid, cb);
};
fs.lchown = function(key, uid, gid, cb) {
chown(key, ps.get, uid, gid, cb);
};
fs.utimes = function(key, atime, mtime, cb) {
if (!cb) cb = noop;
ps.follow(key, function(err, stat, key) {
if (err) return cb(err);
var upd = {};
if (atime) upd.atime = atime;
if (mtime) upd.mtime = mtime;
ps.update(key, upd, listeners.cb(key, cb));
});
};
fs.rename = function(from, to, cb) {
if (!cb) cb = noop;
ps.follow(from, function(err, statFrom, from) {
if (err) return cb(err);
var rename = function() {
cb = listeners.cb(to, listeners.cb(from, cb));
ps.put(to, statFrom, function(err) {
if (err) return cb(err);
ps.del(from, cb);
});
};
ps.follow(to, function(err, statTo, to) {
if (err && err.code !== 'ENOENT') return cb(err);
if (!statTo) return rename();
if (statFrom.isDirectory() !== statTo.isDirectory()) return cb(errno.EISDIR(from));
if (statTo.isDirectory()) {
fs.readdir(to, function(err, list) {
if (err) return cb(err);
if (list.length) return cb(errno.ENOTEMPTY(from));
rename();
});
return;
}
rename();
});
});
};
fs.realpath = function(key, cache, cb) {
if (typeof cache === 'function') return fs.realpath(key, null, cache);
ps.follow(key, function(err, stat, key) {
if (err) return cb(err);
cb(null, key);
});
};
fs.writeFile = function(key, data, opts, cb) {
if (typeof opts === 'function') return fs.writeFile(key, data, null, opts);
if (typeof opts === 'string') opts = {encoding:opts};
if (!opts) opts = {};
if (!cb) cb = noop;
if (!Buffer.isBuffer(data)) data = new Buffer(data, opts.encoding || 'utf-8');
var flags = opts.flags || 'w';
opts.append = flags[0] !== 'w';
ps.follow(key, function(err, stat, key) {
if (err && err.code !== 'ENOENT') return cb(err);
if (stat && stat.isDirectory()) return cb(errno.EISDIR(key));
if (stat && flags[1] === 'x') return cb(errno.EEXIST(key));
var blob = stat && stat.blob || key;
ps.writable(key, function(err) {
if (err) return cb(err);
bl.write(blob, data, opts, function(err) {
if (err) return cb(err);
ps.put(key, {
ctime: stat && stat.ctime,
mtime: new Date(),
mode: opts.mode || octal(666),
type:'file'
}, listeners.cb(key, cb));
});
});
});
};
fs.appendFile = function(key, data, opts, cb) {
if (typeof opts === 'function') return fs.appendFile(key, data, null, opts);
if (typeof opts === 'string') opts = {encoding:opts};
if (!opts) opts = {};
opts.flags = 'a';
fs.writeFile(key, data, opts, cb);
};
fs.unlink = function(key, cb) {
if (!cb) cb = noop;
ps.get(key, function(err, stat, key) {
if (err) return cb(err);
if (stat.isDirectory()) return cb(errno.EISDIR(key));
var clean = function(target) {
peek(links, {start:target+'\xff', end:target+'\xff\xff'}, function(err) {
if (err) return bl.remove(target, cb); // no more links
cb();
});
};
var onlink = function() {
var target = stat.link.slice(0, stat.link.indexOf('\xff'));
links.del(stat.link, function(err) {
if (err) return cb(err);
clean(target);
});
};
ps.del(key, listeners.cb(key, function(err) {
if (err) return cb(err);
if (stat.link) return onlink();
links.del(key+'\xff', function(err) {
if (err) return cb(err);
clean(key);
});
}));
});
};
fs.readFile = function(key, opts, cb) {
if (typeof opts === 'function') return fs.readFile(key, null, opts);
if (typeof opts === 'string') opts = {encoding:opts};
if (!opts) opts = {};
var encoding = opts.encoding || 'binary';
var flag = opts.flag || 'r';
ps.follow(key, function(err, stat, key) {
if (err) return cb(err);
if (stat.isDirectory()) return cb(errno.EISDIR(key));
var blob = stat && stat.blob || key;
bl.read(blob, function(err, data) {
if (err) return cb(err);
cb(null, opts.encoding ? data.toString(opts.encoding) : data);
});
});
};
fs.createReadStream = function(key, opts) {
if (!opts) opts = {};
var closed = false;
var rs = fwd.readable(function(cb) {
ps.follow(key, function(err, stat, key) {
if (err) return cb(err);
if (stat.isDirectory()) return cb(errno.EISDIR(key));
var blob = stat && stat.blob || key;
var r = bl.createReadStream(blob, opts);
rs.emit('open');
r.on('end', function() {
process.nextTick(function() {
if (!closed) rs.emit('close');
});
});
cb(null, r);
});
});
rs.on('close', function() {
closed = true;
});
return rs;
};
fs.createWriteStream = function(key, opts) {
if (!opts) opts = {};
var flags = opts.flags || 'w';
var closed = false;
var mode = opts.mode || octal(666);
opts.append = flags[0] === 'a';
var ws = fwd.writable(function(cb) {
ps.follow(key, function(err, stat, key) {
if (err && err.code !== 'ENOENT') return cb(err);
if (stat && stat.isDirectory()) return cb(errno.EISDIR(key));
if (stat && flags[1] === 'x') return cb(errno.EEXIST(key));
var blob = stat && stat.blob || key;
ps.writable(blob, function(err) {
if (err) return cb(err);
var ctime = stat ? stat.ctime : new Date();
var s = {
ctime: ctime,
mtime: new Date(),
mode: mode,
type:'file'
};
ps.put(key, s, function(err) {
if (err) return cb(err);
var w = bl.createWriteStream(blob, opts);
ws.emit('open');
w.on('finish', function() {
s.mtime = new Date();
ps.put(key, s, function() {
listeners.change(key);
if (!closed) ws.emit('close');
});
});
cb(null, w);
});
});
});
});
ws.on('close', function() {
closed = true;
});
return ws;
};
fs.truncate = function(key, len, cb) {
ps.follow(key, function(err, stat, key) {
if (err) return cb(err);
var blob = stat && stat.blob || key;
bl.size(blob, function(err, size) {
if (err) return cb(err);
ps.writable(key, function(err) {
if (err) return cb(err);
cb = once(listeners.cb(key, cb));
if (!len) return bl.remove(blob, cb);
var ws = bl.createWriteStream(blob, {
start:size < len ? len-1 : len
});
ws.on('error', cb);
ws.on('finish', cb);
if (size < len) ws.write(new Buffer([0]));
ws.end();
});
});
});
};
fs.watchFile = function(key, opts, cb) {
if (typeof opts === 'function') return fs.watchFile(key, null, opts);
return listeners.watch(ps.normalize(key), cb);
};
fs.unwatchFile = function(key, cb) {
listeners.unwatch(ps.normalize(key), cb);
};
fs.watch = function(key, opts, cb) {
if (typeof opts === 'function') return fs.watch(key, null, opts)
return listeners.watcher(ps.normalize(key), cb);
};
fs.notify = function(cb) {
listeners.on('change', cb)
}
fs.open = function(key, flags, mode, cb) {
if (typeof mode === 'function') return fs.open(key, flags, null, mode);
ps.follow(key, function(err, stat, key) {
if (err && err.code !== 'ENOENT') return cb(err);
var fl = flags[0];
var plus = flags[1] === '+' || flags[2] === '+';
var blob = stat && stat.blob || key;
var f = {
key: key,
blob: blob,
mode: mode || octal(666),
readable: fl === 'r' || ((fl === 'w' || fl === 'a') && plus),
writable: fl === 'w' || fl === 'a' || (fl === 'r' && plus),
append: fl === 'a'
};
if (fl === 'r' && err) return cb(err);
if (flags[1] === 'x' && stat) return cb(errno.EEXIST(key));
if (stat && stat.isDirectory()) return cb(errno.EISDIR(key));
bl.size(blob, function(err, size) {
if (err) return cb(err);
if (f.append) f.writePos = size;
ps.writable(key, function(err) {
if (err) return cb(err);
var onready = function(err) {
if (err) return cb(err);
var i = fds.indexOf(null);
if (i === -1) i = 10+fds.push(fds.length+10)-1;
f.fd = i;
fds[i] = f;
listeners.change(key);
cb(null, f.fd);
};
var ontruncate = function(err) {
if (err) return cb(err);
if (stat) return onready();
ps.put(blob, {ctime:stat && stat.ctime, type:'file'}, onready);
};
if (!f.append && f.writable) return bl.remove(blob, ontruncate);
ontruncate();
});
});
});
};
fs.close = function(fd, cb) {
var f = fds[fd];
if (!f) return nextTick(cb, errno.EBADF());
fds[fd] = null;
nextTick(listeners.cb(f.key, cb));
};
fs.write = function(fd, buf, off, len, pos, cb) {
var f = fds[fd];
if (!cb) cb = noop;
if (!f || !f.writable) return nextTick(cb, errno.EBADF());
if (pos === null) pos = f.writePos || 0;
var slice = buf.slice(off, off+len);
f.writePos = pos + slice.length;
bl.write(f.blob, slice, {start:pos, append:true}, function(err) {
if (err) return cb(err);
cb(null, len, buf);
});
};
fs.read = function(fd, buf, off, len, pos, cb) {
var f = fds[fd];
if (!cb) cb = noop;
if (!f || !f.readable) return nextTick(cb, errno.EBADF());
if (pos === null) pos = fs.readPos || 0;
bl.read(f.blob, {start:pos, end:pos+len-1}, function(err, read) {
if (err) return cb(err);
var slice = read.slice(0, len);
slice.copy(buf, off);
fs.readPos = pos+slice.length;
cb(null, slice.length, buf);
});
};
fs.fsync = function(fd, cb) {
var f = fds[fd];
if (!cb) cb = noop;
if (!f || !f.writable) return nextTick(cb, errno.EBADF());
nextTick(cb);
};
fs.ftruncate = function(fd, len, cb) {
var f = fds[fd];
if (!cb) cb = noop;
if (!f) return nextTick(cb, errno.EBADF());
fs.truncate(f.blob, len, cb);
};
fs.fchown = function(fd, uid, gid, cb) {
var f = fds[fd];
if (!cb) cb = noop;
if (!f) return nextTick(cb, errno.EBADF());
fs.chown(f.key, uid, gid, cb);
};
fs.fchmod = function(fd, mode, cb) {
var f = fds[fd];
if (!cb) cb = noop;
if (!f) return nextTick(cb, errno.EBADF());
fs.chmod(f.key, mode, cb);
};
fs.futimes = function(fd, atime, mtime, cb) {
var f = fds[fd];
if (!cb) cb = noop;
if (!f) return nextTick(cb, errno.EBADF());
fs.utimes(f.key, atime, mtime, cb);
};
fs.fstat = function(fd, cb) {
var f = fds[fd];
if (!f) return nextTick(cb, errno.EBADF());
fs.stat(f.key, cb);
};
fs.symlink = function(target, name, cb) {
if (!cb) cb = noop;
ps.follow(target, function(err, stat, target) {
if (err) return cb(err);
ps.get(name, function(err, stat) {
if (err && err.code !== 'ENOENT') return cb(err);
if (stat) return cb(errno.EEXIST(name));
ps.put(name, {type:'symlink', target:target, mode:octal(777)}, cb);
});
});
};
fs.readlink = function(key, cb) {
ps.get(key, function(err, stat) {
if (err) return cb(err);
if (!stat.target) return cb(errno.EINVAL(key));
cb(null, stat.target);
});
};
fs.link = function(target, name, cb) {
if (!cb) cb = noop;
ps.follow(target, function(err, stat, target) {
if (err) return cb(err);
if (!stat.isFile()) return cb(errno.EINVAL(target));
ps.get(name, function(err, st) {
if (err && err.code !== 'ENOENT') return cb(err);
if (st) return cb(errno.EEXIST(name));
var link = target+'\xff'+inc();
links.put(target+'\xff', target, function(err) {
if (err) return cb(err);
links.put(link, target, function(err) {
if (err) return cb(err);
ps.put(name, {type:'file', link:link, blob:target, mode:stat.mode}, cb);
});
});
});
});
};
return fs;
};