feat(backup): review a restore, run it as an Activity job, verify backups
- POST /restore/preview stages an upload in the library's folder and answers what it holds beside the live library, writing nothing live; DELETE discards it; at most two stages are held (429 too-large). - POST /restore/apply rechecks the live fingerprint (409 plan-changed) and starts "Database restore": exclusive, not cancellable, indeterminate. It waits for request leases, publishes the exact staged bytes, swaps the database, prunes safety backups and logs counts, repairs and the backup. - The legacy /restore runs the same stage and publish as an exclusive hold with its old response, so a restore and the health check, Move library and every other job refuse each other. - JobRegistry.lease: Needs review decisions and maintenance and restore previews are refused while an exclusive job runs, and a restore waits for them before it switches the file. - GET /backup holds a guarded slot, verifies a standalone snapshot before streaming it and removes the closed temp file; GET /backup/status lists the safety backups on disk and this session's verified download.
This commit is contained in:
@@ -2057,7 +2057,8 @@ class TestBackupRestoreContract(AdminAPITestCase):
|
||||
resp = await self._restore("database_file", upload)
|
||||
self.assertEqual(resp.status, 200)
|
||||
self.assertIn("verify_database_file", calls)
|
||||
self.assertIn("restore_from_file", calls)
|
||||
self.assertIn("stage_restore", calls)
|
||||
self.assertIn("publish_staged", calls)
|
||||
|
||||
async def test_restore_keeps_at_most_ten_safety_backups(self):
|
||||
import time
|
||||
@@ -2098,12 +2099,12 @@ class TestBackupRestoreContract(AdminAPITestCase):
|
||||
self.api.db.save_prompt(text="restored", prompt_hash="h1")
|
||||
upload = self._snapshot_upload()
|
||||
|
||||
def boom(src_path):
|
||||
def boom(staged):
|
||||
raise PermissionError(
|
||||
13, "Permission denied", os.path.join(self.tmpdir, "prompts.db")
|
||||
)
|
||||
|
||||
self.api.db.model.restore_from_file = boom
|
||||
self.api.db.model.publish_staged = boom
|
||||
resp = await self._restore("database_file", upload)
|
||||
body = await resp.text()
|
||||
self.assertEqual(resp.status, 500)
|
||||
|
||||
Reference in New Issue
Block a user