feat(6849): the command-line restore uses the same rollback, and the docs say how restore works now
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 31s
CI & Build / Python tests (push) Successful in 1m39s
CI & Build / integration (push) Successful in 1m49s
CI & Build / Build & push image (push) Successful in 31s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 31s
CI & Build / Python tests (push) Successful in 1m39s
CI & Build / integration (push) Successful in 1m49s
CI & Build / Build & push image (push) Successful in 31s
python -m grimoire.restore <file> prints what the restore would change (the same preview as Settings) and stops. With --yes it runs backup_job.rollback_to_backup: the current state is saved first, then the whole instance goes back to the backup. An empty instance takes any full backup. A live one only takes a backup of its own, and otherwise exits 3 with the reason. A v1 backup keeps the copy-in path into an empty instance. docs/backups.md Restoring is rewritten: - what a restore puts back and removes; - the same-instance rule; - the safety backup; - what survives; - the web and command-line ways in. Its S3 key advice now asks for read as well as write when restoring from the bucket. README, features, architecture and the API reference follow. Milestone 582, step 5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -272,7 +272,9 @@ These serve the plugin's hooks.
|
||||
| Method | Path | Description |
|
||||
|--------|------|-------------|
|
||||
| GET | `/api/admin/backup` | Export backup (format v4; `?scope=user` for own data) |
|
||||
| POST | `/api/admin/restore` | Restore from a backup |
|
||||
| GET | `/api/admin/restore/sources` | The backups at the backup destination, newest first |
|
||||
| POST | `/api/admin/restore/preview` | What restoring a backup (`?source=<name>` or an uploaded file) would change; 409 with the reason when it can't apply here |
|
||||
| POST | `/api/admin/restore?confirm=rollback` | Put the whole instance back to that backup, after saving the current state |
|
||||
| GET / DELETE | `/api/admin/users` · `/api/admin/users/:id` | List / delete users |
|
||||
| GET / PUT | `/api/admin/registration` | Get / toggle registration |
|
||||
| GET / PUT | `/api/admin/smtp` · POST `/api/admin/smtp/test` | SMTP config + test email |
|
||||
|
||||
@@ -87,7 +87,7 @@ Credentials Grimoire stores for outside services (forge tokens, the backup desti
|
||||
│ ├── routes/ REST blueprints
|
||||
│ ├── mcp/ MCP server and its tool modules
|
||||
│ ├── services/ shared logic
|
||||
│ └── restore.py `python -m grimoire.restore` for a fresh instance
|
||||
│ └── restore.py `python -m grimoire.restore`: the restore, from the command line
|
||||
├── frontend/src/ views, components, stores, composables, api client
|
||||
├── plugin/ Claude Code hooks, skills and commands
|
||||
├── scripts/ CI checks and the plugin version minter
|
||||
|
||||
+52
-21
@@ -88,9 +88,12 @@ You need four things:
|
||||
- **Access key.**
|
||||
- **Secret key.**
|
||||
|
||||
Grimoire uploads and lists; it never deletes. A key that may **only add
|
||||
objects** is therefore enough, and it is the safer key to hand out: a stolen
|
||||
one cannot wipe the history.
|
||||
Grimoire uploads and lists, and reads a backup back when you restore from the
|
||||
bucket; it never deletes. A key that may **add and read objects but not
|
||||
delete them** is therefore enough, and it is the safer key to hand out: a
|
||||
stolen one cannot wipe the history. A key that may only add objects still
|
||||
takes backups; restore then works by uploading a file or from the command
|
||||
line.
|
||||
|
||||
The secret is stored encrypted, under a key derived from `SECRET_KEY`. It is
|
||||
never shown again, and never included in a backup. If an instance's
|
||||
@@ -122,8 +125,10 @@ keeps service backups as "backup targets". Either destination works with it.
|
||||
1. On Archives' **Backups** page, make a target for Grimoire, named `grimoire`
|
||||
for example.
|
||||
2. Choose how Grimoire's files arrive:
|
||||
- **S3**: turn on the S3 way in for the target and make an **append-only**
|
||||
S3 key. In Grimoire, choose **An S3 bucket**:
|
||||
- **S3**: turn on the S3 way in for the target and make an S3 key that
|
||||
may add and read objects but not delete them (an **append-only** key
|
||||
takes backups too, but then Grimoire can't restore from the bucket). In
|
||||
Grimoire, choose **An S3 bucket**:
|
||||
- address: Archives' backup address (`http://<archives>:8081`);
|
||||
- bucket: the target's name;
|
||||
- access key and secret key: the key you just made.
|
||||
@@ -139,26 +144,52 @@ keeps service backups as "backup targets". Either destination works with it.
|
||||
|
||||
## Restoring
|
||||
|
||||
### Into a fresh instance
|
||||
A restore puts the **whole instance** back to the moment a backup was taken,
|
||||
for every user:
|
||||
|
||||
Start Grimoire on an empty database, copy the backup file into the container,
|
||||
and run:
|
||||
- records edited since go back to how they were;
|
||||
- records deleted since come back;
|
||||
- anything made since is removed.
|
||||
|
||||
It matches records by their ids, so a live instance only restores a backup **of
|
||||
its own**. Each instance has an id, and every backup records the one it came
|
||||
from. A backup from another instance, or one made before backups recorded it,
|
||||
only goes into a new, empty instance, which takes it as it is.
|
||||
|
||||
Before changing anything, the restore saves the current state as one more
|
||||
backup. It goes to the backup destination when scheduled backups are on, and
|
||||
otherwise to `/data/pre-restore`. The result says which file it is, so a
|
||||
restore can itself be undone.
|
||||
|
||||
What a backup doesn't carry stays as it is wherever its owner survives the
|
||||
restore: API keys, forge connections, sharing, logs, and the destination's S3
|
||||
secret. The trash isn't in backups, so anything in the trash when the backup
|
||||
was taken is gone afterwards. Everyone is signed out, and search is rebuilt.
|
||||
|
||||
### From the web
|
||||
|
||||
**Settings → Data → Restore from Backup** lists the backups at the backup
|
||||
destination. You can pick one of those, or upload a `.tar` or the JSON
|
||||
**Full Backup** download (uploads are held to the server's request-size
|
||||
limit). The restore then shows what would change: when the backup was taken,
|
||||
and how many accounts, projects, plans, notes and rules there are now and will
|
||||
be afterwards. Type `roll back` to run it. Then sign in again.
|
||||
|
||||
To read backups back from an S3 bucket, the key needs read access as well as
|
||||
write. Grimoire never deletes from the bucket.
|
||||
|
||||
### From the command line
|
||||
|
||||
For a file with no size limit, or a new instance with no backup destination
|
||||
yet, copy the file into the container and run:
|
||||
|
||||
```sh
|
||||
docker exec -it grimoire python -m grimoire.restore /backups/grimoire-20261015T020000Z.tar
|
||||
```
|
||||
|
||||
- The command refuses a database that already has an account. A restore
|
||||
re-creates everything with new ids, so on a live instance it would add a
|
||||
second copy instead of replacing anything.
|
||||
- Nothing limits the file's size.
|
||||
- It also reads the JSON from the admin **Full Backup** download.
|
||||
It prints what would change and stops. Add `--yes` to restore. It reads the
|
||||
JSON download too. Afterwards, **restart Grimoire** so the search indexes are
|
||||
rebuilt.
|
||||
|
||||
Afterwards, **restart Grimoire** so the search indexes are rebuilt. Then issue
|
||||
new API keys, add forge connections again, and set up any sharing.
|
||||
|
||||
### From the web
|
||||
|
||||
**Settings → Data → Restore from Backup** takes the `.tar` as well as the
|
||||
JSON download. The upload is held to the server's request-size limit. For a
|
||||
bigger file, use the command above.
|
||||
After restoring into a new instance, issue new API keys, add forge connections
|
||||
again, and set up any sharing: none of them travel in a backup.
|
||||
|
||||
+2
-2
@@ -85,8 +85,8 @@ Share a project, note or task with users or groups, as viewer, editor or admin.
|
||||
## Export and backup
|
||||
|
||||
- **Personal export**: your notes and tasks as a Markdown ZIP (with YAML frontmatter) or JSON.
|
||||
- **Admin backup and restore**, with ids remapped so a backup restores onto another instance.
|
||||
- **Scheduled backups**: a daily, dated, uncompressed tar to a folder or an S3-compatible bucket, laid out so a deduplicating tool such as restic stores only what changed. `python -m grimoire.restore` brings a fresh instance back from one. See [Backups](backups.md).
|
||||
- **Admin backup and restore**: a restore rolls the whole instance back to one of its own backups, after saving the current state, or brings a new instance back from any backup.
|
||||
- **Scheduled backups**: a daily, dated, uncompressed tar to a folder or an S3-compatible bucket, laid out so a deduplicating tool such as restic stores only what changed. Restoring is in Settings, or `python -m grimoire.restore`. See [Backups](backups.md).
|
||||
|
||||
## Sign-in
|
||||
|
||||
|
||||
Reference in New Issue
Block a user