From e02eb728635d54139bc0473efdd6704db5d06b6f Mon Sep 17 00:00:00 2001 From: LouisLam Date: Thu, 22 Jul 2021 02:02:35 +0800 Subject: [PATCH] add db migration --- db/patch1.sql | 37 +++++++++++++ server/database.js | 119 ++++++++++++++++++++++++++++++++++++++++++ server/server.js | 58 ++++++++++---------- server/util-server.js | 12 +++++ 4 files changed, 198 insertions(+), 28 deletions(-) create mode 100644 db/patch1.sql create mode 100644 server/database.js diff --git a/db/patch1.sql b/db/patch1.sql new file mode 100644 index 000000000..1ab6cc0cf --- /dev/null +++ b/db/patch1.sql @@ -0,0 +1,37 @@ +-- You should not modify if this have pushed to Github, unless it do serious wrong with the db. +-- Change Monitor.created_date from "TIMESTAMP" to "DATETIME" +-- SQL Generated by Intellij Idea +PRAGMA foreign_keys=off; + +BEGIN TRANSACTION; + +create table monitor_dg_tmp +( + id INTEGER not null + primary key autoincrement, + name VARCHAR(150), + active BOOLEAN default 1 not null, + user_id INTEGER + references user + on update cascade on delete set null, + interval INTEGER default 20 not null, + url TEXT, + type VARCHAR(20), + weight INTEGER default 2000, + hostname VARCHAR(255), + port INTEGER, + created_date DATETIME, + keyword VARCHAR(255) +); + +insert into monitor_dg_tmp(id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword) select id, name, active, user_id, interval, url, type, weight, hostname, port, created_date, keyword from monitor; + +drop table monitor; + +alter table monitor_dg_tmp rename to monitor; + +create index user_id on monitor (user_id); + +COMMIT; + +PRAGMA foreign_keys=on; diff --git a/server/database.js b/server/database.js new file mode 100644 index 000000000..49659e613 --- /dev/null +++ b/server/database.js @@ -0,0 +1,119 @@ +const fs = require("fs"); +const {sleep} = require("./util"); +const {R} = require("redbean-node"); +const {setSetting, setting} = require("./util-server"); + + +class Database { + + static templatePath = "./db/kuma.db" + static path = './data/kuma.db'; + static latestVersion = 1; + static noReject = true; + + static async patch() { + let version = parseInt(await setting("database_version")); + + if (! version) { + version = 0; + } + + console.info("Your database version: " + version); + console.info("Latest database version: " + this.latestVersion); + + if (version === this.latestVersion) { + console.info("Database no need to patch"); + } else { + console.info("Database patch is needed") + + console.info("Backup the db") + const backupPath = "./data/kuma.db.bak" + version; + fs.copyFileSync(Database.path, backupPath); + + // 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`; + console.info(`Patching ${sqlFile}`); + await Database.importSQLFile(sqlFile); + console.info(`Patched ${sqlFile}`); + await setSetting("database_version", i); + } + console.log("Database Patched Successfully"); + } catch (ex) { + await Database.close(); + console.error("Patch db failed!!! Restoring the backup") + fs.copyFileSync(backupPath, Database.path); + console.error(ex) + + console.error("Start Uptime-Kuma failed due to patch db failed") + console.error("Please submit the bug report if you still encounter the problem after restart: https://github.com/louislam/uptime-kuma/issues") + process.exit(1); + } + } + } + + /** + * Sadly, multi sql statements is not supported by many sqlite libraries, I have to implement it myself + * @param filename + * @returns {Promise} + */ + 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("--") + }); + + // Split statements by semicolon + // Filter out empty line + text = lines.join("\n") + + let statements = text.split(";") + .map((statement) => { + return statement.trim(); + }) + .filter((statement) => { + return statement !== ""; + }) + + for (let statement of statements) { + await R.exec(statement); + } + } + + /** + * Special handle, because tarn.js throw a promise reject that cannot be caught + * @returns {Promise} + */ + static async close() { + const listener = (reason, p) => { + Database.noReject = false; + }; + process.addListener('unhandledRejection', listener); + + console.log("Closing DB") + + while (true) { + Database.noReject = true; + await R.close() + await sleep(2000) + + if (Database.noReject) { + break; + } else { + console.log("Waiting to close the db") + } + } + console.log("SQLite closed") + + process.removeListener('unhandledRejection', listener); + } +} + +module.exports = Database; diff --git a/server/server.js b/server/server.js index 0d0ce51f1..342397cfc 100644 --- a/server/server.js +++ b/server/server.js @@ -12,6 +12,7 @@ const fs = require("fs"); const {getSettings} = require("./util-server"); const {Notification} = require("./notification") const gracefulShutdown = require('http-graceful-shutdown'); +const Database = require("./database"); const {sleep} = require("./util"); const args = require('args-parser')(process.argv); @@ -27,9 +28,28 @@ const server = http.createServer(app); const io = new Server(server); app.use(express.json()) +/** + * Total WebSocket client connected to server currently, no actual use + * @type {number} + */ let totalClient = 0; + +/** + * Use for decode the auth object + * @type {null} + */ let jwtSecret = null; + +/** + * Main monitor list + * @type {{}} + */ let monitorList = {}; + +/** + * Show Setup Page + * @type {boolean} + */ let needSetup = false; (async () => { @@ -555,19 +575,21 @@ function checkLogin(socket) { } async function initDatabase() { - const path = './data/kuma.db'; - - if (! fs.existsSync(path)) { + if (! fs.existsSync(Database.path)) { console.log("Copying Database") - fs.copyFileSync("./db/kuma.db", path); + fs.copyFileSync(Database.templatePath, Database.path); } console.log("Connecting to Database") R.setup('sqlite', { - filename: path + filename: Database.path }); console.log("Connected") + // Patch the database + await Database.patch() + + // Auto map the model to a bean object R.freeze(true) await R.autoloadModels("./server/model"); @@ -587,6 +609,7 @@ async function initDatabase() { console.log("Load JWT secret from database.") } + // If there is no record in user table, it is a new Uptime Kuma instance, need to setup if ((await R.count("user")) === 0) { console.log("No user, need setup") needSetup = true; @@ -705,11 +728,6 @@ const startGracefulShutdown = async () => { } -let noReject = true; -process.on('unhandledRejection', (reason, p) => { - noReject = false; -}); - async function shutdownFunction(signal) { console.log('Called signal: ' + signal); @@ -718,24 +736,8 @@ async function shutdownFunction(signal) { let monitor = monitorList[id] monitor.stop() } - await sleep(2000) - - console.log("Closing DB") - - // Special handle, because tarn.js throw a promise reject that cannot be caught - while (true) { - noReject = true; - await R.close() - await sleep(2000) - - if (noReject) { - break; - } else { - console.log("Waiting...") - } - } - - console.log("OK") + await sleep(2000); + await Database.close(); } function finalFunction() { diff --git a/server/util-server.js b/server/util-server.js index 6904a65a4..b387f4c7c 100644 --- a/server/util-server.js +++ b/server/util-server.js @@ -45,6 +45,18 @@ exports.setting = async function (key) { ]) } +exports.setSetting = async function (key, value) { + let bean = await R.findOne("setting", " `key` = ? ", [ + key + ]) + if (! bean) { + bean = R.dispense("setting") + bean.key = key; + } + bean.value = value; + await R.store(bean) +} + exports.getSettings = async function (type) { let list = await R.getAll("SELECT * FROM setting WHERE `type` = ? ", [ type