- TypeScript 80.8%
- Svelte 19.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| backend | ||
| frontend | ||
| CLAUDE.md | ||
| opsdeck.module.json | ||
| README.md | ||
| tsconfig.check.json | ||
filebrowser
Browse, move and measure the host's files from OpsDeck.
An external module: its own repository, cloned and built at container startup, pinned by commit. It runs in-process as the OpsDeck server — which is host root — so read the security section before installing it.
OPSDECK_EXTERNAL_MODULES=https://git.imhof.cloud/OpsDeck/module_filebrowser.git#<sha>
What it does
- Browse every configured root: name, size, modified, mode, owner, symlink target, with breadcrumbs, a live filter and a hidden-files toggle. Each row carries a small kind glyph — folder, image, video, archive — which is the first thing dropped when the window is narrow.
- Inspect a file: full metadata, a text preview with a binary refusal, and a streaming sha256 on request. Images render inline — in the details panel, and as a thumbnail when a name is hovered or focused.
- Scan a root in one pass and learn both where the space went (recursive directory sizes, largest directories and files) and what it went into (bytes and counts per category and extension). Off, on demand, or on a schedule.
- Transfer: download a file (with range requests, so an interrupted download resumes), download a folder or a selection as one streamed archive, upload with per-file progress and gzip transport compression — by button or by dropping files anywhere on the page.
- Drag entries onto a folder or a breadcrumb to move them, hold the copy modifier to copy instead; a directory refuses to be dropped on itself or into its own subtree.
- Change: create, rename, copy, cut/paste, delete — the long ones as task runs, so progress survives a reload and appears in Activity.
Everything long-running is a core task run (ctx.tasks): scan, copy, move and
recursive delete. Closing the page does not cancel them.
Configuration
Every variable is OPSDECK_MOD_FILEBROWSER_<KEY>. Defaults are the safe end of
each range; an unparseable value fails at startup naming the variable, and an
unrecognised role or mode narrows rather than widens.
| Key | Default | What it does |
|---|---|---|
ROLE |
admin |
Minimum role that may see the module at all. viewer widens it and logs a warning. |
WRITE_ROLE |
admin |
Minimum role for mutations; never resolves below ROLE. |
READONLY |
0 |
1 refuses every mutation server-side and hides the controls. |
ROOTS |
the whole host | media=/srv/media,configs=/etc. When set it is the complete set; nothing outside is reachable. |
DENY |
— | Extra globs refused for read and write, e.g. /srv/secret/**. Extends the built-ins, never replaces them. |
ARCHIVE |
tar.gz |
Default format for folder downloads: zip, tar.gz or tar. All three stay available per request. |
SCAN |
ondemand |
off, ondemand or periodic. |
SCAN_INTERVAL |
6h |
Schedule for periodic (30m, 6h, 1d). |
SCAN_PACE_MS |
0 |
Sleep every 500 entries, to leave IO for everything else. |
SCAN_STALE_HOURS |
48 |
Beyond this, scan-derived sizes are labelled stale. |
SCAN_CROSS_FS |
0 |
1 lets a scan cross filesystem boundaries. |
TYPES |
— | Extend the category map: video=mkv,mp4;photos=cr2. |
MAX_UPLOAD_MB |
2048 |
Enforced while streaming, not after. |
PREVIEW_KB |
512 |
How much of a text file the preview reads. |
IMAGE_PREVIEW_MB |
12 |
Above this an image is offered as a download; there is no thumbnailer in the container, so the browser scales the original. |
LIST_LIMIT |
2000 |
Entries per listing response; the total is still reported. |
SHOW_HIDDEN |
0 |
Show dotfiles by default. |
STATE_DIR |
$OPSDECK_DATA_DIR/modules-state/filebrowser |
Scan cache. Safe to delete; costs a rescan. |
FAKE |
0 |
Serve a synthetic tree instead of the host (also on with OPSDECK_FAKE_DATA=1). |
The built-in deny list — /proc, /sys, /dev, /run — always applies. If
your OpsDeck data volume is a host directory, add it to DENY: the module would
otherwise let someone delete the dashboard's own database.
Security
Installing this module gives everyone with the read role the ability to read
every file the server can, and everyone with the write role the ability to
change them. With the default ROOTS, that is the whole host.
What the module does about it:
- Every route carries a role guard, GETs included. Mounting under
/api/mod/filebrowserprovides authentication, not authorization. - Path confinement. Client input is resolved segment by segment;
..is collapsed during the walk and the result is re-checked against the roots on a segment boundary, so/srv/data-oldis not inside/srv/data. Anything outside answers 404, never 403 — a distinguishable refusal is a probe. - Symlinks are resolved in the host's namespace. An absolute link target
inside the host mount means the host's root, not the container's;
realPath()would happily walk out of the mount into the container's own/usr. A link leaving every root is listed and marked, never followed. - Downloads are always
Content-Disposition: attachment+nosniff. A planted.htmlor.svgmust not execute in the shell's origin. - One route serves bytes inline, and it never trusts the file name. Image previews are typed from the magic bytes against a raster allow-list (PNG, JPEG, GIF, WebP, AVIF); anything else is refused with 415. SVG is not on the list — it is the one image format that is a document.
- Audit trail. One line per download, upload, mutation and refusal: user, operation, path, bytes, outcome. Never file content.
- A live topic follows the role of the action it reports on, so a progress
stream is not the door the role check forgot.
changedandscanare admin-only outright.scan-progressis admin-only exactly when reads are — scanning is a read-role action, and an operator allowed to start one should be able to watch the one they started.delete-progressfollows the write role, soROLE=viewerwithWRITE_ROLE=admindoes not let a viewer watch deletions they can never start. Either way a line says no more than the subscriber could already list: counts and a name, never file content, and on failure the reason the action declared rather than the error text — run fields are withheld per field, and a topic has no projection to withhold them with.
What it deliberately does not do
- No per-path permissions. OpsDeck has two roles; a finer model invented here would be enforced nowhere else.
- No public share links, no WebDAV. A second, session-less path around every guard above.
- No trash. Deletes are real; the confirmation is the safety net.
- No editor. Editing belongs to a separate module; this one links to it when it is installed.
Platform limits worth knowing
- Creating symlinks is impossible in the container. Deno refuses
symlink()unless--allow-writeis unscoped, and OpsDeck scopes it. A copy that meets a symlink skips it and names it in the run log rather than failing the whole copy. - Mobile cannot download or upload bytes. The portable UI schema has no block that opens a URL and no input kind that picks a file, so the Android app gets browse, inspect, preview, create, delete and the scan; transfers are web-only until the schema grows those verbs.
dfprovides free space — one cached call per path, shared by the web routes and the schema routes, and it is df's Available rather thantotal - used, which counts the filesystem's reserve as space you can write. A deployment without coreutils shows no usage bar rather than a wrong one. The roots behind that number are probed together and by one helper, and the two web surfaces report them through one mapper, so the file listing, the landing page and the dashboard card cannot disagree about whether a root is readable — a root a deny rule covers is unreadable on all three, and the card says so rather than drawing the empty bar it used to. The card renders the roots it has room for and counts the rest; that count is of the configured roots, not of the ones it asked about.- A progress topic belongs to the action, not to the run.
progressTopicis a fixed string in the schema with no room for a run id, soscan-progressanddelete-progressare module-wide: withSCAN=periodica scheduled scan of one root finishing ends the stream a phone was watching for another, and the second of two queued deletes does the same to the first. The run itself is always exact — Activity, the web UI and therunIdthe route answers with all follow one run — so this is a limit of the portable mechanism, not of the work. It goes away when the schema can name a run.
Development
No standalone build — the module builds inside an OpsDeck core checkout:
cp -r . <core>/packages/modules/filebrowser
cd <core>
deno check packages/modules/filebrowser/backend/mod.ts
deno run -A packages/module-builder/main.ts \
packages/modules/filebrowser /tmp/fb-dist filebrowser # the import contract
OPSDECK_AUTH=disabled OPSDECK_FAKE_DATA=1 \
OPSDECK_MODULES=filebrowser deno task start
Tests need no core checkout and no network — the repository has no dependencies:
deno test --allow-read --allow-write --allow-run=python3 backend/
--allow-run=python3 is for the archive tests: the zip and tar output is
verified by Python's zipfile and tarfile, because an archive writer that
only its own reader accepts is not a working archive writer.
Which core it needs
SDK major 1, plus core with:
- the task system (
ctx.tasks/ctx.schedules) — scan, copy, move and delete are runs, not promises; AccessDeniedandActionProgressin@opsdeck/ui— the permissions page and the two-density progress view live in the shared kit rather than here, so both are one Svelte runtime and one copy for every module that wants them.
v0.1.0 predates that second requirement and carries its own copies, so it
installs against core as it was released. Anything after it needs a core that
ships the components.