Releases: cerede2000/NextExplorer
Release list
v3.2.0
Search answers from an index, and the volume is left alone
Every search used to read the volume. On a large one that is minutes of disk
for a question asked in a second, so SEARCH_DEEP was the setting people
turned off and then stopped expecting search to find anything.
SEARCH_INDEX=true keeps a full-text index of the documents instead. It is
built by a background pass that takes the share of one core you give it
(SEARCH_INDEX_CPU_PERCENT, a quarter by default), skips everything it has
already read, and stops when the server is asked to stop. Searches outside the
volume root — personal folders, assigned volumes — go on reading as they
always did.
Half an index does not answer a whole search: until a pass has run to the end,
searches read the tree the old way. An index that answered early would report
that a file found yesterday no longer exists, which is worse than a slow answer
and much harder to explain.
Filename patterns
* and ? in a search term now match filenames rather than text. *.ps1
finds the scripts; conf?g.json finds either spelling; Stacks/*/logs/*.log
reaches across folders. The pattern is matched against the whole name, so
*.ps1 does not return deploy.ps1.bak.
A pattern names a shape, and no file contains the characters *.ps1, so
nothing is read inside files to answer one. Before this, *.ps1 searched the
volume for that literal string: it returned the files that mention the pattern
in their text, took the whole time budget doing it, and returned none of the
scripts.
Searching inside Office documents and PDFs
A .docx is a zip of XML and a PDF keeps its words in compressed streams, so a
plain content search finds nothing in either. Their text is extracted and
searched — including a word an author emphasised halfway through, which Word
stores in pieces. A scanned PDF is a picture of a page and stays unsearchable;
that needs OCR.
A search answers when it is done, and the newest one wins
Three things were making the search box feel unreliable.
A search waited out its whole five-second budget on a reserve it was holding
for content matches, even when every source of them had already been exhausted.
A budget is a ceiling, and it had become the normal duration.
When the budget did end a search, cleanup ran before the answer: resuming every
source at whatever it was in the middle of took six more seconds on a busy
tree, so a search bounded at five answered in eleven. The bound had not been
raised — the wait had moved past it.
And typing sends one search per pause. Nothing said which answer belonged to
which question, so the panel showed whichever came back last: a list of .doc
files under a box reading *.docx. Only the newest search may write to the
panel now, superseding one aborts it, and a search whose reader has gone away
stops instead of running to the end for nobody.
Folders search leaves alone
SEARCH_INDEX_EXCLUDE, and a matching list in Settings → Search index,
name folders search does not walk into — a Docker overlay, a build tree, a mail
spool. Hundreds of thousands of files nobody searches, and reading them is the
whole overhead.
The list is obeyed by the whole of search now, not only by the index. While
only the index obeyed it, a filename search still enumerated the excluded tree
and could not finish inside its budget: the same *.xlsx came back truncated
at 58 matches, then at 57. One question, two answers, cut at a different point
each time.
Standing inside an excluded folder and searching there still works: the list
keeps the crawl out of a corner, it does not make the corner unreadable to
someone who navigated into it.
The index no longer takes the machine it was borrowing
A first pass over a large volume grew to ten gigabytes and drove a host into
swap.
Every document was indexed through a statement prepared for it and never
finalised. That memory is native, invisible to the heap, and it was three and a
half kilobytes a document — over a gigabyte at three hundred thousand files.
Each pass also allocated a buffer per document and carried a set of every path
it had seen, fifty megabytes at two hundred thousand paths. The statements are
cached now, one buffer is reused for the whole pass, and what a folder holds is
asked of the database instead of remembered.
The pacing was measured wrong on top of that: a fixed pause after each document
paces cheap documents and not expensive ones, so the load was whatever the
files happened to be — a hundred and seventy-five percent of a core at its
worst. A pass now works for a slice of time and stands aside for the rest, so
the share you asked for is the share you get.
When a pass does approach the container's memory limit it stops and picks up a
couple of minutes later, rather than for an hour. It reads the limit the
container actually enforces instead of guessing from the process.
Large Markdown opens instead of freezing the tab
A six-megabyte Markdown file could be opened in the editor and not in the
preview: the preview parsed and rendered the whole document in one stretch,
which is a frozen tab for as long as it takes.
It is read in slabs sized from what the last one cost, handing the browser back
between them, so the document appears immediately and fills in behind. Slabs
are cut between blocks, never inside a fenced code block, and link definitions
travel with each slab because Markdown resolves them while lexing. Chunks off
screen are skipped for layout and paint but stay in the document, so Ctrl+F
still crosses all of it. PREVIEW_MAX_RENDER_SIZE sets the ceiling.
A search result opens the folder on the file
Clicking a result opened the folder containing the file and left the list at
the top, so a file below the fold looked like nothing had been found. The row
is scrolled to a third of the way down the viewport — what sits above a file is
the context of where it lives.
The health check answers, and says why when it does not
/healthz sat behind the session store and the identity provider. A container
whose provider was slow to answer was reported unhealthy for a reason that had
nothing to do with whether it was serving. The health routes are mounted before
any of that now.
The check itself reported a bare failure; it now says which of a timeout, a
refused connection, or a non-200 answer it saw. A request that is accepted and
never answered is reported with its path and how long it has been held, so a
hang can be told apart from a slow identity provider — and a long-poll a route
means to hold is not reported at all.
Under the hood
Path containment — the checks that keep a request inside the volume it is
allowed in — stopped the event loop to do its work: lstat, readlink and
realpath were synchronous, and every request pays for them. They are
asynchronous now, and the containment is unchanged.
A pass over code nothing calls removed twenty-one dead functions, fifty-four
unused exports and three hundred and thirty-eight translation strings for text
no longer on screen. Two services that were eighty-two percent the same file —
folder-size exclusions and search-index exclusions — are one factory and two
fifteen-line callers.
v3.1.2
A share password is asked for once
Opening a link protected by a password, typing it, and then reloading the page
asked for it again. The reload calls the same endpoint the first visit does, and
the branch that answered had no idea the check above it had already accepted the
session the visitor was carrying — so it sent them back to the prompt for a
share they had just been given.
It came out of straightening the predicate that answers does this password
apply to this caller?. It required a signed-in user, so it said no for visitors
with no account at all — the very people a public password is for — and each
caller made up the difference in its own way. Those compensations are gone, and
this was hiding under one of them.
Two accounts can no longer share one personal folder
Which folder an account gets was derived from USER_FOLDER_NAME_ORDER on every
request, and nothing about that order guarantees a distinct answer. username
carries no uniqueness constraint, and bob@a.com and bob@b.com both yield
bob under email_local. Two accounts that derived the same name were handed
the same directory, and each saw the other's private files.
A default install was never affected — id comes first and ids are unique — but
the documentation recommends
username,id to reuse an existing /home/<username> layout, which is where it
bites.
The name is claimed now instead of derived: the first account to be given one
keeps it, a second walks down its own preference order to the next free name,
and a unique index makes that a guarantee rather than a check two requests could
race past. Accounts that already exist are assigned oldest first, so where an
instance already had a collision, the account that has been using the folder is
the one that keeps it.
Warning
One behaviour changes with this.
A name that has been given is kept, so changing USER_FOLDER_NAME_ORDER
afterwards applies to accounts created from then on and leaves the existing ones
where they are. It can no longer quietly take a folder away from whoever is
working in it. To move existing accounts deliberately, move their directories
and clear personal_folder_name — the personal folders
page has the statement.
Checking a password no longer holds the only thread
bcrypt is slow on purpose, and its synchronous form stops the server doing
anything else for that time. Verifying a share password is reachable without an
account and rate limited per address, so a handful of addresses could keep the
process busy in a way no other public route can. The asynchronous form runs
now — here and on the sign-in path, which had the same shape.
Also
- Docker Hub keeps the last two versions. Publishing a third removes the oldest
and everything that belongs to it, so the page people reach before the
repository stops being a wall of tags nobody runs. - The check that keeps a path inside its volume root makes its own containment
test rather than trusting each caller to have done it first. - Three modules that had no test have one: the rename path, the OIDC
middleware, and the automatic-fallback half of the chunked upload gate. With
them, every defect and every fragility the code review found is closed.
Images
ghcr.io/cerede2000/explorer:3.1.2
ghcr.io/cerede2000/explorer:3.1.2-lean
cerede2000/explorer:3.1.2
cerede2000/explorer:3.1.2-lean
latest and latest-lean follow main and now carry this release. Docker Hub
keeps the last two versions from here on, so pin one you intend to keep running.
Full changelog: v3.1.1...v3.1.2
v3.1.1
The text editor no longer writes files it will refuse to reopen
Two settings govern the inline editor and nothing tied them together.
EDITOR_MAX_FILESIZE decides what it will open; MAX_JSON_BODY_SIZE decides
how much can be sent back, because saving carries the whole file in a JSON
request body.
Saving checked neither. Open a small file, paste two megabytes into it, save —
accepted and written — and the next attempt to open it answered This file is
too large to open in the text editor. A file the editor had written and would
not take back.
Raising EDITOR_MAX_FILESIZE on its own, which the FAQ
recommended for editing larger documents, produced the other half:
the file opened, and saving answered request entity too large — a message
naming neither of the two settings involved. It is the failure reported
upstream as nxzai#368.
The pair is now one decision:
- Where no body ceiling has been set, it rises to carry whatever the editor
opens, twice over — JSON escaping can double the text, every quote and
newline becoming two characters. - Where one has been set, it is kept. A ceiling someone chose is a guard,
not a detail to be talked out of, so the editor is lowered to what that
ceiling can carry instead, with a warning naming both values.
Either way the editor cannot open a file it would not be able to save, and
saving refuses what it could not reopen. A request that really is too large now
says which setting governs it.
Every navigation reported an error it had recovered from
Moving between folders left Uncaught (in promise) InvalidStateError: Transition was aborted because of invalid state in the browser console, on
every navigation. Nothing was wrong — the view transition simply did not get to
animate, and the promise that says so had no listener. It is now observed, while
the two promises that would carry a genuine failure are deliberately left to
surface.
Also
- A release no longer rebuilds the images a push to
mainhas just built. It
checks that its tag and the manifests agree, which takes seconds instead of
two multi-architecture builds. - The image build itself lives in one place instead of being written out once
per channel — the duplication that had let a version tag exist for one
variant and not the other. - The documentation site is redeployed when documentation changes, rather than
on every push.
Images
ghcr.io/cerede2000/explorer:3.1.1
ghcr.io/cerede2000/explorer:3.1.1-lean
cerede2000/explorer:3.1.1
cerede2000/explorer:3.1.1-lean
latest and latest-lean follow main and now carry this release.
Full changelog: v3.1.0...v3.1.1
v3.1.0
Changing a share's password now ends the access it replaces
Rotating the password on a share is what an owner does when a link has leaked.
It protected nothing until now: a guest session is created the moment someone
gets in, lasts a day, and was never looked at again — so everyone already
inside stayed inside, with the old password, for up to twenty-four hours after
it had been changed.
Setting or replacing a password now revokes the sessions of that share, and
only that share. Removing a password does not: taking the lock off opens the
share to everyone, and throwing out the people currently reading it would be a
surprise rather than a protection.
OIDC group membership is read at every sign-in
Warning
Read this before upgrading if you use OIDC_ADMIN_GROUPS.
Group membership used to be read once, when the account was first created.
Someone added to the admin group afterwards never became an administrator, and
— the half that matters — someone removed from it stayed one. The
documentation said membership was re-evaluated at each login. It was not.
It is now, in both directions, and each change of role is logged. If your
provider's group claim is out of date, or narrower than you think, the roles it
carries will now be applied.
Two conditions have to hold before the provider is allowed to decide anything.
OIDC_ADMIN_GROUPS must be configured — without it every login derives the
plain user role, and applying that would demote the administrator promoted
from Settings, or created by AUTH_ADMIN_EMAIL, at their next sign-in and
leave nobody able to administer the instance. And the provider must actually
have returned a group claim: a missing groups scope looks exactly like a user
who belongs to nothing, and its documented symptom is "not an admin after
login", so acting on that silence would turn a misconfiguration into a
demotion.
If an instance does lock itself out regardless, AUTH_ADMIN_EMAIL still
promotes an account at startup.
Copying to a dataset that refuses a chmod
Preserving a file's permissions means a chmod on the copy, and some
filesystems refuse one: a ZFS dataset with aclmode=restricted, where new
files must inherit the directory's ACL untouched, fails the copy outright
rather than the permission change (#2).
A copy still preserves permissions by default; where the destination refuses,
it is retried without preserving them instead of failing. COPY_PRESERVE_PERMISSIONS=false
skips the attempt altogether, for a deployment where it is always refused.
Uploads: what a killed one leaves, and one that cannot fit
A direct upload writes to <name>.uploading and renames on success. Every
failure it could observe was cleaned up, but nothing survived the process being
killed: a restart mid-upload left holiday.mp4.uploading in the folder, in the
listing, with nothing anywhere that would remove it. The artifact now joins
.download in the hidden-file defaults, and the destination folder is swept of
the ones nothing has written to for a day.
UPLOAD_STORAGE_RESERVE was enforced only on the chunked upload path, which is
off by default — so the free-space guard covered the path a deployment opts
into and missed the one it gets. Both paths now refuse an upload the volume
cannot hold, with a 507. What that protects is not the upload: where
/config shares the filesystem, a full disk stops SQLite being able to write,
and the application stops working for everyone rather than for whoever was
uploading.
A favourite whose volume is not there
Remove a volume from the compose file and the rows pointing into it survive.
The favourite stayed in the sidebar looking perfectly ordinary and answered
with a 404 when clicked.
Such favourites are now shown as unavailable, and startup reports what points
at a volume that is not there — favourites, shares, recent destinations and
folder preferences, counted per volume. Nothing is removed. A volume that
is absent is not a volume that is gone: an NFS mount may not be ready yet, an
external disk may be unplugged for a weekend, a compose line may be mistyped
and corrected a minute later. Only a person can tell those from a volume that
is never coming back.
Markdown opens in the editor, when asked
A per-user setting, off by default, for opening .md files straight in the
text editor rather than in the preview. It is the first row of what should
become a table of per-user rules for which application opens which extension.
Also
- A full review of the codebase
— ten lots, some 67,000 lines — is published, defects and fragilities named
and located. Four of its five defects are fixed in this release; the fifth
needs a decision rather than a correction and is written up with its options. - The default upload path, which had no test of any kind, has three.
- User preferences are declared in one table instead of being spelled out in
three places, which is why a saved Markdown preference used to revert. - Every push to the integration branch publishes
testandtest-leanimages,
so a change can be tried before it reacheslatest.
Images
ghcr.io/cerede2000/explorer:3.1.0
ghcr.io/cerede2000/explorer:3.1.0-lean
cerede2000/explorer:3.1.0
cerede2000/explorer:3.1.0-lean
latest and latest-lean follow main and now carry this release.
Full changelog: v3.0.2...v3.1.0
v3.0.2
Chunked uploads no longer take the server down
Where UPLOAD_CHUNKED_ENABLED was on, every upload by a signed-in user killed the process. The browser reported a lost connection and a 502; the container restarted; and on a deployment whose storage is not persistent, it came back with an empty database — favourites, shares and preferences gone with it.
The upload server finishes its responses with res.end(callback), a form Node accepts. express-session replaces res.end with a two-argument version that reads that callback as a body and passes it to res.write(), which throws where nothing catches it. Because the session store implements touch, an established session took that path on every request — so the failure was systematic rather than occasional.
A poll that never stopped
The client watched for document-editing activity on every navigation, whether or not a document server was configured. Where ONLYOFFICE is not set up the endpoint does not exist, so each poll returned 404, was retried a second later, and was logged server-side with a full stack trace — for as long as a tab stayed open. It now waits for the feature flags and starts only where there is something to watch.
A demo you can walk into
With DEMO_MODE enabled and demo credentials configured, the sign-in form arrives filled in and there is only the button to press. Serving a password to whoever loads a page is right for a demo and wrong everywhere else, so it takes demo mode and both halves of a credential named for the purpose; the variables are separate from the admin bootstrap ones, so nothing set for another reason can publish a password.
DEMO_SAMPLES=false keeps demo mode without the 81 MiB sample archive, worth having where storage is not persistent and the download would repeat at every restart.
Images follow main
latest and latest-lean were built only when a release was cut, so a fix waited for one. Every push to main now builds and publishes both variants to GHCR and Docker Hub, each also tagged with the version in package.json — 3.0.2 and 3.0.2-lean. A release additionally refuses to publish if the tag it was cut from disagrees with the manifests.
Also
- The HTTP API is documented, with examples run against the live demo: signing in, browsing, resumable uploads, sharing and deletion. It states the gap plainly — authentication is by session cookie only, so per-user API tokens are on the list.
- The public demo has sharing enabled, folder sizes in full mode with the usage bar, and a
PUBLIC_URLthat is actually parsed.
Images
ghcr.io/cerede2000/explorer:3.0.2
ghcr.io/cerede2000/explorer:3.0.2-lean
Also on Docker Hub under the same tags. Try it on the live demo — demo@example.com / demo1234.
v3.0.1
Cleanup release, on top of v3.0.0.
What a folder leaves behind
Rows that pointed at a path did not follow it. A favorite outlived the folder it named, a share kept pointing at a path that no longer existed, and a folder's sort order was inherited by whatever folder was created there next. Deleting cleaned up the favorites of whoever pressed delete and nobody else's — which made it a bug rather than an omission, since those are other people's rows.
Favorites, shares, recent destinations and per-folder preferences now follow a folder when it is renamed or moved, and are forgotten when it is deleted, for every user who had them. Everything inside the folder comes along too. A copy leaves the original's bindings where they are.
Per-folder preferences without a ceiling
Sorting and view mode were kept as one JSON document per user, rewritten whole on every change and shipped entire on every load. It had to be capped, so the hundred-and-first folder silently forgot the oldest — and a document cannot be cleaned up when a folder disappears.
They are rows now: no cap, no silent forgetting, and they can be maintained. Existing preferences are carried over on first start.
Also
- The view a folder opens in is remembered per folder and per user, with a default in Settings → User preferences (nxzai#360).
- Per-folder sorting, contributed by @jimaek (nxzai#356), reworked after review to store per user.
npm run version:setsets the version in every manifest at once, and CI fails if they disagree.
docker pull ghcr.io/cerede2000/explorer:3.0.1
docker pull ghcr.io/cerede2000/explorer:3.0.1-lean
v3.0.0
First release of this fork.
NextExplorer was created by Vikram Soni and developed at nxzai/NextExplorer. That repository has had no commits since 13 July 2026, and its documentation and demo sites are offline. This release gathers the work that accumulated since, and the version goes to 3.0.0 to make clear that this is a different line of development rather than an upstream release.
Documentation: https://cerede2000.github.io/NextExplorer/
Images
Published to GHCR and Docker Hub, for linux/amd64 and linux/arm64:
ghcr.io/cerede2000/explorer:3.0.0 # full: hardware video acceleration + RAW photos
ghcr.io/cerede2000/explorer:3.0.0-lean # same application, considerably smaller, without either
latest and latest-lean follow this release.
Uploads
- Chunked, resumable uploads (TUS), with automatic fallback when a reverse proxy refuses a large body, and a per-origin chunk size that is learned rather than guessed.
- Transfer rate shown while anything is moving, measured over a trailing window so it reflects the speed you actually have.
- The final server-side copy reported separately, instead of a progress bar that appeared frozen at 100% for its duration.
- Empty files, and folders containing them, upload correctly.
Files
- Move to and Copy to from the context menu, offering recent destinations and favorites before any browsing — the only route to a transfer on a touch device, where dragging is unavailable.
- Native, cancellable copy and move with real progress, and the view repositions on what was just transferred.
- Copy by drag-and-drop onto folders and favorites, and per-folder sorting that is remembered.
- Recursive folder sizes and volume usage indicators.
- Large directories and bulk deletion handled without stalling the interface.
- Keyboard navigation through folders.
Media
- One gallery for pictures and videos, navigable by swipe, arrow keys or on-screen arrows.
- Pinch, double-tap and ctrl-wheel zoom, where dragging pans a zoomed picture and turns the page otherwise.
- Portrait videos can be closed again on Android.
Documents
- ONLYOFFICE: reliable background autosave, shared editing sessions, the editor's own close button, rename and save-as, mentions, comparison, insertion from your own storage, and the app's theme.
- New blank Word, Excel and PowerPoint documents from a drawer beside New file.
- Secure shared text editor with granular write controls.
Access and security
- Per-user access control, personal folders and user volumes.
- OIDC sessions persisted in SQLite, and the configured origin preserved across authentication.
- Every credential can be read from a file via a
_FILEvariable, keeping secrets out ofdocker inspect. - Public share activity tracking, clean direct share URLs, and hidden-file visibility honoured by download artifacts.
Under the hood
- Container dependencies refreshed.
- Thumbnail queue and cache stabilised, with prefetching only while idle.
- Translations completed across all 13 locales.
- Test suites repaired and considerably extended: 301 backend, 179 frontend.
Thanks
@jimaek for per-folder sorting and the media gallery with swiping.
Licensed GPL-3.0, as the original.
cerede-2026.07.11 upstream main
Fork pre-release built from nxzai/NextExplorer origin/main at 4751560.
This is not an official upstream release; it is a fork build for Docker Compose testing while upstream changes wait for an official release.
Included since the latest visible upstream release v2.2.4:
- Korean locale from nxzai#306
- terminal opens in the current folder from nxzai#309
- open shell/script files in terminal from nxzai#310
- per-user sidebar visibility preferences from nxzai#311
- non-selectable static UI text from nxzai#312
- safe cleanup for aborted uploads on NFS/FUSE from nxzai#317
Docker image:
ghcr.io/cerede2000/explorer:cerede-2026.07.11-upstream-main
Build run: https://github.com/cerede2000/NextExplorer/actions/runs/29145055333
TUS chunked uploads test
Test package for optional TUS chunked uploads with Uppy.