mirror of
https://github.com/louislam/uptime-kuma.git
synced 2025-06-03 12:02:33 +02:00

Reformatted code across multiple modules, standardizing string quotes, indentation, and spacing. Improved readability by restructuring blocks and aligning object properties consistently. These changes ensure better code maintainability and follow standard conventions. Signed-off-by: Toby Liddicoat <toby@codesure.co.uk>
462 lines
15 KiB
JavaScript
462 lines
15 KiB
JavaScript
const { BeanModel } = require("redbean-node/dist/bean-model");
|
|
const {
|
|
parseTimeObject,
|
|
parseTimeFromTimeObject,
|
|
log,
|
|
} = require("../../src/util");
|
|
const { R } = require("redbean-node");
|
|
const dayjs = require("dayjs");
|
|
const Cron = require("croner");
|
|
const { UptimeKumaServer } = require("../uptime-kuma-server");
|
|
const apicache = require("../modules/apicache");
|
|
|
|
class Maintenance extends BeanModel {
|
|
|
|
/**
|
|
* Return an object that ready to parse to JSON for public
|
|
* Only show necessary data to public
|
|
* @returns {Promise<object>} Object ready to parse
|
|
*/
|
|
async toPublicJSON() {
|
|
|
|
let dateRange = [];
|
|
if (this.start_date) {
|
|
dateRange.push(this.start_date);
|
|
} else {
|
|
dateRange.push(null);
|
|
}
|
|
|
|
if (this.end_date) {
|
|
dateRange.push(this.end_date);
|
|
}
|
|
|
|
let timeRange = [];
|
|
let startTime = parseTimeObject(this.start_time);
|
|
timeRange.push(startTime);
|
|
let endTime = parseTimeObject(this.end_time);
|
|
timeRange.push(endTime);
|
|
|
|
let obj = {
|
|
id: this.id,
|
|
title: this.title,
|
|
description: this.description,
|
|
strategy: this.strategy,
|
|
intervalDay: this.interval_day,
|
|
active: !!this.active,
|
|
dateRange: dateRange,
|
|
timeRange: timeRange,
|
|
weekdays: (this.weekdays) ? JSON.parse(this.weekdays) : [],
|
|
daysOfMonth: (this.days_of_month) ? JSON.parse(this.days_of_month) : [],
|
|
timeslotList: [],
|
|
cron: this.cron,
|
|
duration: this.duration,
|
|
durationMinutes: parseInt(this.duration / 60),
|
|
timezone: await this.getTimezone(), // Only valid timezone
|
|
timezoneOption: this.timezone, // Mainly for dropdown menu, because there is a option "SAME_AS_SERVER"
|
|
timezoneOffset: await this.getTimezoneOffset(),
|
|
status: await this.getStatus(),
|
|
};
|
|
|
|
if (this.strategy === "manual") {
|
|
// Do nothing, no timeslots
|
|
} else if (this.strategy === "single") {
|
|
obj.timeslotList.push({
|
|
startDate: this.start_date,
|
|
endDate: this.end_date,
|
|
});
|
|
} else {
|
|
// Should be cron or recurring here
|
|
if (this.beanMeta.job) {
|
|
let runningTimeslot = this.getRunningTimeslot();
|
|
|
|
if (runningTimeslot) {
|
|
obj.timeslotList.push(runningTimeslot);
|
|
}
|
|
|
|
let nextRunDate = this.beanMeta.job.nextRun();
|
|
if (nextRunDate) {
|
|
let startDateDayjs = dayjs(nextRunDate);
|
|
|
|
let startDate = startDateDayjs.toISOString();
|
|
let endDate = startDateDayjs.add(this.duration, "second").toISOString();
|
|
|
|
obj.timeslotList.push({
|
|
startDate,
|
|
endDate,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(obj.weekdays)) {
|
|
obj.weekdays = [];
|
|
}
|
|
|
|
if (!Array.isArray(obj.daysOfMonth)) {
|
|
obj.daysOfMonth = [];
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
/**
|
|
* Return an object that ready to parse to JSON
|
|
* @param {string} timezone If not specified, the timeRange will be in UTC
|
|
* @returns {Promise<object>} Object ready to parse
|
|
*/
|
|
async toJSON(timezone = null) {
|
|
return this.toPublicJSON(timezone);
|
|
}
|
|
|
|
/**
|
|
* Get a list of weekdays that the maintenance is active for
|
|
* Monday=1, Tuesday=2 etc.
|
|
* @returns {number[]} Array of active weekdays
|
|
*/
|
|
getDayOfWeekList() {
|
|
log.debug("timeslot", "List: " + this.weekdays);
|
|
return JSON.parse(this.weekdays).sort(function (a, b) {
|
|
return a - b;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get a list of days in month that maintenance is active for
|
|
* @returns {number[]|string[]} Array of active days in month
|
|
*/
|
|
getDayOfMonthList() {
|
|
return JSON.parse(this.days_of_month).sort(function (a, b) {
|
|
return a - b;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get the duration of maintenance in seconds
|
|
* @returns {number} Duration of maintenance
|
|
*/
|
|
calcDuration() {
|
|
let duration = dayjs.utc(this.end_time, "HH:mm").diff(dayjs.utc(this.start_time, "HH:mm"), "second");
|
|
// Add 24hours if it is across day
|
|
if (duration < 0) {
|
|
duration += 24 * 3600;
|
|
}
|
|
return duration;
|
|
}
|
|
|
|
/**
|
|
* Convert data from socket to bean
|
|
* @param {Bean} bean Bean to fill in
|
|
* @param {object} obj Data to fill bean with
|
|
* @returns {Promise<Bean>} Filled bean
|
|
*/
|
|
static async jsonToBean(bean, obj) {
|
|
if (obj.id) {
|
|
bean.id = obj.id;
|
|
}
|
|
|
|
bean.title = obj.title;
|
|
bean.description = obj.description;
|
|
bean.strategy = obj.strategy;
|
|
bean.interval_day = obj.intervalDay;
|
|
bean.timezone = obj.timezoneOption;
|
|
bean.active = obj.active;
|
|
|
|
if (obj.dateRange[0]) {
|
|
bean.start_date = obj.dateRange[0];
|
|
} else {
|
|
bean.start_date = null;
|
|
}
|
|
|
|
if (obj.dateRange[1]) {
|
|
bean.end_date = obj.dateRange[1];
|
|
} else {
|
|
bean.end_date = null;
|
|
}
|
|
|
|
if (bean.strategy === "cron") {
|
|
bean.duration = obj.durationMinutes * 60;
|
|
bean.cron = obj.cron;
|
|
this.validateCron(bean.cron);
|
|
}
|
|
|
|
if (bean.strategy.startsWith("recurring-")) {
|
|
bean.start_time = parseTimeFromTimeObject(obj.timeRange[0]);
|
|
bean.end_time = parseTimeFromTimeObject(obj.timeRange[1]);
|
|
bean.weekdays = JSON.stringify(obj.weekdays);
|
|
bean.days_of_month = JSON.stringify(obj.daysOfMonth);
|
|
await bean.generateCron();
|
|
this.validateCron(bean.cron);
|
|
}
|
|
return bean;
|
|
}
|
|
|
|
/**
|
|
* Throw error if cron is invalid
|
|
* @param {string|Date} cron Pattern or date
|
|
* @returns {void}
|
|
*/
|
|
static validateCron(cron) {
|
|
let job = new Cron(cron, () => {
|
|
});
|
|
job.stop();
|
|
}
|
|
|
|
/**
|
|
* Run the cron
|
|
* @param {boolean} throwError Should an error be thrown on failure
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async run(throwError = false) {
|
|
if (this.beanMeta.job) {
|
|
log.debug("maintenance", "Maintenance is already running, stop it first. id: " + this.id);
|
|
this.stop();
|
|
}
|
|
|
|
log.debug("maintenance", "Run maintenance id: " + this.id);
|
|
|
|
// 1.21.2 migration
|
|
if (!this.cron) {
|
|
await this.generateCron();
|
|
if (!this.timezone) {
|
|
this.timezone = "UTC";
|
|
}
|
|
if (this.cron) {
|
|
await R.store(this);
|
|
}
|
|
}
|
|
|
|
if (this.strategy === "manual") {
|
|
// Do nothing, because it is controlled by the user
|
|
} else if (this.strategy === "single") {
|
|
this.beanMeta.job = new Cron(this.start_date, { timezone: await this.getTimezone() }, () => {
|
|
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
|
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
|
apicache.clear();
|
|
});
|
|
} else if (this.cron != null) {
|
|
// Here should be cron or recurring
|
|
try {
|
|
this.beanMeta.status = "scheduled";
|
|
|
|
let startEvent = (customDuration = 0) => {
|
|
log.info("maintenance", "Maintenance id: " + this.id + " is under maintenance now");
|
|
|
|
this.beanMeta.status = "under-maintenance";
|
|
clearTimeout(this.beanMeta.durationTimeout);
|
|
|
|
let duration = this.inferDuration(customDuration);
|
|
|
|
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
|
|
|
this.beanMeta.durationTimeout = setTimeout(() => {
|
|
// End of maintenance for this timeslot
|
|
this.beanMeta.status = "scheduled";
|
|
UptimeKumaServer.getInstance().sendMaintenanceListByUserID(this.user_id);
|
|
}, duration);
|
|
};
|
|
|
|
// Create Cron
|
|
if (this.strategy === "recurring-interval") {
|
|
// For recurring-interval, Croner needs to have interval and startAt
|
|
const startDate = dayjs(this.startDate);
|
|
const [ hour, minute ] = this.startTime.split(":");
|
|
const startDateTime = startDate.hour(hour).minute(minute);
|
|
this.beanMeta.job = new Cron(this.cron, {
|
|
timezone: await this.getTimezone(),
|
|
interval: this.interval_day * 24 * 60 * 60,
|
|
startAt: startDateTime.toISOString(),
|
|
}, startEvent);
|
|
} else {
|
|
this.beanMeta.job = new Cron(this.cron, {
|
|
timezone: await this.getTimezone(),
|
|
}, startEvent);
|
|
}
|
|
|
|
// Continue if the maintenance is still in the window
|
|
let runningTimeslot = this.getRunningTimeslot();
|
|
let current = dayjs();
|
|
|
|
if (runningTimeslot) {
|
|
let duration = dayjs(runningTimeslot.endDate).diff(current, "second") * 1000;
|
|
log.debug("maintenance", "Maintenance id: " + this.id + " Remaining duration: " + duration + "ms");
|
|
startEvent(duration);
|
|
}
|
|
|
|
} catch (e) {
|
|
log.error("maintenance", "Error in maintenance id: " + this.id);
|
|
log.error("maintenance", "Cron: " + this.cron);
|
|
log.error("maintenance", e);
|
|
|
|
if (throwError) {
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
} else {
|
|
log.error("maintenance", "Maintenance id: " + this.id + " has no cron");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get timeslots where maintenance is running
|
|
* @returns {object|null} Maintenance time slot
|
|
*/
|
|
getRunningTimeslot() {
|
|
let start = dayjs(this.beanMeta.job.nextRun(dayjs().add(-this.duration, "second").toDate()));
|
|
let end = start.add(this.duration, "second");
|
|
let current = dayjs();
|
|
|
|
if (current.isAfter(start) && current.isBefore(end)) {
|
|
return {
|
|
startDate: start.toISOString(),
|
|
endDate: end.toISOString(),
|
|
};
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate the maintenance duration
|
|
* @param {number} customDuration - The custom duration in milliseconds.
|
|
* @returns {number} The inferred duration in milliseconds.
|
|
*/
|
|
inferDuration(customDuration) {
|
|
// Check if duration is still in the window. If not, use the duration from the current time to the end of the window
|
|
if (customDuration > 0) {
|
|
return customDuration;
|
|
} else if (this.end_date) {
|
|
let d = dayjs(this.end_date).diff(dayjs(), "second");
|
|
if (d < this.duration) {
|
|
return d * 1000;
|
|
}
|
|
}
|
|
return this.duration * 1000;
|
|
}
|
|
|
|
/**
|
|
* Stop the maintenance
|
|
* @returns {void}
|
|
*/
|
|
stop() {
|
|
if (this.beanMeta.job) {
|
|
this.beanMeta.job.stop();
|
|
delete this.beanMeta.job;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is this maintenance currently active
|
|
* @returns {Promise<boolean>} The maintenance is active?
|
|
*/
|
|
async isUnderMaintenance() {
|
|
return (await this.getStatus()) === "under-maintenance";
|
|
}
|
|
|
|
/**
|
|
* Get the timezone of the maintenance
|
|
* @returns {Promise<string>} timezone
|
|
*/
|
|
async getTimezone() {
|
|
if (!this.timezone || this.timezone === "SAME_AS_SERVER") {
|
|
return await UptimeKumaServer.getInstance().getTimezone();
|
|
}
|
|
return this.timezone;
|
|
}
|
|
|
|
/**
|
|
* Get offset for timezone
|
|
* @returns {Promise<string>} offset
|
|
*/
|
|
async getTimezoneOffset() {
|
|
return dayjs.tz(dayjs(), await this.getTimezone()).format("Z");
|
|
}
|
|
|
|
/**
|
|
* Get the current status of the maintenance
|
|
* @returns {Promise<string>} Current status
|
|
*/
|
|
async getStatus() {
|
|
if (!this.active) {
|
|
return "inactive";
|
|
}
|
|
|
|
if (this.strategy === "manual") {
|
|
return "under-maintenance";
|
|
}
|
|
|
|
// Check if the maintenance is started
|
|
if (this.start_date && dayjs().isBefore(dayjs.tz(this.start_date, await this.getTimezone()))) {
|
|
return "scheduled";
|
|
}
|
|
|
|
// Check if the maintenance is ended
|
|
if (this.end_date && dayjs().isAfter(dayjs.tz(this.end_date, await this.getTimezone()))) {
|
|
return "ended";
|
|
}
|
|
|
|
if (this.strategy === "single") {
|
|
return "under-maintenance";
|
|
}
|
|
|
|
if (!this.beanMeta.status) {
|
|
return "unknown";
|
|
}
|
|
|
|
return this.beanMeta.status;
|
|
}
|
|
|
|
/**
|
|
* Generate Cron for recurring maintenance
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async generateCron() {
|
|
log.info("maintenance", "Generate cron for maintenance id: " + this.id);
|
|
|
|
if (this.strategy === "cron") {
|
|
// Do nothing for cron
|
|
} else if (!this.strategy.startsWith("recurring-")) {
|
|
this.cron = "";
|
|
} else if (this.strategy === "recurring-interval") {
|
|
// For intervals, the pattern is calculated in the run function as the interval-option is set
|
|
this.cron = "* * * * *";
|
|
this.duration = this.calcDuration();
|
|
log.debug("maintenance", "Cron: " + this.cron);
|
|
log.debug("maintenance", "Duration: " + this.duration);
|
|
} else if (this.strategy === "recurring-weekday") {
|
|
let list = this.getDayOfWeekList();
|
|
let array = this.start_time.split(":");
|
|
let hour = parseInt(array[0]);
|
|
let minute = parseInt(array[1]);
|
|
this.cron = minute + " " + hour + " * * " + list.join(",");
|
|
this.duration = this.calcDuration();
|
|
} else if (this.strategy === "recurring-day-of-month") {
|
|
let list = this.getDayOfMonthList();
|
|
let array = this.start_time.split(":");
|
|
let hour = parseInt(array[0]);
|
|
let minute = parseInt(array[1]);
|
|
|
|
let dayList = [];
|
|
|
|
for (let day of list) {
|
|
if (typeof day === "string" && day.startsWith("lastDay")) {
|
|
if (day === "lastDay1") {
|
|
dayList.push("L");
|
|
}
|
|
// Unfortunately, lastDay2-4 is not supported by cron
|
|
} else {
|
|
dayList.push(day);
|
|
}
|
|
}
|
|
|
|
// Remove duplicate
|
|
dayList = [ ...new Set(dayList) ];
|
|
|
|
this.cron = minute + " " + hour + " " + dayList.join(",") + " * *";
|
|
this.duration = this.calcDuration();
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
module.exports = Maintenance;
|