An archive this server wrote cannot be restored into it #195
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
An archive is written, listed and its manifest read; restoring one is refused.
The refusal is deliberate and is argued in
ENDPOINTS.md— the archives ondisk, the manifest of one, and the writing of one upstream's own restore reads;
nothing here restores, so that write refuses. It is the one route in the group
that answers
500on a well-formed request, and the dashboard's restore buttontherefore fails on a server that made the archive itself.
What exists already, and what it gives the restore for free
The write half is complete and generated, which means the descriptor a restore
needs is already in the tree:
crates/ephyra-db/src/generated/backup.rs—TABLES, one entry per table inthe order upstream writes them, one
Fieldper entity property with itsEncoding. Generated bycargo xtask gen-backupfrom the pin; never edited.crates/ephyra-db/src/backup.rs— turns a row into one JSON element undereach of those descriptors. A restore is the same table walked the other way.
crates/ephyra-http/src/routes/admin/backup.rs— the four routes, includingthe manifest reader a restore would check the archive's version with.
So the missing piece is a decoder per
Encoding, the order the tables arereplayed in, and what the server does around it.
What upstream does
upstream/Jellyfin.Server.Implementations/FullSystemBackup/BackupService.cs:RestoreBackupAsync(line 88) — opens the zip, readsmanifest.json,refuses an archive made by a newer server version, then replays the
tables and the configuration.
ScheduleRestoreAndRestartServer(line 75) — a restore does not happen inthe request. The path is stashed on the application host, a restart is
notified, and the work runs on the way back up.
That second one is the shape question: upstream restores outside a running
server because the tables it is rewriting are the ones the server is reading.
Open
hook, and doing it in-process means every open handle on the database is
wrong afterwards. The honest options are a restart hook of our own or a
documented
restore-then-startmode ofephyra-server.write one from a fixture, restore into an empty state directory, and diff the
resulting database against the fixture it came from. That test needs no
upstream beside it, which makes this one of the few things in the tree whose
proof is not a diff. Worth deciding whether an upstream-written archive is
also replayed, since upstream's own restore reads ours already.
Done in
72b953be, with the review's findings in02bfac62.The replay is
crates/ephyra-db/src/backup.rs— the generatedTABLESdescriptors walked the other way, one
decodeperEncodinginverting thecolumnbeside it, over a streaming reader so a table larger than memoryreplays.
crates/ephyra-server/src/backup.rsdoes the guards, the tables andthen upstream's five tree copies.
Where it runs
The restart loop
main.rsalready had.Server::rundrops the context, closesthe pool and answers
Again::Restoring(path);mainreplays with nothingholding the database and builds a fresh server on the result. That is upstream's
own shape — stash the path, restart, do the work on the way back up — without a
second entry point to document.
An archive upstream wrote replays too: the guards and the decode are written
against what upstream's serializer spells rather than against what ours does,
and a hand-built archive claiming
10.11.0.0with lowercase guids andZstamps is one of the tests.
What the review found
Four of the six were real defects and one was the whole feature being untested.
Again::Restoringappeared inthree places and in no test: deleting the hook turned a restore into a plain
restart with the suite still green. Two tests now assemble a real server, do
what the route does, run it, and assert on what it hands back.
HomeSectionwas destroyed where upstream keeps it. A cascade actionstill fires under
defer_foreign_keys— deferral delays the check, not thecascade — so every restore lost the home-screen sections. Upstream purges
under
foreign_keys = OFF; so does this now, with a per-table key check atthe end to keep what deferral was giving.
deserializer would have left:
default(T)where the column takes no null.Since this port has no migrations, an older archive is the only kind the
version guard admits, and it is exactly the kind that has a column missing.
"nonsense"parsed as[0,0,0,0]andcompared as older; it is refused, as
System.Versionrefuses it.What it is not
There is no durable marker for an interrupted restore, so a crash between the
tables committing and the tree copies finishing leaves a directory that the
next start will serve. Upstream has the same hazard —
RestoreBackupPathis inmemory there too — so this matches rather than improves on it. Worth its own
issue if it should be better than upstream.
One deliberate cost: the closing key check is a scan per foreign key, so a
large library adds seconds to a restore. That buys back what upstream gets free
from EF's insert ordering.