diff --git a/server/server.js b/server/server.js index 29e0857a0..bfebb89fc 100644 --- a/server/server.js +++ b/server/server.js @@ -593,6 +593,82 @@ let indexHTML = fs.readFileSync("./dist/index.html").toString(); } }); + socket.on("uploadBackup", async (uploadedJSON, callback) => { + try { + checkLogin(socket) + + let backupData = JSON.parse(uploadedJSON); + + console.log(`Importing Backup, User ID: ${socket.userID}, Version: ${backupData.version}`) + + let notificationList = backupData.notificationList; + let monitorList = backupData.monitorList; + + if (notificationList.length >= 1) { + for (let i = 0; i < notificationList.length; i++) { + let notification = JSON.parse(notificationList[i].config); + await Notification.save(notification, null, socket.userID) + } + } + + if (monitorList.length >= 1) { + for (let i = 0; i < monitorList.length; i++) { + let monitor = { + name: monitorList[i].name, + type: monitorList[i].type, + url: monitorList[i].url, + interval: monitorList[i].interval, + hostname: monitorList[i].hostname, + maxretries: monitorList[i].maxretries, + port: monitorList[i].port, + keyword: monitorList[i].keyword, + ignoreTls: monitorList[i].ignoreTls, + upsideDown: monitorList[i].upsideDown, + maxredirects: monitorList[i].maxredirects, + accepted_statuscodes: monitorList[i].accepted_statuscodes, + dns_resolve_type: monitorList[i].dns_resolve_type, + dns_resolve_server: monitorList[i].dns_resolve_server, + notificationIDList: {}, + } + + let bean = R.dispense("monitor") + + let notificationIDList = monitor.notificationIDList; + delete monitor.notificationIDList; + + monitor.accepted_statuscodes_json = JSON.stringify(monitor.accepted_statuscodes); + delete monitor.accepted_statuscodes; + + bean.import(monitor) + bean.user_id = socket.userID + await R.store(bean) + + await updateMonitorNotification(bean.id, notificationIDList) + + if (monitorList[i].active == 1) { + await startMonitor(socket.userID, bean.id); + } else { + await pauseMonitor(socket.userID, bean.id); + } + } + + await sendNotificationList(socket) + await sendMonitorList(socket); + } + + callback({ + ok: true, + msg: "Backup successfully restored.", + }); + + } catch (e) { + callback({ + ok: false, + msg: e.message, + }); + } + }); + socket.on("clearEvents", async (monitorID, callback) => { try { checkLogin(socket) diff --git a/src/languages/de-DE.js b/src/languages/de-DE.js index 7c2dbb9c6..cfadf1709 100644 --- a/src/languages/de-DE.js +++ b/src/languages/de-DE.js @@ -113,11 +113,19 @@ export default { "Create your admin account": "Erstelle dein Admin Konto", "Repeat Password": "Wiederhole das Passwort", "Resource Record Type": "Resource Record Type", + "Import/Export Backup": "Import/Export Backup", + "Export": "Export", + "Import": "Import", respTime: "Antw. Zeit (ms)", notAvailableShort: "N/A", "Default enabled": "Standardmäßig aktiviert", "Also apply to existing monitors": "Auch für alle existierenden Monitore aktivieren", enableDefaultNotificationDescription: "Für jeden neuen Monitor wird diese Benachrichtigung standardmäßig aktiviert. Die Benachrichtigung kann weiterhin für jeden Monitor separat deaktiviert werden.", Create: "Erstellen", - "Auto Get": "Auto Get" + "Auto Get": "Auto Get", + backupDescription: "Es können alle Monitore und alle Benachrichtigungen in einer JSON-Datei gesichert werden.", + backupDescription2: "PS: Verlaufs- und Ereignisdaten sind nicht enthalten.", + backupDescription3: "Sensible Daten wie Benachrichtigungstoken sind in der Exportdatei enthalten, bitte bewahre sie sorgfältig auf.", + alertNoFile: "Bitte wähle eine Datei zum importieren aus.", + alertWrongFileType: "Bitte wähle eine JSON Datei aus.", } diff --git a/src/languages/en.js b/src/languages/en.js index cff061a09..aca7b27e1 100644 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -111,6 +111,9 @@ export default { "Last Result": "Last Result", "Create your admin account": "Create your admin account", "Repeat Password": "Repeat Password", + "Import/Export Backup": "Import/Export Backup", + "Export": "Export", + "Import": "Import", respTime: "Resp. Time (ms)", notAvailableShort: "N/A", "Default enabled": "Default enabled", @@ -119,5 +122,10 @@ export default { "Clear Data": "Clear Data", Events: "Events", Heartbeats: "Heartbeats", - "Auto Get": "Auto Get" + "Auto Get": "Auto Get", + backupDescription: "You can backup all monitors and all notifications into a JSON file.", + backupDescription2: "PS: History and event data is not included.", + backupDescription3: "Sensitive data such as notification tokens is included in the export file, please keep it carefully.", + alertNoFile: "Please select a file to import.", + alertWrongFileType: "Please select a JSON file.", } diff --git a/src/mixins/socket.js b/src/mixins/socket.js index 672027fa1..22cc25bfd 100644 --- a/src/mixins/socket.js +++ b/src/mixins/socket.js @@ -254,6 +254,10 @@ export default { this.importantHeartbeatList = {} }, + uploadBackup(uploadedJSON, callback) { + socket.emit("uploadBackup", uploadedJSON, callback) + }, + clearEvents(monitorID, callback) { socket.emit("clearEvents", monitorID, callback) }, diff --git a/src/pages/Settings.vue b/src/pages/Settings.vue index ad454d3f2..c308e9b3d 100644 --- a/src/pages/Settings.vue +++ b/src/pages/Settings.vue @@ -120,6 +120,27 @@ +

{{ $t("Import/Export Backup") }}

+ +

+ {{ $t("backupDescription") }}
+ ({{ $t("backupDescription2") }})
+

+ +
+ + + +
+
+ {{ importAlert }} +
+ +

{{ $t("backupDescription3") }}

+

{{ $t("Advanced") }}

@@ -275,6 +296,8 @@ export default { }, loaded: false, + importAlert: null, + processing: false, } }, watch: { @@ -351,6 +374,52 @@ export default { this.$root.storage().removeItem("token"); }, + downloadBackup() { + let time = dayjs().format("YYYY_MM_DD-hh_mm_ss"); + let fileName = `Uptime_Kuma_Backup_${time}.json`; + let monitorList = Object.values(this.$root.monitorList); + let exportData = { + version: this.$root.info.version, + notificationList: this.$root.notificationList, + monitorList: monitorList, + } + exportData = JSON.stringify(exportData); + let downloadItem = document.createElement("a"); + downloadItem.setAttribute("href", "data:application/json;charset=utf-8," + encodeURI(exportData)); + downloadItem.setAttribute("download", fileName); + downloadItem.click(); + }, + + importBackup() { + this.processing = true; + let uploadItem = document.getElementById("importBackup").files; + + if (uploadItem.length <= 0) { + this.processing = false; + return this.importAlert = this.$t("alertNoFile") + } + + if (uploadItem.item(0).type !== "application/json") { + this.processing = false; + return this.importAlert = this.$t("alertWrongFileType") + } + + let fileReader = new FileReader(); + fileReader.readAsText(uploadItem.item(0)); + + fileReader.onload = item => { + this.$root.uploadBackup(item.target.result, (res) => { + this.processing = false; + + if (res.ok) { + toast.success(res.msg); + } else { + toast.error(res.msg); + } + }) + } + }, + clearStatistics() { this.$root.clearStatistics((res) => { if (res.ok) { @@ -388,6 +457,18 @@ export default { .btn-check:hover + .btn-outline-primary { color: #000; } + + #importBackup { + &::file-selector-button { + color: $primary; + background-color: $dark-bg; + } + + &:hover:not(:disabled):not([readonly])::file-selector-button { + color: $dark-font-color2; + background-color: $primary; + } + } } footer {