@iobroker/db-objects-redis
Version:
The Library contains the Database classes for Redis based objects database client.
1,231 lines (1,230 loc) • 141 kB
JavaScript
"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;
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") {
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(`${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;
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, newName, options3, callback, meta);
});
}
renameAsync(id, oldName, newName, options2) {
return new Promise((resolve, reject) => this.rename(id, oldName, newName, options2, (err) => err ? reject(err) : resolve()));
}
async _touch(id, name, callback, meta) {
const metaID = this.getFileId(id, name, true);
if (!this.client) {
return import_db_base.tools.maybeCallbackWithError(callback, ERRORS.ERROR_DB_CLOSED);