@iobroker/db-base
Version:
This Library contains base classes that are used by the database classes for ioBroker
396 lines (395 loc) • 15 kB
JavaScript
;
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