Free unused attachments, and keep versions by count rather than by age
Two rules governed version retention: the newest 50 per note, and a sweep of anything past 90 days. The second was wrong. The count already bounds what a note can accumulate, so age only ever deleted history nothing had replaced — and it bit hardest exactly where history is most wanted, a note left alone for a quarter of a year losing its whole past for having done nothing. It is off unless VERSION_RETENTION_DAYS is set. How many versions to keep is now a notebook's own setting, falling back to the server default when left empty. A reference notebook can keep fifty of everything while a scratch one keeps three. That makes freeing attachments answerable. A picture taken out of a note cannot be deleted straight away: the versions from before still show it, and restoring one would find a hole. It is released once no current note and no kept version refers to it — which, with retention by count, is when the last version holding it is pushed out. The server cannot look for itself, bodies being encrypted, so the client reports a note's attachments on every save as it already does for links, and a version inherits what the note held when it was snapshotted — by definition what that version contains. Two things make it safe rather than merely plausible. Silence is not "uses nothing": a note written before this existed has no rows, which looks identical to one with no pictures, so nothing is released for a user while any of their notes has yet to report — the prefetch that fills the search index reports for them on its first pass. And an upload has a row before the note using it does, so recent attachments are left alone. POST /api/admin/housekeeping runs the sweep on demand. Caught while testing: the same block of markup had been pasted into the create-notebook dialog as well, where the state it referenced did not exist — the dialog crashed on open. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a32d6eba99
commit
3acb8b66a4
12 files changed
+507
-28
No files matched your search
@@ -331,6 +331,7 @@ docker compose -f docker-compose.dev.yml up -d --build # must be running
|
||||
| `history` | 10 | a large deletion is snapshotted, restoring works, restoring is itself undoable |
|
||||
| `drawing` | 7 | strokes survive the poller, and match across four viewport widths |
|
||||
| `pdf` | 41 | import, page proportions, lazy loading, and reading with the network off |
|
||||
| `attachments-lifecycle` | 10 | a picture is kept while any version shows it, and freed once none does |
|
||||
| `conflict` | 10 | a late write wins but the state it buries survives as a version |
|
||||
| `export` | 21 | the downloaded bytes are real Markdown, ZIP and PNG, unpacked and read back |
|
||||
|
||||
@@ -595,6 +596,33 @@ one, never a half-written one that would fail its tag forever after.
|
||||
|
||||
---
|
||||
|
||||
## Freeing attachments
|
||||
|
||||
Take a picture out of a note and the file cannot simply be deleted: the versions
|
||||
from before still show it, and restoring one would find a hole where the image
|
||||
was. So an attachment is released only once **no current note and no kept
|
||||
version** refers to it — which, with retention by count, is the moment the last
|
||||
version holding it is pushed out by newer ones.
|
||||
|
||||
The server cannot look for itself: bodies are encrypted. So the client reports
|
||||
which attachments a note uses on every save, exactly as it already does for note
|
||||
links, and a version inherits whatever the note held at the moment it was
|
||||
snapshotted — which is by definition what that version contains.
|
||||
|
||||
Two things make this safe rather than merely plausible:
|
||||
|
||||
- **Silence is not "uses nothing".** A note written before references existed has
|
||||
no rows, which looks identical to a note with no pictures. Nothing is released
|
||||
for a user while any of their notes has yet to report; the background prefetch
|
||||
that fills the search index reports for them on its first pass.
|
||||
- **An upload in flight has a row before the note that uses it does.** Attachments
|
||||
younger than the grace period are left alone.
|
||||
|
||||
`POST /api/admin/housekeeping` runs the sweep on demand instead of waiting for
|
||||
its timer — admin only, and it accepts `graceSeconds` to shorten that wait.
|
||||
|
||||
---
|
||||
|
||||
## Attachments
|
||||
|
||||
Images used to be inlined into the note body as base64 data URIs — a few
|
||||
@@ -638,13 +666,24 @@ Restoring snapshots the current state first (`before-restore`), so picking the
|
||||
wrong version is not a one-way door. A restored body also rebuilds its entries
|
||||
in the backlink index.
|
||||
|
||||
**Retention** — the newest 50 versions per note, trimmed on every write, plus a
|
||||
daily sweep of anything older than 90 days. Both are configurable:
|
||||
**Retention is by count, not by age.** The newest N versions of a note are kept
|
||||
and trimmed on every write; nothing expires merely for being old.
|
||||
|
||||
There used to be a second rule dropping anything past 90 days, and it was a bad
|
||||
one: the count already bounds how much a note can accumulate, so age only ever
|
||||
deleted history that nothing had replaced — and it bit hardest exactly where
|
||||
history is most wanted. A note left alone for a quarter of a year lost its whole
|
||||
past for having done nothing at all. `VERSION_RETENTION_DAYS` still exists and
|
||||
still works, but it is off unless set.
|
||||
|
||||
**How many is decided per notebook.** Edit a notebook to give it its own number;
|
||||
leave the field empty and it follows the server's default. A reference notebook
|
||||
can keep fifty versions of everything while a scratch one keeps three.
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|----------------------------|---------|------------------------------------------|
|
||||
| `MAX_VERSIONS_PER_NOTE` | `50` | Versions kept per note |
|
||||
| `VERSION_RETENTION_DAYS` | `90` | Age after which a version is dropped |
|
||||
| `MAX_VERSIONS_PER_NOTE` | `50` | Default for notebooks with no setting |
|
||||
| `VERSION_RETENTION_DAYS` | *off* | Optional age limit; unset means never |
|
||||
| `VERSION_COALESCE_MINUTES` | `5` | Window in which saves share one snapshot |
|
||||
|
||||
**Deleting a note** — moving one to the trash keeps its history, because the
|
||||
|
||||
Reference in new issue
Block a user