Repository navigation
Document Backup Restore
How to put documents back from the Azure archive backup, and how to run the restore drill. The account, access rules and backup run are in Document-Backup.
Measured 2026-10-06, test only. The revert tool has not written to any bucket. Its read-only steps
(plan, status, and the dry runs of rehydrate and apply) ran against the test account; the
writes are covered by unit tests with fake stores only. The drill passed on test: 20 of 20 copies
matched. Any command marked "not yet run as written" has not been run on a real environment.
The object store honours the Content-MD5 header the tool relies on: a wrong value returns 400
InvalidDigest, tested against the test bucket on 2026-10-05. The tool does not send If-Match,
because the NRS store answers 412 to it on objects stored years ago, even when the ETag matches.
Each write is verified afterwards by reading the object back.
The revert tool, src/scripts/restore-originals.js, copies the original from the archive back over
the bucket object and marks each document's pdfTitle record restored. It has four modes, and
only the last one writes. Each mode is a dry run unless given --live.
Before starting:
- Stop the PDF title worker. On test, disable the function
pdf_title_runondemi-pdf-title-test(or setPDF_TITLE_LIVEoff), then wait for the running tick to finish; a tick takes about 3 minutes. Prod has no worker yet. Re-enable the worker after the restore. The tool re-reads each document's lease before it writes and refuses a leased row. - Blob storage and Cosmos need different identities, and no one identity holds both. Cosmos
clients sign in as
AZURE_CLIENT_ID. Blob storage clients sign in asBACKUP_CLIENT_IDwhen it is set, and asAZURE_CLIENT_IDwhen it is not.restore-originals.jsshares this withbackup-originals.js(defaultContainers). Tested on test 2026-10-06 with one identity: asdemi-identity-<env>(the devbox default),planworks butstatusfails onrestore, where that identity has no role; as the writer,planstops with a Cosmos RBAC error. With Cosmos asdemi-identity-<env>and blob storage as the writer in one process,plan,statusand therehydrateandapplydry runs all worked. - Run shape on test:
AZURE_CLIENT_IDis the client id ofdemi-identity-test,BACKUP_CLIENT_IDis the client id ofeagle-backup-writer-test, during agrantWriter=truewindow, from a VM that has both identities attached. Liverehydrateandapplyhave not been run with this shape. Take the object-store settings from the backup env file (see "Run a backup" in Document-Backup). - The tool needs
STORAGE_BACKEND=minioand an emptyMINIO_KEY_PREFIX, because blob names are the storeds3Key. It refuses to run otherwise.
Choose the documents with exactly one of --id <docId>, --ids <file> (one id per line),
--project <projectId> or --all-changed (every document, keeping rows that need work). A row is
one bucket key with the selected documents that store it. The examples use --ids ids.txt. Every
mode also takes --max N, --concurrency N (default 8), --container and --restore-container.
-
Download the manifest of the verify run and run
plan. The manifest path inmanifestsis<env>/<runId>/objects.jsonl.gz. The tool logs the manifest's sha256; compare it with the manifest line insummary.json.plan --idran on test 2026-10-06 asdemi-identity-test(decisionunchanged, exit 0, logged sha256 equal tosummary.json). The manifest was fetched with the storage SDK; theaz storage blob downloadline and--all-changedare not yet run as written:az storage blob download --auth-mode login --account-name <account> -c manifests -n <env>/<runId>/objects.jsonl.gz -f objects.jsonl.gz node src/scripts/restore-originals.js plan --all-changed --manifest objects.jsonl.gz --out ids.txtplanis read-only. It logs one line per row with a decision.--outsaves the ids of the rows that need work, so later steps act on the same set.Decision Meaning restoreThe bucket object differs from the backup, by size, MD5 or ETag record-onlyThe object already matches but a record still says titled. A rerun finishes a run that stopped after the write unchangedNothing to do refusedThe row is not touched. Reasons include no-backup,backup-not-verified:<status>,missing-in-bucket, a lease or in-flight title, andrecord-original-differs -
Rehydrate. Archived blobs cannot be read. This copies each
restorerow into therestorecontainer at Cool tier, standard priority. Run it without--livefirst, then with it. The dry run worked on test 2026-10-06 (outcomeplanned) with the split identities above;--liveis not yet run as written:node src/scripts/restore-originals.js rehydrate --ids ids.txt --manifest objects.jsonl.gz --account <account> --liveRerunning is safe: pending and ready copies are skipped and failed ones start again.
-
Wait up to 15 hours, then check.
statuscounts rows asabsent,pending,readyandfailed. It exits 2 while any row is not ready. On test 2026-10-06 it counted a rowabsentand exited 2 with the split identities; asdemi-identity-testalone it failed the row. Not yet run as written withBACKUP_CLIENT_IDset:node src/scripts/restore-originals.js status --ids ids.txt --manifest objects.jsonl.gz --account <account> -
Apply. The dry run prints
would write N rows; rerun with --live --confirm N. Run it, then repeat with--live --confirm N, using that N. A missing or different count writes nothing and exits 1. The dry run on test 2026-10-06 held a not-yet-rehydrated row aspendingand exited 2.--liveis not yet run as written:node src/scripts/restore-originals.js apply --ids ids.txt --manifest objects.jsonl.gz --account <account> node src/scripts/restore-originals.js apply --ids ids.txt --manifest objects.jsonl.gz --account <account> --live --confirm <N>Rows are written one at a time. For each row the tool:
- Downloads the rehydrated blob and checks its size, MD5 and sha256 against the manifest.
- Re-reads the documents and skips the row if one has been leased since the plan.
- Reads the object's ETag just before the write. If it differs from the ETag seen at plan
time, the row is reported as
changedand skipped. Otherwise it sends a signed PUT with aContent-MD5header and noIf-Match, because the store rejectsIf-Matchon older objects. - Reads the whole object back and checks size, MD5 and sha256. A mismatch is
failed: read-back-mismatchand the record is left alone. - Updates each stale record:
pdfTitle.statusisrestored,reasonisarchive-restore,title,titledLengthandtitledSha256are null, andatis set. The original fields are kept.
-
Ask the owner to remove the grants from the identity.
Exit codes: 0 clean; 1 any row refused, changed or failed, or an id not found; 2 rows still
rehydrating. Each row logs [restore] <mode> key=<key> docs=<ids> outcome=<o> reason=<r>, then a
summary line.
Nothing is deleted. The tool never deletes from the bucket or the archive. Both buckets have
versioning enabled and expire older versions after 60 days (bucket lifecycle setting, read
2026-10-05), so a replaced file stays recoverable from the bucket for 60 days. The rehydrated copies
in restore are removed by the lifecycle rule 30 days after creation.
- Rehydration takes up to 15 hours at standard priority. The tool does not offer high priority; its price was not checked.
- It refuses a row whose object is missing from the bucket, and a row whose record has an in-flight title.
- The
restoredstatus offers the file for titling again if the document is eligible. - Blob storage uses
BACKUP_CLIENT_IDand Cosmos usesAZURE_CLIENT_ID; see "Before starting".
Cost: about $0.001 for a 3 MB file. A full restore of 1 TB is about $22.50 for the read-back and about $82 in operations, plus Cool storage while the copies are held. Egress back to the object store is not priced.
The drill proves that archived content comes back intact. It copies a random sample of backup blobs
into restore, waits for rehydration, compares each file's sha256 with its metadata, and deletes
the copies. Use 20 blobs on test and 10 on prod. It runs as the writer identity (grant steps are in
Document-Backup), in two phases up to 15 hours apart, so the grant must stay on until the check
has run: the check reads and deletes in restore.
-
Start. Writes the list to
--drill-list(live only) and starts the copies. This is the command the test run used:node src/scripts/backup-originals.js drill --account $ACCT --drill-list /home/demi/backup-run/drill.jsonl --sample 20 --live -
Check, after rehydration ends. With
--liveit deletes each copy that matches. Exit 0 means all matched, 2 means some are still rehydrating (rerun later), 1 means a mismatch or a failed copy. Passed on test 2026-10-06, about 22 hours after the start: 20 matched, 0 mismatch, 0 pending, 0 failed, and the 20 copies were deleted. The first attempt, a minute after the grant was deployed, failed all 20 because the role had not reached the storage service yet; a rerun two minutes later passed:node src/scripts/backup-originals.js drill --account $ACCT --drill-list /home/demi/backup-run/drill.jsonl --check --live
Copies left in restore are deleted by the lifecycle rule 30 days after creation.
- Environment Reality & Gotchas
- Architecture
- Seeding — filling Cosmos from Track and Eagle
- Sources and Status — every source, one state
- Search & Retrieval — overview
- Search Index Reference
- Search Query Construction
- Search Measurements
- Extraction Pipeline
- Extraction Quality — coverage, word-joining, tiling
- Identity Model — which id means what
- Attribute-Level Access — field-visibility levels, catalog
- Sync Architecture — how records get in
- Eagle Mirror Reference — mirrored kinds, refusals, drift classes
- ENGAGE Rows Reference — ENGAGE-owned comment periods, reconcile
- Public-Read Backfill — filling and repairing the mirror containers
-
Short Links —
/s/{code}redirect and/api/links -
Recent Uploads —
/api/documents/recent-uploads, eagle-public homepage panel - Reports — comment export and BCGW CSV reads
-
User Data — saved map areas, preferences,
/meroutes -
Bulk Download —
/api/bulk-downloads, zip jobs - Bulk Download: Operations — enable, poison-queue runbook
- Document Intake — upload link, malware scan, release
- Connecting an Application to DEMI
- Azure Environments
- Document Backup — archive copy of originals, run and check
- Document Backup: Revert and Drill — put documents back from the archive
- PDF Titles — public name written into stored PDFs, run, restore
- Secret Sync — Key Vault, secret-sync Function app, required names
-
Operating DEMI in Azure — deploy, verify
- Operating the DEMI Data Plane — devbox access, probes, audit and cost reads
- Edge ban detector — hourly timer, modes, kill switch, soak and promote
-
CI Workflows — every GitHub Actions workflow, trigger, identity, roles
- CI Deploy Workflows — eagle-demi staging and production deploys
- CI Admin Frontend Workflows — eagle-demi-admin checks and deploys
- Release Process — cutting, tagging, shipping and rolling back a version
- Runbook: Search Outage — index/app schema drift, diagnose, fix, roll back
- BC Gov Landing Zone
- Business Rules
- EPIC.search Business Rules — where DEMI differs from EPIC.search
- 001: Typesense for DFL Search — superseded
- 002: Async Processing (MongoDB Queue) — superseded
- 003: Rule-Based Classification
- 004: Read ACL Authorization Model
- 005: Cosmos Full-Text Search Ruled Out
- 006: AI Summarization over BM25
- 007: Service-to-Service Credentials
- 008: Audit and Usage Analytics Store
- 009: Track Feed — proposed
- 010: Attribute-Level Access Control
- 011: Public Read Surface
- 012: Public Config Surface
- 013: Stored Project Summaries — proposed
- 014: Sync-out to Eagle
- 015: Malware scanning on intake