Skip to content

Commit

Permalink
OMV widget implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
userXinos authored and shamoon committed Aug 13, 2023
1 parent 19bf7a0 commit bf8c10b
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 0 deletions.
8 changes: 8 additions & 0 deletions public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -701,5 +701,13 @@
"errored": "Errors",
"noRecent": "Out of Date",
"totalUsed": "Used Storage"
},
"openmediavault": {
"downloading": "Downloading",
"total": "Total",
"running": "Running",
"stopped": "Stopped",
"passed": "Passed",
"failed": "Failed"
}
}
4 changes: 4 additions & 0 deletions src/utils/config/service-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ export function cleanServiceGroups(groups) {
metric, // glances
stream, // mjpeg
fit,
method, // openmediavault widget
} = cleanedService.widget;

let fieldsList = fields;
Expand Down Expand Up @@ -368,6 +369,9 @@ export function cleanServiceGroups(groups) {
if (stream) cleanedService.widget.stream = stream;
if (fit) cleanedService.widget.fit = fit;
}
if (type === "openmediavault") {
if (method) cleanedService.widget.method = method;
}
}

return cleanedService;
Expand Down
1 change: 1 addition & 0 deletions src/widgets/components.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const components = {
ombi: dynamic(() => import("./ombi/component")),
opnsense: dynamic(() => import("./opnsense/component")),
overseerr: dynamic(() => import("./overseerr/component")),
openmediavault: dynamic(() => import("./openmediavault/component")),
paperlessngx: dynamic(() => import("./paperlessngx/component")),
pfsense: dynamic(() => import("./pfsense/component")),
photoprism: dynamic(() => import("./photoprism/component")),
Expand Down
16 changes: 16 additions & 0 deletions src/widgets/openmediavault/component.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ServicesGetStatus from "./methods/services_get_status";
import SmartGetList from "./methods/smart_get_list";
import DownloaderGetDownloadList from "./methods/downloader_get_downloadlist";

export default function Component({ service }) {
switch (service.widget.method) {
case "services.getStatus":
return <ServicesGetStatus service={service} />;
case "smart.getListBg":
return <SmartGetList service={service} />;
case "downloader.getDownloadList":
return <DownloaderGetDownloadList service={service} />;
default:
return null;
}
}
36 changes: 36 additions & 0 deletions src/widgets/openmediavault/methods/downloader_get_downloadlist.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import useWidgetAPI from "utils/proxy/use-widget-api";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";

const downloadReduce = (acc, e) => {
if (e.downloading) {
return acc + 1;
}
return acc;
};

const items = [
{ label: "openmediavault.downloading", getNumber: (data) => (!data ? null : data.reduce(downloadReduce, 0)) },
{ label: "openmediavault.total", getNumber: (data) => (!data ? null : data?.length) },
];

export default function Component({ service }) {
const { data, error } = useWidgetAPI(service.widget);

if (error) {
return <Container service={service} error={error} />;
}

const itemsWithData = items.map((item) => ({
...item,
number: item.getNumber(data?.response?.data),
}));

return (
<Container service={service}>
{itemsWithData.map((e) => (
<Block key={e.label} label={e.label} value={e.number} />
))}
</Container>
);
}
43 changes: 43 additions & 0 deletions src/widgets/openmediavault/methods/services_get_status.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import useWidgetAPI from "utils/proxy/use-widget-api";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";

const isRunningReduce = (acc, e) => {
if (e.running) {
return acc + 1;
}
return acc;
};
const notRunningReduce = (acc, e) => {
if (!e.running) {
return acc + 1;
}
return acc;
};

const items = [
{ label: "openmediavault.running", getNumber: (data) => (!data ? null : data.reduce(isRunningReduce, 0)) },
{ label: "openmediavault.stopped", getNumber: (data) => (!data ? null : data.reduce(notRunningReduce, 0)) },
{ label: "openmediavault.total", getNumber: (data) => (!data ? null : data?.length) },
];

export default function Component({ service }) {
const { data, error } = useWidgetAPI(service.widget);

if (error) {
return <Container service={service} error={error} />;
}

const itemsWithData = items.map((item) => ({
...item,
number: item.getNumber(data?.response?.data),
}));

return (
<Container service={service}>
{itemsWithData.map((e) => (
<Block key={e.label} label={e.label} value={e.number} />
))}
</Container>
);
}
42 changes: 42 additions & 0 deletions src/widgets/openmediavault/methods/smart_get_list.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import useWidgetAPI from "utils/proxy/use-widget-api";
import Container from "components/services/widget/container";
import Block from "components/services/widget/block";

const passedReduce = (acc, e) => {
if (e.overallstatus === "GOOD") {
return acc + 1;
}
return acc;
};
const failedReduce = (acc, e) => {
if (e.overallstatus !== "GOOD") {
return acc + 1;
}
return acc;
};

const items = [
{ label: "openmediavault.passed", getNumber: (data) => (!data ? null : data.reduce(passedReduce, 0)) },
{ label: "openmediavault.failed", getNumber: (data) => (!data ? null : data.reduce(failedReduce, 0)) },
];

export default function Component({ service }) {
const { data, error } = useWidgetAPI(service.widget);

if (error) {
return <Container service={service} error={error} />;
}

const itemsWithData = items.map((item) => ({
...item,
number: item.getNumber(JSON.parse(data?.response?.output || "{}")?.data),
}));

return (
<Container service={service}>
{itemsWithData.map((e) => (
<Block key={e.label} label={e.label} value={e.number} />
))}
</Container>
);
}
151 changes: 151 additions & 0 deletions src/widgets/openmediavault/proxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { formatApiCall } from "utils/proxy/api-helpers";
import { httpProxy } from "utils/proxy/http";
import getServiceWidget from "utils/config/service-helpers";
import { addCookieToJar, setCookieHeader } from "utils/proxy/cookie-jar";
import createLogger from "utils/logger";
import widgets from "widgets/widgets";

const PROXY_NAME = "OMVProxyHandler";
const BG_MAX_RETRIES = 50;
const BG_POLL_PERIOD = 500;

const logger = createLogger(PROXY_NAME);

async function getWidget(req) {
const { group, service } = req.query;

if (!group || !service) {
logger.debug("Invalid or missing service '%s' or group '%s'", service, group);
return null;
}

const widget = await getServiceWidget(group, service);

if (!widget) {
logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group);
return null;
}

return widget;
}

