diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 4c0c61a30..63d4dd3b1 100755 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -158,6 +158,12 @@ "leech": "Leech", "seed": "Seed" }, + "qnap": { + "cpuUsage": "CPU Usage", + "memUsage": "MEM Usage", + "systemTempC": "System Temp", + "poolUsage": "Pool Usage" + }, "deluge": { "download": "Download", "upload": "Upload", diff --git a/src/widgets/components.js b/src/widgets/components.js index c909bfe00..7aeb8b451 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -63,6 +63,7 @@ const components = { pterodactyl: dynamic(() => import("./pterodactyl/component")), pyload: dynamic(() => import("./pyload/component")), qbittorrent: dynamic(() => import("./qbittorrent/component")), + qnap: dynamic(() => import("./qnap/component")), radarr: dynamic(() => import("./radarr/component")), readarr: dynamic(() => import("./readarr/component")), rutorrent: dynamic(() => import("./rutorrent/component")), diff --git a/src/widgets/qnap/component.jsx b/src/widgets/qnap/component.jsx new file mode 100644 index 000000000..812cfd486 --- /dev/null +++ b/src/widgets/qnap/component.jsx @@ -0,0 +1,61 @@ +/* eslint no-underscore-dangle: ["error", { "allow": ["_text", "_cdata"] }] */ + +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"; + +export default function Component({ service }) { + const { t } = useTranslation("common"); + + const { widget } = service; + + const { data: statusData, error: statusError } = useWidgetAPI(widget, "status"); + + if (statusError) { + return ; + } + + if (!statusData) { + return ( + + + + + + + ); + } + + const cpuUsage = statusData.system.cpu_usage._cdata.replace(' %',''); + const totalMemory = statusData.system.total_memory._cdata; + const freeMemory = statusData.system.free_memory._cdata; + const systemTempC = statusData.system.cpu_tempc._text; + + const volumeTotalSize = statusData.volume.volumeUse.total_size._cdata; + const volumeFreeSize = statusData.volume.volumeUse.free_size._cdata; + + + + return ( + + + + + + + ); +} diff --git a/src/widgets/qnap/proxy.js b/src/widgets/qnap/proxy.js new file mode 100644 index 000000000..15c2fa299 --- /dev/null +++ b/src/widgets/qnap/proxy.js @@ -0,0 +1,95 @@ +/* eslint no-underscore-dangle: ["error", { "allow": ["_text", "_cdata"] }] */ + +import cache from "memory-cache"; +import { xml2json } from "xml-js"; + +import { httpProxy } from "utils/proxy/http"; +import { formatApiCall } from "utils/proxy/api-helpers"; +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; + +const proxyName = "qnapProxyHandler"; +const sessionTokenCacheKey = `${proxyName}__sessionToken`; +const logger = createLogger(proxyName); + +async function login(widget, service) { + const endpoint = "{url}/cgi-bin/authLogin.cgi"; + const loginUrl = new URL(formatApiCall(endpoint, widget )); + const headers = { "Content-Type": "application/x-www-form-urlencoded" }; + + const [, , data,] = await httpProxy(loginUrl, { + method: "POST", + body: new URLSearchParams({ + user: widget.username, + pwd: Buffer.from(`${widget.password}`).toString("base64") + }).toString(), + headers, + }); + + try { + const dataDecoded = xml2json(data.toString(), { compact: true }); + const jsonData = JSON.parse(dataDecoded); + const token = jsonData.QDocRoot.authSid._cdata; + cache.put(`${sessionTokenCacheKey}.${service}`, token); + return { token }; + } catch (e) { + logger.error("Unable to login to QNAP API: %s", e); + } + + return { token: false }; +} + +async function apiCall(widget, endpoint, service) { + let key = cache.get(`${sessionTokenCacheKey}.${service}`); + const method = "GET"; + + let apiUrl = new URL(formatApiCall(`${endpoint}&sid=${key}`, widget)); + let [status, contentType, data, responseHeaders] = await httpProxy(apiUrl, { + method + }); + + if (status === 404) { + logger.error("QNAP API rejected the request, attempting to obtain new session token"); + key = await login(widget, service); + apiUrl = new URL(formatApiCall(`${endpoint}&sid=${key}`, widget)); + [status, contentType, data, responseHeaders] = await httpProxy(apiUrl, { + method + }); + } + + if (status !== 200) { + logger.error("Error getting data from QNAP: %s status %d. Data: %s", apiUrl, status, data); + return { status, contentType, data: null, responseHeaders }; + } + + const dataDecoded = xml2json(data.toString(), { compact: true }); + logger.debug("Dayta '%s'", dataDecoded); + return { status, contentType, data: JSON.parse(dataDecoded.toString()), responseHeaders }; +} + +export default async function qnapProxyHandler(req, res) { + const { group, service } = req.query; + + if (!group || !service) { + logger.debug("Invalid or missing service '%s' or group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const widget = await getServiceWidget(group, service); + if (!widget) { + logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + if (!cache.get(`${sessionTokenCacheKey}.${service}`)) { + await login(widget, service); + } + + const { data: systemStatsData } = await apiCall(widget, "{url}/cgi-bin/management/manaRequest.cgi?subfunc=sysinfo&hd=no&multicpu=1", service); + const { data: volumeStatsData } = await apiCall(widget, "{url}/cgi-bin/management/chartReq.cgi?chart_func=disk_usage&disk_select=all&include=all", service); + + return res.status(200).send({ + system: systemStatsData.QDocRoot.func.ownContent.root, + volume: volumeStatsData.QDocRoot.volumeUseList + }); +} diff --git a/src/widgets/qnap/widget.js b/src/widgets/qnap/widget.js new file mode 100644 index 000000000..ebaf93c9d --- /dev/null +++ b/src/widgets/qnap/widget.js @@ -0,0 +1,8 @@ +import qnapProxyHandler from "./proxy"; + +const widget = { + api: "{url}", + proxyHandler: qnapProxyHandler, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 20f36a2b6..85fb62f85 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -57,6 +57,7 @@ import proxmox from "./proxmox/widget"; import pterodactyl from "./pterodactyl/widget"; import pyload from "./pyload/widget"; import qbittorrent from "./qbittorrent/widget"; +import qnap from "./qnap/widget"; import radarr from "./radarr/widget"; import readarr from "./readarr/widget"; import rutorrent from "./rutorrent/widget"; @@ -140,6 +141,7 @@ const widgets = { pterodactyl, pyload, qbittorrent, + qnap, radarr, readarr, rutorrent,