From 992516cebdc7efeaaed375d60e47762d776d993a Mon Sep 17 00:00:00 2001 From: Stephen Donchez Date: Sun, 30 Jul 2023 10:19:31 -0400 Subject: [PATCH] Feature: UrBackup Widget (#1735) * Add initial UrBackup widget with counts of ok, errored, and out-of date clients * Add configurable number of days since last backup before a client is considered out-of-date * Don't count a lack of recent (or error free) image backup if image backup isn't supported. * Add support for reporting total disk usage * add support for "fields" from services.yaml * fix field filtering, syntax * Consolidate urbackup code, syntax changes * Revert pnpm changes * re-add urbackup-server-api --------- Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- package.json | 1 + pnpm-lock.yaml | 45 +++++++++++++++ public/locales/en/common.json | 6 ++ src/widgets/components.js | 1 + src/widgets/urbackup/component.jsx | 93 ++++++++++++++++++++++++++++++ src/widgets/urbackup/proxy.js | 33 +++++++++++ src/widgets/urbackup/widget.js | 7 +++ src/widgets/widgets.js | 2 + 8 files changed, 188 insertions(+) create mode 100644 src/widgets/urbackup/component.jsx create mode 100644 src/widgets/urbackup/proxy.js create mode 100644 src/widgets/urbackup/widget.js diff --git a/package.json b/package.json index 8c7741888..14fedf036 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "swr": "^1.3.0", "systeminformation": "^5.17.12", "tough-cookie": "^4.1.2", + "urbackup-server-api": "^0.8.9", "winston": "^3.8.2", "xml-js": "^1.6.11" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 78cff3420..a1231d8ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -73,6 +73,9 @@ dependencies: tough-cookie: specifier: ^4.1.2 version: 4.1.2 + urbackup-server-api: + specifier: ^0.8.9 + version: 0.8.9 winston: specifier: ^3.8.2 version: 3.8.2 @@ -659,6 +662,12 @@ packages: resolution: {integrity: sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==} dev: true + /async-mutex@0.3.2: + resolution: {integrity: sha512-HuTK7E7MT7jZEh1P9GtRW9+aTWiDWWi9InbZ5hjxrnRa39KS4BW04+xLBhYNS2aXhHUIKZSw3gj4Pn1pj+qGAA==} + dependencies: + tslib: 2.5.0 + dev: false + /async@3.2.4: resolution: {integrity: sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==} dev: false @@ -2691,6 +2700,18 @@ packages: - babel-plugin-macros dev: false + /node-fetch@2.6.12: + resolution: {integrity: sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false + /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: false @@ -3719,6 +3740,10 @@ packages: url-parse: 1.5.10 dev: false + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /triple-beam@1.3.0: resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==} dev: false @@ -3822,6 +3847,15 @@ packages: picocolors: 1.0.0 dev: true + /urbackup-server-api@0.8.9: + resolution: {integrity: sha512-Igu6A0xSZeMsiN6PWT7zG4aD+iJR5fXT/j5+xwAvnD/vCNfvVrettIsXv6MftxOajvTmtlgaYu8KDoH1EJQ6DQ==} + dependencies: + async-mutex: 0.3.2 + node-fetch: 2.6.12 + transitivePeerDependencies: + - encoding + dev: false + /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} dependencies: @@ -3867,6 +3901,17 @@ packages: engines: {node: '>=0.10.0'} dev: false + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 66aa19abd..46d8e991f 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -686,5 +686,11 @@ "maxPlayers": "Max players", "bots": "Bots", "ping": "Ping" + }, + "urbackup": { + "ok" : "Ok", + "errored": "Errors", + "noRecent": "Out of Date", + "totalUsed": "Used Storage" } } diff --git a/src/widgets/components.js b/src/widgets/components.js index 0c372ce95..ddaca4cf7 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -90,6 +90,7 @@ const components = { unifi: dynamic(() => import("./unifi/component")), unmanic: dynamic(() => import("./unmanic/component")), uptimekuma: dynamic(() => import("./uptimekuma/component")), + urbackup: dynamic(() => import("./urbackup/component")), watchtower: dynamic(() => import("./watchtower/component")), whatsupdocker: dynamic(() => import("./whatsupdocker/component")), xteve: dynamic(() => import("./xteve/component")), diff --git a/src/widgets/urbackup/component.jsx b/src/widgets/urbackup/component.jsx new file mode 100644 index 000000000..9b03b5958 --- /dev/null +++ b/src/widgets/urbackup/component.jsx @@ -0,0 +1,93 @@ +import { useTranslation } from "next-i18next"; + +import Container from "components/services/widget/container"; +import Block from "components/services/widget/block"; +import useWidgetAPI from "utils/proxy/use-widget-api"; + +const Status = Object.freeze({ + ok: Symbol("Ok"), + errored: Symbol("Errored"), + noRecent: Symbol("No Recent Backups") +}); + +function hasRecentBackups(client, maxDays){ + const days = maxDays || 3; + const diffTime = days*24*60*60 // 7 days + const recentFile = (client.lastbackup > (Date.now() / 1000 - diffTime)); + const recentImage = ((client.lastbackup_image > (Date.now() / 1000 - diffTime)||client.image_not_supported)); + return (recentFile && recentImage); +} + +function determineStatuses(urbackupData) { + let ok = 0; + let errored = 0; + let noRecent = 0; + let status; + urbackupData.clientStatuses.forEach((client) => { + status = Status.noRecent; + if (hasRecentBackups(client, urbackupData.maxDays)) { + status = (client.file_ok && (client.image_ok || client.image_not_supported)) ? Status.ok : Status.errored; + } + switch (status) { + case Status.ok: + ok += 1; + break; + case Status.errored: + errored += 1; + break; + case Status.noRecent: + noRecent += 1; + break; + default: + break; + } + }); + + let totalUsage = false; + + // calculate total disk space if provided + if (urbackupData.diskUsage) { + totalUsage = 0.0; + urbackupData.diskUsage.forEach((client) => { + totalUsage += client.used; + }); + } + + return { ok, errored, noRecent, totalUsage }; +} + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const showDiskUsage = widget.fields?.includes('totalUsed') + + const { data: urbackupData, error: urbackupError } = useWidgetAPI(widget, "status"); + + if (urbackupError) { + return ; + } + + if (!urbackupData) { + return ( + + + + + {showDiskUsage && } + + ); + } + + const statusData = determineStatuses(urbackupData, widget); + + return ( + + + + + {showDiskUsage && } + + ); +} diff --git a/src/widgets/urbackup/proxy.js b/src/widgets/urbackup/proxy.js new file mode 100644 index 000000000..3075afd0c --- /dev/null +++ b/src/widgets/urbackup/proxy.js @@ -0,0 +1,33 @@ +import {UrbackupServer} from "urbackup-server-api"; + +import getServiceWidget from "utils/config/service-helpers"; + +export default async function urbackupProxyHandler(req, res) { + const {group, service} = req.query; + const serviceWidget = await getServiceWidget(group, service); + + const server = new UrbackupServer({ + url: serviceWidget.url, + username: serviceWidget.username, + password: serviceWidget.password + }); + +await (async () => { + try { + const allClients = await server.getStatus({includeRemoved: false}); + let diskUsage = false + if (serviceWidget.fields?.includes("totalUsed")) { + diskUsage = await server.getUsage(); + } + res.status(200).send({ + clientStatuses: allClients, + diskUsage, + maxDays: serviceWidget.maxDays + }); + } catch (error) { + res.status(500).json({ error: "Something Broke" }) + } + })(); + + +} diff --git a/src/widgets/urbackup/widget.js b/src/widgets/urbackup/widget.js new file mode 100644 index 000000000..5eac66d07 --- /dev/null +++ b/src/widgets/urbackup/widget.js @@ -0,0 +1,7 @@ +import urbackupProxyHandler from "./proxy"; + +const widget = { + proxyHandler: urbackupProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 14dc2cd29..46143447e 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -87,6 +87,7 @@ import uptimekuma from "./uptimekuma/widget"; import watchtower from "./watchtower/widget"; import whatsupdocker from "./whatsupdocker/widget"; import xteve from "./xteve/widget"; +import urbackup from "./urbackup/widget"; const widgets = { adguard, @@ -177,6 +178,7 @@ const widgets = { unifi_console: unifi, unmanic, uptimekuma, + urbackup, watchtower, whatsupdocker, xteve,