M4.37: Data Recovery keeps copies of modified documents and offers them after a crash

- Every Preferences > File Handling > Data Recovery interval, modified documents
  changed since their last copy are written to the recovery store in the
  background (the same snapshot and encoder as Background Save; the status bar
  shows it). Complex documents (over 20,000 objects) are skipped while Turn off
  Data Recovery for complex documents is on.
- A copy goes when its document is saved, reverted, closed or undone back to its
  saved state, and when the app quits with nothing unsaved.
- Each running app keeps its copies in an area of its own: a sub-folder whose
  .lock file it keeps locked for its lifetime (File::try_lock), or on the web a
  heartbeat refreshed every minute. Only areas nobody holds (lock free, or no
  heartbeat for three intervals, at least three minutes) are offered, and an
  area is held while it is restored or discarded, so a second running instance
  never offers or deletes a live instance's copies. Empty leftover areas are
  tidied away.
- The next launch after a crash asks with the Recover Documents dialog (Restore /
  Discard / Cancel): restored documents open as "<name> [Recovered]", unsaved, and
  Save asks where to save them, suggesting their original file; the copy moves
  into the new app's area.
- Store: the recoveryFolder preference, else Data Recovery beside the desktop
  app's preferences; browser storage on the web (Services::recovery_store);
  tests use an in-memory store (with in-memory locks, or heartbeats).
- Engine: file.recovery.save, file.recovery.list, file.recovery.restore {file?},
  file.recovery.discard {file?}; DocState.recovered and DocState.recovery.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-05 15:24:16 +07:00
co-authored by Claude Opus 5.5
parent d8d1d472cf
commit 975067b44a
21 changed files with 1777 additions and 24 deletions
+7
View File
@@ -339,3 +339,10 @@ EPS Options: Export As… with the EPS format (`file.exportAs {format: "eps"}`,
`cmykPostScript`, `compatibleGradients`, `level` (`2`, `3`), plus `path` and the artboard choice (`useArtboards`,
`range`). `ui.dialog.confirm` checks them (a bad value or an unknown preset keeps the dialog open), remembers them for
the next EPS Options and writes the file(s), asking for a path when there is none.
Data Recovery: at launch, copies left behind by a crash open the `recovery` dialog (never copies another running
instance keeps, so instances started side by side for screenshots don't get it). Fields: `copies` (`[{file, title,
saved}]`, from `file.recovery.list`) and `discard`. `ui.dialog.confirm` restores them all (`file.recovery.restore`
each, opening "<name> [Recovered]" documents), `discard: true` then confirm deletes them, `ui.dialog.cancel` keeps them
for the next launch. While copies are written in the background `ui.inspect` lists them under `background`
("Saving recovery data for <name>").
+26
View File
@@ -1265,3 +1265,29 @@ as they do for art larger than the imageable area and for overprints in composit
{"name":"run_command","arguments":{"command":"print.preview","params":{"settings":{"output":{"mode":"separations"}}}}}
{"name":"run_command","arguments":{"command":"file.print","params":{"path":"/tmp/job.pdf","settings":{"copies":2}}}}
```
## Data Recovery
Modified documents get recovery copies (`file.recovery.save`; the app runs it every `autosaveInterval` minutes while
`autosaveRecovery` is on, skipping documents of more than 20,000 objects while `recoveryOffForComplex` is on). A copy
goes when its document is saved, reverted or closed, so copies still there after the app quits were left by a crash.
The copies live in the `recoveryFolder` preference's folder (default: `Data Recovery` beside the app's preferences;
none when the app runs with `VECTORCRAFT_NO_PREFS`) or, on the web, in browser storage.
Each running app (each browser tab) keeps its copies in an area of its own (`<area>/<name>`): a sub-folder whose
`.lock` file it keeps locked while it runs, or on the web an area with a heartbeat it refreshes every minute. Only
areas nobody holds are offered: their lock is free, or their heartbeat is older than three intervals (at least three
minutes). Several apps running at once (agents' instances included) never see each other's copies as crash leftovers,
and an area being restored or discarded is held, so two apps launched together never both take it.
`file.recovery.list` → `{copies: [{file, title, path, format, saved, open, running}], location}` (`open`: the copy of a
document open here; `running`: kept by another VectorCraft that is running). `file.recovery.restore {file?}` opens
copies left behind (default: all) as `"<name> [Recovered]"`: modified, and Save asks where to save them (suggesting
their original file); the copy moves into this app's area. `file.recovery.discard {file?}` deletes them. Without a
folder or browser storage the commands are disabled and say how to set one.
```json
{"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"recoveryFolder","value":"/tmp/vc-recovery"}}}
{"name":"run_command","arguments":{"command":"file.recovery.save","params":{}}}
{"name":"run_command","arguments":{"command":"file.recovery.restore","params":{"file":"1759650000-1/Poster-1"}}}
```