async function rpc(url, request) {
const params = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
};
setCookieHeader(url, params);
const [status, contentType, data, headers] = await httpProxy(url, params);

return { status, contentType, data, headers };
}

async function poll(attemptsLeft, makeReqByPos, pos = 0) {
if (attemptsLeft <= 0) {
return null;
}

const resp = await makeReqByPos(pos);

const data = JSON.parse(resp.data.toString()).response;
if (data.running === true || data.outputPending) {
await new Promise((resolve) => {
setTimeout(resolve, BG_POLL_PERIOD);
});
return poll(attemptsLeft - 1, makeReqByPos, data.pos);
}
return resp;
}

async function tryLogin(widget) {
const url = new URL(formatApiCall(widgets?.[widget.type]?.api, { ...widget }));
const { username, password } = widget;
const resp = await rpc(url, {
method: "login",
service: "session",
params: { username, password },
});

if (resp.status !== 200) {
logger.error("HTTP %d logging in to OpenMediaVault. Data: %s", resp.status, resp.data);
return [false, resp];
}

const json = JSON.parse(resp.data.toString());
if (json.response.authenticated !== true) {
logger.error("Login error in OpenMediaVault. Data: %s", resp.data);
resp.status = 401;
return [false, resp];
}

return [true, resp];
}
async function processBg(url, filename) {
const resp = await poll(BG_MAX_RETRIES, (pos) =>
rpc(url, {
service: "exec",
method: "getOutput",
params: { pos, filename },
})
);

if (resp == null) {
const errText = "The maximum number of attempts to receive a response from Bg data has been exceeded.";
logger.error(errText);
return errText;
}
if (resp.status !== 200) {
logger.error("HTTP %d getting Bg data from OpenMediaVault RPC. Data: %s", resp.status, resp.data);
}
return resp;
}

export default async function proxyHandler(req, res) {
const widget = await getWidget(req);
if (!widget) {
return res.status(400).json({ error: "Invalid proxy service type" });
}

const api = widgets?.[widget.type]?.api;
if (!api) {
return res.status(403).json({ error: "Service does not support RPC calls" });
}

const url = new URL(formatApiCall(api, { ...widget }));
const [service, method] = widget.method.split(".");
const rpcReq = { params: { limit: -1, start: 0 }, service, method };

let resp = await rpc(url, rpcReq);

if (resp.status === 401) {
logger.debug("Session not authenticated.");
const [success, lResp] = await tryLogin(widget);

if (success) {
addCookieToJar(url, lResp.headers);
} else {
res.status(lResp.status).json({ error: { message: `HTTP Error ${lResp.status}`, url, data: lResp.data } });
}

logger.debug("Retrying OpenMediaVault request after login.");
resp = await rpc(url, rpcReq);
}

if (resp.status !== 200) {
logger.error("HTTP %d getting data from OpenMediaVault RPC. Data: %s", resp.status, resp.data);
return res.status(resp.status).json({ error: { message: `HTTP Error ${resp.status}`, url, data: resp.data } });
}

if (method.endsWith("Bg")) {
const json = JSON.parse(resp.data.toString());
const bgResp = await processBg(url, json.response);

if (typeof bgResp === "string") {
return res.status(400).json({ error: bgResp });
}
return res.status(bgResp.status).send(bgResp.data);
}

return res.status(resp.status).send(resp.data);
}
8 changes: 8 additions & 0 deletions src/widgets/openmediavault/widget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import proxyHandler from "./proxy";

const widget = {
api: "{url}/rpc.php",
proxyHandler,
};

export default widget;
2 changes: 2 additions & 0 deletions src/widgets/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import omada from "./omada/widget";
import ombi from "./ombi/widget";
import opnsense from "./opnsense/widget";
import overseerr from "./overseerr/widget";
import openmediavault from "./openmediavault/widget";
import paperlessngx from "./paperlessngx/widget";
import pfsense from "./pfsense/widget";
import photoprism from "./photoprism/widget";
Expand Down Expand Up @@ -148,6 +149,7 @@ const widgets = {
ombi,
opnsense,
overseerr,
openmediavault,
paperlessngx,
pfsense,
photoprism,
Expand Down

0 comments on commit bf8c10b

Please sign in to comment.