uptime-kuma/server/database.js

808 lines
26 KiB
JavaScript
Raw Normal View History

2021-07-21 19:02:35 +01:00
const fs = require("fs");
2021-07-30 05:24:46 +01:00
const { R } = require("redbean-node");
const { log, sleep } = require("../src/util");
const knex = require("knex");
const path = require("path");
2023-02-05 09:45:36 +00:00
const { EmbeddedMariaDB } = require("./embedded-mariadb");
2023-04-03 12:35:31 +01:00
const mysql = require("mysql2/promise");
const { Settings } = require("./settings");
2024-09-27 18:22:45 +01:00
const { UptimeCalculator } = require("./uptime-calculator");
const dayjs = require("dayjs");
2021-07-21 19:02:35 +01:00
/**
* Database & App Data Folder
*/
2021-07-21 19:02:35 +01:00
class Database {
2023-10-16 03:18:28 +01:00
/**
* Boostrap database for SQLite
* @type {string}
*/
static templatePath = "./db/kuma.db";
/**
* Data Dir (Default: ./data)
2023-10-16 03:18:28 +01:00
* @type {string}
*/
2021-09-02 14:08:00 +01:00
static dataDir;
/**
* User Upload Dir (Default: ./data/upload)
2023-10-16 03:18:28 +01:00
* @type {string}
*/
static uploadDir;
2023-10-16 03:18:28 +01:00
/**
* Chrome Screenshot Dir (Default: ./data/screenshots)
* @type {string}
*/
2023-06-27 08:54:33 +01:00
static screenshotDir;
2023-10-16 03:18:28 +01:00
/**
* SQLite file path (Default: ./data/kuma.db)
* @type {string}
*/
static sqlitePath;
2023-10-16 03:18:28 +01:00
/**
* For storing Docker TLS certs (Default: ./data/docker-tls)
* @type {string}
*/
2023-08-04 16:08:44 +01:00
static dockerTLSDir;
/**
* @type {boolean}
*/
static patched = false;
/**
2023-02-11 14:21:06 +00:00
* SQLite only
* Add patch filename in key
* Values:
* true: Add it regardless of order
* false: Do nothing
* { parents: []}: Need parents before add it
2023-02-11 14:21:06 +00:00
* @deprecated
*/
static patchList = {
"patch-setting-value-type.sql": true,
"patch-improve-performance.sql": true,
2021-09-11 15:37:33 +01:00
"patch-2fa.sql": true,
"patch-add-retry-interval-monitor.sql": true,
2021-09-16 15:48:28 +01:00
"patch-incident-table.sql": true,
"patch-group-table.sql": true,
2021-09-30 17:09:43 +01:00
"patch-monitor-push_token.sql": true,
"patch-http-monitor-method-body-and-headers.sql": true,
2021-10-18 23:42:33 +01:00
"patch-2fa-invalidate-used-token.sql": true,
"patch-notification_sent_history.sql": true,
"patch-monitor-basic-auth.sql": true,
"patch-add-docker-columns.sql": true,
2021-12-27 10:54:48 +00:00
"patch-status-page.sql": true,
"patch-proxy.sql": true,
"patch-monitor-expiry-notification.sql": true,
"patch-status-page-footer-css.sql": true,
2021-11-04 01:46:43 +00:00
"patch-added-mqtt-monitor.sql": true,
"patch-add-clickable-status-page-link.sql": true,
2022-05-12 18:48:03 +01:00
"patch-add-sqlserver-monitor.sql": true,
2022-05-13 18:58:23 +01:00
"patch-add-other-auth.sql": { parents: [ "patch-monitor-basic-auth.sql" ] },
"patch-grpc-monitor.sql": true,
2022-05-12 10:48:38 +01:00
"patch-add-radius-monitor.sql": true,
2022-01-24 08:18:12 +00:00
"patch-monitor-add-resend-interval.sql": true,
"patch-ping-packet-size.sql": true,
2022-10-11 14:48:43 +01:00
"patch-maintenance-table2.sql": true,
2023-01-08 08:22:36 +00:00
"patch-add-gamedig-monitor.sql": true,
"patch-add-google-analytics-status-page-tag.sql": true,
2023-02-25 09:59:25 +00:00
"patch-http-body-encoding.sql": true,
"patch-add-description-monitor.sql": true,
2023-02-28 08:58:36 +00:00
"patch-api-key-table.sql": true,
"patch-monitor-tls.sql": true,
2023-03-30 21:04:17 +01:00
"patch-maintenance-cron.sql": true,
"patch-add-parent-monitor.sql": true,
2023-04-06 01:10:21 +01:00
"patch-add-invert-keyword.sql": true,
✨ feat: json-query monitor added (#3253) * ✨ feat: json-query monitor added Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: import warning error Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: br tag and remove comment Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: supporting compare string with other types Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: switch to a better lib for json query Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: better description on json query and using `v-html` in jsonQueryDescription element to fix `a` tags Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: result variable in error message Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: typos in json query description Co-authored-by: Frank Elsinga <frank@elsinga.de> * 📝 docs: `HTTP(s) Json Query` added to monitor list in `README.md` Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: needed white space in `README.md` Co-authored-by: Frank Elsinga <frank@elsinga.de> * Nostr dm notifications (#3051) * Add nostr DM notification provider * require crypto for node 18 compatibility * remove whitespace Co-authored-by: Frank Elsinga <frank@elsinga.de> * move closer to where it is used * simplify success or failure logic * don't clobber the non-alert msg * Update server/notification-providers/nostr.js Co-authored-by: Frank Elsinga <frank@elsinga.de> * polyfills required for node <= 18 * resolve linter warnings * missing comma --------- Co-authored-by: Frank Elsinga <frank@elsinga.de> * Drop nostr * Rebuild package-lock.json * Lint --------- Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> Co-authored-by: Frank Elsinga <frank@elsinga.de> Co-authored-by: zappityzap <128872140+zappityzap@users.noreply.github.com> Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-07-13 16:37:26 +01:00
"patch-added-json-query.sql": true,
✨ feat: added kafka producer (#3268) * ✨ feat: added kafka producer Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: eslint warn Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: typings and auth problems Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: better variable name to trrack disconnection Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: grouping Kafka Producer special settings into one template Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * ✨ feat: add kafka producer translations into `en.json` Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: disable close-on-select on kafka broker picker Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * 🐛 fix: `en.json` invalid json (conflict resolve) Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> * Nostr dm notifications (#3051) * Add nostr DM notification provider * require crypto for node 18 compatibility * remove whitespace Co-authored-by: Frank Elsinga <frank@elsinga.de> * move closer to where it is used * simplify success or failure logic * don't clobber the non-alert msg * Update server/notification-providers/nostr.js Co-authored-by: Frank Elsinga <frank@elsinga.de> * polyfills required for node <= 18 * resolve linter warnings * missing comma --------- Co-authored-by: Frank Elsinga <frank@elsinga.de> * Drop nostr * Minor * Fix a bug of clone --------- Signed-off-by: Muhammed Hussein Karimi <info@karimi.dev> Co-authored-by: Frank Elsinga <frank@elsinga.de> Co-authored-by: Louis Lam <louislam@users.noreply.github.com>
2023-07-17 09:15:44 +01:00
"patch-added-kafka-producer.sql": true,
2023-07-05 00:37:45 +01:00
"patch-add-certificate-expiry-status-page.sql": true,
"patch-monitor-oauth-cc.sql": true,
"patch-add-timeout-monitor.sql": true,
"patch-add-gamedig-given-port.sql": true,
2023-11-13 13:19:43 +00:00
"patch-notification-config.sql": true,
"patch-fix-kafka-producer-booleans.sql": true,
"patch-timeout.sql": true,
"patch-monitor-tls-info-add-fk.sql": true, // The last file so far converted to a knex migration file
2022-04-26 00:26:57 +01:00
};
/**
* The final version should be 10 after merged tag feature
* @deprecated Use patchList for any new feature
*/
static latestVersion = 10;
2021-07-21 19:02:35 +01:00
static noReject = true;
2023-02-11 14:21:06 +00:00
static dbConfig = {};
static knexMigrationsPath = "./db/knex_migrations";
/**
* Initialize the data directory
* @param {object} args Arguments to initialize DB with
* @returns {void}
*/
static initDataDir(args) {
2021-09-20 09:29:18 +01:00
// Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
Database.sqlitePath = path.join(Database.dataDir, "kuma.db");
2021-09-20 09:29:18 +01:00
if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true });
}
Database.uploadDir = path.join(Database.dataDir, "upload/");
if (! fs.existsSync(Database.uploadDir)) {
fs.mkdirSync(Database.uploadDir, { recursive: true });
}
2023-06-27 08:54:33 +01:00
// Create screenshot dir
Database.screenshotDir = path.join(Database.dataDir, "screenshots/");
2023-06-27 08:54:33 +01:00
if (! fs.existsSync(Database.screenshotDir)) {
fs.mkdirSync(Database.screenshotDir, { recursive: true });
}
2023-08-04 16:08:44 +01:00
Database.dockerTLSDir = path.join(Database.dataDir, "docker-tls/");
if (! fs.existsSync(Database.dockerTLSDir)) {
fs.mkdirSync(Database.dockerTLSDir, { recursive: true });
}
log.info("server", `Data Dir: ${Database.dataDir}`);
2021-09-20 09:29:18 +01:00
}
/**
* Read the database config
* @throws {Error} If the config is invalid
* @typedef {string|undefined} envString
* @returns {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} Database config
*/
static readDBConfig() {
let dbConfig;
let dbConfigString = fs.readFileSync(path.join(Database.dataDir, "db-config.json")).toString("utf-8");
dbConfig = JSON.parse(dbConfigString);
if (typeof dbConfig !== "object") {
throw new Error("Invalid db-config.json, it must be an object");
}
if (typeof dbConfig.type !== "string") {
throw new Error("Invalid db-config.json, type must be a string");
}
return dbConfig;
}
/**
* @typedef {string|undefined} envString
* @param {{type: "sqlite"} | {type:envString, hostname:envString, port:envString, database:envString, username:envString, password:envString}} dbConfig the database configuration that should be written
* @returns {void}
*/
static writeDBConfig(dbConfig) {
fs.writeFileSync(path.join(Database.dataDir, "db-config.json"), JSON.stringify(dbConfig, null, 4));
2021-09-20 09:29:18 +01:00
}
/**
* Connect to the database
* @param {boolean} testMode Should the connection be started in test mode?
* @param {boolean} autoloadModels Should models be automatically loaded?
* @param {boolean} noLog Should logs not be output?
* @returns {Promise<void>}
*/
static async connect(testMode = false, autoloadModels = true, noLog = false) {
2021-08-22 16:35:24 +01:00
const acquireConnectionTimeout = 120 * 1000;
2022-12-23 14:43:56 +00:00
let dbConfig;
try {
dbConfig = this.readDBConfig();
2023-02-11 14:21:06 +00:00
Database.dbConfig = dbConfig;
} catch (err) {
log.warn("db", err.message);
2022-12-23 14:43:56 +00:00
dbConfig = {
2023-02-06 14:26:13 +00:00
type: "sqlite",
2022-12-23 14:43:56 +00:00
};
}
let config = {};
let mariadbPoolConfig = {
2024-05-19 15:46:22 +01:00
min: 0,
max: 10,
idleTimeoutMillis: 30000,
};
2023-02-11 19:44:15 +00:00
log.info("db", `Database Type: ${dbConfig.type}`);
2022-12-23 14:43:56 +00:00
if (dbConfig.type === "sqlite") {
2023-02-11 14:21:06 +00:00
if (! fs.existsSync(Database.sqlitePath)) {
log.info("server", "Copying Database");
fs.copyFileSync(Database.templatePath, Database.sqlitePath);
}
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
Dialect.prototype._driver = () => require("@louislam/sqlite3");
2022-12-23 14:43:56 +00:00
config = {
client: Dialect,
2022-12-23 14:43:56 +00:00
connection: {
filename: Database.sqlitePath,
2022-12-23 14:43:56 +00:00
acquireConnectionTimeout: acquireConnectionTimeout,
},
useNullAsDefault: true,
pool: {
min: 1,
max: 1,
idleTimeoutMillis: 120 * 1000,
propagateCreateError: false,
acquireTimeoutMillis: acquireConnectionTimeout,
}
};
2023-02-11 14:21:06 +00:00
} else if (dbConfig.type === "mariadb") {
2023-04-03 12:35:31 +01:00
if (!/^\w+$/.test(dbConfig.dbName)) {
throw Error("Invalid database name. A database name can only consist of letters, numbers and underscores");
2023-04-03 12:35:31 +01:00
}
const connection = await mysql.createConnection({
host: dbConfig.hostname,
port: dbConfig.port,
user: dbConfig.username,
password: dbConfig.password,
});
await connection.execute("CREATE DATABASE IF NOT EXISTS " + dbConfig.dbName + " CHARACTER SET utf8mb4");
2023-06-30 19:48:42 +01:00
connection.end();
2023-04-03 12:35:31 +01:00
2023-02-11 14:21:06 +00:00
config = {
client: "mysql2",
connection: {
host: dbConfig.hostname,
port: dbConfig.port,
user: dbConfig.username,
password: dbConfig.password,
database: dbConfig.dbName,
timezone: "Z",
typeCast: function (field, next) {
if (field.type === "DATETIME") {
// Do not perform timezone conversion
return field.string();
}
return next();
},
},
pool: mariadbPoolConfig,
2023-02-11 14:21:06 +00:00
};
2023-02-05 09:45:36 +00:00
} else if (dbConfig.type === "embedded-mariadb") {
let embeddedMariaDB = EmbeddedMariaDB.getInstance();
await embeddedMariaDB.start();
log.info("mariadb", "Embedded MariaDB started");
2022-12-23 14:43:56 +00:00
config = {
2023-02-05 09:45:36 +00:00
client: "mysql2",
2022-12-23 14:43:56 +00:00
connection: {
2023-02-05 09:45:36 +00:00
socketPath: embeddedMariaDB.socketPath,
user: "node",
2023-02-11 14:21:06 +00:00
database: "kuma",
timezone: "Z",
typeCast: function (field, next) {
if (field.type === "DATETIME") {
// Do not perform timezone conversion
return field.string();
}
return next();
},
},
pool: mariadbPoolConfig,
2022-12-23 14:43:56 +00:00
};
} else {
2023-02-05 09:45:36 +00:00
throw new Error("Unknown Database type: " + dbConfig.type);
2022-12-23 14:43:56 +00:00
}
2023-02-11 14:21:06 +00:00
// Set to utf8mb4 for MariaDB
if (dbConfig.type.endsWith("mariadb")) {
config.pool = {
afterCreate(conn, done) {
conn.query("SET CHARACTER SET utf8mb4;", (err) => done(err, conn));
},
};
}
2022-12-23 14:43:56 +00:00
const knexInstance = knex(config);
R.setup(knexInstance);
2021-08-16 19:09:40 +01:00
if (process.env.SQL_LOG === "1") {
R.debug(true);
}
2021-08-09 06:34:44 +01:00
// Auto map the model to a bean object
R.freeze(true);
if (autoloadModels) {
await R.autoloadModels("./server/model");
}
2023-02-11 14:21:06 +00:00
if (dbConfig.type === "sqlite") {
await this.initSQLite(testMode, noLog);
} else if (dbConfig.type.endsWith("mariadb")) {
await this.initMariaDB();
}
}
/**
@param {boolean} testMode Should the connection be started in test mode?
@param {boolean} noLog Should logs not be output?
@returns {Promise<void>}
*/
2023-02-11 14:21:06 +00:00
static async initSQLite(testMode, noLog) {
await R.exec("PRAGMA foreign_keys = ON");
if (testMode) {
// Change to MEMORY
await R.exec("PRAGMA journal_mode = MEMORY");
} else {
// Change to WAL
await R.exec("PRAGMA journal_mode = WAL");
}
await R.exec("PRAGMA cache_size = -12000");
await R.exec("PRAGMA auto_vacuum = INCREMENTAL");
2022-04-06 13:48:13 +01:00
// This ensures that an operating system crash or power failure will not corrupt the database.
// FULL synchronous is very safe, but it is also slower.
// Read more: https://sqlite.org/pragma.html#pragma_synchronous
await R.exec("PRAGMA synchronous = NORMAL");
2022-04-06 13:48:13 +01:00
if (!noLog) {
log.debug("db", "SQLite config:");
log.debug("db", await R.getAll("PRAGMA journal_mode"));
log.debug("db", await R.getAll("PRAGMA cache_size"));
log.debug("db", "SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
}
}
/**
* Initialize MariaDB
* @returns {Promise<void>}
*/
2023-02-11 14:21:06 +00:00
static async initMariaDB() {
log.debug("db", "Checking if MariaDB database exists...");
let hasTable = await R.hasTable("docker_host");
if (!hasTable) {
2023-02-11 19:44:15 +00:00
const { createTables } = require("../db/knex_init_db");
2023-02-11 14:21:06 +00:00
await createTables();
} else {
log.debug("db", "MariaDB database already exists");
}
}
/**
* Patch the database
* @returns {Promise<void>}
*/
2021-07-21 19:02:35 +01:00
static async patch() {
2023-06-30 10:26:37 +01:00
// Still need to keep this for old versions of Uptime Kuma
2023-02-11 14:21:06 +00:00
if (Database.dbConfig.type === "sqlite") {
await this.patchSqlite();
}
2023-06-30 10:26:37 +01:00
// Using knex migrations
2023-02-11 14:21:06 +00:00
// https://knexjs.org/guide/migrations.html
// https://gist.github.com/NigelEarle/70db130cc040cc2868555b29a0278261
2023-06-30 10:26:37 +01:00
try {
// Disable foreign key check for SQLite
// Known issue of knex: https://github.com/drizzle-team/drizzle-orm/issues/1813
if (Database.dbConfig.type === "sqlite") {
await R.exec("PRAGMA foreign_keys = OFF");
}
2023-06-30 10:26:37 +01:00
await R.knex.migrate.latest({
directory: Database.knexMigrationsPath,
});
// Enable foreign key check for SQLite
if (Database.dbConfig.type === "sqlite") {
await R.exec("PRAGMA foreign_keys = ON");
}
await this.migrateAggregateTable();
2023-06-30 10:26:37 +01:00
} catch (e) {
// Allow missing patch files for downgrade or testing pr.
if (e.message.includes("the following files are missing:")) {
log.warn("db", e.message);
log.warn("db", "Database migration failed, you may be downgrading Uptime Kuma.");
} else {
log.error("db", "Database migration failed");
throw e;
}
2023-06-30 10:26:37 +01:00
}
}
/**
* TODO
2023-06-30 10:26:37 +01:00
* @returns {Promise<void>}
*/
static async rollbackLatestPatch() {
2023-02-11 14:21:06 +00:00
}
/**
* Patch the database for SQLite
* @returns {Promise<void>}
2023-02-11 14:21:06 +00:00
* @deprecated
*/
static async patchSqlite() {
let version = parseInt(await Settings.get("database_version"));
2021-07-21 19:02:35 +01:00
if (! version) {
version = 0;
}
if (version !== this.latestVersion) {
log.info("db", "Your database version: " + version);
log.info("db", "Latest database version: " + this.latestVersion);
}
2021-07-21 19:02:35 +01:00
if (version === this.latestVersion) {
log.debug("db", "Database patch not needed");
} else if (version > this.latestVersion) {
log.warn("db", "Warning: Database version is newer than expected");
2021-07-21 19:02:35 +01:00
} else {
log.info("db", "Database patch is needed");
2021-07-21 19:02:35 +01:00
// Try catch anything here
2021-07-21 19:02:35 +01:00
try {
for (let i = version + 1; i <= this.latestVersion; i++) {
const sqlFile = `./db/old_migrations/patch${i}.sql`;
log.info("db", `Patching ${sqlFile}`);
2021-07-21 19:02:35 +01:00
await Database.importSQLFile(sqlFile);
log.info("db", `Patched ${sqlFile}`);
await Settings.set("database_version", i);
2021-07-21 19:02:35 +01:00
}
} catch (ex) {
await Database.close();
log.error("db", ex);
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
log.error("db", "Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
2021-07-21 19:02:35 +01:00
process.exit(1);
}
}
2023-02-11 14:21:06 +00:00
await this.patchSqlite2();
2022-03-08 06:33:35 +00:00
await this.migrateNewStatusPage();
}
/**
* Patch DB using new process
* Call it from patch() only
2023-02-11 14:21:06 +00:00
* @deprecated
* @private
* @returns {Promise<void>}
*/
2023-02-11 14:21:06 +00:00
static async patchSqlite2() {
log.debug("db", "Database Patch 2.0 Process");
let databasePatchedFiles = await Settings.get("databasePatchedFiles");
if (! databasePatchedFiles) {
databasePatchedFiles = {};
}
log.debug("db", "Patched files:");
log.debug("db", databasePatchedFiles);
try {
for (let sqlFilename in this.patchList) {
await this.patch2Recursion(sqlFilename, databasePatchedFiles);
}
if (this.patched) {
log.info("db", "Database Patched Successfully");
}
} catch (ex) {
await Database.close();
log.error("db", ex);
log.error("db", "Start Uptime-Kuma failed due to issue patching the database");
log.error("db", "Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues");
process.exit(1);
}
await Settings.set("databasePatchedFiles", databasePatchedFiles);
}
/**
2023-02-11 14:21:06 +00:00
* SQlite only
2022-03-08 06:33:35 +00:00
* Migrate status page value in setting to "status_page" table
* @returns {Promise<void>}
*/
static async migrateNewStatusPage() {
2022-03-24 15:43:07 +00:00
// Fix 1.13.0 empty slug bug
await R.exec("UPDATE status_page SET slug = 'empty-slug-recover' WHERE TRIM(slug) = ''");
let title = await Settings.get("title");
2022-03-08 06:33:35 +00:00
if (title) {
log.info("database", "Migrating Status Page");
2022-03-08 06:33:35 +00:00
2022-03-15 04:00:29 +00:00
let statusPageCheck = await R.findOne("status_page", " slug = 'default' ");
2022-03-08 06:33:35 +00:00
if (statusPageCheck !== null) {
log.info("database", "Migrating Status Page - Skip, default slug record is already existing");
2022-03-08 06:33:35 +00:00
return;
}
let statusPage = R.dispense("status_page");
statusPage.slug = "default";
2022-03-08 06:33:35 +00:00
statusPage.title = title;
statusPage.description = await Settings.get("description");
statusPage.icon = await Settings.get("icon");
statusPage.theme = await Settings.get("statusPageTheme");
statusPage.published = !!await Settings.get("statusPagePublished");
statusPage.search_engine_index = !!await Settings.get("searchEngineIndex");
statusPage.show_tags = !!await Settings.get("statusPageTags");
2022-03-08 06:33:35 +00:00
statusPage.password = null;
if (!statusPage.title) {
statusPage.title = "My Status Page";
}
if (!statusPage.icon) {
statusPage.icon = "";
}
if (!statusPage.theme) {
statusPage.theme = "light";
}
let id = await R.store(statusPage);
await R.exec("UPDATE incident SET status_page_id = ? WHERE status_page_id IS NULL", [
id
]);
await R.exec("UPDATE [group] SET status_page_id = ? WHERE status_page_id IS NULL", [
id
]);
2022-03-08 06:33:35 +00:00
await R.exec("DELETE FROM setting WHERE type = 'statusPage'");
// Migrate Entry Page if it is status page
let entryPage = await Settings.get("entryPage");
if (entryPage === "statusPage") {
await Settings.set("entryPage", "statusPage-default", "general");
}
log.info("database", "Migrating Status Page - Done");
2022-03-08 06:33:35 +00:00
}
}
/**
* Patch database using new patching process
* Used it patch2() only
* @private
* @param {string} sqlFilename Name of SQL file to load
* @param {object} databasePatchedFiles Patch status of database files
* @returns {Promise<void>}
*/
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
let value = this.patchList[sqlFilename];
if (! value) {
log.info("db", sqlFilename + " skip");
return;
}
// Check if patched
if (! databasePatchedFiles[sqlFilename]) {
log.info("db", sqlFilename + " is not patched");
if (value.parents) {
log.info("db", sqlFilename + " need parents");
for (let parentSQLFilename of value.parents) {
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
}
}
log.info("db", sqlFilename + " is patching");
this.patched = true;
await this.importSQLFile("./db/old_migrations/" + sqlFilename);
databasePatchedFiles[sqlFilename] = true;
log.info("db", sqlFilename + " was patched successfully");
} else {
log.debug("db", sqlFilename + " is already patched, skip");
}
2021-07-21 19:02:35 +01:00
}
/**
* Load an SQL file and execute it
* @param {string} filename Filename of SQL file to import
2021-07-21 19:02:35 +01:00
* @returns {Promise<void>}
*/
static async importSQLFile(filename) {
// Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
2021-07-21 19:02:35 +01:00
await R.getCell("SELECT 1");
let text = fs.readFileSync(filename).toString();
// Remove all comments (--)
let lines = text.split("\n");
lines = lines.filter((line) => {
return ! line.startsWith("--");
2021-07-21 19:02:35 +01:00
});
// Split statements by semicolon
// Filter out empty line
text = lines.join("\n");
2021-07-21 19:02:35 +01:00
let statements = text.split(";")
.map((statement) => {
return statement.trim();
})
.filter((statement) => {
return statement !== "";
});
2021-07-21 19:02:35 +01:00
for (let statement of statements) {
2021-09-01 08:02:04 +01:00
await R.exec(statement);
2021-07-21 19:02:35 +01:00
}
}
/**
* Special handle, because tarn.js throw a promise reject that cannot be caught
* @returns {Promise<void>}
*/
static async close() {
const listener = (reason, p) => {
Database.noReject = false;
};
process.addListener("unhandledRejection", listener);
log.info("db", "Closing the database");
2023-02-15 07:30:28 +00:00
// Flush WAL to main database
if (Database.dbConfig.type === "sqlite") {
await R.exec("PRAGMA wal_checkpoint(TRUNCATE)");
}
2023-02-15 07:30:28 +00:00
while (true) {
Database.noReject = true;
await R.close();
await sleep(2000);
if (Database.noReject) {
break;
} else {
log.info("db", "Waiting to close the database");
}
}
log.info("db", "Database closed");
process.removeListener("unhandledRejection", listener);
}
/**
* Get the size of the database (SQLite only)
* @returns {number} Size of database
*/
static getSize() {
if (Database.dbConfig.type === "sqlite") {
log.debug("db", "Database.getSize()");
let stats = fs.statSync(Database.sqlitePath);
log.debug("db", stats);
return stats.size;
}
return 0;
}
/**
* Shrink the database
* @returns {Promise<void>}
*/
static async shrink() {
if (Database.dbConfig.type === "sqlite") {
await R.exec("VACUUM");
}
}
2023-02-12 08:59:07 +00:00
/**
* @returns {string} Get the SQL for the current time plus a number of hours
*/
2023-02-12 08:59:07 +00:00
static sqlHourOffset() {
if (Database.dbConfig.type === "sqlite") {
2023-02-12 08:59:07 +00:00
return "DATETIME('now', ? || ' hours')";
} else {
return "DATE_ADD(NOW(), INTERVAL ? HOUR)";
}
}
2024-09-01 10:19:18 +01:00
/**
* TODO: Migrate the old data in the heartbeat table to the new format (stat_daily, stat_hourly, stat_minutely)
* It should be run once while upgrading V1 to V2
* @returns {Promise<void>}
*/
static async migrateAggregateTable() {
log.debug("db", "Enter Migrate Aggregate Table function");
//
2024-09-27 18:22:45 +01:00
let migrated = await Settings.get("migratedAggregateTable");
if (migrated) {
log.debug("db", "Migrated, skip migration");
return;
}
log.info("db", "Migrating Aggregate Table");
// Migrate heartbeat to stat_minutely, using knex transaction
const trx = await R.knex.transaction();
// Get a list of unique dates from the heartbeat table, using raw sql
let dates = await trx.raw(`
SELECT DISTINCT DATE(time) AS date
FROM heartbeat
2024-09-27 18:22:45 +01:00
ORDER BY date ASC
`);
// Get a list of unique monitors from the heartbeat table, using raw sql
let monitors = await trx.raw(`
SELECT DISTINCT monitor_id
FROM heartbeat
`);
2024-09-27 18:22:45 +01:00
// Stop if stat_* tables are not empty
2024-09-27 14:42:20 +01:00
for (let table of [ "stat_minutely", "stat_hourly", "stat_daily" ]) {
2024-09-27 18:22:45 +01:00
let countResult = await trx.raw(`SELECT COUNT(*) AS count FROM ${table}`);
let count = countResult[0].count;
if (count > 0) {
log.warn("db", `Aggregate table ${table} is not empty, migration will not be started (Maybe you were using 2.0.0-dev?)`);
2024-10-07 11:56:07 +01:00
trx.commit();
2024-09-27 18:22:45 +01:00
return;
2024-09-27 14:42:20 +01:00
}
}
console.log("Dates", dates);
console.log("Monitors", monitors);
2024-09-01 10:19:18 +01:00
2024-09-27 18:22:45 +01:00
for (let monitor of monitors) {
for (let date of dates) {
log.info("db", `Migrating monitor ${monitor.monitor_id} on date ${date.date}`);
// New Uptime Calculator
let calculator = new UptimeCalculator();
// TODO: Pass transaction to the calculator
// calculator.setTransaction(trx);
// Get all the heartbeats for this monitor and date
let heartbeats = await trx("heartbeat")
.where("monitor_id", monitor.monitor_id)
.whereRaw("DATE(time) = ?", [ date.date ])
.orderBy("time", "asc");
for (let heartbeat of heartbeats) {
calculator.update(heartbeat.status, heartbeat.ping, dayjs(heartbeat.time));
}
}
}
trx.commit();
2024-09-27 14:42:20 +01:00
//await Settings.set("migratedAggregateTable", true);
2024-09-01 10:19:18 +01:00
}
2021-07-21 19:02:35 +01:00
}
module.exports = Database;