v0.37.0
filex v0.37.0
Self-hosted file manager — Go single binary + multi-framework frontend.
Download a binary below, or pull a Docker image:
docker pull ghcr.io/brf-tech/filex:slim-v0.37.0
docker pull ghcr.io/brf-tech/filex:full-v0.37.0What changed
Upgrade notes
-
⚠ If OnlyOffice or drawio "tests fine" but fails when you open a file,
press Test again. It now probes from your browser as well as from the
filex server and reports the two separately, because they answer different
questions and only one of them was ever asked. A container-internal address
likehttp://onlyofficeis reachable from filex and not from the browser
that has to load the editor — the green light said "configured" and meant
"the server can reach it". Reported from the outside, twice, by the same
person before we saw it. -
⚠
GET /metricswas inside the admin group but registered withr.Handle,
so chi bound it to every method and the demo guard refused none of them.
Read-only either way, so nothing was exposed that a GET did not already
expose — closed so the rule has no exceptions left.
Added
-
The Test button now probes from the browser too, and says which machine
answered. Issue #17's reporter came back: the v0.34.2 fix was real, and it
did not close his problem. Three different machines have to reach three
different addresses before the Office editor works — the browser loads
the editor's JavaScript from the Document Server URL, the filex process
polls the same URL, and the Document Server fetches the document and
POSTs the save back toFILEX_PUBLIC_URL— and only the middle one was ever
checked. So an operator on podman typed the container name
http://onlyoffice, filex reached it, Test went green, and his browser
could not resolve that name at all; the editor then failed with the same
message as a missing configuration. The defect was never the probe. It was
that the check was narrower than the badge implied — the same family as
everything else fixed this week: a control that reads as verified and is not.-
Two probes, two results, one badge. The admin page runs in the very
browser that will open the editor, so it now tests that leg directly
instead of disclaiming it. The result is reported as two separate
sentences — From the filex server: reachable / From this browser: not
reachable — plus a third line for the leg nobody can probe. When they
disagree the page says what it means: a container-internal address filex
can use and a browser cannot. -
The mechanism is the one the real viewer uses, not a
fetch. ⚠ A
plainfetch()is blocked by CORS on a document server that is working
perfectly, so a naivecatchwould report failure for a healthy service.
OnlyOffice is probed with a<script>at
/web-apps/apps/api/documents/api.js— load detection is not subject to
CORS, and a successful load defineswindow.DocsAPI, which proves the
thing that answered really is a Document Server. drawio is probed with a
hidden<iframe>at?embed=1&proto=jsonand its{"event":"init"}
handshake, the same oneDrawioViewer.vueuses. Both were verified
against live servers before being relied on. Each distinguishes
could not reach from reached, wrong thing with a second,no-cors
signal, and each is bounded by a timeout that reports as its own state. -
The badge no longer conflates "reachable" with "configured".
Completeis reserved for a service where both probes answered and
nothing is warned about; a service the server can reach and the browser
cannot readsServer-reachable only, in a different colour. -
The reverse path gets the only honest treatment available. filex
cannot make the Document Server issue a request on demand, so it does not
claim a check it cannot perform: it warns on the shapes that certainly
cannot work (a public URL oflocalhost,127.0.0.1or0.0.0.0) and
notes the ones that are merely suspicious (a container-name public URL, a
hostname filex itself cannot resolve). ⚠ A warning that fires on a working
setup is worse than none, so every trigger is paired with a test that
proves it stays silent on a setup that works —https://office.example.com,
http://192.168.1.10:8080, andhttp://localhost:8080when filex itself
is reached over localhost. That last suppression is the interesting one:
a loopback document server is correct for somebody browsing from the
same host, so it is never warned about there. -
Advisories ride on the list, not only on Test, because the whole
defect was a control that read as settled without anyone pressing
anything. Opening the page is enough to see a browser-unreachable address. -
The three-address requirement moved to where the field is filled in —
the top of the admin page and the top ofdocs/ONLYOFFICE.md, with the
two commands that tell an operator which half is wrong. It was already in
the docs, around a prerequisites list further down the page, and he hit it
anyway: a green Test outranks a prerequisites list. -
⚠ Known limitation, deliberately not fixed here: filex has one public
URL, and some setups need two.FILEX_PUBLIC_URLis a single value that
the OnlyOffice fetch/callback and every share link, invite mail, drop link,
OIDC redirect and WebSocket ticket are all built from. An install whose
Document Server can only reach filex by a container-network name
(http://filex:5212), while its users need a browser-facing address,
cannot express that today. The admin page now raises a note when it
sees a container-name public URL rather than leaving it silent; splitting
the value is tracked separately.
-
-
A release gate for the shop window — the surfaces a stranger touches
before they trust us. On 2026-09-07, hours before the public launch, a
person looking at filex from outside found seven defects, and not one had
been caught by a test, a lint, or any of the eleven steps of the release
process. Several had been shipping for months: a deadIssueslink on 104
of the 105 published release pages, a public demo that answered all 101
admin routes with no refusal,GET /api/files/capabilitieshanding
anonymous callers the operator's internal hostname, a docs site built from
the private tree, release bodies that were a commit hash, a headline
docker runthat put the reader's files in the database directory, and the
demo's own advertised search returning nothing. The pattern is why this is
a gate and not a checklist: everything a stranger touches first is the
least tested surface in the project.It is split by what each check needs, because a check that cannot say
which it is ends up either useless or dishonest:- Offline, repo-only —
web/tests/deploy/shopWindow.test.ts, so it runs
on every push, inpnpm test, and in CI, which already gates the tag. It
covers the URL grammar (nothing pastes a GitLab route onto a GitHub host,
and every GitLab route we link is one the export can translate — the
export's own guard cannot see a route it drops the/-/from and gets
wrong anyway), the headlinedocker runagainst the compose file it is a
shorthand for,site/— which reaches filex.sh verbatim with no converter
in the way — and the names the export does not rewrite, such as a bare
IP address. - Against a running instance —
node scripts/check-shop-window.mjs --instance --boot bin/filex. It boots a throwaway in demo mode on a pinned
port with its own data directory, then proves a signed-in visitor is
refused on the state-changing admin routes and still gets the read-only
ones, that an anonymous capabilities call carries the feature flags and
not the operator's host, and that the searches the demo advertises return
the files they promise. - Against the published product —
--published. Release bodies carry no
/-/route and no bare commit hash where prose belongs, their links
answer, and docs.filex.sh serves the released build with no private
repository URL on it.
Three exit codes, and the last two are the point:
0passed,1checked
and wrong — fail the release — and2could not check (no binary, no
network, a GitHub rate limit, a fixture that is not set up). ⚠ A gate that
turns an outage into a failed build is an outage of its own, so a network
failure is never1; it is also never0, and the run names the check that
did not happen. The same rule applies to a fixture: an instance with no
external service configured would satisfy "the anonymous answer names no
host" while leaking the moment an operator configured one, so that reports
skip, notok.Every check was proved against the defect it is for — the bug
re-introduced, the check watched going red, then green: a/-/link, an
unknown GitLab route, the olddocker run, a production IP in a published
file, the private repo on the landing page, the runbook publishing from the
wrong tree, an advertised query nobody is shown, the redaction removed from
capabilities.go, the demo guard removed from the router, an advertised
query that answers nothing, and a fixture serving each published defect in
turn. Release process step 12 indocs/CONTRIBUTING.mdsays what it does
not cover. - Offline, repo-only —
-
The shop-window gate now walks the route table instead of sampling it, and
covers four surfaces it had listed as gaps. The gate above shipped with an
honest account of what it did not reach; this closes what could be closed
and says plainly what could not.- Every route, not six of them. The live check probes six admin routes,
which proves the demo guard is installed and nothing more — a new operator
surface at a fourth prefix would have passed, and that is exactly how the
first hole appeared, because/api/ai/adminwas the same admin panel behind
a different front door.backend/internal/api/shop_window_route_table_test.go
walks all 359 routes out of chi and classifies each one by asking the
running server: a route an anonymous caller gets 401 on and a signed-in
non-admin gets 403 on is an operator surface, one that answers both
identically is not role-gated at all, and everything else is the product.
Nothing in it names a route, so a new admin prefix goes red the day it is
added with no list to update — and a second test measures the other
direction, so the guard cannot be widened over the product to make the first
one quiet. (Middleware would have been the obvious signal and chi cannot
show it:r.Groupbakes its chain into the handler before registration, and
all 359 entries report the same four top-level middlewares.) /metricswas the fourth prefix, found by that walk on its first run.
It is mounted inside the admin-only group withr.Handle, so chi registers
it for every method, and a public demo refused none of them. The exposition
is read-only, so nothing was ever going to break — it is guarded now because
that lets the walk state its rule with no exceptions at all.
GET/HEAD/OPTIONSstill pass: a Prometheus scrape job is untouched.- All three external services are proved, not one. The redaction covers
OnlyOffice, drawio and the converter through one loop and three flat
aliases blanked by name, so a single seeded host exercised the loop and left
two aliases unproved. The booted instance now carries a distinct sentinel
per service, and the anonymous payload is additionally asserted to carry
nourlkey anywhere underexternal— the only form that reaches a
service nobody has added yet (mermaidis already there, with no
environment variable and no alias). - The demo's own corpus, asked of the demo. The instance check seeds the
file it then searches for, so it proves the query grammar and not what
demo.filex.sh holds — and the corpus is where the defect was.--published
now signs in to the live demo with the credentials the demo itself
publishes, and types the queries the splash advertises. Unreachable is a
2, never a false green. - GitHub's About box, and filex.sh. Neither is code, so neither was ever
checked. The About blurb had no source of truth at all — it is typed into a
settings form and lived only in GitHub's database — soREPO_ABOUTin
scripts/shop-window-data.mjsis now that source: asserted offline to fit
GitHub's 350 characters, to name every driver the backend registers
(measured from thestorage.Registercalls, and the blurb that was
published when this was written still said five after SMB shipped), and to
punctuate the way every other surface does.--publishedcompares it with
the live value and prints the exact line to paste, GitHub having no deploy
step for it. The front page is checked for the hosts it has to link —
docs.filex.sh above all, whichsite/index.htmllinks in three places and
the deployed page did not link at all. - Screenshot staleness, with its limits written down. A picture committed
before the last change to the code that draws it cannot be showing that
change.SCREENSHOTSdeclares what each README picture depicts — the one
thing here a person has to know — and the offline half asserts every
declared path still exists, so a renamed component cannot leave a picture
looking fresh for ever. The threshold is measured, not chosen:
admin-plugins.pngshipped six releases stale, so six released versions
of unfollowed change is the line, and lesser drift is reported and passes.
⚠ What it cannot see is whether a picture is actually wrong: it compares
commit dates, so a comment counts and a theme change does not. Looking is
still release step 2.
- Every route, not six of them. The live check probes six admin routes,
Fixed
- Two release-gate suites could report success while running no tests.
describe.skipIfskips every assertion inside it — including the "this list
is not empty" guards written to stop those blocks passing vacuously, which had
been placed inside the very blocks they guard. Measured across the suite: in
the published tree, which CI runs,siteAssets.test.tsreported 0 of its
8 tests andshopWindow.test.ts8 of its 17, both green. Both files now
assert the shape of the checkout unconditionally, and as one fact — the
export withholdssite/andscripts/export-public.shtogether, so a tree
holding exactly one of them is neither the source nor the published product,
and says so instead of skipping in silence.
Verify: sha256sum -c checksums.txt
- Documentation — https://docs.filex.sh
- Report a bug — https://github.com/BRF-Tech/filex/issues
- Full changelog — https://github.com/BRF-Tech/filex/blob/main/CHANGELOG.md
- Every release — https://github.com/BRF-Tech/filex/releases