add db migration
This commit is contained in:
		
							parent
							
								
									1c0dc18d72
								
							
						
					
					
						commit
						e02eb72863
					
				
					 4 changed files with 198 additions and 28 deletions
				
			
		
							
								
								
									
										37
									
								
								db/patch1.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								db/patch1.sql
									
									
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
							
								
								
									
										119
									
								
								server/database.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								server/database.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -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<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("--") | ||||
|         }); | ||||
| 
 | ||||
|         // 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<void>} | ||||
|      */ | ||||
|     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; | ||||
|  | @ -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() { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue