Skip to content

[21/33] Office saves: keep what they replace, and never empty the document - #410

Merged
vikramsoni2 merged 3 commits into
nxzai:mainfrom
cerede2000:upstream-21-office-saves
Sep 26, 2026
Merged

vikramsoni2 merged 3 commits into
nxzai:mainfrom
cerede2000:upstream-21-office-saves

Conversation

@cerede2000

Copy link
Copy Markdown

Batch 21 of the plan in #373: the office editors' saves. Branched from main at 7b07337 — the stack is gone now that 10 through 20 are in, so this one stands on its own.

The versions engine landed in #406 inert, by design: a version is born when something saves over a file, and nothing in the application did. This is the first batch that does. It also fixes the way a save through ONLYOFFICE was written, which could destroy a document outright.

What it does

  • A save through ONLYOFFICE keeps what it replaced, and never empties the document. The Document Server does not send the document back; it sends a URL to fetch it from. The file was emptied first and written as the answer arrived, so a stream that stopped — a dropped network, a Document Server restarting mid-answer — left an empty file where the work had been. The document is now pulled into a file of its own beside it and handed to versions.saveFile, which keeps the previous content and puts the new one in place only once it is whole.
  • A save through Collabora keeps what it replaced. Nothing could be lost here (Collabora sends the document itself, and the write already went through a temporary), but the state each save replaced was dropped: a document edited all afternoon had no history at all. The WOPI PutFile goes through the same engine.
  • Saves that were asked for are told apart from the editors' own timers. Both editors save by themselves every few minutes. Those belong to the editing session rather than standing as states of their own — the ONLYOFFICE document key and the WOPI lock are exactly that session, shared by everyone co-editing — while the editor's Save, and the last save once everybody has left, are marked as deliberate and kept. This is what the engine's session checkpoint and its retention tiers were built for.
  • Who saved is recorded. The account whose changes the callback carries, or the one the editing session was opened for; somebody who came through a share link is credited as the link, since there is no account to name.
  • A saved document is fetched only from the Document Server it was sent to. The callback is answered without a session — it is how a separate service reaches us — so anybody able to call it could name any address the server can reach, including one inside the container's network, and have the answer written into a file in a volume. ONLYOFFICE_DOWNLOAD_ORIGINS declares the extra hosts a Document Server reports itself under behind a proxy; the configured address is always allowed.

Notes for review

  • No schema change, no new dependency, no frontend change: the editors already save this way, and what changed is what the server does with the save.
  • The history is kept but not yet readable: the versions API and the panel that shows it come with the editor batch, as the trash UI followed the trash engine. What this batch settles is that the histories exist and are correct from now on.
  • Still to come for these editors, deliberately left out so this stays one subject: the document key store (so a reopened document is not served from the Document Server's cache), editing sessions and presence, Save as, rename from the title bar, mentions in comments, and the co-editing settings.
  • parseOriginList in the config now takes the variable name it is parsing, so its warning names the right one.

How it was checked

On main at 7b07337, Node 24 as the CI and the image use:

  • the frontend bundles and every backend module loads (the CI's own two steps);
  • the backend suite passes — 1880 tests, and the only two failures are the two that already fail on main untouched (auth.test.js "current password is incorrect", browse-hidden-files.test.js), which I re-ran on main to be sure;
  • 15 new tests across the two routes. Each claim fails when the change behind it is put back: five of them on the old ONLYOFFICE write (the document is emptied by a download that never finishes, and nothing is kept), five on Collabora's plain rename, and the origin check is proved with a server that answers perfectly well, so its refusal cannot be mistaken for a network failure;
  • the Docker image builds, and against it a scripted editing session: a save from a stand-in Document Server replaces the document and leaves one version holding what it replaced; a save whose download dies halfway leaves the document exactly as it was and keeps nothing; a callback naming another server — one that answers — fetches nothing.

Benjy added 3 commits September 25, 2026 09:24
…ument

The Document Server does not send the document back: it sends a URL to fetch
it from. The file was emptied first and then written as the answer arrived, so
a stream that stopped arriving — a dropped network, a Document Server
restarting mid-answer — left an empty file where the work had been. The state
the save replaced was gone in every case anyway, since nothing kept it.

Both are the same change. The document is pulled into a file of its own beside
it and handed to the versions, which keep what it replaces and put the new
content in place only once it is whole. A save nobody could finish therefore
leaves the document exactly as it was, and the history the engine has been
keeping since it arrived is finally written by somebody: the versions have been
inert until now, because nothing in the application saved over a file.

A save on purpose — the editor's own Save, or the last one made once everybody
has left — is marked as such, so the automatic saves in between do not each
stand as a state of their own; everyone editing together shares the document
key, which is the session those saves belong to. The person credited is the one
whose changes the callback carries, or the account the editing session was
opened for when it carries none; somebody who came through a share link is
credited as the link, since there is no account to name.

Seven tests against a Document Server that hands the document over, refuses to,
or dies halfway through. Five of them fail on the previous write: the document
is emptied by a download that never finishes, and nothing is kept.
The callback says where the saved document is, and the server fetched whatever
it was told to. The callback is answered without a session — it is how a
separate service reaches us — so anybody able to call it could name any address
the server can reach: one on the machine itself, one inside the container's
network, a metadata service. The answer was then written into a file in the
volume, where it could be read back.

The URL now has to come from the Document Server the document was handed to.
`ONLYOFFICE_DOWNLOAD_ORIGINS` declares the others it may report itself under,
which happens behind a proxy or inside a container network; the configured
address is always allowed, and a refusal says in the log which origin was
turned away and which are allowed, since that is the only way to tell this
apart from a Document Server that cannot be reached.

The test answers perfectly well from a server that is not the Document Server,
so a refusal cannot be mistaken for a network failure: without the check the
document is replaced by what that server handed over.
Collabora hands the document over itself, so nothing here could lose a file the
way an unfinished download could. What was missing is the other half: the state
each save replaced was dropped, so a document edited all afternoon in Collabora
had no history at all, while the same document saved from anywhere else now
does.

The save goes through the versions. Who saved it is the account the editing
session was opened for, and the editor is recorded beside it, so a history
shows where each state came from.

Collabora saves on its own every few minutes. Those saves belong to the session
rather than standing as states of their own, and the session is the lock every
co-editor of the document holds — one per open document, which is exactly what
a session is. A save somebody asked for, or the one made on closing, is marked
as such and is kept even when the timer saves over it a minute later.

Five tests, all failing on the plain rename: what a save replaced is gone, and
nothing says who saved or how.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants