uptime-kuma/server/database.js

393 lines
11 KiB
JavaScript
Raw Normal View History

2021-07-21 11:02:35 -07:00
const fs = require("fs");
2021-07-29 21:24:46 -07:00
const { R } = require("redbean-node");
2021-08-06 04:12:49 -07:00
const { setSetting, setting } = require("./util-server");
2021-11-11 03:31:28 -08:00
const { log, sleep } = require("../src/util");
const dayjs = require("dayjs");
const knex = require("knex");
2021-07-21 11:02:35 -07:00
/**
* Database & App Data Folder
*/
2021-07-21 11:02:35 -07:00
class Database {
static templatePath = "./db/kuma.db";
/**
* Data Dir (Default: ./data)
*/
2021-09-02 06:08:00 -07:00
static dataDir;
/**
* User Upload Dir (Default: ./data/upload)
*/
static uploadDir;
2021-09-02 06:08:00 -07:00
static path;
/**
* @type {boolean}
*/
static patched = false;
/**
* For Backup only
*/
static backupPath = null;
/**
* Add patch filename in key
* Values:
* true: Add it regardless of order
* false: Do nothing
* { parents: []}: Need parents before add it
*/
static patchList = {
"patch-setting-value-type.sql": true,
"patch-improve-performance.sql": true,
2021-09-11 07:37:33 -07:00
"patch-2fa.sql": true,
"patch-add-retry-interval-monitor.sql": true,
2021-09-16 07:48:28 -07:00
"patch-incident-table.sql": true,
"patch-group-table.sql": true,
2021-09-30 09:09:43 -07:00
"patch-monitor-push_token.sql": true,
"patch-http-monitor-method-body-and-headers.sql": true,
2021-10-18 15:42:33 -07:00
"patch-2fa-invalidate-used-token.sql": true,
"patch-notification_sent_history.sql": true,
}
/**
* The final version should be 10 after merged tag feature
* @deprecated Use patchList for any new feature
*/
static latestVersion = 10;
2021-07-21 11:02:35 -07:00
static noReject = true;
2021-09-20 01:29:18 -07:00
static init(args) {
// Data Directory (must be end with "/")
Database.dataDir = process.env.DATA_DIR || args["data-dir"] || "./data/";
Database.path = Database.dataDir + "kuma.db";
if (! fs.existsSync(Database.dataDir)) {
fs.mkdirSync(Database.dataDir, { recursive: true });
}
Database.uploadDir = Database.dataDir + "upload/";
if (! fs.existsSync(Database.uploadDir)) {
fs.mkdirSync(Database.uploadDir, { recursive: true });
}
2021-11-11 03:31:28 -08:00
log("db", `Data Dir: ${Database.dataDir}`);
2021-09-20 01:29:18 -07:00
}
2021-08-08 22:34:44 -07:00
static async connect() {
2021-08-22 08:35:24 -07:00
const acquireConnectionTimeout = 120 * 1000;
const Dialect = require("knex/lib/dialects/sqlite3/index.js");
Dialect.prototype._driver = () => require("@louislam/sqlite3");
const knexInstance = knex({
client: Dialect,
connection: {
filename: Database.path,
acquireConnectionTimeout: acquireConnectionTimeout,
},
useNullAsDefault: true,
pool: {
min: 1,
max: 1,
idleTimeoutMillis: 120 * 1000,
propagateCreateError: false,
acquireTimeoutMillis: acquireConnectionTimeout,
}
});
R.setup(knexInstance);
2021-08-16 11:09:40 -07:00
if (process.env.SQL_LOG === "1") {
R.debug(true);
}
2021-08-08 22:34:44 -07:00
// Auto map the model to a bean object
R.freeze(true);
2021-08-08 22:34:44 -07:00
await R.autoloadModels("./server/model");
await R.exec("PRAGMA foreign_keys = ON");
// Change to WAL
await R.exec("PRAGMA journal_mode = WAL");
await R.exec("PRAGMA cache_size = -12000");
await R.exec("PRAGMA auto_vacuum = FULL");
2021-11-11 03:31:28 -08:00
log("db", "SQLite config:");
log("db", await R.getAll("PRAGMA journal_mode"));
log("db", await R.getAll("PRAGMA cache_size"));
log("db","SQLite Version: " + await R.getCell("SELECT sqlite_version()"));
}
2021-07-21 11:02:35 -07:00
static async patch() {
let version = parseInt(await setting("database_version"));
if (! version) {
version = 0;
}
2021-11-11 03:31:28 -08:00
log("db", "Your database version: " + version);
log("db", "Latest database version: " + this.latestVersion);
2021-07-21 11:02:35 -07:00
if (version === this.latestVersion) {
2021-11-11 03:31:28 -08:00
log("db", "Database patch not needed");
} else if (version > this.latestVersion) {
2021-11-11 03:31:28 -08:00
log("db", "Warning: Database version is newer than expected");
2021-07-21 11:02:35 -07:00
} else {
2021-11-11 03:31:28 -08:00
log("db", "Database patch is needed");
2021-07-21 11:02:35 -07:00
this.backup(version);
2021-08-19 02:49:19 -07:00
2021-07-21 11:02:35 -07:00
// Try catch anything here, if gone wrong, restore the backup
try {
for (let i = version + 1; i <= this.latestVersion; i++) {
const sqlFile = `./db/patch${i}.sql`;
2021-11-11 03:31:28 -08:00
log("db", `Patching ${sqlFile}`);
2021-07-21 11:02:35 -07:00
await Database.importSQLFile(sqlFile);
2021-11-11 03:31:28 -08:00
log("db", `Patched ${sqlFile}`);
2021-07-21 11:02:35 -07:00
await setSetting("database_version", i);
}
} catch (ex) {
await Database.close();
2021-11-11 03:31:28 -08:00
log("db", ex, "error");
log("db", "Start Uptime-Kuma failed due to issue patching the database", "error");
log("db", "Please submit a bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues", "error");
this.restore();
2021-07-21 11:02:35 -07:00
process.exit(1);
}
}
await this.patch2();
}
/**
* Call it from patch() only
* @returns {Promise<void>}
*/
static async patch2() {
2021-11-11 03:31:28 -08:00
log("db", "Database Patch 2.0 Process");
let databasePatchedFiles = await setting("databasePatchedFiles");
if (! databasePatchedFiles) {
databasePatchedFiles = {};
}
2021-11-11 03:31:28 -08:00
log("db", "Patched files:", "debug");
log("db", databasePatchedFiles, "debug");
try {
for (let sqlFilename in this.patchList) {
await this.patch2Recursion(sqlFilename, databasePatchedFiles);
}
if (this.patched) {
2021-11-11 03:31:28 -08:00
log("db", "Database Patched Successfully");
}
} catch (ex) {
await Database.close();
2021-11-11 03:31:28 -08:00
log("db", ex, "error");
log("db", "Start Uptime-Kuma failed due to issue patching the database", "error");
log("db", "Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues", "error");
this.restore();
process.exit(1);
}
await setSetting("databasePatchedFiles", databasePatchedFiles);
}
/**
* Used it patch2() only
* @param sqlFilename
* @param databasePatchedFiles
*/
static async patch2Recursion(sqlFilename, databasePatchedFiles) {
let value = this.patchList[sqlFilename];
if (! value) {
2021-11-11 03:31:28 -08:00
log("db", sqlFilename + " skip");
return;
}
// Check if patched
if (! databasePatchedFiles[sqlFilename]) {
2021-11-11 03:31:28 -08:00
log("db", sqlFilename + " is not patched");
if (value.parents) {
2021-11-11 03:31:28 -08:00
log("db", sqlFilename + " need parents");
for (let parentSQLFilename of value.parents) {
await this.patch2Recursion(parentSQLFilename, databasePatchedFiles);
}
}
this.backup(dayjs().format("YYYYMMDDHHmmss"));
2021-11-11 03:31:28 -08:00
log("db", sqlFilename + " is patching");
this.patched = true;
await this.importSQLFile("./db/" + sqlFilename);
databasePatchedFiles[sqlFilename] = true;
2021-11-11 03:31:28 -08:00
log("db", sqlFilename + " was patched successfully");
} else {
2021-11-11 03:31:28 -08:00
log("db", sqlFilename + " is already patched, skip", "debug");
}
2021-07-21 11:02:35 -07:00
}
/**
* Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself
* @param filename
* @returns {Promise<void>}
*/
static async importSQLFile(filename) {
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 11:02:35 -07:00
});
// Split statements by semicolon
// Filter out empty line
text = lines.join("\n");
2021-07-21 11:02:35 -07:00
let statements = text.split(";")
.map((statement) => {
return statement.trim();
})
.filter((statement) => {
return statement !== "";
});
2021-07-21 11:02:35 -07:00
for (let statement of statements) {
2021-09-01 00:02:04 -07:00
await R.exec(statement);
2021-07-21 11:02:35 -07:00
}
}
static getBetterSQLite3Database() {
return R.knex.client.acquireConnection();
}
2021-07-21 11:02:35 -07: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);
2021-11-11 03:31:28 -08:00
log("db", "Closing the database");
while (true) {
Database.noReject = true;
await R.close();
await sleep(2000);
if (Database.noReject) {
break;
} else {
2021-11-11 03:31:28 -08:00
log("db", "Waiting to close the database");
}
}
2021-11-11 03:31:28 -08:00
log("db", "SQLite closed");
process.removeListener("unhandledRejection", listener);
}
/**
* One backup one time in this process.
* Reset this.backupPath if you want to backup again
* @param version
*/
static backup(version) {
if (! this.backupPath) {
2021-11-11 03:31:28 -08:00
log("db", "Backing up the database");
this.backupPath = this.dataDir + "kuma.db.bak" + version;
fs.copyFileSync(Database.path, this.backupPath);
const shmPath = Database.path + "-shm";
if (fs.existsSync(shmPath)) {
this.backupShmPath = shmPath + ".bak" + version;
fs.copyFileSync(shmPath, this.backupShmPath);
}
const walPath = Database.path + "-wal";
if (fs.existsSync(walPath)) {
this.backupWalPath = walPath + ".bak" + version;
fs.copyFileSync(walPath, this.backupWalPath);
}
}
}
/**
*
*/
static restore() {
if (this.backupPath) {
2021-11-11 03:31:28 -08:00
log("db", "Patching the database failed!!! Restoring the backup", "error");
const shmPath = Database.path + "-shm";
const walPath = Database.path + "-wal";
// Delete patch failed db
try {
if (fs.existsSync(Database.path)) {
fs.unlinkSync(Database.path);
}
if (fs.existsSync(shmPath)) {
fs.unlinkSync(shmPath);
}
if (fs.existsSync(walPath)) {
fs.unlinkSync(walPath);
}
} catch (e) {
2021-11-11 03:31:28 -08:00
log("db", "Restore failed; you may need to restore the backup manually", "error");
process.exit(1);
}
// Restore backup
fs.copyFileSync(this.backupPath, Database.path);
if (this.backupShmPath) {
fs.copyFileSync(this.backupShmPath, shmPath);
}
if (this.backupWalPath) {
fs.copyFileSync(this.backupWalPath, walPath);
}
} else {
2021-11-11 03:31:28 -08:00
log("db", "Nothing to restore");
2021-07-21 11:02:35 -07:00
}
}
static getSize() {
2021-11-11 03:31:28 -08:00
log("db", "Database.getSize()", "debug");
let stats = fs.statSync(Database.path);
2021-11-11 03:31:28 -08:00
log("db", stats, "debug");
return stats.size;
}
static async shrink() {
await R.exec("VACUUM");
}
2021-07-21 11:02:35 -07:00
}
module.exports = Database;