UNPKG

dirdb

Version:

DirDB key-value directory database

952 lines (941 loc) 50.8 kB
/* SOURCE FILE - Copyright (c) 2017 dirdb - Tanase Laurentiu Iulian - https://github.com/RealTimeCom/dirdb */ 'use strict'; // TODO: // db.vstats(uid, hash) // db.vdel(uid, hash) // db.vget(uid, hash) - alias db.val() // db.rungc(optimize) - delete all empty dirs and, if optimize is true: delete all keys without value and all values without key, and all dir contents if not in schema level/algorithm/digest, except .dridb.json file // db.cpdir(fromdir, todir, overwrite) - copy fromdir contents into todir, optional overwrite (true|false) todir keys value const fs = require('fs'), path = require('path'), crypto = require('crypto'), zlib = require('zlib'), //snappy = require('snappy'), rpc = require('rpc-json'), rmdir = require('rm-dir'); function request(resp, head, body) { if ('s' in head && typeof head.s === 'boolean' && 'f' in head && typeof head.f === 'string') { if (head.s) { // sync try { let r; switch (head.f) { case 'mkdir': case 'setgc': case 'keys': resp({ f: head.f, r: this.db[head.f](head.d, head.o) }); break; case 'put': case 'set': case 'add': r = this.db[head.f](head.d, body.slice(0, head.k), body.slice(head.k)); resp({ f: head.f, u: r.uid, h: r.hash, p: r.path }); break; case 'stats': r = this.db.stats(head.d, body); resp({ f: head.f, u: r.uid, h: r.hash, p: r.path, s: r.stats }); break; case 'del': resp({ f: head.f, r: this.db.del(head.d, body) }); break; case 'get': r = this.db.get(head.d, body); resp({ f: head.f, u: r.uid, h: r.hash, p: r.path }, r.value); break; case 'val': r = this.db.val(head.d, head.u, head.h); resp({ f: head.f, k: r.key.length, p: r.path }, Buffer.concat([r.key, r.value])); break; case 'rmdir': this.db.rmdir(head.d); resp({ f: head.f }); break; case 'list': resp({ f: head.f, r: this.db.c }); break; case 'isdir': resp({ f: head.f, r: this.db.isdir(head.d) }); break; default: throw new Error('function "' + head.f + '" not found'); } } catch (e) { resp({ f: head.f, e: e.message }); } } else { // async switch (head.f) { case 'mkdir': case 'setgc': case 'keys': this.db[head.f](head.d, head.o, (e, r) => resp({ f: head.f, e: e ? e.message : undefined, r: r })); break; case 'put': case 'set': case 'add': this.db[head.f](head.d, body.slice(0, head.k), body.slice(head.k), (e, uid, hash, path) => resp({ f: head.f, e: e ? e.message : undefined, u: uid, h: hash, p: path })); break; case 'stats': this.db.stats(head.d, body, (e, uid, hash, path, stats) => { if (e) { resp({ f: head.f, e: e.message }); } else { resp({ f: head.f, u: uid, h: hash, p: path, s: stats }); } }); break; case 'del': this.db.del(head.d, body, (e, r) => resp({ f: head.f, e: e ? e.message : undefined, r: r })); break; case 'get': this.db.get(head.d, body, (e, value, uid, hash, path) => { if (e) { resp({ f: head.f, e: e.message }); } else { resp({ f: head.f, u: uid, h: hash, p: path}, value); } }); break; case 'val': this.db.val(head.d, head.u, head.h, (e, key, value, path) => { if (e) { resp({ f: head.f, e: e.message }); } else { resp({ f: head.f, k: key.length, p: path }, Buffer.concat([key, value])); } }); break; case 'rmdir': this.db.rmdir(head.d, e => resp({ f: head.f, e: e ? e.message : undefined })); break; case 'list': resp({ f: head.f, r: this.db.c }); break; // safe sync case 'isdir': resp({ f: head.f, r: this.db.isdir(head.d) }); break; // safe sync default: resp({ e: 'function "' + head.f + '" not found' }); } } } else { resp({ e: 'invalid params' }); } } function filter(resp, head, body) { if (resp) { if ('f' in head && typeof head.f === 'string') { switch (head.f) { case 'mkdir': case 'keys': case 'del': resp(head.e ? new Error(head.e) : undefined, head.r); break; case 'put': case 'set': case 'add': resp(head.e ? new Error(head.e) : undefined, head.u, head.h, head.p); break; case 'get': resp(head.e ? new Error(head.e) : undefined, body, head.u, head.h, head.p); break; case 'stats': resp(head.e ? new Error(head.e) : undefined, head.u, head.h, head.p, head.s); break; case 'val': if (head.e) { resp(new Error(head.e)); } else { resp(undefined, body.slice(0, head.k), body.slice(head.k), head.p); } break; case 'rmdir': resp(head.e ? new Error(head.e) : undefined); break; case 'list': case 'isdir': resp(head.r); break; default: resp(new Error(head.e)); } } else { resp(new Error(head.e)); } } } class client extends rpc.client { constructor(sync) { super(filter); this.sync = sync ? true : false; } } client.prototype.mkdir = function(dir, opt, resp, sync) { if (typeof opt === 'function' && resp === undefined) { resp = opt; } this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'mkdir', d: dir, o: opt }); return this; }; client.prototype.rmdir = function(dir, resp, sync) { this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'rmdir', d: dir }); return this; }; client.prototype.put = function(dir, key, val, resp, sync) { key = toBuffer(key); this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'put', d: dir, k: key.length }, Buffer.concat([key, toBuffer(val)])); return this; }; client.prototype.set = function(dir, key, val, resp, sync) { key = toBuffer(key); this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'set', d: dir, k: key.length }, Buffer.concat([key, toBuffer(val)])); return this; }; client.prototype.add = function(dir, key, val, resp, sync) { key = toBuffer(key); this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'add', d: dir, k: key.length }, Buffer.concat([key, toBuffer(val)])); return this; }; client.prototype.get = function(dir, key, resp, sync) { this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'get', d: dir }, key); return this; }; client.prototype.del = function(dir, key, resp, sync) { this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'del', d: dir }, key); return this; }; client.prototype.stats = function(dir, key, resp, sync) { this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'stats', d: dir }, key); return this; }; client.prototype.list = function(resp) { this.exec(resp, { s: this.sync, f: 'list' }); return this; }; client.prototype.isdir = function(dir, resp) { this.exec(resp, { s: this.sync, f: 'list', d: dir }); return this; }; client.prototype.keys = function(dir, opt, resp, sync) { if (typeof opt === 'function' && resp === undefined) { resp = opt; } this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'keys', d: dir, o: opt }); return this; }; client.prototype.val = function(dir, uid, hash, resp, sync) { this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'val', d: dir, u: uid, h: hash }); return this; }; client.prototype.setgc = function(dir, opt, resp, sync) { this.exec(resp, { s: sync === undefined ? this.sync : Boolean(sync), f: 'setgc', d: dir, o: opt ? true : false }); return this; }; class dirdb { constructor(dir, opt) { this.s = typeof opt === 'object' ? option(opt, dirdb.p) : dirdb.p; // overwrite default dir options this.f = '.dirdb.json'; // dir conf file name this.i = 0; // unique id this.conf(dir); // cache dir config, sync parse this.f files from each base dir } } dirdb.p = { level: 3, dmode: 0o700, fmode: 0o600, algorithm: 'md5', digest: 'base64', compress: 'none', gc: true }; // default dir options function option(opt, p) { if (typeof opt === 'object') { const algorithm = 'algorithm' in opt && typeof opt.algorithm === 'string' && (opt.algorithm === 'md5' || opt.algorithm === 'sha1' || opt.algorithm === 'sha256' || opt.algorithm === 'sha512') ? opt.algorithm : p.algorithm; const digest = 'digest' in opt && typeof opt.digest === 'string' && (opt.digest === 'base64' || opt.digest === 'hex') ? opt.digest : p.digest; let level = p.level; if ('level' in opt) { const l = parseInt(opt.level); if (l >= 0 && l < 128) { if (algorithm === 'md5') { if (digest === 'base64' && l < 22) { level = l; } else if (digest === 'hex' && l < 32) { level = l; } } else if (algorithm === 'sha1') { if (digest === 'base64' && l < 27) { level = l; } else if (digest === 'hex' && l < 40) { level = l; } } else if (algorithm === 'sha256') { if (digest === 'base64' && l < 43) { level = l; } else if (digest === 'hex' && l < 64) { level = l; } } else if (algorithm === 'sha512') { if (digest === 'base64' && l < 86) { level = l; } else if (digest === 'hex' && l < 128) { level = l; } } } } return { level: level, dmode: 'dmode' in opt ? parseInt(opt.dmode) : p.dmode, fmode: 'fmode' in opt ? parseInt(opt.fmode) : p.fmode, algorithm: algorithm, digest: digest, compress: 'compress' in opt && typeof opt.compress === 'string' && (opt.compress === 'none' || opt.compress === 'deflate' || opt.compress === 'gzip') ? opt.compress : p.compress, // || opt.compress === 'snappy' gc: 'gc' in opt ? Boolean(opt.gc) : p.gc }; } else { return p; } } function divisor(c) { switch (c.algorithm) { case 'md5': return c.digest === 'base64' ? 22 + c.level : 32 + c.level; case 'sha1': return c.digest === 'base64' ? 27 + c.level : 40 + c.level; case 'sha256': return c.digest === 'base64' ? 43 + c.level : 64 + c.level; case 'sha512': return c.digest === 'base64' ? 86 + c.level : 128 + c.level; } throw new Error('invalid algorithm'); } dirdb.prototype.conf = function(dir) { if (typeof dir !== 'string') { throw new Error('invalid dir type "' + (typeof dir) + '", String expected'); } dir = path.normalize(dir); if (dir === '' || dir === '.' || dir === '..') { throw new Error('invalid dir path name "' + dir + '"'); } let c = {}, s = fs.lstatSync(dir); if (!s.isDirectory()) { throw new Error('dir "' + dir + '" is not directory'); } else { for (let v of fs.readdirSync(dir)) { // scan each dir s = fs.lstatSync(dir + path.sep + v); if (s.isDirectory()) { s = fs.lstatSync(dir + path.sep + v + path.sep + this.f); // read dir config file if (s.isFile() && s.size > 0) { c[v] = option(JSON.parse(fs.readFileSync(dir + path.sep + v + path.sep + this.f).toString()), this.s); // parse options } } } } this.c = c; this.d = dir; }; dirdb.prototype.mkdir = function(dir, opt, cb) { // don't use slashes \ / or dots . in the dir name if (typeof opt === 'function' && cb === undefined) { cb = opt; } if (typeof cb === 'function') { // async, those callbacks are MUCH faster and compact than async/await or Promise ;) if (!(dir = safeDir(dir))) { cb(new Error('invalid dir value')); } else { if (dir in this.c) { cb(new Error('dir "' + dir + '" exists')); } else { opt = option(opt, this.s); // parse options fs.mkdir(this.d + path.sep + dir, opt.dmode, e => { if (e) { cb(e); } else { fs.writeFile(this.d + path.sep + dir + path.sep + this.f, JSON.stringify(opt), { mode: opt.fmode }, e => { if (e) { cb(e); } else { this.c[dir] = opt; cb(undefined, dir); } }); } }); } } return this; } else { // sync if (!(dir = safeDir(dir))) { throw new Error('invalid dir value'); } if (dir in this.c) { throw new Error('dir "' + dir + '" exists'); } opt = option(opt, this.s); // parse options fs.mkdirSync(this.d + path.sep + dir, opt.dmode); fs.writeFileSync(this.d + path.sep + dir + path.sep + this.f, JSON.stringify(opt), { mode: opt.fmode }); this.c[dir] = opt; return dir; } }; dirdb.prototype.rmdir = function(dir, cb) { if (typeof cb === 'function') { // async if (!(typeof dir === 'string' && dir in this.c)) { cb(new Error('dir "' + dir + '" not found')); } else { rmdir(this.d + path.sep + dir, e => { if (e) { cb(e); } else { if (dir in this.c) { delete this.c[dir]; } // async delete cb(); } }); } return this; } else { // sync if (!(typeof dir === 'string' && dir in this.c)) { throw new Error('dir "' + dir + '" not found'); } rmdir(this.d + path.sep + dir); delete this.c[dir]; } }; function find(d, a, key, cb, r) { if (a.length > 0) { // verify if key exists const x = path.parse(a[0]); if (x.ext === '.k') { fs.lstat(d + path.sep + a[0], (e, s) => { if (e) { cb(e); } else { if (s.isFile() && s.size === key.length) { fs.readFile(d + path.sep + a[0], (e, b) => { if (e) { cb(e); } else { if (key.compare(b) === 0) { cb(new Error('key exists'), r ? x.name : undefined); } else { find(d, a.splice(0, 1) ? a : a, key, cb, r); } } }); } else { find(d, a.splice(0, 1) ? a : a, key, cb, r); } } }); } else { find(d, a.splice(0, 1) ? a : a, key, cb, r); } } else { cb(); } } dirdb.prototype.uid = function() { if (this.i === 1e9) { this.i = 0; } // max id, reset return new Date().getTime().toString(36) + '.' + (this.i++).toString(36); // parseInt(uid.split('.')[0], 36) - birthtime }; dirdb.prototype.put = function(dir, key, val, cb) { if (typeof cb === 'function') { // async, those callbacks are MUCH faster and compact than async/await or Promise ;) if (!(typeof dir === 'string' && dir in this.c)) { cb(new Error('dir "' + dir + '" not found')); } else { key = toBuffer(key); if (key.length === 0) { cb(new Error('empty key')); } else { const h = hash(key, this.c[dir].algorithm, this.c[dir].digest); const p = xpath(h, this.c[dir].level); make(this.d + path.sep + dir, p, this.c[dir].dmode, e => { if (e) { cb(e); } else { const d = this.d + path.sep + dir + path.sep + p; fs.readdir(d, (e, a) => { if (e) { cb(e); } else { find(d, a, key, e => { // verify if key exists if (e) { cb(e); } else { const uid = this.uid(); compress(toBuffer(val), this.c[dir].compress, (e, b) => { if (e) { cb(e); } else { fs.writeFile(d + path.sep + uid + '.v', b, { mode: this.c[dir].fmode }, e => { if (e) { cb(e); } else { fs.writeFile(d + path.sep + uid + '.k', key, { mode: this.c[dir].fmode }, e => { if (e) { cb(e); } else { cb(undefined, uid, h, d + path.sep + uid); } }); } }); } }); } }); } }); } }); } } return this; } else { // sync if (!(typeof dir === 'string' && dir in this.c)) { throw new Error('dir "' + dir + '" not found'); } key = toBuffer(key); if (key.length === 0) { throw new Error('empty key'); } const h = hash(key, this.c[dir].algorithm, this.c[dir].digest); const p = xpath(h, this.c[dir].level); make(this.d + path.sep + dir, p, this.c[dir].dmode); const d = this.d + path.sep + dir + path.sep + p; let s; for (let v of fs.readdirSync(d)) { // verify if key exists if (path.parse(v).ext === '.k') { s = fs.lstatSync(d + path.sep + v); if (s.isFile() && s.size === key.length && key.compare(fs.readFileSync(d + path.sep + v)) === 0) { throw new Error('key exists'); } } } const uid = this.uid(); fs.writeFileSync(d + path.sep + uid + '.v', compress(toBuffer(val), this.c[dir].compress), { mode: this.c[dir].fmode }); fs.writeFileSync(d + path.sep + uid + '.k', key, { mode: this.c[dir].fmode }); return { uid: uid, hash: h, path: d + path.sep + uid }; } }; dirdb.prototype.set = function(dir, key, val, cb) { if (typeof cb === 'function') { // async, those callbacks are MUCH faster and compact than async/await or Promise ;) if (!(typeof dir === 'string' && dir in this.c)) { cb(new Error('dir "' + dir + '" not found')); } else { key = toBuffer(key); if (key.length === 0) { cb(new Error('empty key')); } else { const h = hash(key, this.c[dir].algorithm, this.c[dir].digest); const p = xpath(h, this.c[dir].level); make(this.d + path.sep + dir, p, this.c[dir].dmode, e => { if (e) { cb(e); } else { const d = this.d + path.sep + dir + path.sep + p; fs.readdir(d, (e, a) => { if (e) { cb(e); } else { find(d, a, key, (e, uid) => { // verify if key exists if (e) { if (e.message === 'key exists' && uid) { // key found compress(toBuffer(val), this.c[dir].compress, (e, b) => { if (e) { cb(e); } else { fs.writeFile(d + path.sep + uid + '.v', b, { mode: this.c[dir].fmode }, e => { if (e) { cb(e); } else { cb(undefined, uid, h, d + path.sep + uid); } }); } }); } else { cb(e); } } else { // key not found uid = this.uid(); compress(toBuffer(val), this.c[dir].compress, (e, b) => { if (e) { cb(e); } else { fs.writeFile(d + path.sep + uid + '.v', b, { mode: this.c[dir].fmode }, e => { if (e) { cb(e); } else { fs.writeFile(d + path.sep + uid + '.k', key, { mode: this.c[dir].fmode }, e => { if (e) { cb(e); } else { cb(undefined, uid, h, d + path.sep + uid); } }); } }); } }); } }, true); } }); } }); } } return this; } else { // sync if (!(typeof dir === 'string' && dir in this.c)) { throw new Error('dir "' + dir + '" not found'); } key = toBuffer(key); if (key.length === 0) { throw new Error('empty key'); } const h = hash(key, this.c[dir].algorithm, this.c[dir].digest); const p = xpath(h, this.c[dir].level); make(this.d + path.sep + dir, p, this.c[dir].dmode); const d = this.d + path.sep + dir + path.sep + p; let s, x, k = false; for (let v of fs.readdirSync(d)) { // verify if key exists x = path.parse(v); if (x.ext === '.k') { s = fs.lstatSync(d + path.sep + v); if (s.isFile() && s.size === key.length && key.compare(fs.readFileSync(d + path.sep + v)) === 0) { k = true; break; } } } if (k) { // key found fs.writeFileSync(d + path.sep + x.name + '.v', compress(toBuffer(val), this.c[dir].compress), { mode: this.c[dir].fmode }); return { uid: x.name, hash: h, path: d + path.sep + x.name }; } else { // key not found const uid = this.uid(); fs.writeFileSync(d + path.sep + uid + '.v', compress(toBuffer(val), this.c[dir].compress), { mode: this.c[dir].fmode }); fs.writeFileSync(d + path.sep + uid + '.k', key, { mode: this.c[dir].fmode }); return { uid: uid, hash: h, path: d + path.sep + uid }; } } }; dirdb.prototype.add = function(dir, key, val, cb) { if (typeof cb === 'function') { // async, those callbacks are MUCH faster and compact than async/await or Promise ;) if (!(typeof dir === 'string' && dir in this.c)) { cb(new Error('dir "' + dir + '" not found')); } else { //if (this.c[dir].compress !== 'none') { cb(new Error('compress "' + this.c[dir].compress + '" enabled on dir "' + dir + '"')); } else { key = toBuffer(key); if (key.length === 0) { cb(new Error('empty key')); } else { const h = hash(key, this.c[dir].algorithm, this.c[dir].digest); const p = xpath(h, this.c[dir].level); make(this.d + path.sep + dir, p, this.c[dir].dmode, e => { if (e) { cb(e); } else { const d = this.d + path.sep + dir + path.sep + p; fs.readdir(d, (e, a) => { if (e) { cb(e); } else { find(d, a, key, (e, uid) => { // verify if key exists if (e) { if (e.message === 'key exists' && uid) { // key found compress(toBuffer(val), this.c[dir].compress, (e, b) => { if (e) { cb(e); } else { fs.appendFile(d + path.sep + uid + '.v', b, { mode: this.c[dir].fmode }, e => { if (e) { cb(e); } else { cb(undefined, uid, h, d + path.sep + uid); } }); } }); } else { cb(e); } } else { // key not found uid = this.uid(); compress(toBuffer(val), this.c[dir].compress, (e, b) => { if (e) { cb(e); } else { fs.writeFile(d + path.sep + uid + '.v', b, { mode: this.c[dir].fmode }, e => { if (e) { cb(e); } else { fs.writeFile(d + path.sep + uid + '.k', key, { mode: this.c[dir].fmode }, e => { if (e) { cb(e); } else { cb(undefined, uid, h, d + path.sep + uid); } }); } }); } }); } }, true); } }); } }); } //} } return this; } else { // sync if (!(typeof dir === 'string' && dir in this.c)) { throw new Error('dir "' + dir + '" not found'); } //if (this.c[dir].compress !== 'none') { throw new Error('compress "' + this.c[dir].compress + '" enabled on dir "' + dir + '"'); } key = toBuffer(key); if (key.length === 0) { throw new Error('empty key'); } const h = hash(key, this.c[dir].algorithm, this.c[dir].digest); const p = xpath(h, this.c[dir].level); make(this.d + path.sep + dir, p, this.c[dir].dmode); const d = this.d + path.sep + dir + path.sep + p; let s, x, k = false; for (let v of fs.readdirSync(d)) { // verify if key exists x = path.parse(v); if (x.ext === '.k') { s = fs.lstatSync(d + path.sep + v); if (s.isFile() && s.size === key.length && key.compare(fs.readFileSync(d + path.sep + v)) === 0) { k = true; break; } } } if (k) { // key found fs.appendFileSync(d + path.sep + x.name + '.v', compress(toBuffer(val), this.c[dir].compress), { mode: this.c[dir].fmode }); return { uid: x.name, hash: h, path: d + path.sep + x.name }; } else { // key not found const uid = this.uid(); fs.writeFileSync(d + path.sep + uid + '.v', compress(toBuffer(val), this.c[dir].compress), { mode: this.c[dir].fmode }); fs.writeFileSync(d + path.sep + uid + '.k', key, { mode: this.c[dir].fmode }); return { uid: uid, hash: h, path: d + path.sep + uid }; } } }; dirdb.prototype.get = function(dir, key, cb) { if (typeof cb === 'function') { // async, those callbacks are MUCH faster and compact than async/await or Promise ;) if (!(typeof dir === 'string' && dir in this.c)) { cb(new Error('dir "' + dir + '" not found')); } else { key = toBuffer(key); if (key.length === 0) { cb(new Error('empty key')); } else { const h = hash(key, this.c[dir].algorithm, this.c[dir].digest); const d = this.d + path.sep + dir + path.sep + xpath(h, this.c[dir].level); fs.readdir(d, (e, a) => { if (e) { cb(e); } else { find(d, a, key, (e, uid) => { // verify if key exists if (e) { if (e.message === 'key exists' && uid) { fs.readFile(d + path.sep + uid + '.v', (e, value) => { if (e) { cb(e); } else { uncompress(value, this.c[dir].compress, (e, b) => { if (e) { cb(e); } else { cb(undefined, b, uid, h, d + path.sep + uid); } }); } }); } else { cb(e); } } else { cb(new Error('key not found')); } }, true); } }); } } return this; } else { // sync if (!(typeof dir === 'string' && dir in this.c)) { throw new Error('dir "' + dir + '" not found'); } key = toBuffer(key); if (key.length === 0) { throw new Error('empty key'); } const h = hash(key, this.c[dir].algorithm, this.c[dir].digest); const d = this.d + path.sep + dir + path.sep + xpath(h, this.c[dir].level); let s, x; for (let v of fs.readdirSync(d)) { // verify if key exists x = path.parse(v); if (x.ext === '.k') { s = fs.lstatSync(d + path.sep + v); if (s.isFile() && s.size === key.length && key.compare(fs.readFileSync(d + path.sep + v)) === 0) { return { value: uncompress(fs.readFileSync(d + path.sep + x.name + '.v'), this.c[dir].compress), uid: x.name, hash: h, path: d + path.sep + x.name }; } } } throw new Error('key not found'); } }; dirdb.prototype.del = function(dir, key, cb) { if (typeof cb === 'function') { // async, those callbacks are MUCH faster and compact than async/await or Promise ;) if (!(typeof dir === 'string' && dir in this.c)) { cb(new Error('dir "' + dir + '" not found')); } else { key = toBuffer(key); if (key.length === 0) { cb(new Error('empty key')); } else { const d = this.d + path.sep + dir + path.sep + xpath(hash(key, this.c[dir].algorithm, this.c[dir].digest), this.c[dir].level); fs.readdir(d, (e, a) => { if (e) { cb(e); } else { find(d, a, key, (e, uid) => { // verify if key exists if (e) { if (e.message === 'key exists' && uid) { fs.unlink(d + path.sep + uid + '.k', e => { if (e) { cb(e); } else { fs.unlink(d + path.sep + uid + '.v', e => { if (e) { cb(e); } else { // run GC, delete last dir if (this.c[dir].gc) { fs.rmdir(d, e => cb(undefined, uid)); } else { cb(undefined, uid); } } }); } }); } else { cb(e); } } else { cb(new Error('key not found')); } }, true); } }); } } return this; } else { // sync if (!(typeof dir === 'string' && dir in this.c)) { throw new Error('dir "' + dir + '" not found'); } key = toBuffer(key); if (key.length === 0) { throw new Error('empty key'); } const d = this.d + path.sep + dir + path.sep + xpath(hash(key, this.c[dir].algorithm, this.c[dir].digest), this.c[dir].level); let s, x; for (let v of fs.readdirSync(d)) { // verify if key exists x = path.parse(v); if (x.ext === '.k') { s = fs.lstatSync(d + path.sep + v); if (s.isFile() && s.size === key.length && key.compare(fs.readFileSync(d + path.sep + v)) === 0) { fs.unlinkSync(d + path.sep + v); fs.unlinkSync(d + path.sep + x.name + '.v'); if (this.c[dir].gc) { try { fs.rmdirSync(d); } catch (e) { } } // run GC, delete last dir return x.name; } } } throw new Error('key not found'); } }; dirdb.prototype.stats = function(dir, key, cb) { if (typeof cb === 'function') { // async, those callbacks are MUCH faster and compact than async/await or Promise ;) if (!(typeof dir === 'string' && dir in this.c)) { cb(new Error('dir "' + dir + '" not found')); } else { key = toBuffer(key); if (key.length === 0) { cb(new Error('empty key')); } else { const h = hash(key, this.c[dir].algorithm, this.c[dir].digest); const d = this.d + path.sep + dir + path.sep + xpath(h, this.c[dir].level); fs.readdir(d, (e, a) => { if (e) { cb(e); } else { find(d, a, key, (e, uid) => { // verify if key exists if (e) { if (e.message === 'key exists' && uid) { fs.lstat(d + path.sep + uid + '.v', (e, stats) => { if (e) { cb(e); } else { cb(undefined, uid, h, d + path.sep + uid, stats); } }); } else { cb(e); } } else { cb(new Error('key not found')); } }, true); } }); } } return this; } else { // sync if (!(typeof dir === 'string' && dir in this.c)) { throw new Error('dir "' + dir + '" not found'); } key = toBuffer(key); if (key.length === 0) { throw new Error('empty key'); } const h = hash(key, this.c[dir].algorithm, this.c[dir].digest); const d = this.d + path.sep + dir + path.sep + xpath(h, this.c[dir].level); let s, x; for (let v of fs.readdirSync(d)) { // verify if key exists x = path.parse(v); if (x.ext === '.k') { s = fs.lstatSync(d + path.sep + v); if (s.isFile() && s.size === key.length && key.compare(fs.readFileSync(d + path.sep + v)) === 0) { return { uid: x.name, hash: h, path: d + path.sep + x.name, stats: fs.lstatSync(d + path.sep + x.name + '.v') }; } } } throw new Error('key not found'); } }; function each(a, r, d, l, n, opt, k, cb) { if (typeof cb === 'function') { // async, those callbacks are MUCH faster and compact than async/await or Promise ;) if (a.length > 0) { fs.lstat(d + path.sep + a[0], (e, s) => { if (e) { cb(e); } else { if (s.isDirectory()) { readdir(r, d + path.sep + a[0], l, n, opt, k, e => { if (e) { cb(e); } else { each(a.splice(0, 1) ? a : a, r, d, l, n, opt, k, cb); } }); } else { if (s.isFile() && d.length === l + n) { const x = path.parse(a[0]); if (x.ext === '.k') { if (opt && typeof opt.start === 'number') { // start point if (k.count >= opt.start) { if (opt && typeof opt.end === 'number') { // and end point (both) if (k.count < opt.end) { r[x.name] = d.substr(l).split(path.sep).join(''); } } else { r[x.name] = d.substr(l).split(path.sep).join(''); } // only start point } } else if (opt && typeof opt.end === 'number') { // only end point if (k.count < opt.end) { r[x.name] = d.substr(l).split(path.sep).join(''); } } else { r[x.name] = d.substr(l).split(path.sep).join(''); } // no start or end point k.count++; } } each(a.splice(0, 1) ? a : a, r, d, l, n, opt, k, cb); } } }); } else { cb(undefined, r); } } else { // sync if (a.length > 0) { const s = fs.lstatSync(d + path.sep + a[0]); if (s.isDirectory()) { readdir(r, d + path.sep + a[0], l, n, opt, k); } else { if (s.isFile() && d.length === l + n) { const x = path.parse(a[0]); if (x.ext === '.k') { if (opt && typeof opt.start === 'number') { // start point if (k.count >= opt.start) { if (opt && typeof opt.end === 'number') { // and end point (both) if (k.count < opt.end) { r[x.name] = d.substr(l).split(path.sep).join(''); } } else { r[x.name] = d.substr(l).split(path.sep).join(''); } // only start point } } else if (opt && typeof opt.end === 'number') { // only end point if (k.count < opt.end) { r[x.name] = d.substr(l).split(path.sep).join(''); } } else { r[x.name] = d.substr(l).split(path.sep).join(''); } // no start or end point k.count++; } } } each(a.splice(0, 1) ? a : a, r, d, l, n, opt, k); } } } function readdir(r, d, l, n, opt, k, cb) { if (typeof cb === 'function') { // async fs.readdir(d, (e, a) => { if (e) { cb(e); } else { each(a, r, d, l, n, opt, k, cb); } }); } else { // sync each(fs.readdirSync(d), r, d, l, n, opt, k); } } dirdb.prototype.keys = function(dir, opt, cb) { if (typeof opt === 'function' && cb === undefined) { cb = opt; } if (typeof cb === 'function') { // async, range is disabled if (!(typeof dir === 'string' && dir in this.c)) { cb(new Error('dir "' + dir + '" not found')); } else { const l = (this.d + path.sep + dir + path.sep).length, n = divisor(this.c[dir]); // total dir length let r = {}, k = { count: 0 }; // init return object and number of keys range(opt, (e, p) => { if (e) { cb(e); } else { readdir(r, this.d + path.sep + dir, l, n, p, k, cb); } }); } return this; } else { // sync if (!(typeof dir === 'string' && dir in this.c)) { throw new Error('dir "' + dir + '" not found'); } const l = (this.d + path.sep + dir + path.sep).length, n = divisor(this.c[dir]); // total dir length let r = {}, k = { count: 0 }; // init return object and number of keys readdir(r, this.d + path.sep + dir, l, n, range(opt), k); return r; } }; dirdb.prototype.val = function(dir, uid, hash, cb) { if (typeof cb === 'function') { // async, those callbacks are MUCH faster and compact than async/await or Promise ;) if (!(typeof dir === 'string' && dir in this.c)) { cb(new Error('dir "' + dir + '" not found')); } else { if (!(typeof uid === 'string' && uid.split('.').length === 2)) { cb(new Error('invalid uid "' + uid + '"')); } else { const l = hash.length; if (!(typeof hash === 'string' && l >= 22 && l <= 128)) { cb(new Error('invalid hash "' + hash + '"')); } else { const f = this.d + path.sep + dir + path.sep + xpath(hash, this.c[dir].level) + path.sep + uid; fs.readFile(f + '.k', (e, key) => { if (e) { cb(e); } else { fs.readFile(f + '.v', (e, v) => { if (e) { cb(e); } else { uncompress(v, this.c[dir].compress, (e, value) => { if (e) { cb(e); } else { cb(undefined, key, value, f); } }); } }); } }); } } } return this; } else { // sync if (!(typeof dir === 'string' && dir in this.c)) { throw new Error('dir "' + dir + '" not found'); } if (!(typeof uid === 'string' && uid.split('.').length === 2)) { throw new Error('invalid uid "' + uid + '"'); } const l = hash.length; if (!(typeof hash === 'string' && l >= 22 && l <= 128)) { throw new Error('invalid hash "' + hash + '"'); } const f = this.d + path.sep + dir + path.sep + xpath(hash, this.c[dir].level) + path.sep + uid; return { key: fs.readFileSync(f + '.k'), value: uncompress(fs.readFileSync(f + '.v'), this.c[dir].compress), path: f }; } }; dirdb.prototype.setgc = function(dir, opt, cb) { if (typeof cb === 'function') { // async, those callbacks are MUCH faster and compact than async/await or Promise ;) if (!(typeof dir === 'string' && dir in this.c)) { cb(new Error('dir "' + dir + '" not found')); } else { opt = opt ? true : false; const p = this.c[dir]; p.gc = opt; fs.writeFile(this.d + path.sep + dir + path.sep + this.f, JSON.stringify(p), { mode: p.fmode }, e => { if (e) { cb(e); } else { this.c[dir].gc = opt; cb(undefined, this.c[dir]); } }); } return this; } else { // sync if (!(typeof dir === 'string' && dir in this.c)) { throw new Error('dir "' + dir + '" not found'); } opt = opt ? true : false; const p = this.c[dir]; p.gc = opt; fs.writeFileSync(this.d + path.sep + dir + path.sep + this.f, JSON.stringify(p), { mode: p.fmode }); this.c[dir].gc = opt; return this.c[dir]; } }; dirdb.prototype.list = function(cb) { if (typeof cb === 'function') { // async cb(this.c); return this; } else { // sync return this.c; } }; dirdb.prototype.isdir = function(dir, cb) { if (typeof cb === 'function') { // async cb(typeof dir === 'string' && dir in this.c ? this.c[dir] : undefined); return this; } else { // sync return typeof dir === 'string' && dir in this.c ? this.c[dir] : undefined; } }; class server extends rpc.server { constructor(db) { super(request); this.db = db; } } dirdb.prototype.server = function() { return new server(this); }; dirdb.prototype.client = function() { return new client; }; // cache common values const empty = Buffer.allocUnsafeSlow(0); function safeDir(dir) { if (typeof dir !== 'string') { return false; } dir = path.parse(dir).name; if (dir === '' || dir === '.' || dir === '..') { return false; } return dir; } function toBuffer(v) { if (v === undefined) { return empty; } if (!Buffer.isBuffer(v)) { return typeof v === 'string' ? Buffer.from(v) : Buffer.from(v + ''); } return v; } String.prototype.safe64 = function() { if (this.substr(-1) === '=') { return this.substr(0, this.length - 1).safe64(); } return this.replace(/(\/)|(\+)/g, s => { return s === '+' ? '@' : '$'; }); }; function hash(s, h, d) { // safe sync, non-blocking (no I/O) call return d === 'base64' ? crypto.createHash(h).update(s).digest(d).safe64() : crypto.createHash(h).update(s).digest(d); } function xpath(p, l) { // xpath(hash, level) const j = p.length; let r = [], i = 0; for (; i < l; i++) { // for each hash character if (i < j) { r.push(p[i]); } else { break; } } if (i < j) { r.push(p.substring(i)); } // add characters left return r.join(path.sep); } function make(b, p, m, cb) { // make(dirdb.d + path.sep + dir, xpath(hash, level), dirdb.p.dmode) - sync , for async - add 'cb' callback function const i = p.indexOf(path.sep); if (i > 0) { // loop b += path.sep + p.substring(0, i); if (typeof cb === 'function') { // async fs.mkdir(b, m, e => { if (e && e.code !== 'EEXIST') { cb(e); } else { make(b, p.substring(i + 1), m, cb); } }); } else { // sync try { fs.mkdirSync(b, m); make(b, p.substring(i + 1), m); } catch (e) { if (e.code !== 'EEXIST') { throw e; } else { make(b, p.substring(i + 1), m); } } } } else { // final if (typeof cb === 'function') { // async fs.mkdir(b + path.sep + p, m, e => { if (e && e.code !== 'EEXIST') { cb(e); } else { cb(); } }); } else { // sync try { fs.mkdirSync(b + path.sep + p, m); } catch (e) { if (e.code !== 'EEXIST') { throw e; } } } } } function range(opt, cb) { let r; if (typeof opt === 'object' && ('start' in opt || 'end' in opt)) { const start = 'start' in opt ? parseInt(opt.start) : undefined; const end = 'end' in opt ? parseInt(opt.end) : undefined; if (start !== undefined && end !== undefined) { if (start < end && start >= 0 && end > 0) { r = { start: start, end: end }; } else { if (typeof cb === 'function') { cb(new Error('invalid range start "' + start + '", end "' + end + '"')); return; } else { throw new Error('invalid range start "' + start + '", end "' + end + '"'); } } } else if (start !== undefined) { if (start >= 0) { r = { start: start }; } else { if (typeof cb === 'function') { cb(new Error('invalid range start "' + start + '"')); return; } else { throw new Error('invalid range start "' + start + '"'); } } } else if (end !== undefined) { if (end > 0) { r = { end: end }; } else { if (typeof cb === 'function') { cb(new Error('invalid range end "' + end + '"')); return; } else { throw new Error('invalid range end "' + end + '"'); } } } } if (typeof cb === 'function') { cb(undefined, r); } else { return r; } } function compress(value, type, cb) { if (typeof cb === 'function') { // async switch (type) { case 'deflate': zlib.deflate(value, (e, b) => cb(e, b)); break; case 'gzip': zlib.gzip(value, (e, b) => cb(e, b)); break; // case 'snappy': snappy.compress(value, (e, b) => cb(e, b)); break; case 'none': cb(undefined, value); break; default: cb(new Error('invalid compress type')); } } else { // sync switch (type) { case 'deflate': return zlib.deflateSync(value); case 'gzip': return zlib.gzipSync(value); // case 'snappy': return snappy.compressSync(value); case 'none': return value; default: throw new Error('invalid compress type'); } } } function uncompress(value, type, cb) { if (typeof cb === 'function') { // a