UNPKG

level-blobs

Version:

Save binary blobs in level and stream then back

392 lines (307 loc) 9.16 kB
var Writable = require('readable-stream/writable'); var Readable = require('readable-stream/readable'); var peek = require('level-peek'); var util = require('util'); var once = require('once'); var EMPTY = new Buffer(0); var ENCODER = { encode: function(data) { return typeof data === 'string' ? data = new Buffer(data) : data; }, decode: function(data) { return Buffer.isBuffer(data) ? data : new Buffer(data); }, buffer: true, type: 'raw' }; var noop = function() {}; var pad = function(n) { n = n.toString(16); return '00000000'.slice(0, -n.length)+n; }; var expand = function(buf, len) { var tmp = new Buffer(len); buf.copy(tmp); return tmp; }; module.exports = function(db, opts) { if (!opts) opts = {}; var blobs = {}; var blockSize = opts.blockSize || 65536; var maxBatch = opts.batch || 100; var blank = new Buffer(blockSize); db.put('\x00', 'ignore', noop); // memdown#12 workaround var reservations = {}; var mutateBlock = function(key, offset, block, append, cb) { var release = function() { if (!--reservations[key].locks) delete reservations[key]; }; var onreservation = function(r) { r.locks++; if (!r.block && !offset) { r.block = block; cb(null, r.block, release); return; } if (!r.block) r.block = new Buffer(blockSize); if (r.block.length < offset + block.length) r.block = expand(r.block, offset + block.length); block.copy(r.block, offset); if (!append && offset + block.length < r.block.length) r.block = r.block.slice(0, offset+block.length); cb(null, r.block, release); }; if (reservations[key]) return onreservation(reservations[key]); db.get(key, {valueEncoding:ENCODER}, function(err, block) { if (err && !err.notFound) return cb(err); if (!reservations[key]) reservations[key] = {locks:0, block:block}; onreservation(reservations[key]); }); }; var WriteStream = function(name, opts) { if (!(this instanceof WriteStream)) return new WriteStream(name, opts); if (!opts) opts = {}; this.name = name; this.blocks = []; this.batch = []; this.bytesWritten = 0; this.truncate = !opts.append; this.append = opts.append; this._shouldInitAppend = this.append && opts.start === undefined; this._destroyed = false; this._init(opts.start || 0); Writable.call(this); }; util.inherits(WriteStream, Writable); WriteStream.prototype._init = function(start) { this.blockIndex = (start / blockSize) | 0; this.blockOffset = start - this.blockIndex * blockSize; this.blockLength = this.blockOffset; }; WriteStream.prototype._flush = function(cb) { if (!this.batch.length) return cb(); var key = this.batch[this.batch.length-1].key; var batch = this.batch; this.batch = []; if (!this.truncate) return db.batch(batch, cb); this.truncate = false; this._truncate(batch, key, cb); }; WriteStream.prototype._truncate = function(batch, key, cb) { cb = once(cb); var dels = []; var keys = db.createKeyStream({ start: key, end: this.name+'\xff\xff' }); keys.on('error', cb); keys.on('data', function(key) { dels.push({type:'del', key:key}); }); keys.on('end', function() { dels.push.apply(dels, batch); db.batch(dels, cb); }); }; WriteStream.prototype._writeBlock = function(cb) { var block = this.blocks.length === 1 ? this.blocks[0] : Buffer.concat(this.blocks, this.blockLength - this.blockOffset); var index = this.blockIndex; var offset = this.blockOffset; var self = this; this.blockOffset = 0; this.blockLength = 0; this.blockIndex++; this.blocks = []; var key = this.name+'\xff'+pad(index); var append = function(block, force, cb) { if (block.length) { self.batch.push({ type: 'put', key: key, value: block, valueEncoding: ENCODER }); } if (!force && self.batch.length < maxBatch) return cb(); return self._flush(cb); }; if (!offset && block.length === blockSize) return append(block, false, cb); if (!offset && !this.append) return append(block, false, cb); // partial write mutateBlock(key, offset, block, this.append, function(err, block, release) { if (err) return cb(err); append(block, true, function(err) { release(); cb(err); }); }); }; WriteStream.prototype._initAppend = function(data, enc, cb) { var self = this; this._shouldInitAppend = false; blobs.size(this.name, function(err, size) { if (err) return cb(err); self._init(size); self._write(data, enc, cb); }); }; WriteStream.prototype._write = function(data, enc, cb) { if (!data.length || this._destroyed) return cb(); if (this._shouldInitAppend) return this._initAppend(data, enc, cb); var self = this; var overflow; var free = blockSize - this.blockLength; var done = function(err) { if (err) return cb(err); if (overflow) return self._write(overflow, enc, cb); cb(); }; if (data.length > free) { overflow = data.slice(free); data = data.slice(0, free); } this.bytesWritten += data.length; this.blockLength += data.length; this.blocks.push(data); if (data.length < free) return done(); this._writeBlock(done); }; WriteStream.prototype.destroy = function() { if (this._destroyed) return; this._destroyed = true; process.nextTick(this.emit.bind(this, 'close')); }; WriteStream.prototype.end = function(data) { var self = this; var args = arguments; if (data && typeof data !== 'function') { this.write(data); data = EMPTY; } this.write(EMPTY, function() { self._writeBlock(function(err) { if (err) return self.emit('error', err); self._flush(function(err) { if (err) return self.emit('error', err); Writable.prototype.end.apply(self, args); }); }); }); }; var ReadStream = function(name, opts) { if (!opts) opts = {}; var self = this; var start = opts.start || 0; var blockIndex = (start / blockSize) | 0; var blockOffset = start - blockIndex * blockSize; var key = name+'\xff'+pad(blockIndex); this.name = name; this._missing = (typeof opts.end === 'number' ? opts.end : Infinity) - start + 1; this._paused = false; this._destroyed = false; this._reader = db.createReadStream({ start: key, end: name+'\xff\xff', valueEncoding: ENCODER }); var onblock = function(val) { key = name+'\xff'+pad(++blockIndex); if (!self._missing) return false; if (blockOffset) { val = val.slice(blockOffset); blockOffset = 0; if (!val.length) return true; } if (val.length > self._missing) val = val.slice(0, self._missing); self._missing -= val.length; self._pause(!self.push(val)); return !!self._missing; }; this._reader.on('data', function(data) { while (data.key > key) { if (!onblock(blank)) return; } onblock(data.value); }); this._reader.on('error', function(err) { self.emit('error', err); }); this._reader.on('end', function() { self.push(null); }); Readable.call(this); }; util.inherits(ReadStream, Readable); ReadStream.prototype.destroy = function() { if (this._destroyed) return; this._destroyed = true; this._reader.destroy(); process.nextTick(this.emit.bind(this, 'close')); }; ReadStream.prototype._pause = function(paused) { if (this._paused === paused) return; this._paused = paused; if (this._paused) this._reader.pause(); else this._reader.resume(); }; ReadStream.prototype._read = function() { this._pause(false); }; blobs.remove = function(name, cb) { cb = once(cb || noop); var batch = []; var keys = db.createKeyStream({ start: name+'\xff', end: name+'\xff\xff' }); keys.on('error', cb); keys.on('data', function(key) { batch.push({type:'del', key:key}); }); keys.on('end', function() { db.batch(batch, cb); }); }; blobs.size = function(name, cb) { peek.last(db, { start: name+'\xff', end: name+'\xff\xff', valueEncoding:ENCODER }, function(err, latest, val) { if (err && err.message === 'range not found') return cb(null, 0); if (err) return cb(err); if (latest.slice(0, name.length+1) !== name+'\xff') return cb(null, 0); cb(null, parseInt(latest.toString().slice(name.length+1), 16) * blockSize + val.length); }); }; blobs.write = function(name, data, opts, cb) { if (typeof opts === 'function') return blobs.write(name, data, null, opts); if (!opts) opts = {}; if (!cb) cb = noop; var ws = blobs.createWriteStream(name, opts); ws.on('error', cb); ws.on('finish', function() { cb(); }); ws.write(data); ws.end(); } blobs.read = function(name, opts, cb) { if (typeof opts === 'function') return blobs.read(name, null, opts); if (!opts) opts = {}; var rs = blobs.createReadStream(name, opts); var list = []; rs.on('error', cb); rs.on('data', function(data) { list.push(data); }); rs.on('end', function() { cb(null, list.length === 1 ? list[0] : Buffer.concat(list)); }); }; blobs.createReadStream = function(name, opts) { return new ReadStream(name, opts); }; blobs.createWriteStream = function(name, opts) { return new WriteStream(name, opts); }; return blobs; };