whaler
Version:
Define and run multi-container applications with Docker
295 lines (262 loc) • 6.8 kB
JavaScript
;
const path = require('path');
const util = require('util');
const crypto = require('crypto');
const fs = require('fs').promises;
const stackTrace = require('stack-trace');
const filenameReservedRegex = /[<>:"/\\|?*\u0000-\u001F]/g;
const md5 = data => crypto.createHash('md5').update(data).digest('hex');
class StorageError extends Error {
/**
* @param {string} message
* @param {string} code
*/
constructor(message, code) {
super(message);
this.name = 'StorageError';
this.code = code;
}
}
class NotImplementedError extends StorageError {
constructor() {
const trace = stackTrace.parse(new Error('not-implemented'));
trace.shift();
super(util.format('`%s` isn\'t implemented.', trace[0]['functionName']), 'ERR_NOT_IMPLEMENTED');
}
}
class BaseAdapter {
async *[Symbol.asyncIterator]() {
const db = await this.all();
for (let key in db) {
yield [key, db[key]];
}
}
/**
* @returns {Promise.<Object>}
*/
async all() {
throw new NotImplementedError();
}
/**
* @param {string} key
* @returns {Promise.<Object>}
*/
async get(key) {
throw new NotImplementedError();
}
/**
* @param {string} key
* @param {Object} value
* @returns {Promise.<Object>}
*/
async set(key, value) {
throw new NotImplementedError();
}
/**
* @param {string} key
* @param {Object} value
* @returns {Promise.<Object>}
*/
async insert(key, value) {
throw new NotImplementedError();
}
/**
* @param {string} key
* @param {Object} value
* @returns {Promise.<Object>}
*/
async update(key, value) {
throw new NotImplementedError();
}
/**
* @param {string} key
*/
async remove(key) {
throw new NotImplementedError();
}
}
class InMemoryAdapter extends BaseAdapter {
/**
* @param {string} name
*/
constructor(/* name */) {
super();
this.db = {};
}
/**
* @private
*/
exists(key) {
return this.db.hasOwnProperty(key);
}
/**
* @returns {Promise.<Object>}
*/
async all() {
return this.db;
}
/**
* @param {string} key
* @returns {Promise.<Object>}
*/
async get(key) {
if (!this.exists(key)) {
throw new StorageError(util.format('`%s` not found.', key), 'ERR_NOT_FOUND');
}
return this.db[key];
}
/**
* @param {string} key
* @param {Object} value
* @returns {Promise.<Object>}
*/
async set(key, value) {
return this.db[key] = value;
}
/**
* @param {string} key
* @param {Object} value
* @returns {Promise.<Object>}
*/
async insert(key, value) {
if (this.exists(key)) {
throw new StorageError(util.format('`%s` already exists.', key), 'ERR_ALREADY_EXISTS');
}
return this.set(key, value);
}
/**
* @param {string} key
* @param {Object} value
* @returns {Promise.<Object>}
*/
async update(key, value) {
const data = await this.get(key);
return this.set(key, { ...data, ...value });
}
/**
* @param {string} key
*/
async remove(key) {
if (!this.exists(key)) {
throw new StorageError(util.format('`%s` not found.', key), 'ERR_NOT_FOUND');
}
delete this.db[key];
}
}
class FileSystemAdapter extends BaseAdapter {
/**
* @param {string} name
*/
constructor(name) {
super();
this.path = path.join('/var/lib/whaler/storage/fs', name);
}
/**
* @private
*/
getFilePath(key) {
const fileName = md5(key).substr(0, 7) + '@' + key.replace(filenameReservedRegex, '_');
return path.join(this.path, fileName);
}
/**
* @private
*/
async exists(key) {
try {
await fs.stat(this.getFilePath(key));
return true;
} catch (e) {}
return false;
}
/**
* @returns {Promise.<Object>}
*/
async all() {
const db = {};
try {
const arr = await fs.readdir(this.path);
for (let fileName of arr) {
const filePath = path.join(this.path, fileName);
try {
const content = await fs.readFile(filePath, 'utf8');
const data = JSON.parse(content);
db[data['key']] = data['value'];
} catch (e) {}
}
} catch (e) {}
return db;
}
/**
* @param {string} key
* @returns {Promise.<Object>}
*/
async get(key) {
if (await this.exists(key)) {
const content = await fs.readFile(this.getFilePath(key), 'utf8');
const data = JSON.parse(content);
return data['value'];
}
throw new StorageError(util.format('`%s` not found.', key), 'ERR_NOT_FOUND');
}
/**
* @param {string} key
* @param {Object} value
* @returns {Promise.<Object>}
*/
async set(key, value) {
await fs.mkdir(this.path, { recursive: true });
await fs.writeFile(this.getFilePath(key), JSON.stringify({ key, value }, null, ' '.repeat(4)));
return value;
}
/**
* @param {string} key
* @param {Object} value
* @returns {Promise.<Object>}
*/
async insert(key, value) {
if (await this.exists(key)) {
throw new StorageError(util.format('`%s` already exists.', key), 'ERR_ALREADY_EXISTS');
}
return this.set(key, value);
}
/**
* @param {string} key
* @param {Object} value
* @returns {Promise.<Object>}
*/
async update(key, value) {
const data = await this.get(key);
return this.set(key, { ...data, ...value });
}
/**
* @param {string} key
*/
async remove(key) {
if (await this.exists(key)) {
return fs.unlink(this.getFilePath(key));
}
throw new StorageError(util.format('`%s` not found.', key), 'ERR_NOT_FOUND');
}
}
class Storage {
/**
* @param {mixed} adapter
*/
constructor(adapter = InMemoryAdapter) {
this.adapter = adapter;
}
/**
* @param {string} name
* @returns {mixed}
*/
create(name) {
if (name) {
return new this.adapter(name);
}
return new InMemoryAdapter(); // for tests
}
}
Storage.Error = StorageError;
Storage.InMemoryAdapter = InMemoryAdapter;
Storage.FileSystemAdapter = FileSystemAdapter;
module.exports = Storage;