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
@@ -122,6 +122,22 @@ export function extractNoteLinks(doc: DocNode, selfId?: string): string[] {
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
/**
|
||||
* Ids of every attachment this document embeds.
|
||||
*
|
||||
* The server stores content encrypted and cannot look inside, so this is the
|
||||
* only account of what a note still uses. It is what lets a picture removed from
|
||||
* a note eventually be freed — and, just as important, what stops one still in
|
||||
* use from being deleted.
|
||||
*/
|
||||
export function extractAttachmentIds(doc: DocNode): string[] {
|
||||
const ids = new Set<string>();
|
||||
walk(doc, (n) => {
|
||||
if (n.type === "image" && n.attrs?.attachmentId) ids.add(n.attrs.attachmentId);
|
||||
});
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
/** Plain text of a document — used for list previews and local search. */
|
||||
export function docToText(doc: unknown, limit = 400): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef, useCallback } from "react";
|
||||
import { useEditor, EditorContent } from "@tiptap/react";
|
||||
|
||||
import { buildExtensions } from "./extensions.js";
|
||||
import { parseNoteContent, serializeNoteContent, extractNoteLinks } from "./content";
|
||||
import { parseNoteContent, serializeNoteContent, extractNoteLinks, extractAttachmentIds } from "./content";
|
||||
import { uploadImage, ACCEPTED_TYPES } from "./attachments.js";
|
||||
import "./editor.css";
|
||||
|
||||
@@ -78,6 +78,9 @@ export function useNoteEditor({ noteId, content, editable = true, onChange, sear
|
||||
onChangeRef.current?.({
|
||||
content: serializeNoteContent(doc),
|
||||
links: extractNoteLinks(doc, noteId),
|
||||
// Travels with every save: the server cannot read the body, so this is
|
||||
// its only account of which pictures the note still uses.
|
||||
attachments: extractAttachmentIds(doc),
|
||||
});
|
||||
},
|
||||
// Rebuilt per note so each one starts with its own document and history.
|
||||
|
||||
Reference in new issue
Block a user