@iobroker/db-objects-file
Version:
The Library contains the Database classes for File based objects database client and server.
938 lines • 43 kB
JavaScript
/**
* Objects DB in memory - Server with Redis protocol
*
* Copyright 2013-2024 bluefox <dogafox@gmail.com>
*
* MIT License
*
*/
import net from 'node:net';
import fs from 'fs-extra';
import path from 'node:path';
import crypto from 'node:crypto';
import { objectsUtils as utils } from '@iobroker/db-objects-redis';
import { tools } from '@iobroker/db-base';
import { getLocalAddress } from '@iobroker/js-controller-common-db/tools';
import { EXIT_CODES } from '@iobroker/js-controller-common-db';
import { RedisHandler } from '@iobroker/db-base';
import { ObjectsInMemoryFileDB } from './objectsInMemFileDB.js';
// settings = {
// change: function (id, state) {},
// connected: function (nameOfServer) {},
// logger: {
// silly: function (msg) {},
// debug: function (msg) {},
// info: function (msg) {},
// warn: function (msg) {},
// error: function (msg) {}
// },
// connection: {
// dataDir: 'relative path'
// },
// auth: null, //unused
// secure: true/false,
// certificates: as required by createServer
// port: 9001,
// host: localhost
// };
//
/**
* This class inherits statesInMemoryFileDB class and adds redis communication layer
* to access the methods via redis protocol
*/
export class ObjectsInMemoryServer extends ObjectsInMemoryFileDB {
/**
* Constructor
*
* @param settings State and InMem-DB settings
*/
constructor(settings) {
super(settings);
this.serverConnections = {};
this.namespaceObjects = `${this.settings.redisNamespace || (settings.connection && settings.connection.redisNamespace) || 'cfg'}.`;
this.namespaceFile = `${this.namespaceObjects}f.`;
this.namespaceObj = `${this.namespaceObjects}o.`;
this.namespaceSet = `${this.namespaceObjects}s.`;
this.namespaceSetLen = this.namespaceSet.length;
// this.namespaceObjectsLen = this.namespaceObjects.length;
this.namespaceFileLen = this.namespaceFile.length;
this.namespaceObjLen = this.namespaceObj.length;
this.namespaceMeta = `${this.settings.namespaceMeta || 'meta'}.`;
this.namespaceMetaLen = this.namespaceMeta.length;
this.knownScripts = {};
this.normalizeFileRegex1 = new RegExp('^(.*)\\$%\\$(.*)\\$%\\$(meta|data)$');
this.normalizeFileRegex2 = new RegExp('^(.*)\\$%\\$(.*)\\/?\\*$');
this.open()
.then(() => {
return this._initRedisServer(this.settings.connection);
})
.then(() => {
this.log.debug(`${this.namespace} ${settings.secure ? 'Secure ' : ''} Redis inMem-objects listening on port ${settings.port || 9001}`);
if (typeof this.settings.connected === 'function') {
setImmediate(() => this.settings.connected());
}
})
.catch(e => {
this.log.error(`${this.namespace} Cannot start inMem-objects on port ${settings.port || 9001}: ${e.message}`);
process.exit(EXIT_CODES.NO_CONNECTION_TO_OBJ_DB);
});
}
/**
* Separate Namespace from ID and return both
*
* @param idWithNamespace ID or Array of IDs containing a redis namespace and the real ID
* @returns Object with namespace and the
* ID/Array of IDs without the namespace
*/
_normalizeId(idWithNamespace) {
let ns = this.namespaceObjects;
let id = null;
let name = '';
let isMeta;
if (Array.isArray(idWithNamespace)) {
const ids = [];
idWithNamespace.forEach(el => {
const { id, namespace } = this._normalizeId(el);
ids.push(id);
ns = namespace; // we ignore the pot. case from arrays with different namespaces
});
id = ids;
}
else if (typeof idWithNamespace === 'string') {
id = idWithNamespace;
if (idWithNamespace.startsWith(this.namespaceObjects)) {
let idx = -1;
if (idWithNamespace.startsWith(this.namespaceObj)) {
idx = this.namespaceObjLen;
}
else if (idWithNamespace.startsWith(this.namespaceFile)) {
idx = this.namespaceFileLen;
}
else if (idWithNamespace.startsWith(this.namespaceSet)) {
idx = this.namespaceSetLen;
}
if (idx !== -1) {
ns = idWithNamespace.substr(0, idx);
id = idWithNamespace.substr(idx);
}
if (ns === this.namespaceFile) {
let fileIdDetails = id.match(this.normalizeFileRegex1);
if (fileIdDetails) {
id = fileIdDetails[1];
name = fileIdDetails[2] || '';
isMeta = fileIdDetails[3] === 'meta';
}
else {
fileIdDetails = id.match(this.normalizeFileRegex2);
if (fileIdDetails) {
id = fileIdDetails[1];
name = fileIdDetails[2] || '';
isMeta = undefined;
}
else {
name = '';
isMeta = undefined;
}
}
}
}
else if (idWithNamespace.startsWith(this.namespaceMeta)) {
const idx = this.namespaceMetaLen;
if (idx !== -1) {
ns = idWithNamespace.substr(0, idx);
id = idWithNamespace.substr(idx);
}
}
}
return { id, namespace: ns, name, isMeta };
}
/**
* Publish a subscribed value to one of the redis connections in redis format
*
* @param client Instance of RedisHandler
* @param type Type of subscribed key
* @param id Subscribed ID
* @param obj Object to publish
* @returns Publish counter 0 or 1 depending on if send out or not
*/
publishToClients(client, type, id, obj) {
if (!client._subscribe || !client._subscribe[type]) {
return 0;
}
const s = client._subscribe[type];
const found = s.find(sub => sub.regex.test(id));
if (found) {
if (type === 'meta') {
this.log.silly(`${this.namespace} Redis Publish Meta ${id}=${obj}`);
const sendPattern = this.namespaceMeta + found.pattern;
const sendId = this.namespaceMeta + id;
client.sendArray(null, ['pmessage', sendPattern, sendId, obj]);
}
else if (type === 'files') {
const objString = JSON.stringify(obj);
this.log.silly(`${this.namespace} Redis Publish File ${id}=${objString}`);
const sendPattern = this.namespaceFile + found.pattern;
const sendId = this.namespaceFile + id;
client.sendArray(null, ['pmessage', sendPattern, sendId, objString]);
}
else {
const objString = JSON.stringify(obj);
this.log.silly(`${this.namespace} Redis Publish Object ${id}=${objString}`);
const sendPattern = (type === 'objects' ? '' : this.namespaceObjects) + found.pattern;
const sendId = (type === 'objects' ? this.namespaceObj : this.namespaceObjects) + id;
client.sendArray(null, ['pmessage', sendPattern, sendId, objString]);
}
return 1;
}
return 0;
}
/**
* Generate ID for a File
*
* @param id ID of the File
* @param name Name of the file
* @param isMeta generate a META ID or a Data ID?
* @returns File-ID
*/
getFileId(id, name, isMeta) {
// e.g. ekey.admin and admin/ekey.png
if (id.endsWith('.admin')) {
if (name.startsWith('admin/')) {
name = name.replace(/^admin\//, '');
}
else if (name.match(/^iobroker.[-\d\w]\/admin\//i)) {
// e.g. ekey.admin and iobroker.ekey/admin/ekey.png
name = name.replace(/^iobroker.[-\d\w]\/admin\//i, '');
}
}
return `${this.namespaceFile + id}$%$${name}${isMeta !== undefined ? (isMeta ? '$%$meta' : '$%$data') : ''}`;
}
/**
* Register all event listeners for Handler and implement the relevant logic
*
* @param handler RedisHandler instance
*/
_socketEvents(handler) {
let connectionName = null;
let namespaceLog = this.namespace;
// Handle Redis "INFO" request
handler.on('info', (_data, responseId) => {
let infoString = '# Server\r\n';
infoString += 'redis_version:3.0.0-iobroker\r\n';
infoString += '# Clients\r\n';
infoString += '# Memory\r\n';
infoString += '# Persistence\r\n';
infoString += '# Stats\r\n';
infoString += '# Replication\r\n';
infoString += '# CPU\r\n';
infoString += '# Cluster\r\n';
infoString += '# Keyspace\r\n';
infoString += `db0:keys=${Object.keys(this.dataset).length},expires=0,avg_ttl=98633637897`;
handler.sendBulk(responseId, infoString);
});
// Handle Redis "QUIT" request
handler.on('quit', (_data, responseId) => {
this.log.silly(`${namespaceLog} Redis QUIT received, close connection`);
handler.sendString(responseId, 'OK');
handler.close();
});
// Handle Redis "SCRIPT" request
handler.on('script', (data, responseId) => {
data[0] = data[0].toLowerCase();
if (data[0] === 'exists') {
data.shift();
const scripts = [];
data.forEach(checksum => scripts.push(this.knownScripts[checksum] ? 1 : 0));
handler.sendArray(responseId, scripts);
}
else if (data[0] === 'load') {
const shasum = crypto.createHash('sha1');
const buf = Buffer.from(data[1]);
shasum.update(buf);
const scriptChecksum = shasum.digest('hex');
const scriptDesign = data[1].match(/^-- design: ([a-z0-9A-Z-.]+)\s/m);
const scriptFunc = data[1].match(/^-- func: (.+)$/m);
if (scriptDesign && scriptDesign[1]) {
const design = scriptDesign[1];
let search = null;
const scriptSearch = data[1].match(/^-- search: ([a-z0-9A-Z-.]*)\s/m);
if (scriptSearch && scriptSearch[1]) {
search = scriptSearch[1];
}
this.knownScripts[scriptChecksum] = { design: design, search: search };
if (this.settings.connection.enhancedLogging) {
this.log.silly(`${namespaceLog} Register View LUA Script: ${scriptChecksum} = ${JSON.stringify(this.knownScripts[scriptChecksum])}`);
}
handler.sendBulk(responseId, scriptChecksum);
}
else if (scriptFunc && scriptFunc[1]) {
this.knownScripts[scriptChecksum] = { func: scriptFunc[1] };
if (this.settings.connection.enhancedLogging) {
this.log.silly(`${namespaceLog} Register Func LUA Script: ${scriptChecksum} = ${JSON.stringify(this.knownScripts[scriptChecksum])}`);
}
handler.sendBulk(responseId, scriptChecksum);
}
else if (data[1].includes('-- REDLOCK SCRIPT')) {
// redlock scripts are currently not needed for Simulator
this.knownScripts[scriptChecksum] = { redlock: true };
if (this.settings.connection.enhancedLogging) {
this.log.silly(`${namespaceLog} Register Func LUA Script: ${scriptChecksum} = ${JSON.stringify(this.knownScripts[scriptChecksum])}`);
}
handler.sendBulk(responseId, scriptChecksum);
}
else {
handler.sendError(responseId, new Error(`Unknown LUA script ${data[1]}`));
}
}
else {
handler.sendError(responseId, new Error(`Unsupported Script command ${data[0]}`));
}
});
// Handle Redis "EVALSHA" request
handler.on('evalsha', (data, responseId) => {
if (!this.knownScripts[data[0]]) {
return void handler.sendError(responseId, new Error(`Unknown Script ${data[0]}`));
}
if (this.knownScripts[data[0]].design) {
const scriptDesign = this.knownScripts[data[0]].design;
if (typeof data[2] === 'string' && data[2].startsWith(this.namespaceObj) && data.length > 4) {
let scriptSearch = this.knownScripts[data[0]].search;
if (scriptDesign === 'system' && !scriptSearch && data[5]) {
scriptSearch = data[5];
}
if (!scriptSearch) {
scriptSearch = 'state';
}
if (this.settings.connection.enhancedLogging) {
this.log.silly(`${namespaceLog} Script transformed into getObjectView: design=${scriptDesign}, search=${scriptSearch}`);
}
let objs;
try {
objs = this._getObjectView(scriptDesign, scriptSearch, {
startkey: data[3],
endkey: data[4],
include_docs: true,
});
}
catch (err) {
return void handler.sendError(responseId, new Error(`_getObjectView Error for ${scriptDesign}/${scriptSearch}: ${err.message}`));
}
const res = objs.rows.map(obj => JSON.stringify(this.dataset[obj.value._id || obj.id]));
handler.sendArray(responseId, res);
}
}
else if (this.knownScripts[data[0]].func && data.length > 4) {
const scriptFunc = { map: this.knownScripts[data[0]].func.replace('%1', data[5]) };
if (this.settings.connection.enhancedLogging) {
this.log.silly(`${namespaceLog} Script transformed into _applyView: func=${scriptFunc.map}`);
}
const objs = this._applyView(scriptFunc, {
startkey: data[3],
endkey: data[4],
include_docs: true,
});
const res = objs.rows.map(obj => JSON.stringify(this.dataset[obj.value._id || obj.id]));
return void handler.sendArray(responseId, res);
}
else if (this.knownScripts[data[0]].redlock) {
// just return a dummy
return void handler.sendArray(responseId, [0]);
}
else {
handler.sendError(responseId, new Error(`Unknown LUA script eval call ${JSON.stringify(data)}`));
}
});
// Handle Redis "PUBLISH" request
handler.on('publish', (data, responseId) => {
const { id, namespace } = this._normalizeId(data[0]);
if (namespace === this.namespaceObj ||
namespace === this.namespaceMeta ||
namespace === this.namespaceFile) {
// a "set" always comes afterwards, so do not publish
return void handler.sendInteger(responseId, 0); // do not publish for now
}
const publishCount = this.publishAll(namespace.substr(0, namespace.length - 1), id, JSON.parse(data[1]));
handler.sendInteger(responseId, publishCount);
});
// Handle Redis "MGET" requests
handler.on('mget', (data, responseId) => {
if (!data || !data.length) {
return void handler.sendArray(responseId, []);
}
const { namespace, isMeta } = this._normalizeId(data[0]);
if (namespace === this.namespaceObj) {
const keys = [];
data.forEach(dataId => {
const { id, namespace } = this._normalizeId(dataId);
if (namespace !== this.namespaceObj) {
keys.push(null);
this.log.warn(`${namespaceLog} Got MGET request for non Object-ID in Objects-ID chunk for ${namespace} / ${dataId}`);
return;
}
keys.push(id);
});
let result;
try {
result = this._getObjects(keys);
}
catch (err) {
return void handler.sendError(responseId, new Error(`ERROR _getObjects: ${err.message}`));
}
result = result.map(el => (el ? JSON.stringify(el) : null));
handler.sendArray(responseId, result);
}
else if (namespace === this.namespaceFile) {
// Handle request for Meta data for files
if (isMeta) {
const response = [];
data.forEach(dataId => {
const { id, namespace, name } = this._normalizeId(dataId);
if (namespace !== this.namespaceFile) {
response.push(null);
this.log.warn(`${namespaceLog} Got MGET request for non File ID in File-ID chunk for ${dataId}`);
return;
}
this._loadFileSettings(id);
if (!this.fileOptions[id] || !this.fileOptions[id][name]) {
response.push(null);
return;
}
const obj = this._clone(this.fileOptions[id][name]);
try {
obj.stats = fs.statSync(path.join(this.objectsDir, id, name));
}
catch (err) {
if (!name.endsWith('/_data.json')) {
this.log.warn(`${namespaceLog} Got MGET request for non existing file ${dataId}, err: ${err.message}`);
}
response.push(null);
return;
}
response.push(JSON.stringify(obj));
});
handler.sendArray(responseId, response);
}
else {
// Handle request for File data
handler.sendError(responseId, new Error('MGET-UNSUPPORTED for file data'));
}
}
else {
handler.sendError(responseId, new Error(`MGET-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`));
}
});
// Handle Redis "GET" requests
handler.on('get', (data, responseId) => {
const { id, namespace, name, isMeta } = this._normalizeId(data[0]);
if (namespace === this.namespaceObj) {
const result = this._getObject(id);
if (!result) {
handler.sendNull(responseId);
}
else {
handler.sendBulk(responseId, JSON.stringify(result));
}
}
else if (namespace === this.namespaceFile) {
// Handle request for Meta data for files
if (isMeta) {
let stats;
try {
stats = fs.statSync(path.join(this.objectsDir, id, name));
}
catch {
return void handler.sendNull(responseId);
}
if (stats.isDirectory()) {
return void handler.sendBulk(responseId, JSON.stringify({
file: name,
stats: {},
isDir: true,
}));
}
this._loadFileSettings(id);
if (!this.fileOptions[id] || !this.fileOptions[id][name]) {
return void handler.sendNull(responseId);
}
let obj = this._clone(this.fileOptions[id][name]);
if (typeof obj !== 'object') {
obj = {
mimeType: obj,
acl: {
owner: (this.defaultNewAcl && this.defaultNewAcl.owner) || utils.CONSTS.SYSTEM_ADMIN_USER,
ownerGroup: (this.defaultNewAcl && this.defaultNewAcl.ownerGroup) ||
utils.CONSTS.SYSTEM_ADMIN_GROUP,
permissions: (this.defaultNewAcl && this.defaultNewAcl.file.permissions) ||
utils.CONSTS.ACCESS_USER_ALL |
utils.CONSTS.ACCESS_GROUP_ALL |
utils.CONSTS.ACCESS_EVERY_ALL, // 777
},
};
}
obj.stats = stats;
handler.sendBulk(responseId, JSON.stringify(obj));
}
else {
// Handle request for File data
let data;
try {
data = this._readFile(id, name);
}
catch {
return void handler.sendNull(responseId);
}
if (data.fileContent === undefined || data.fileContent === null) {
return void handler.sendNull(responseId);
}
let fileData = data.fileContent;
if (!Buffer.isBuffer(fileData) && tools.isObject(fileData)) {
// if its an invalid object, stringify it and log warning
fileData = JSON.stringify(fileData);
this.log.warn(`${namespaceLog} Data of "${id}/${name}" has invalid structure at file data request: ${fileData}`);
}
handler.sendBufBulk(responseId, Buffer.from(fileData));
}
}
else if (namespace === this.namespaceMeta) {
// special handling for the primaryHost
if (id === 'objects.primaryHost') {
// we are the server -> we are primary
handler.sendString(this.settings.hostname);
}
else {
const result = this.getMeta(id);
if (result === undefined || result === null) {
handler.sendNull(responseId);
}
else {
handler.sendBulk(responseId, result);
}
}
}
else {
handler.sendError(responseId, new Error(`GET-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`));
}
});
// Handle Redis "SET" requests
handler.on('set', (data, responseId) => {
const { id, namespace, name, isMeta } = this._normalizeId(data[0]);
if (namespace === this.namespaceObj) {
try {
const obj = JSON.parse(data[1].toString('utf-8'));
this._setObjectDirect(id, obj);
}
catch (err) {
return void handler.sendError(responseId, new Error(`ERROR setObject id=${id}: ${err.message}`));
}
handler.sendString(responseId, 'OK');
}
else if (namespace === this.namespaceFile) {
// Handle request to set meta-data, we ignore it because
// will be set when data are written
if (isMeta) {
this._loadFileSettings(id);
try {
fs.ensureDirSync(path.join(this.objectsDir, id, path.dirname(name)));
// only set if the meta-object is already/still existing
if (this.fileOptions[id]) {
this.fileOptions[id][name] = JSON.parse(data[1].toString('utf-8'));
fs.writeFileSync(path.join(this.objectsDir, id, '_data.json'), JSON.stringify(this.fileOptions[id]));
}
}
catch (err) {
return void handler.sendError(responseId, new Error(`ERROR writeFile-Meta id=${id}: ${err.message}`));
}
handler.sendString(responseId, 'OK');
}
else {
// Handle request to write the file
try {
this._writeFile(id, name, data[1]);
}
catch (err) {
return void handler.sendError(responseId, new Error(`ERROR writeFile id=${id}: ${err.message}`));
}
handler.sendString(responseId, 'OK');
}
}
else if (namespace === this.namespaceMeta) {
this.setMeta(id, data[1].toString('utf-8'));
handler.sendString(responseId, 'OK');
}
else {
handler.sendError(responseId, new Error(`SET-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`));
}
});
// Handle Redis "RENAME" requests
handler.on('rename', (data, responseId) => {
const oldDetails = this._normalizeId(data[0]);
const newDetails = this._normalizeId(data[1]);
if (oldDetails.namespace === this.namespaceFile) {
if (oldDetails.id !== newDetails.id) {
return void handler.sendError(responseId, new Error('ERROR renameObject: id needs to stay the same'));
}
// Handle request for Meta data for files
if (oldDetails.isMeta) {
handler.sendString(responseId, 'OK');
}
else {
// Handle request for File data
try {
this._rename(oldDetails.id, oldDetails.name, newDetails.name);
}
catch {
return void handler.sendNull(responseId);
}
handler.sendString(responseId, 'OK');
}
}
else {
handler.sendError(responseId, new Error(`RENAME-UNSUPPORTED for namespace ${oldDetails.namespace}: Data=${JSON.stringify(data)}`));
}
});
// Handle Redis "DEL" request for state and session namespace
handler.on('del', (data, responseId) => {
const { id, namespace, name, isMeta } = this._normalizeId(data[0]);
if (namespace === this.namespaceObj) {
try {
this._delObject(id);
}
catch (err) {
return void handler.sendError(responseId, err);
}
handler.sendInteger(responseId, 1);
}
else if (namespace === this.namespaceFile) {
// Handle request to delete meta-data, we ignore it because
// will be removed when data are deleted
if (isMeta) {
handler.sendString(responseId, 'OK');
}
else {
// Handle request to remove the file
try {
this._unlink(id, name);
}
catch (err) {
return void handler.sendError(responseId, err);
}
handler.sendString(responseId, 'OK');
}
}
else {
handler.sendError(responseId, new Error(`DEL-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`));
}
});
handler.on('exists', (data, responseId) => {
if (!data || !data.length) {
return void handler.sendInteger(responseId, 0);
}
// Note: we only simulate single key existence check
const { id, namespace, name } = this._normalizeId(data[0]);
if (namespace === this.namespaceObj) {
let exists;
try {
exists = this._objectExists(id);
}
catch (e) {
return void handler.sendError(responseId, e);
}
handler.sendInteger(responseId, exists ? 1 : 0);
}
else if (namespace === this.namespaceFile) {
let exists;
try {
exists = this._fileExists(id, name);
}
catch (e) {
return void handler.sendError(responseId, e);
}
handler.sendInteger(responseId, exists ? 1 : 0);
}
else if (namespace === this.namespaceSet) {
// we are not using sets in simulator, so just say it exists
return void handler.sendInteger(responseId, 1);
}
else {
handler.sendError(responseId, new Error(`EXISTS-UNSUPPORTED for namespace ${namespace}`));
}
});
// handle Redis "SCAN" request for objects namespace
handler.on('scan', (data, responseId) => {
if (!data || data.length < 3) {
return void handler.sendArray(responseId, ['0', []]);
}
return this._handleScanOrKeys(handler, data[2], responseId, true);
});
// Handle Redis "KEYS" request for state namespace
handler.on('keys', (data, responseId) => {
if (!data || !data.length) {
return void handler.sendArray(responseId, []);
}
return this._handleScanOrKeys(handler, data[0], responseId);
});
// commands for redis SETS, just dummies
handler.on('sadd', (data, responseId) => {
return void handler.sendInteger(responseId, 1);
});
handler.on('srem', (data, responseId) => {
return void handler.sendInteger(responseId, 1);
});
handler.on('eval', (data, responseId) => {
return void handler.sendNull(responseId);
});
handler.on('sscan', (data, responseId) => {
// for file DB it does the same as scan but data looks different
if (!data || data.length < 4) {
return void handler.sendArray(responseId, ['0', []]);
}
return this._handleScanOrKeys(handler, data[3], responseId, true);
});
// Handle Redis "PSUBSCRIBE" request for state, log and session namespace
handler.on('psubscribe', (data, responseId) => {
const { id, namespace, name } = this._normalizeId(data[0]);
if (namespace === this.namespaceObj) {
this._subscribeConfigForClient(handler, id);
handler.sendArray(responseId, ['psubscribe', data[0], 1]);
}
else if (namespace === this.namespaceMeta) {
this._subscribeMeta(handler, id);
handler.sendArray(responseId, ['psubscribe', data[0], 1]);
}
else if (namespace === this.namespaceFile) {
this._subscribeFileForClient(handler, id, name);
handler.sendArray(responseId, ['psubscribe', data[0], 1]);
}
else {
handler.sendError(responseId, new Error(`PSUBSCRIBE-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`));
}
});
// Handle Redis "UNSUBSCRIBE" request for state, log and session namespace
handler.on('punsubscribe', (data, responseId) => {
const { id, namespace, name } = this._normalizeId(data[0]);
if (namespace === this.namespaceObj) {
this._unsubscribeConfigForClient(handler, id);
handler.sendArray(responseId, ['punsubscribe', data[0], 1]);
}
else if (namespace === this.namespaceFile) {
this._unsubscribeFileForClient(handler, id, name);
handler.sendArray(responseId, ['punsubscribe', data[0], 1]);
}
else {
handler.sendError(responseId, new Error(`PUNSUBSCRIBE-UNSUPPORTED for namespace ${namespace}: Data=${JSON.stringify(data)}`));
}
});
// Handle Redis "SUBSCRIBE" ... currently mainly ignored
handler.on('subscribe', (data, responseId) => {
if (data[0].startsWith('__keyevent@')) {
// we ignore these type of events because we publish expires anyway directly
handler.sendArray(responseId, ['subscribe', data[0], 1]);
}
else {
handler.sendError(responseId, new Error(`SUBSCRIBE-UNSUPPORTED for ${data[0]}`));
}
});
// Handle Redis "CONFIG" ... currently mainly ignored
handler.on('config', (data, responseId) => {
const command = typeof data[0] === 'string' ? data[0].toLowerCase() : data[0].toString().toLowerCase();
if (command === 'set' && data[1] === 'notify-keyspace-events') {
// we ignore these type of commands for now, should only be to subscribe to keyspace events
handler.sendString(responseId, 'OK');
}
else if (command === 'set' && data[1] === 'lua-time-limit') {
// we ignore these type of commands for now, irrelevant
handler.sendString(responseId, 'OK');
}
else {
handler.sendError(responseId, new Error(`CONFIG-UNSUPPORTED for ${JSON.stringify(data)}`));
}
});
// handle client SETNAME/GETNAME
handler.on('client', (data, responseId) => {
if (data[0] === 'setname' && typeof data[1] === 'string') {
if (data[1] === '') {
// on empty string redis sets null again and sends 'OK'
connectionName = null;
}
else {
connectionName = data[1];
namespaceLog = connectionName;
}
handler.sendString(responseId, 'OK');
}
else if (data[0] === 'getname') {
if (typeof connectionName === 'string' && connectionName !== '') {
handler.sendString(responseId, connectionName);
}
else {
// redis sends null if no name defined
handler.sendNull(responseId);
}
}
else {
handler.sendError(responseId, new Error(`CLIENT-UNSUPPORTED for ${JSON.stringify(data)}`));
}
});
handler.on('error', err => this.log.warn(`${namespaceLog} Redis objects: ${err}`));
}
/**
* Return connected RedisHandlers/Connections
*
* @returns
*/
getClients() {
return this.serverConnections;
}
/**
* Destructor of the class. Called by shutting down.
*/
async destroy() {
if (this.server) {
Object.keys(this.serverConnections).forEach(s => {
this.serverConnections[s].close();
delete this.serverConnections[s];
});
await new Promise(resolve => {
if (!this.server) {
return void resolve();
}
try {
this.server.close(() => resolve());
}
catch (e) {
console.log(e.message);
resolve();
}
});
}
await super.destroy();
}
/**
* Get keys matching pattern and send it to given responseId, for "SCAN" and "KEYS" - Objects and files supported
*
* @param handler RedisHandler instance
* @param pattern - pattern without namespace prefix
* @param responseId - Id where response will be sent to
* @param isScan - if used by "SCAN" this flag should be true
*/
_handleScanOrKeys(handler, pattern, responseId, isScan = false) {
const { id, namespace, name, isMeta } = this._normalizeId(pattern);
let response = [];
if (namespace === this.namespaceObj || namespace === this.namespaceObjects) {
try {
response = this._getKeys(id).map(val => this.namespaceObj + val);
}
catch (e) {
return void handler.sendError(responseId, e);
}
// if scan, we send the cursor as first argument
if (namespace !== this.namespaceObjects) {
// When it was not the full DB namespace send out response
return void handler.sendArray(responseId, isScan ? ['0', response] : response);
}
}
if (namespace === this.namespaceFile || namespace === this.namespaceObjects) {
// Handle request to get meta data keys
if (isMeta === undefined) {
let res;
try {
res = this._readDir(id, name);
if (!res || !res.length) {
res = [
{
file: '_data.json',
stats: {},
isDir: false,
virtualFile: true,
notExists: true,
},
];
}
}
catch (e) {
if (!e.message.endsWith(utils.ERRORS.ERROR_NOT_FOUND)) {
return void handler.sendError(responseId, new Error(`ERROR readDir id=${id}: ${e.message}`));
}
res = [];
}
let baseName = name || '';
if (baseName.length && !baseName.endsWith('/')) {
baseName += '/';
}
res.forEach(arr => {
let entryId = id;
if (arr.isDir) {
if (entryId === '' || entryId === '*') {
entryId = arr.file;
arr.file = '_data.json'; // We return a "virtual file" to mark the directory as existing
}
else {
arr.file += '/_data.json'; // We return a "virtual file" to mark the directory as existing
}
}
// We need to simulate the Meta data here, so return both
response.push(this.getFileId(entryId, baseName + arr.file, true));
response.push(this.getFileId(entryId, baseName + arr.file, false));
});
handler.sendArray(responseId, isScan ? ['0', response] : response); // send out file or full db response
}
else {
// such a request should never happen
handler.sendArray(responseId, isScan ? ['0', []] : []); // send out file or full db response
}
}
else if (namespace === this.namespaceSet) {
handler.sendArray(responseId, isScan ? ['0', []] : []); // send out empty array, we have no sets
}
else {
handler.sendError(responseId, new Error(`${isScan ? 'SCAN' : 'KEYS'}-UNSUPPORTED for namespace ${namespace}: Pattern=${pattern}`));
}
}
/**
* Initialize RedisHandler for a new network connection
*
* @param socket Network socket
*/
_initSocket(socket) {
if (this.settings.connection.enhancedLogging) {
this.log.silly(`${this.namespace} Handling new Redis Objects connection`);
}
const options = {
log: this.log,
logScope: `${this.settings.namespace || ''} Objects`,
handleAsBuffers: true,
enhancedLogging: this.settings.connection.enhancedLogging,
};
const handler = new RedisHandler(socket, options);
this._socketEvents(handler);
this.serverConnections[`${socket.remoteAddress}:${socket.remotePort}`] = handler;
socket.on('close', () => {
if (this.serverConnections[`${socket.remoteAddress}:${socket.remotePort}`]) {
delete this.serverConnections[`${socket.remoteAddress}:${socket.remotePort}`];
}
});
}
/**
* Initialize Redis Server
*
* @param settings Settings object
* @returns
*/
_initRedisServer(settings) {
return new Promise((resolve, reject) => {
if (settings.secure) {
reject(new Error('Secure Redis unsupported for File-DB'));
}
try {
this.server = net.createServer();
this.server.on('error', err => this.log.info(`${this.namespace} ${settings.secure ? 'Secure ' : ''} Error inMem-objects listening on port ${settings.port || 9001}: ${err}`));
this.server.on('connection', socket => this._initSocket(socket));
this.server.listen(settings.port || 9001, settings.host === 'localhost' ? getLocalAddress() : settings.host ? settings.host : undefined, () => resolve());
}
catch (err) {
reject(err);
}
});
}
}
//# sourceMappingURL=objectsInMemServerRedis.js.map