Merge branch 'master' into Spiritreader_master

# Conflicts:
#	src/pages/EditMonitor.vue
This commit is contained in:
LouisLam
2021-07-22 17:34:41 +08:00
20 changed files with 363 additions and 80 deletions

119
server/database.js Normal file
View 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;

View File

@@ -3,8 +3,6 @@ const utc = require('dayjs/plugin/utc')
var timezone = require('dayjs/plugin/timezone')
dayjs.extend(utc)
dayjs.extend(timezone)
const axios = require("axios");
const {R} = require("redbean-node");
const {BeanModel} = require("redbean-node/dist/bean-model");

View File

@@ -49,8 +49,6 @@ class Monitor extends BeanModel {
let retries = 0;
const beat = async () => {
console.log(`Monitor ${this.id}: Heartbeat`)
if (! previousBeat) {
previousBeat = await R.findOne("heartbeat", " monitor_id = ? ORDER BY time DESC", [
this.id
@@ -131,8 +129,6 @@ class Monitor extends BeanModel {
this.id
])
let promiseList = [];
let text;
if (bean.status === 1) {
text = "✅ Up"
@@ -143,16 +139,24 @@ class Monitor extends BeanModel {
let msg = `[${this.name}] [${text}] ${bean.msg}`;
for(let notification of notificationList) {
promiseList.push(Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON()));
try {
await Notification.send(JSON.parse(notification.config), msg, await this.toJSON(), bean.toJSON())
} catch (e) {
console.error("Cannot send notification to " + notification.name)
}
}
await Promise.all(promiseList);
}
} else {
bean.important = false;
}
if (bean.status === 1) {
console.info(`Monitor #${this.id} '${this.name}': Successful Response: ${bean.ping} ms | Interval: ${this.interval} seconds | Type: ${this.type}`)
} else {
console.warn(`Monitor #${this.id} '${this.name}': Failing: ${bean.msg} | Type: ${this.type}`)
}
io.to(this.user_id).emit("heartbeat", bean.toJSON());
await R.store(bean)

View File

@@ -72,7 +72,7 @@ class Notification {
finalData = data;
}
let res = await axios.post(notification.webhookURL, finalData, config)
await axios.post(notification.webhookURL, finalData, config)
return okMsg;
} catch (error) {
@@ -90,7 +90,7 @@ class Notification {
username: 'Uptime-Kuma',
content: msg
}
let res = await axios.post(notification.discordWebhookUrl, data)
await axios.post(notification.discordWebhookUrl, data)
return okMsg;
}
// If heartbeatJSON is not null, we go into the normal alerting loop.
@@ -116,7 +116,7 @@ class Notification {
]
}]
}
let res = await axios.post(notification.discordWebhookUrl, data)
await axios.post(notification.discordWebhookUrl, data)
return okMsg;
} catch(error) {
throwGeneralAxiosError(error)
@@ -131,7 +131,7 @@ class Notification {
};
let config = {};
let res = await axios.post(notification.signalURL, data, config)
await axios.post(notification.signalURL, data, config)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
@@ -141,7 +141,7 @@ class Notification {
try {
if (heartbeatJSON == null) {
let data = {'text': "Uptime Kuma Slack testing successful.", 'channel': notification.slackchannel, 'username': notification.slackusername, 'icon_emoji': notification.slackiconemo}
let res = await axios.post(notification.slackwebhookURL, data)
await axios.post(notification.slackwebhookURL, data)
return okMsg;
}
@@ -186,7 +186,7 @@ class Notification {
}
]
}
let res = await axios.post(notification.slackwebhookURL, data)
await axios.post(notification.slackwebhookURL, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
@@ -199,7 +199,7 @@ class Notification {
let data = {'message': "<b>Uptime Kuma Pushover testing successful.</b>",
'user': notification.pushoveruserkey, 'token': notification.pushoverapptoken, 'sound':notification.pushoversounds,
'priority': notification.pushoverpriority, 'title':notification.pushovertitle, 'retry': "30", 'expire':"3600", 'html': 1}
let res = await axios.post(pushoverlink, data)
await axios.post(pushoverlink, data)
return okMsg;
}
@@ -214,7 +214,7 @@ class Notification {
"expire": "3600",
"html": 1
}
let res = await axios.post(pushoverlink, data)
await axios.post(pushoverlink, data)
return okMsg;
} catch (error) {
throwGeneralAxiosError(error)
@@ -278,7 +278,7 @@ class Notification {
});
// send mail with defined transport object
let info = await transporter.sendMail({
await transporter.sendMail({
from: `"Uptime Kuma" <${notification.smtpFrom}>`,
to: notification.smtpTo,
subject: msg,

View File

@@ -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 () => {
@@ -163,10 +183,6 @@ let needSetup = false;
msg: e.message
});
}
});
// Auth Only API
@@ -556,19 +572,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");
@@ -588,6 +606,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;
@@ -706,11 +725,6 @@ const startGracefulShutdown = async () => {
}
let noReject = true;
process.on('unhandledRejection', (reason, p) => {
noReject = false;
});
async function shutdownFunction(signal) {
console.log('Called signal: ' + signal);
@@ -719,24 +733,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() {

View File

@@ -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