Repository navigation
v0.24.0
filex v0.24.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.24.0
docker pull ghcr.io/brf-tech/filex:full-v0.24.0What changed
Added
- Transfers run in parallel. The engine walked its plan one action at a
time, so a tree of small files was priced at one full round-trip each —
measured on a live deployment, 2 GB of ~400 KB files crawled at 0.24 MB/s
with the network mostly idle. Uploads and downloads now run on a small
worker pool (default 4,--transfersto tune, 1 restores the serial
engine); directory creation stays first and deletes/conflicts stay serial
in the planner's careful order.
Fixed
-
An interrupted first run RESUMES instead of conflicting every finished
file. With no baseline, "present on both sides" always meant a conflict —
so a restart mid-first-run turned every already-downloaded file into a
"(remote copy)" duplicate; on the tree above that would have been ~7,800 of
them. Downloads now stamp the server's own mtime on the local copy, and
twins with no history that match by (size, mtime) are adopted silently. -
Changing the mirror root keeps each pair's history. Migration used to
remove and re-add the pair, throwing the baseline away — and the first-run
merge that followed conflicted every file the machine had ever uploaded.
Newfilex sync move <id> <path>repoints a pair and keeps its baseline;
the desktop root change uses it, and stops the account's watcher first so
a mid-round rename can never read as a mass delete. -
The remote walk lists eight folders at a time. One round-trip per
folder made the inventory the slow phase of a big sync: measured behind a
CDN proxy (~0.35s per request), a 3,328-folder invoice tree took ~19
minutes to list serially — twice per run, since the settle pass walks
again. The walk is breadth-first by level now, eight listings in flight,
with the snapshot merge kept single-threaded; the same tree lists in a
couple of minutes. -
A filename containing ".." is a filename, not a traversal. Eleven API
guards rejected any path CONTAINING the substring — so a real invoice
named "… Tic. Sic. Gaz..pdf" could be stored but never previewed,
downloaded or synced (400 "bad path" everywhere). Traversal needs a whole
../segment, and that is what every guard now checks — the same rule
sync addalready applied. -
No-history adoption tolerates coarse mtimes (±2s). FAT stores mtimes
in 2-second steps, and any tool that stamps times through float seconds
can land a millisecond off — measured: a repair pass one millisecond short
turned 1,667 identical files into conflict pairs. Change detection against
a baseline stays exact; only the adopt rule for twins with no history is
tolerant, rsync's modify-window logic. -
A half-dead connection can no longer freeze a sync forever. All the
CLI's parallel streams ride one HTTP/2 connection; when a CDN proxy killed
it silently mid-first-sync, every stream blocked — for good, since Go's
http2 sends no health pings by default and the client had no transport
limits at all. The client now pings an idle connection (ReadIdleTimeout
30s), bounds dialing, TLS and response headers, and leaves bodies
unbounded — a big transfer may take long, a hang may not. -
A folder that could not be listed is not a folder that is gone. The
remote walk skipped ANY failed sub-listing as "vanished" — and it now runs
eight listings wide through proxies the client expects to die under it.
One 502 on a subtree would have read as "folder removed on the server"
and binned the local copy of everything below it; in the settle pass it
would have dropped the subtree from the baseline instead, and every
uploaded file in it would have come back as a conflict pair next round.
Only a 404 is skipped now; any other listing error fails the run, names
the folder, and touches nothing. -
A pair cannot be pointed at a path that is not there.
sync move
accepted a non-existent path; the next run would have created it empty
under a surviving baseline, and an empty mirror with history means "every
file deleted here" — carried to the server. The path must exist and be
the pair's kind: a folder for a folder pair, a file for a file pair. -
A missing mirror with history refuses to run. The engine created a
missing sync folder and carried on, which turned an unplugged drive or a
folder moved by hand into a mass delete on the server. A pair whose
folder is gone while its baseline still remembers files now stops and
says what to do —sync moveif it moved, plug the drive back in,sync removeto stop syncing it. A pair that never synced a file still gets
its folder created: there is nothing to lose. -
Moving the filex folder to another drive keeps modification times.
The cross-device copy stamped every file "now", and change detection is
(size, mtime) — so the very historysync movepreserves would have read
as every file edited here, and re-uploaded the whole tree. The copy now
preserves timestamps. If a move fails halfway the pair follows whichever
side holds the COMPLETE tree (a finished copy with a half-failed cleanup
points at the new place; a failed copy discards its partial litter), and
unpairs as the last resort — an unpaired folder syncs nothing and deletes
nothing. -
A replaced watcher's late exit no longer unhooks its successor.
Stopping an account's watcher for the root move and starting a new one
could race: the old process's exit handler deleted the supervisor's entry
for the NEW process, so the next reconcile started a second watcher for
the same account — two engines over one baseline. -
The ".." guard also reads Windows paths. Segments are split on both
separators, and a segment made only of dots and spaces is refused
(Windows trims ".. " to "..").