From 0c000c1ecd8be9aa2f7eb03eae7d9892381cc8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albin=20M=C3=A9doc?= Date: Sat, 26 Jul 2025 02:08:43 +0200 Subject: [PATCH] Feature: Filebrowser service widget (#5546) Co-authored-by: shamoon <4887959+shamoon@users.noreply.github.com> --- docs/widgets/services/filebrowser.md | 19 +++++++ docs/widgets/services/index.md | 1 + mkdocs.yml | 1 + public/locales/en/common.json | 5 ++ src/widgets/components.js | 1 + src/widgets/filebrowser/component.jsx | 38 +++++++++++++ src/widgets/filebrowser/proxy.js | 80 +++++++++++++++++++++++++++ src/widgets/filebrowser/widget.js | 14 +++++ src/widgets/widgets.js | 2 + 9 files changed, 161 insertions(+) create mode 100644 docs/widgets/services/filebrowser.md create mode 100644 src/widgets/filebrowser/component.jsx create mode 100644 src/widgets/filebrowser/proxy.js create mode 100644 src/widgets/filebrowser/widget.js diff --git a/docs/widgets/services/filebrowser.md b/docs/widgets/services/filebrowser.md new file mode 100644 index 000000000..24fe629fd --- /dev/null +++ b/docs/widgets/services/filebrowser.md @@ -0,0 +1,19 @@ +--- +title: Filebrowser +description: Filebrowser Widget Configuration +--- + +Learn more about [Filebrowser](https://filebrowser.org). + +If you are using [Proxy header authentication](https://filebrowser.org/configuration/authentication-method#proxy-header) you have to set `authHeader` and `username`. + +Allowed fields: `["available", "used", "total"]`. + +```yaml +widget: + type: filebrowser + url: http://filebrowserhostorip:port + username: username + password: password + authHeader: X-My-Header # If using Proxy header authentication +``` diff --git a/docs/widgets/services/index.md b/docs/widgets/services/index.md index 80ff72ba0..0d0db2030 100644 --- a/docs/widgets/services/index.md +++ b/docs/widgets/services/index.md @@ -33,6 +33,7 @@ You can also find a list of all available service widgets in the sidebar navigat - [Emby](emby.md) - [ESPHome](esphome.md) - [EVCC](evcc.md) +- [Filebrowser](filebrowser.md) - [Fileflows](fileflows.md) - [Firefly III](firefly.md) - [Flood](flood.md) diff --git a/mkdocs.yml b/mkdocs.yml index 8bb19e435..adde9180d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ nav: - widgets/services/emby.md - widgets/services/esphome.md - widgets/services/evcc.md + - widgets/services/filebrowser.md - widgets/services/fileflows.md - widgets/services/firefly.md - widgets/services/flood.md diff --git a/public/locales/en/common.json b/public/locales/en/common.json index a4bc77210..f37329e46 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -1072,6 +1072,11 @@ "stacks": "Stacks", "containers": "Containers" }, + "filebrowser": { + "available": "Available", + "used": "Used", + "total": "Total" + }, "wallos": { "activeSubscriptions": "Subscriptions", "thisMonthlyCost": "This Month", diff --git a/src/widgets/components.js b/src/widgets/components.js index b67ebc7ea..9cb02c2af 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -31,6 +31,7 @@ const components = { emby: dynamic(() => import("./emby/component")), esphome: dynamic(() => import("./esphome/component")), evcc: dynamic(() => import("./evcc/component")), + filebrowser: dynamic(() => import("./filebrowser/component")), fileflows: dynamic(() => import("./fileflows/component")), firefly: dynamic(() => import("./firefly/component")), flood: dynamic(() => import("./flood/component")), diff --git a/src/widgets/filebrowser/component.jsx b/src/widgets/filebrowser/component.jsx new file mode 100644 index 000000000..cf5a28000 --- /dev/null +++ b/src/widgets/filebrowser/component.jsx @@ -0,0 +1,38 @@ +import Block from "components/services/widget/block"; +import Container from "components/services/widget/container"; +import { useTranslation } from "next-i18next"; + +import useWidgetAPI from "utils/proxy/use-widget-api"; + +export default function Component({ service }) { + const { t } = useTranslation(); + + const { widget } = service; + + const { data: usage, error: usageError } = useWidgetAPI(widget, "usage"); + + if (usageError) { + return ; + } + + if (!usage) { + return ( + + + + + + ); + } + + return ( + + + + + + ); +} diff --git a/src/widgets/filebrowser/proxy.js b/src/widgets/filebrowser/proxy.js new file mode 100644 index 000000000..f416c2873 --- /dev/null +++ b/src/widgets/filebrowser/proxy.js @@ -0,0 +1,80 @@ +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; +import { formatApiCall } from "utils/proxy/api-helpers"; +import { httpProxy } from "utils/proxy/http"; +import widgets from "widgets/widgets"; + +const proxyName = "filebrowserProxyHandler"; +const logger = createLogger(proxyName); + +async function login(widget, service) { + const url = formatApiCall(widgets[widget.type].api, { ...widget, endpoint: "login" }); + const headers = {}; + if (widget.authHeader) { + headers[widget.authHeader] = widget.username; + } + const [status, , data] = await httpProxy(url, { + method: "POST", + headers, + body: JSON.stringify({ + username: widget.username, + password: widget.password, + }), + }); + + switch (status) { + case 200: + return data; + case 401: + logger.error("Unauthorized access to Filebrowser API for service '%s'. Check credentials.", service); + break; + default: + logger.error("Unexpected status code %d when logging in to Filebrowser API for service '%s'", status, service); + break; + } +} + +export default async function filebrowserProxyHandler(req, res) { + const { group, service, endpoint, index } = req.query; + + if (!group || !service) { + logger.error("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, index); + if (!widget || !widgets[widget.type].api) { + logger.error("Invalid or missing widget for service '%s' in group '%s'", service, group); + return res.status(400).json({ error: "Invalid widget configuration" }); + } + + const token = await login(widget, service); + if (!token) { + return res.status(500).json({ error: "Failed to authenticate with Filebrowser" }); + } + + const url = new URL(formatApiCall(widgets[widget.type].api, { endpoint, ...widget })); + + try { + const params = { + method: "GET", + headers: { + "X-AUTH": token, + }, + }; + + logger.debug("Calling Filebrowser API endpoint: %s", endpoint); + + const [status, , data] = await httpProxy(url, params); + + if (status !== 200) { + logger.error("Error calling Filebrowser API: %d. Data: %s", status, data); + return res.status(status).json({ error: "Filebrowser API Error", data }); + } + + return res.status(status).send(data); + } catch (error) { + logger.error("Exception calling Filebrowser API: %s", error.message); + return res.status(500).json({ error: "Filebrowser API Error", message: error.message }); + } +} diff --git a/src/widgets/filebrowser/widget.js b/src/widgets/filebrowser/widget.js new file mode 100644 index 000000000..8ab0d9b13 --- /dev/null +++ b/src/widgets/filebrowser/widget.js @@ -0,0 +1,14 @@ +import filebrowserProxyHandler from "./proxy"; + +const widget = { + api: "{url}/api/{endpoint}", + proxyHandler: filebrowserProxyHandler, + + mappings: { + usage: { + endpoint: "usage", + }, + }, +}; + +export default widget; diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index c1865fce3..cfd28c590 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -25,6 +25,7 @@ import downloadstation from "./downloadstation/widget"; import emby from "./emby/widget"; import esphome from "./esphome/widget"; import evcc from "./evcc/widget"; +import filebrowser from "./filebrowser/widget"; import fileflows from "./fileflows/widget"; import firefly from "./firefly/widget"; import flood from "./flood/widget"; @@ -167,6 +168,7 @@ const widgets = { emby, esphome, evcc, + filebrowser, fileflows, firefly, flood,