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>
pull/1742/head
Stephen Donchez 9 months ago committed by GitHub
parent 2f4d4e52be
commit 992516cebd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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"
},

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

@ -686,5 +686,11 @@
"maxPlayers": "Max players",
"bots": "Bots",
"ping": "Ping"
},
"urbackup": {
"ok" : "Ok",
"errored": "Errors",
"noRecent": "Out of Date",
"totalUsed": "Used Storage"
}
}

@ -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")),

@ -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 <Container service={service} error={urbackupError} />;
}
if (!urbackupData) {
return (
<Container service={service}>
<Block label="urbackup.ok" />
<Block label="urbackup.errored" />
<Block label="urbackup.noRecent" />
{showDiskUsage && <Block label="urbackup.totalUsed" />}
</Container>
);
}
const statusData = determineStatuses(urbackupData, widget);
return (
<Container service={service}>
<Block label="urbackup.ok" value={t("common.number", { value: parseInt(statusData.ok, 10) })} />
<Block label="urbackup.errored" value={t("common.number", { value: parseInt(statusData.errored, 10) })} />
<Block label="urbackup.noRecent" value={t("common.number", { value: parseInt(statusData.noRecent, 10) })} />
{showDiskUsage && <Block label="urbackup.totalUsed" value={t("common.bbytes", {value: parseFloat(statusData.totalUsage, 10)})} />}
</Container>
);
}

@ -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" })
}
})();
}

@ -0,0 +1,7 @@
import urbackupProxyHandler from "./proxy";
const widget = {
proxyHandler: urbackupProxyHandler,
};
export default widget;

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

Loading…
Cancel
Save