UNPKG

@iobroker/db-base

Version:

This Library contains base classes that are used by the database classes for ioBroker

396 lines (395 loc) 15 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 inMemFileDB_exports = {}; __export(inMemFileDB_exports, { InMemoryFileDB: () => InMemoryFileDB }); module.exports = __toCommonJS(inMemFileDB_exports); var import_fs_extra = __toESM(require("fs-extra"), 1); var import_node_path = __toESM(require("node:path"), 1); var import_js_controller_common_db = require("@iobroker/js-controller-common-db"); var import_node_zlib = require("node:zlib"); class InMemoryFileDB { settings; change; dataset; namespace; lastSave; stateTimer; callbackSubscriptionClient; dataDir; datasetName; log; backupDir; constructor(settings) { this.settings = settings || {}; this.change = this.settings.change; this.dataset = {}; this.namespace = this.settings.namespace || ""; this.lastSave = null; this.callbackSubscriptionClient = {}; this.settings.backup = this.settings.connection.backup || { disabled: false, // deactivates files: 24, // minimum number of files hours: 48, // hours period: 120, // minutes path: "" // use default path }; this.dataDir = this.settings.connection.dataDir || import_js_controller_common_db.tools.getDefaultDataDir(); if (!import_node_path.default.isAbsolute(this.dataDir)) { this.dataDir = import_node_path.default.normalize(import_node_path.default.join(import_js_controller_common_db.tools.getControllerDir(), this.dataDir)); } this.dataDir = this.dataDir.replace(/\\/g, "/"); const fileName = this.settings.jsonlDB ? this.settings.jsonlDB.fileName : this.settings.fileDB.fileName; this.datasetName = import_node_path.default.join(this.dataDir, fileName); const parts = import_node_path.default.dirname(this.datasetName); import_fs_extra.default.ensureDirSync(parts); this.stateTimer = null; this.backupDir = this.settings.backup.path || import_node_path.default.join(this.dataDir, this.settings.fileDB.backupDirName); this.log = import_js_controller_common_db.tools.getLogger(this.settings.logger); if (!this.settings.backup.disabled) { try { this.initBackupDir(); } catch (e) { this.log.error(`Database backups are disabled, because backup directory could not be initialized: ${e.message}`); this.log.error("This leads to an increased risk of data loss, please check that the configured backup directory is available and restart the controller"); this.settings.backup.disabled = true; } } this.log.debug(`${this.namespace} Data File: ${this.datasetName}`); } async open() { this.dataset = await this.loadDataset(this.datasetName); } /** * Loads a dataset file * * @param datasetName Filename of the file to load * @returns obj read data, normally as object */ async loadDatasetFile(datasetName) { if (!await import_fs_extra.default.pathExists(datasetName)) { throw new Error(`Database file ${datasetName} does not exists.`); } return import_fs_extra.default.readJSON(datasetName); } /** * Loads the dataset including pot. Fallback handling * * @param datasetName Filename of the file to load * @returns obj dataset read as object */ async loadDataset(datasetName) { let ret = {}; try { ret = await this.loadDatasetFile(datasetName); try { await import_fs_extra.default.readJSON(`${datasetName}.bak`); } catch (e) { this.log.info(`${this.namespace} Rewrite bak file, because error on verify ${datasetName}.bak: ${e.message}`); try { const jsonString = JSON.stringify(ret); await import_fs_extra.default.writeFile(`${datasetName}.bak`, jsonString); } catch (e2) { this.log.error(`${this.namespace} Cannot save ${datasetName}.bak: ${e2.message}`); } } } catch (err) { this.log.error(`${this.namespace} Cannot load ${datasetName}: ${err.message}. We try last Backup!`); try { ret = await this.loadDatasetFile(`${datasetName}.bak`); try { if (await import_fs_extra.default.pathExists(datasetName)) { try { await import_fs_extra.default.move(datasetName, `${datasetName}.broken`, { overwrite: true }); } catch (e) { this.log.error(`${this.namespace} Cannot copy the broken file ${datasetName} to ${datasetName}.broken ${e.message}`); } try { await import_fs_extra.default.writeFile(datasetName, JSON.stringify(ret)); } catch (e) { this.log.error(`${this.namespace} Cannot restore backup file as new main ${datasetName}: ${e.message}`); } } } catch { } } catch (err2) { this.log.error(`${this.namespace} Cannot load ${datasetName}.bak: ${err2.message}. Continue with empty dataset!`); this.log.error(`${this.namespace} If this is no Migration or initial start please restore the last backup from ${this.backupDir}`); } } return ret; } initBackupDir() { this.settings.backup.period = this.settings.backup.period === void 0 ? 120 : parseInt(String(this.settings.backup.period)); if (isNaN(this.settings.backup.period)) { this.settings.backup.period = 120; } const maxTimeoutMinutes = Math.floor((2 ** 31 - 1) / 6e4); if (this.settings.backup.period > maxTimeoutMinutes) { this.log.warn(`${this.namespace} Configured backup period ${this.settings.backup.period} is larger than the supported maximum of ${maxTimeoutMinutes} minutes. Defaulting to 120 minutes.`); this.settings.backup.period = 120; } this.settings.backup.period *= 6e4; this.settings.backup.files = this.settings.backup.files === void 0 ? 24 : parseInt(String(this.settings.backup.files)); if (isNaN(this.settings.backup.files)) { this.settings.backup.files = 24; } this.settings.backup.hours = this.settings.backup.hours === void 0 ? 48 : parseInt(String(this.settings.backup.hours)); if (isNaN(this.settings.backup.hours)) { this.settings.backup.hours = 48; } import_fs_extra.default.ensureDirSync(this.backupDir); } handleSubscribe(client, type, pattern, options, cb) { if (typeof options === "function") { cb = options; options = void 0; } client._subscribe = client._subscribe || {}; client._subscribe[type] = client._subscribe[type] || []; const s = client._subscribe[type]; if (pattern instanceof Array) { pattern.forEach((pattern2) => { if (s.find((sub) => sub.pattern === pattern2)) { return; } s.push({ pattern: pattern2, regex: new RegExp(import_js_controller_common_db.tools.pattern2RegEx(pattern2)), options }); }); } else { if (!s.find((sub) => sub.pattern === pattern)) { s.push({ pattern, regex: new RegExp(import_js_controller_common_db.tools.pattern2RegEx(pattern)), options }); } } typeof cb === "function" && cb(); } handleUnsubscribe(client, type, pattern, cb) { const s = client?._subscribe?.[type]; if (s) { const removeEntry = (p) => { const index = s.findIndex((sub) => sub.pattern === p); if (index > -1) { s.splice(index, 1); } }; if (pattern instanceof Array) { pattern.forEach((p) => { removeEntry(p); }); } else { removeEntry(pattern); } } return import_js_controller_common_db.tools.maybeCallback(cb); } publishToClients(_client, _type, _id, _obj) { throw new Error("no communication handling implemented"); } deleteOldBackupFiles(baseFilename) { let files = import_fs_extra.default.readdirSync(this.backupDir); files.sort(); const limit = Date.now() - this.settings.backup.hours * 36e5; files = files.filter((f) => f.endsWith(`${baseFilename}.gz`)); while (files.length > this.settings.backup.files) { const file = files.shift(); if (!file) { continue; } const ms = (/* @__PURE__ */ new Date(`${file.substring(0, 10)} ${file.substring(11, 16).replace("-", ":")}:00`)).getTime(); if (limit > ms) { try { import_fs_extra.default.unlinkSync(import_node_path.default.join(this.backupDir, file)); } catch (e) { this.log.error(`${this.namespace} Cannot delete file "${import_node_path.default.join(this.backupDir, file)}: ${e.message}`); } } } } getTimeStr(date) { const dateObj = new Date(date); let text = `${dateObj.getFullYear().toString()}-`; let v = dateObj.getMonth() + 1; if (v < 10) { text += "0"; } text += `${v.toString()}-`; v = dateObj.getDate(); if (v < 10) { text += "0"; } text += `${v.toString()}_`; v = dateObj.getHours(); if (v < 10) { text += "0"; } text += `${v.toString()}-`; v = dateObj.getMinutes(); if (v < 10) { text += "0"; } text += v.toString(); return text; } /** * Handle saving the dataset incl. backups */ async saveState() { try { const jsonString = await this.saveDataset(); if (!this.settings.backup.disabled && jsonString) { this.saveBackup(jsonString); } } finally { if (this.stateTimer) { clearTimeout(this.stateTimer); this.stateTimer = null; } } } /** * Saves the dataset into File incl. handling of a fallback backup file * * @returns dataset JSON string of the complete dataset to also be stored into a compressed backup file */ async saveDataset() { const jsonString = JSON.stringify(this.dataset); try { await import_fs_extra.default.writeFile(`${this.datasetName}.new`, jsonString); } catch (e) { this.log.error(`${this.namespace} Cannot save Dataset to ${this.datasetName}.new: ${e.message}`); return jsonString; } let bakOk = true; try { if (await import_fs_extra.default.pathExists(this.datasetName)) { try { await import_fs_extra.default.move(this.datasetName, `${this.datasetName}.bak`, { overwrite: true }); } catch (e) { bakOk = false; this.log.error(`${this.namespace} Cannot backup file ${this.datasetName}.bak: ${e.message}`); } } else { bakOk = false; } } catch { bakOk = false; } try { await import_fs_extra.default.move(`${this.datasetName}.new`, this.datasetName, { overwrite: true }); } catch (e) { this.log.error(`${this.namespace} Cannot move ${this.datasetName}.new to ${this.datasetName}: ${e.message}. Try direct write as fallback`); try { await import_fs_extra.default.writeFile(this.datasetName, jsonString); } catch (e2) { this.log.error(`${this.namespace} Cannot directly write Dataset to ${this.datasetName}: ${e2.message}`); return jsonString; } } if (!bakOk) { try { await import_fs_extra.default.writeFile(`${this.datasetName}.bak`, jsonString); } catch (e) { this.log.error(`${this.namespace} Cannot save ${this.datasetName}.bak: ${e.message}`); } } return jsonString; } /** * Stores a compressed backup of the DB in definable intervals * * @param jsonString JSON string of the complete dataset to also be stored into a compressed backup file */ saveBackup(jsonString) { const now = Date.now(); if (this.settings.backup.period && (!this.lastSave || now - this.lastSave > this.settings.backup.period)) { this.lastSave = now; const backFileName = import_node_path.default.join(this.backupDir, `${this.getTimeStr(now)}_${this.settings.fileDB.fileName}.gz`); try { if (!import_fs_extra.default.existsSync(backFileName)) { const output = import_fs_extra.default.createWriteStream(backFileName); output.on("error", (err) => { this.log.error(`${this.namespace} Cannot save ${this.datasetName}: ${err.stack}`); }); const compress = (0, import_node_zlib.createGzip)(); compress.pipe(output); compress.write(jsonString); compress.end(); this.deleteOldBackupFiles(this.settings.fileDB.fileName); } } catch (e) { this.log.error(`${this.namespace} Cannot save backup ${backFileName}: ${e.message}`); } } } getStatus() { return { type: "file", server: true }; } getClients() { return {}; } publishAll(type, id, obj) { if (id === void 0) { this.log.error(`${this.namespace} Can not publish empty ID`); return 0; } const clients = this.getClients(); let publishCount = 0; if (clients && typeof clients === "object") { for (const i of Object.keys(clients)) { publishCount += this.publishToClients(clients[i], type, id, obj); } } if (this.change && this.callbackSubscriptionClient._subscribe && this.callbackSubscriptionClient._subscribe[type]) { for (const entry of this.callbackSubscriptionClient._subscribe[type]) { if (entry.regex.test(id)) { setImmediate(() => this.change(id, obj)); break; } } } return publishCount; } // Destructor of the class. Called by shutting down. async destroy() { if (this.stateTimer) { clearTimeout(this.stateTimer); await this.saveState(); } } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { InMemoryFileDB }); //# sourceMappingURL=inMemFileDB.js.map