UNPKG

@iobroker/db-objects-redis

Version:

The Library contains the Database classes for Redis based objects database client.

1,230 lines 141 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var objectsInRedisClient_exports = {}; __export(objectsInRedisClient_exports, { ObjectsInRedisClient: () => ObjectsInRedisClient }); module.exports = __toCommonJS(objectsInRedisClient_exports); var __import_meta_url = typeof document === "undefined" ? new (require("url".replace("", ""))).URL("file:" + __filename).href : document.currentScript && document.currentScript.src || new URL("main.js", document.baseURI).href; var import_node = __toESM(require("node.extend"), 1); var import_ioredis = __toESM(require("ioredis"), 1); var import_db_base = require("@iobroker/db-base"); var import_node_fs = __toESM(require("node:fs"), 1); var import_node_path = __toESM(require("node:path"), 1); var import_node_crypto = __toESM(require("node:crypto"), 1); var import_node_util = require("node:util"); var import_deep_clone = __toESM(require("deep-clone"), 1); var utils = __toESM(require("../../lib/objects/objectsUtils.js"), 1); var import_semver = __toESM(require("semver"), 1); var CONSTS = __toESM(require("../../lib/objects/constants.js"), 1); var url = __toESM(require("node:url"), 1); const thisDir = url.fileURLToPath(new URL(".", __import_meta_url || `file://${__filename}`)); const ERRORS = CONSTS.ERRORS; class ObjectsInRedisClient { client; fileNamespace; redisNamespace; fileNamespaceL; objNamespace; setNamespace; metaNamespace; objNamespaceL; supportedProtocolVersions; stop; sub; subSystem; settings; preserveSettings; defaultNewAcl; namespace; hostname; scripts; existingMetaObjects; log; activeProtocolVersion; useSets; noLegacyMultihost; userSubscriptions; systemSubscriptions; constructor(settings) { this.settings = settings || {}; this.redisNamespace = `${this.settings.redisNamespace || this.settings.connection?.redisNamespace || "cfg"}.`; this.fileNamespace = `${this.redisNamespace}f.`; this.fileNamespaceL = this.fileNamespace.length; this.objNamespace = `${this.redisNamespace}o.`; this.setNamespace = `${this.redisNamespace}s.`; this.metaNamespace = `${this.settings.metaNamespace || "meta"}.`; this.objNamespaceL = this.objNamespace.length; this.supportedProtocolVersions = ["4"]; this.stop = false; this.client = null; this.sub = null; this.subSystem = null; this.preserveSettings = ["custom", "smartName", "material", "habpanel", "mobile"]; this.defaultNewAcl = this.settings.defaultNewAcl || null; this.namespace = this.settings.namespace || this.settings.hostname || ""; this.hostname = this.settings.hostname || import_db_base.tools.getHostName(); this.scripts = {}; this.userSubscriptions = {}; this.systemSubscriptions = {}; this.existingMetaObjects = {}; this.log = import_db_base.tools.getLogger(this.settings.logger); if (this.settings.autoConnect !== false) { this.connectDb(); } } /** * Checks if we are allowed to start and sets the protocol version accordingly */ async _determineProtocolVersion() { if (!this.client) { throw new Error(ERRORS.ERROR_DB_CLOSED); } let protoVersion; try { protoVersion = await this.client.get(`${this.metaNamespace}objects.protocolVersion`); } catch (e) { if (e.message.includes("GET-UNSUPPORTED")) { return; } } if (!protoVersion) { const highestVersion = Math.max(...this.supportedProtocolVersions.map((value) => parseInt(value))); await this.setProtocolVersion(highestVersion); this.activeProtocolVersion = highestVersion.toString(); return; } if (this.supportedProtocolVersions.includes(protoVersion)) { this.activeProtocolVersion = protoVersion; } else { throw new Error(`This host does not support protocol version "${protoVersion}"`); } } connectDb() { this.settings.connection = this.settings.connection || {}; const onChange = this.settings.change; const onChangeUser = this.settings.changeUser; const onChangeFileUser = this.settings.changeFileUser; this.settings.connection.options = this.settings.connection.options || {}; const retry_max_delay = this.settings.connection.options.retry_max_delay || 5e3; const retry_max_count = this.settings.connection.options.retry_max_count || 19; let ready = false; let initError = false; let connected = false; let reconnectCounter = 0; let errorLogged = false; const isSentinel = Array.isArray(this.settings.connection.host); let wasReady = false; this.settings.connection.options.retryStrategy = (reconnectCount) => { if (!ready && initError) { return new Error("No more tries"); } if (this.stop) { return new Error("Client has stopped ... no retries anymore"); } if (ready && reconnectCount >= retry_max_count) { return new Error("Stop trying to reconnect"); } if (!ready) { return 300; } return retry_max_delay; }; delete this.settings.connection.options.retry_max_delay; this.settings.connection.options.enableReadyCheck = true; if (this.settings.connection.port === 0) { this.settings.connection.options.path = this.settings.connection.host; this.log.debug(`${this.namespace} Redis Objects: Use File Socket for connection: ${this.settings.connection.options.path}`); } else if (Array.isArray(this.settings.connection.host)) { const configuredPort = this.settings.connection.port; const defaultPort = Array.isArray(configuredPort) ? null : configuredPort; this.settings.connection.options.sentinels = this.settings.connection.host.map((redisNode, idx) => ({ host: redisNode, // @ts-expect-error ts does not get that if defPort is null we have an array port: defaultPort === null ? configuredPort[idx] : defaultPort })); this.settings.connection.options.name = this.settings.connection.sentinelName ? this.settings.connection.sentinelName : "mymaster"; this.log.debug(`${this.namespace} Redis Objects: Use Sentinel for connection: ${this.settings.connection.options.name}, ${JSON.stringify(this.settings.connection.options.sentinels)}`); } else { this.settings.connection.options.host = this.settings.connection.host; this.settings.connection.options.port = this.settings.connection.port; this.log.debug(`${this.namespace} Redis Objects: Use Redis connection: ${this.settings.connection.options.host}:${this.settings.connection.options.port}`); } this.settings.connection.options.db = this.settings.connection.options.db || 0; this.settings.connection.options.family = this.settings.connection.options.family || 0; this.settings.connection.options.password = this.settings.connection.options.auth_pass || this.settings.connection.pass || null; this.settings.connection.options.autoResubscribe = false; this.settings.connection.options.connectionName = this.namespace.replace(/\s/g, ""); this.client = new import_ioredis.default(this.settings.connection.options); this.client.on("error", (error) => { if (this.settings.connection.enhancedLogging) { this.log.silly(`${this.namespace} Redis ERROR Objects: (${this.stop}) ${error.message} / ${error.stack}`); } if (this.stop) { return; } if (!ready) { initError = true; if (error.message.startsWith('Protocol error, got "H" as reply type byte.')) { this.log.error(`${this.namespace} Could not connect to objects database at ${this.settings.connection.options.host}:${this.settings.connection.options.port} (invalid protocol). Please make sure the configured IP and port points to a host running JS-Controller >= 2.0. and that the port is not occupied by other software!`); } return; } this.log.error(`${this.namespace} Objects database error: ${error.message}`); errorLogged = true; }); this.client.on("end", () => { if (this.settings.connection.enhancedLogging) { this.log.silly(`${this.namespace} Objects-Redis Event end (stop=${this.stop})`); } if (ready && typeof this.settings.disconnected === "function") { if (!this.stop) { this.log.warn(`\u274C ${this.namespace} Objects database disconnected`); } this.settings.disconnected(); } }); this.client.on("connect", () => { if (this.settings.connection.enhancedLogging) { this.log.silly(`${this.namespace} Objects-Redis Event connect (stop=${this.stop})`); } connected = true; if (errorLogged) { this.log.info(`\u2705 ${this.namespace} Objects database successfully reconnected`); errorLogged = false; } }); this.client.on("close", () => { if (this.settings.connection.enhancedLogging) { this.log.silly(`${this.namespace} Objects-Redis Event close (stop=${this.stop})`); } }); this.client.on("reconnecting", () => { if (connected && !ready && !initError) { reconnectCounter++; } if (this.settings.connection.enhancedLogging) { this.log.silly(`${this.namespace} Objects-Redis Event reconnect (reconnectCounter=${reconnectCounter}, stop=${this.stop})`); } if (reconnectCounter > 2) { this.log.error(`${this.namespace} The DB port ${this.settings.connection.options.port} is occupied by something that is not a Redis protocol server. Please check other software running on this port or, if you use iobroker, make sure to update to js-controller 2.0 or higher!`); return; } connected = false; initError = false; }); this.client.on("ready", async () => { if (this.stop || !this.client) { return; } initError = false; if (isSentinel && wasReady) { this.log.info(`\u2705 ${this.namespace} Objects DB reconnected via Redis Sentinel (master group "${this.settings.connection.options.name}")`); } wasReady = true; this.log.debug(`${this.namespace} Objects client ready ... initialize now`); try { await this.client.config("SET", "lua-time-limit", 1e4); } catch (e) { this.log.warn(`${this.namespace} Unable to increase LUA script timeout: ${e.message}`); } let initCounter = 0; if (!this.subSystem && typeof onChange === "function") { initCounter++; this.log.debug(`${this.namespace} Objects create System PubSub Client`); this.subSystem = new import_ioredis.default(this.settings.connection.options); if (typeof this.settings.primaryHostLost === "function") { try { await this.client.config("SET", "notify-keyspace-events", "Exe"); } catch (e) { this.log.warn(`${this.namespace} Unable to enable Expiry Keyspace events from Redis Server: ${e.message}`); } this.subSystem.on("message", (channel, message) => { if (channel === `__keyevent@${this.settings.connection.options.db}__:expired` || channel === `__keyevent@${this.settings.connection.options.db}__:evicted`) { this.log.silly(`${this.namespace} redis message expired/evicted ${channel}:${message}`); if (message === `${this.metaNamespace}objects.primaryHost` && typeof this.settings.primaryHostLost === "function") { this.settings.primaryHostLost(); } } }); } if (typeof onChange === "function") { this.subSystem.on("pmessage", (pattern, channel, message) => setImmediate(() => { this.log.silly(`${this.namespace} Objects system redis pmessage ${pattern}/${channel}:${message}`); if (channel.startsWith(this.metaNamespace)) { if (channel === `${this.metaNamespace}objects.protocolVersion` && message !== this.activeProtocolVersion) { if (typeof this.settings.disconnected === "function") { this.log.info(`${this.namespace} Objects protocol version has changed, disconnecting!`); this.settings.disconnected(); } } else if (channel === `${this.metaNamespace}objects.features.useSets`) { const newUseSets = !!parseInt(message); if (newUseSets !== this.useSets) { this.log.info(`${this.namespace} Sets ${newUseSets ? "activated" : "deactivated"}: restarting ...`); this.useSets = newUseSets; if (typeof this.settings.disconnected === "function") { this.settings.disconnected(); } } } return; } try { if (channel.startsWith(this.objNamespace) && channel.length > this.objNamespaceL) { const id = channel.substring(this.objNamespaceL); try { const obj2 = message ? JSON.parse(message) : null; if (id === "system.config" && obj2?.common?.defaultNewAcl && !(0, import_node_util.isDeepStrictEqual)(obj2.common.defaultNewAcl, this.defaultNewAcl)) { this.defaultNewAcl = (0, import_deep_clone.default)(obj2.common.defaultNewAcl); if (this.settings.controller) { this.setDefaultAcl(this.defaultNewAcl); } } onChange(id, obj2); } catch (e) { this.log.warn(`${this.namespace} Objects Cannot process system pmessage ${id} - ${message}: ${e.message}`); this.log.warn(`${this.namespace} ${e.stack}`); } } else { this.log.warn(`${this.namespace} Objects Received unexpected system pmessage: ${channel}`); } } catch (e) { this.log.warn(`${this.namespace} Objects system pmessage ${channel} ${JSON.stringify(message)} ${e.message}`); this.log.warn(`${this.namespace} ${e.stack}`); } })); } this.subSystem.on("end", () => { if (this.settings.connection.enhancedLogging) { this.log.silly(`${this.namespace} Objects-Redis System Event end sub (stop=${this.stop})`); } if (ready && typeof this.settings.disconnected === "function") { this.settings.disconnected(); } }); this.subSystem.on("error", (error) => { if (this.stop) { return; } if (this.settings.connection.enhancedLogging) { this.log.silly(`${this.namespace} PubSub System client Objects No redis connection: ${JSON.stringify(error)}`); } }); if (this.settings.connection.enhancedLogging) { this.subSystem.on("connect", () => this.log.silly(`${this.namespace} PubSub System client Objects-Redis Event connect (stop=${this.stop})`)); this.subSystem.on("close", () => this.log.silly(`${this.namespace} PubSub System client Objects-Redis Event close (stop=${this.stop})`)); this.subSystem.on("reconnecting", (reconnectCounter2) => this.log.silly(`${this.namespace} PubSub System client Objects-Redis Event reconnect (reconnectCounter=${reconnectCounter2}, stop=${this.stop})`)); } this.subSystem.on("ready", async () => { if (--initCounter < 1) { if (this.settings.connection.port === 0) { this.log.debug(`${this.namespace} Objects ${ready ? "system re" : ""}connected to redis: ${import_db_base.tools.maybeArrayToString(this.settings.connection.host)}`); } else { this.log.debug(`${this.namespace} Objects ${ready ? "system re" : ""}connected to redis: ${import_db_base.tools.maybeArrayToString(this.settings.connection.host)}:${import_db_base.tools.maybeArrayToString(this.settings.connection.port)}`); } if (!ready && typeof this.settings.connected === "function") { this.settings.connected(); } ready = true; } try { if (this.subSystem) { await this.subSystem.psubscribe(`${this.objNamespace}system.config`); } } catch { } try { if (this.subSystem) { await this.subSystem.psubscribe(`${this.metaNamespace}*`); } } catch (e) { this.log.warn(`${this.namespace} Unable to subscribe to meta namespace "${this.metaNamespace}" changes: ${e.message}`); } if (this.subSystem) { for (const sub of Object.keys(this.systemSubscriptions)) { try { await this.subSystem.psubscribe(sub); } catch { } } } }); } if (!this.sub && (typeof onChangeUser === "function" || typeof onChangeFileUser === "function")) { initCounter++; this.log.debug(`${this.namespace} Objects create User PubSub Client`); this.sub = new import_ioredis.default(this.settings.connection.options); this.sub.on("pmessage", (pattern, channel, message) => { setImmediate(() => { this.log.silly(`${this.namespace} Objects user redis pmessage ${pattern}/${channel}:${message}`); try { if (channel.startsWith(this.objNamespace) && channel.length > this.objNamespaceL) { if (onChangeUser) { const id = channel.substring(this.objNamespaceL); try { const obj2 = message ? JSON.parse(message) : null; onChangeUser(id, obj2); } catch (e) { this.log.warn(`${this.namespace} Objects user cannot process pmessage ${id} - ${message}: ${e.message}`); this.log.warn(`${this.namespace} ${e.stack}`); } } } else if (channel.startsWith(this.fileNamespace) && channel.length > this.fileNamespaceL) { if (onChangeFileUser) { const [id, fileName] = channel.substring(this.fileNamespaceL).split("$%$"); try { const obj2 = message ? JSON.parse(message) : null; onChangeFileUser(id, fileName, obj2); } catch (e) { this.log.warn(`${this.namespace} Objects user cannot process pmessage ${id}/${fileName} - ${message}: ${e.message}`); this.log.warn(`${this.namespace} ${e.stack}`); } } } else { this.log.warn(`${this.namespace} Objects user received unexpected pmessage: ${channel}`); } } catch (e) { this.log.warn(`${this.namespace} Objects user pmessage ${channel} ${JSON.stringify(message)} ${e.message}`); this.log.warn(`${this.namespace} ${e.stack}`); } }); }); this.sub.on("end", () => { if (this.settings.connection.enhancedLogging) { this.log.silly(`${this.namespace} Objects-Redis Event end user sub (stop=${this.stop})`); } if (ready && typeof this.settings.disconnected === "function") { this.settings.disconnected(); } }); this.sub.on("error", (error) => { if (this.stop) { return; } this.settings.connection.enhancedLogging && this.log.silly(`${this.namespace} PubSub user client Objects No redis connection: ${JSON.stringify(error)}`); }); if (this.settings.connection.enhancedLogging) { this.sub.on("connect", () => this.log.silly(`${this.namespace} PubSub user client Objects-Redis Event connect (stop=${this.stop})`)); this.sub.on("close", () => this.log.silly(`${this.namespace} PubSub user client Objects-Redis Event close (stop=${this.stop})`)); this.sub.on("reconnecting", (reconnectCounter2) => this.log.silly(`${this.namespace} PubSub user client Objects-Redis Event reconnect (reconnectCounter=${reconnectCounter2}, stop=${this.stop})`)); } this.sub.on("ready", async () => { if (!this.sub) { return; } if (--initCounter < 1) { if (this.settings.connection.port === 0) { this.log.debug(`${this.namespace} Objects ${ready ? "user re" : ""}connected to redis: ${import_db_base.tools.maybeArrayToString(this.settings.connection.host)}`); } else { this.log.debug(`${this.namespace} Objects ${ready ? "user re" : ""}connected to redis: ${import_db_base.tools.maybeArrayToString(this.settings.connection.host)}:${import_db_base.tools.maybeArrayToString(this.settings.connection.port)}`); } !ready && typeof this.settings.connected === "function" && this.settings.connected(); ready = true; } for (const sub of Object.keys(this.userSubscriptions)) { try { await this.sub.psubscribe(sub); } catch { } } }); } if (!this.client) { return; } initCounter++; try { this.useSets = !!parseInt(await this.client.get(`${this.metaNamespace}objects.features.useSets`) || "0"); } catch (e) { if (!e.message.includes("UNSUPPORTED")) { this.log.error(`${this.namespace} Cannot determine Set feature status: ${e.message}`); return; } this.useSets = false; } try { await this._determineProtocolVersion(); } catch (e) { this.log.error(`${this.namespace} ${e.message}`); throw new Error("Objects DB is not allowed to start in the current Multihost environment"); } let keys2 = await this._getKeysViaScan(`${this.objNamespace}system.host.*`); const hostRegex = new RegExp(`^${this.objNamespace.replace(/\./g, "\\.")}system\\.host\\.[^.]+$`); keys2 = keys2.filter((id) => hostRegex.test(id)); this.noLegacyMultihost = true; try { if (keys2.length) { const objs2 = await this.client.mget(keys2); for (const strObj of objs2) { const obj2 = strObj !== null ? JSON.parse(strObj) : strObj; if (obj2 && obj2.type === "host" && obj2._id !== `system.host.${this.hostname}` && obj2.common && obj2.common.installedVersion && import_semver.default.lt(obj2.common.installedVersion, "4.0.0")) { this.noLegacyMultihost = false; this.log.info(`${this.namespace} Sets unsupported`); } } } } catch (e) { this.log.error(`${this.namespace} Cannot determine Lua scripts strategy: ${e.message} ${JSON.stringify(keys2)}`); return; } this.log.debug(`${this.namespace} Objects client initialize lua scripts`); try { await this.loadLuaScripts(); } catch (err) { this.log.error(`${this.namespace} Cannot initialize database scripts: ${err.message}`); return; } let obj; try { obj = await this.client.get(`${this.objNamespace}system.config`); } catch { } if (obj) { try { obj = JSON.parse(obj); } catch { this.log.error(`${this.namespace} Cannot parse JSON system.config: ${obj}`); obj = null; } if (obj && obj.common && obj.common.defaultNewAcl) { this.defaultNewAcl = obj.common.defaultNewAcl; } } else { this.log.error(`${this.namespace} Cannot read system.config: ${obj} (OK when migrating or restoring)`); } if (--initCounter < 1) { if (this.settings.connection.port === 0) { this.log.debug(`${this.namespace} Objects ${ready ? "client re" : ""}connected to redis: ${import_db_base.tools.maybeArrayToString(this.settings.connection.host)}`); } else { this.log.debug(`${this.namespace} Objects ${ready ? "client re" : ""}connected to redis: ${import_db_base.tools.maybeArrayToString(this.settings.connection.host)}:${import_db_base.tools.maybeArrayToString(this.settings.connection.port)}`); } !ready && typeof this.settings.connected === "function" && this.settings.connected(); ready = true; } }); } getStatus() { return { type: "redis", server: false }; } /** * Checks if given ID is a meta-object, else throws error * * @param id to check * @throws Error if id is invalid */ async validateMetaObject(id) { if (this.existingMetaObjects[id] === void 0) { const obj = await this.getObject(id); if (obj && obj.type === "meta") { this.existingMetaObjects[id] = true; } else { this.existingMetaObjects[id] = false; throw new Error(`${id} is not an object of type "meta"`); } } else if (this.existingMetaObjects[id] === false) { throw new Error(`${id} is not an object of type "meta"`); } } normalizeFilename(name) { return name ? name.replace(/[/\\]+/g, "/") : name; } // -------------- FILE FUNCTIONS ------------------------------------------- /** * Sets a buffer to the Redis DB * * @param id id of the file * @param data content, if string is passed it will be converted to a Buffer */ async _setBinaryState(id, data) { if (!this.client) { throw new Error(ERRORS.ERROR_DB_CLOSED); } if (!Buffer.isBuffer(data)) { data = Buffer.from(data); } await this.client.set(id, data); await this.client.publish(id, data.byteLength.toString(10)); } /** * get buffer of given id from redis * * @param id - id of the data with namespace prefix */ _getBinaryState(id) { if (!this.client) { throw new Error(ERRORS.ERROR_DB_CLOSED); } return this.client.getBuffer(id); } /** * deletes binary state of given id from redis db * * @param id - id to delete, with namespace prefix */ async _delBinaryState(id) { if (!this.client) { throw new Error(ERRORS.ERROR_DB_CLOSED); } else { await this.client.del(id); await this.client.publish(id, "null"); } } getFileId(id, name, isMeta) { name = this.normalizeFilename(name); if (id.endsWith(".admin")) { if (name.startsWith("admin/")) { name = name.replace(/^admin\//, ""); } else if (name.match(/^iobroker.[-\d\w]\/admin\//i)) { name = name.replace(/^iobroker.[-\d\w]\/admin\//i, ""); } } let normalized; try { normalized = utils.sanitizePath(id, name); } catch { this.log.debug(`${this.namespace} Invalid file path ${id}/${name}`); return ""; } if (id !== "*") { id = normalized.id; } name = normalized.name; return `${this.fileNamespace + id}$%$${name}${isMeta !== void 0 ? isMeta ? "$%$meta" : "$%$data" : ""}`; } async checkFile(id, name, options2, flag, callback) { const fileId = this.getFileId(id, name, true); if (!fileId) { const fileOptions2 = { notExists: true }; if (utils.checkFile(fileOptions2, options2, flag, this.defaultNewAcl)) { return import_db_base.tools.maybeCallback(callback, false, options2, fileOptions2); } return import_db_base.tools.maybeCallback(callback, true, options2); } if (!this.client) { return import_db_base.tools.maybeCallbackWithRedisError(callback, ERRORS.ERROR_DB_CLOSED, options2); } let fileOptions; try { fileOptions = await this.client.get(fileId); } catch { } fileOptions = fileOptions || '{"notExists": true}'; try { fileOptions = JSON.parse(fileOptions); } catch { this.log.error(`${this.namespace} Cannot parse JSON ${id}: ${fileOptions}`); fileOptions = { notExists: true }; } if (utils.checkFile(fileOptions, options2, flag, this.defaultNewAcl)) { return import_db_base.tools.maybeCallback(callback, false, options2, fileOptions); } return import_db_base.tools.maybeCallback(callback, true, options2); } checkFileRights(id, name, options2, flag, callback) { return utils.checkFileRights(this, id, name, options2, flag, callback); } async _setDefaultAcl(ids, defaultAcl) { for (const id of ids) { try { const obj = await this.getObject(id); if (obj && !obj.acl) { obj.acl = defaultAcl; await this.setObject(id, obj, null); } } catch (e) { this.log.error(`${this.namespace} _setDefaultAcl error on id "${id}" with acl "${JSON.stringify(defaultAcl)}": ${e.message}`); } } } async setDefaultAcl(defaultNewAcl) { this.defaultNewAcl = defaultNewAcl || { owner: CONSTS.SYSTEM_ADMIN_USER, ownerGroup: CONSTS.SYSTEM_ADMIN_GROUP, object: 1636, state: 1636, file: 1636 }; try { const ids = await this.getKeysAsync("*"); if (ids) { await this._setDefaultAcl(ids, this.defaultNewAcl); } } catch (e) { this.log.error(`${this.namespace} Could not update default acl: ${e.message}`); } } getUserGroup(user, callback) { return utils.getUserGroup(this, user, (error, user2, userGroups, userAcl) => { if (error) { this.log.error(`${this.namespace} ${error.stack}`); } return import_db_base.tools.maybeCallback(callback, user2, userGroups, userAcl); }); } async _writeFile(id, name, data, options2, callback, meta) { const matchedExtension = name.match(/\.[^.]+$/); const ext = matchedExtension ? matchedExtension[0] : ""; const isTextData = typeof data === "string"; const { mimeType, isBinary } = utils.getMimeType(ext, isTextData); const metaID = this.getFileId(id, name, true); if (!this.client) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED); } if (options2.virtualFile) { meta = { notExists: true, virtualFile: true }; try { await this.client.set(metaID, JSON.stringify(meta)); return import_db_base.tools.maybeCallback(callback); } catch (e) { return import_db_base.tools.maybeCallbackWithRedisError(callback, e); } } else { if (!meta) { meta = { createdAt: Date.now() }; } if (!meta.acl) { meta.acl = { owner: options2.user || this.defaultNewAcl && this.defaultNewAcl.owner || CONSTS.SYSTEM_ADMIN_USER, ownerGroup: options2.group || this.defaultNewAcl && this.defaultNewAcl.ownerGroup || CONSTS.SYSTEM_ADMIN_GROUP, permissions: options2.mode || this.defaultNewAcl && this.defaultNewAcl.file || 1604 }; } meta.stats = { size: data ? data.length : 0 }; if (Object.prototype.hasOwnProperty.call(meta, "notExists")) { delete meta.notExists; } meta.mimeType = options2.mimeType || mimeType; meta.binary = isBinary; meta.acl.ownerGroup = meta.acl.ownerGroup || this.defaultNewAcl && this.defaultNewAcl.ownerGroup || CONSTS.SYSTEM_ADMIN_GROUP; meta.modifiedAt = Date.now(); try { await this._setBinaryState(this.getFileId(id, name, false), data); await this.client.set(metaID, JSON.stringify(meta)); return import_db_base.tools.maybeCallback(callback); } catch (e) { return import_db_base.tools.maybeCallbackWithRedisError(callback, e); } } } async writeFile(id, name, data, options2, callback) { if (typeof options2 === "function") { callback = options2; options2 = null; } if (typeof options2 === "string") { options2 = { mimeType: options2 }; } if (options2?.acl) { options2.acl = null; } if (!callback) { return this.writeFileAsync(id, name, data, options2); } try { await this.validateMetaObject(id); } catch (e) { this.log.error(`${this.namespace} Cannot write file ${name}: ${e.message}`); return import_db_base.tools.maybeCallbackWithError(callback, e); } if (typeof name !== "string" || !name.length || name === "/") { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_NOT_FOUND); } if (name.startsWith("/")) { name = name.substring(1); } if (data === void 0) { data = null; } return this.checkFileRights(id, name, options2, CONSTS.ACCESS_WRITE, (err, options3, meta) => { if (err) { return import_db_base.tools.maybeCallbackWithError(callback, err); } return this._writeFile(id, name, data, options3, callback, meta); }); } writeFileAsync(id, name, data, options2) { return new Promise((resolve, reject) => this.writeFile(id, name, data, options2, (err) => err ? reject(err) : resolve())); } async _readFile(id, name, meta) { if (meta.notExists) { throw new Error(ERRORS.ERROR_NOT_FOUND); } if (!this.client) { throw new Error(ERRORS.ERROR_DB_CLOSED); } let buffer; buffer = await this._getBinaryState(this.getFileId(id, name, false)); const mimeType = meta?.mimeType; if (meta && !meta.binary && buffer) { buffer = buffer.toString(); } return { file: buffer, mimeType }; } readFile(id, name, options2, callback) { if (typeof options2 === "function") { callback = options2; options2 = null; } if (options2?.acl) { options2.acl = null; } if (!callback) { return new Promise((resolve, reject) => this.readFile(id, name, options2, (err, res, mimeType) => err ? reject(err) : resolve({ file: res, mimeType }))); } if (typeof name !== "string" || !name.length || name === "/") { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_NOT_FOUND); } if (name.startsWith("/")) { name = name.substring(1); } options2 = options2 || {}; this.checkFileRights(id, name, options2, CONSTS.ACCESS_READ, async (err, options3, meta) => { if (err) { return import_db_base.tools.maybeCallbackWithError(callback, err); } try { const { file, mimeType } = await this._readFile(id, name, meta); return import_db_base.tools.maybeCallbackWithError(callback, null, file, mimeType); } catch (e) { return import_db_base.tools.maybeCallbackWithError(callback, e); } }); } /** * Check if given object exists * * @param id id of the object * @param options optional user context */ async objectExists(id, options2) { if (!this.client) { throw new Error(ERRORS.ERROR_DB_CLOSED); } if (!id || typeof id !== "string") { throw new Error(`invalid id ${JSON.stringify(id)}`); } try { await new Promise((resolve, reject) => { utils.checkObjectRights(this, null, null, options2, CONSTS.ACCESS_LIST, (err) => { if (err) { reject(err); } else { resolve(); } }); }); const exists = await this.client.exists(this.objNamespace + id); return !!exists; } catch (e) { this.log.error(`${this.namespace} Cannot check object existence of "${id}": ${e.message}`); return Promise.reject(new Error(`Cannot check object existence of "${id}": ${e.message}`)); } } /** * Check if given file exists * * @param id id of the namespace * @param name name of the file * @param options optional user context */ async fileExists(id, name, options2) { if (typeof name !== "string") { name = ""; } if (name.startsWith("/")) { name = name.substring(1); } try { await new Promise((resolve, reject) => { this.checkFileRights(id, name, options2, CONSTS.ACCESS_READ, (err) => { if (err) { reject(err); } else { resolve(); } }); }); if (!this.client) { throw new Error(ERRORS.ERROR_DB_CLOSED); } id = this.getFileId(id, name, false); const exists = await this.client.exists(id); return !!exists; } catch (e) { this.log.error(`${this.namespace} Cannot check file existence of "${id}": ${e.message}`); throw new Error(`Cannot check file existence of "${id}": ${e.message}`); } } async _unlink(id, name, options2, meta) { if (!this.client) { throw new Error(ERRORS.ERROR_DB_CLOSED); } if (meta && meta.notExists) { return this._rm(id, name, options2); } const metaID = this.getFileId(id, name, true); const dataID = this.getFileId(id, name, false); await this._delBinaryState(dataID); await this.client.del(metaID); } unlink(id, name, options2, callback) { if (typeof options2 === "function") { callback = options2; options2 = null; } if (options2?.acl) { options2.acl = null; } if (typeof name !== "string") { name = ""; } if (name.startsWith("/")) { name = name.substring(1); } this.checkFileRights(id, name, options2, CONSTS.ACCESS_DELETE, async (err, options3, meta) => { if (err) { return import_db_base.tools.maybeCallbackWithError(callback, err); } if (!options3.acl.file.delete) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION); } try { const files = await this._unlink(id, name, options3, meta); return import_db_base.tools.maybeCallbackWithError(callback, null, files); } catch (e) { return import_db_base.tools.maybeCallbackWithError(callback, e); } }); } unlinkAsync(id, name, options2) { return new Promise((resolve, reject) => this.unlink(id, name, options2, (err) => err ? reject(err) : resolve())); } delFile(id, name, options2, callback) { return this.unlink(id, name, options2, callback); } delFileAsync(id, name, options2) { return this.unlinkAsync(id, name, options2); } async _readDir(id, name, options2, callback) { name = this.normalizeFilename(name); if (!this.client) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED); } if (id === "") { const dirID2 = this.getFileId("*", "*"); let keys3; try { keys3 = await this._getKeysViaScan(dirID2); } catch (e) { return import_db_base.tools.maybeCallbackWithRedisError(callback, e); } if (!this.client) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED); } const result3 = []; if (!keys3 || !keys3.length) { return import_db_base.tools.maybeCallbackWithError(callback, null, result3); } let lastDir; keys3.sort().forEach((dir) => { dir = dir.substring(this.fileNamespaceL, dir.indexOf("$%$")); if (dir !== lastDir) { result3.push({ file: dir, stats: {}, isDir: true }); } lastDir = dir; }); return import_db_base.tools.maybeCallbackWithError(callback, null, result3); } try { await this.validateMetaObject(id); } catch (e) { return import_db_base.tools.maybeCallbackWithRedisError(callback, e); } const dirID = this.getFileId(id, `${name}${name.length ? "/" : ""}*`); let keys2; try { keys2 = await this._getKeysViaScan(dirID); } catch (e) { return import_db_base.tools.maybeCallbackWithRedisError(callback, e); } if (!this.client) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED); } const start = dirID.indexOf("$%$") + 3; const end = "$%$meta".length; const baseName = name + (name.length ? "/" : ""); const dirs = []; const deepLevel = baseName.split("/").length; if (!keys2 || !keys2.length) { return import_db_base.tools.maybeCallbackWithError(callback, null, []); } keys2 = keys2.sort().filter((key) => { if (key.endsWith("$%$meta")) { const parts = key.substr(start, key.length - end).split("/"); if (parts.length === deepLevel) { return !key.includes("/_data.json$%$") && key !== "_data.json"; } const dir = parts[deepLevel - 1]; if (!dirs.includes(dir)) { dirs.push(dir); } } }); if (!keys2.length) { const result3 = dirs.map((file) => ({ file, stats: {}, isDir: true })); return import_db_base.tools.maybeCallbackWithError(callback, null, result3); } let strObjs; try { strObjs = await this.client.mget(keys2); } catch (e) { return import_db_base.tools.maybeCallbackWithRedisError(callback, e); } const result2 = []; const dontCheck = options2.user === CONSTS.SYSTEM_ADMIN_USER || options2.group !== CONSTS.SYSTEM_ADMIN_GROUP || options2.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP); for (let i = 0; i < keys2.length; i++) { const file = keys2[i].substring(start + baseName.length, keys2[i].length - end); while (dirs.length && dirs[0] < file) { result2.push({ file: dirs.shift(), stats: {}, isDir: true }); } const strObj = strObjs[i]; let obj; try { obj = strObj ? JSON.parse(strObj) : null; } catch { this.log.error(`${this.namespace} Cannot parse JSON ${keys2[i]}: ${strObj}`); continue; } if (dontCheck || utils.checkObject(obj, options2, CONSTS.ACCESS_READ)) { if (!obj || obj.virtualFile) { continue; } obj.acl = obj.acl || {}; if (options2.user !== CONSTS.SYSTEM_ADMIN_USER && !options2.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP)) { obj.acl.read = !!(obj.acl.permissions & CONSTS.ACCESS_EVERY_READ); obj.acl.write = !!(obj.acl.permissions & CONSTS.ACCESS_EVERY_WRITE); } else { obj.acl.read = true; obj.acl.write = true; } result2.push({ file, stats: obj.stats, isDir: false, acl: obj.acl, modifiedAt: obj.modifiedAt, createdAt: obj.createdAt }); } } while (dirs.length) { result2.push({ file: dirs.shift(), stats: {}, isDir: true }); } return import_db_base.tools.maybeCallbackWithError(callback, null, result2); } readDir(id, name, options2, callback) { if (typeof options2 === "function") { callback = options2; options2 = null; } if (options2?.acl) { options2.acl = null; } if (typeof name !== "string") { name = ""; } if (name.startsWith("/")) { name = name.substring(1); } if (name.endsWith("/")) { name = name.substring(0, name.length - 1); } this.checkFileRights(id, name, options2, CONSTS.ACCESS_READ, (err, options3) => { if (err) { return import_db_base.tools.maybeCallbackWithError(callback, err); } if (!options3.acl.file.list) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION); } this._readDir(id, name, options3, callback); }); } readDirAsync(id, name, options2) { return new Promise((resolve, reject) => this.readDir(id, name, options2, (err, res) => err ? reject(err) : resolve(res))); } async _renameHelper(keys2, oldBase, newBase, callback) { if (!keys2 || !keys2.length) { return import_db_base.tools.maybeCallback(callback); } if (!this.client) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED); } for (const id of keys2) { try { try { await this.client.rename(id.replace(/\$%\$meta$/, "$%$data"), id.replace(oldBase, newBase).replace(/\$%\$meta$/, "$%$data")); } catch (e) { if (!(id.endsWith("/_data.json$%$meta") && e.message.includes("no such key"))) { throw e; } } await this.client.rename(id, id.replace(oldBase, newBase)); } catch (e) { return import_db_base.tools.maybeCallbackWithRedisError(callback, e); } } return import_db_base.tools.maybeCallback(callback); } async _rename(id, oldName, newName, options2, callback, meta) { const oldMetaID = this.getFileId(id, oldName, true); const oldDataID = this.getFileId(id, oldName, false); const newMetaID = this.getFileId(id, newName, true); const newDataID = this.getFileId(id, newName, false); if (!meta || !this.client) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED); } else if (meta.notExists) { oldName = this.normalizeFilename(oldName); newName = this.normalizeFilename(newName); if (!oldName.endsWith("/*")) { oldName += "/*"; } else if (oldName.endsWith("/")) { oldName += "*"; } if (!newName.endsWith("/*")) { newName += "/*"; } else if (newName.endsWith("/")) { newName += "*"; } const oldBase = oldName.substring(0, oldName.length - 1); const newBase = newName.substring(0, newName.length - 1); const dirID = this.getFileId(id, oldName); let keys2; try { keys2 = await this._getKeysViaScan(dirID); } catch (e) { return import_db_base.tools.maybeCallbackWithRedisError(callback, e); } if (!this.client) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED); } if (!keys2) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_NOT_FOUND); } keys2 = keys2.sort().filter((key) => key.endsWith("$%$meta")); if (!keys2.length) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_NOT_FOUND); } let strObjs; try { strObjs = await this.client.mget(keys2); } catch (e) { return import_db_base.tools.maybeCallbackWithRedisError(callback, e); } let result2; const dontCheck = options2.user === CONSTS.SYSTEM_ADMIN_USER || options2.group !== CONSTS.SYSTEM_ADMIN_GROUP || options2.groups?.includes(CONSTS.SYSTEM_ADMIN_GROUP); if (!dontCheck) { result2 = []; for (let i = 0; i < keys2.length; i++) { const strObj = strObjs[i]; let obj; try { obj = strObj ? JSON.parse(strObj) : null; } catch { this.log.error(`${this.namespace} Cannot parse JSON ${keys2[i]}: ${strObj}`); continue; } if (utils.checkObject(obj, options2, CONSTS.ACCESS_READ)) { result2.push(keys2[i]); } } } else { result2 = keys2; } return this._renameHelper(result2, oldBase, newBase, callback); } try { await this.client.rename(oldDataID, newDataID); await this.client.rename(oldMetaID, newMetaID); return import_db_base.tools.maybeCallback(callback); } catch (e) { return import_db_base.tools.maybeCallbackWithRedisError(callback, e); } } rename(id, oldName, newName, options2, callback) { if (typeof options2 === "function") { callback = options2; options2 = null; } if (options2?.acl) { options2.acl = null; } if (typeof oldName !== "string" || !oldName.length || oldName === "/" || oldName === "//" || typeof newName !== "string" || !newName.length || newName === "/" || newName === "//") { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_NOT_FOUND); } if (oldName.startsWith("/")) { oldName = oldName.substring(1); } if (newName.startsWith("/")) { newName = newName.substring(1); } if (oldName.endsWith("/")) { oldName = oldName.substring(0, oldName.length - 1); } if (newName.endsWith("/")) { newName = newName.substring(0, newName.length - 1); } this.checkFileRights(id, oldName, options2, CONSTS.ACCESS_WRITE, (err, options3, meta) => { if (err) { return import_db_base.tools.maybeCallbackWithError(callback, err); } if (!options3.acl.file.write) { return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_PERMISSION); } this._rename(id, oldName, ne