UNPKG

level-filesystem

Version:

Full implementation of the fs module on top of leveldb

598 lines (479 loc) 14.4 kB
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; };