dirdb
Version:
DirDB key-value directory database
952 lines (941 loc) • 50.8 kB
JavaScript
/* 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