docs: Rechercheergebnisse zum Notiz-Handling uebernehmen
Beide Recherchen zur Map #16 lagen nur auf lokalen Wegwerf-Branches. Die Kurzfassungen stehen an den Tickets #21 und #22, die Langfassungen sind mit Quellen und der Trennung zwischen Belegtem, Abgeleitetem und Unbelegtem aber das, worauf man zurueckkommt. Beide enthalten Messungen, die keine Dokumentation hergibt: dass trash 5.2.6 auf Netzlaufwerken gar nicht funktioniert und Windows dort sonst still und endgueltig loeschen wuerde, und dass ein ungeprunter Append-Log im Yjs-Benchmark das Siebzigfache des Rohtexts misst. Ausserdem /.claude in die .gitignore - dort liegen lokale Einstellungen und die temporaeren Worktrees der Recherche-Agenten.
This commit is contained in:
941
docs/research/historie-nachruesten.md
Normal file
941
docs/research/historie-nachruesten.md
Normal file
@@ -0,0 +1,941 @@
|
||||
# Was es kostet, Historie später möglich zu halten
|
||||
|
||||
> Research zu Gitea-Issue [#22](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/22). Stand: 2026-08-28.
|
||||
> Recherchiert gegen Primärquellen (Hersteller-Doku, Quellcode auf GitHub, Microsoft Learn, docs.rs, crates.io).
|
||||
> **Nichts davon wurde gebaut oder kompiliert.** Es wurde kein Anwendungscode geschrieben und keine Dependency
|
||||
> hinzugefügt. Alles unter „Belegt" stammt aus der jeweils genannten Quelle; alles Abgeleitete ist als solches
|
||||
> markiert.
|
||||
>
|
||||
> Historie ist **nicht geplant**. Dieses Dokument beschafft nur die Fakten für die Schreib-Entscheidung in
|
||||
> [#19](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/19).
|
||||
|
||||
---
|
||||
|
||||
## Kurzantwort
|
||||
|
||||
**Nein — ein Schreibverfahren „temporäre Datei schreiben, dann umbenennen" schließt eine spätere Historie
|
||||
nicht aus. Es ist im Gegenteil das *einzige* der beiden zur Wahl stehenden Verfahren, das Historie
|
||||
überhaupt billig macht.**
|
||||
|
||||
Die Begründung in einem Satz: Historie braucht die **Vorfassung**, und beim Umbenennen liegt die Vorfassung
|
||||
zum Zeitpunkt des Schreibens noch unversehrt auf der Platte — ein direktes `fs::write` hat sie da schon
|
||||
vernichtet.
|
||||
|
||||
Der Einwand aus der Beratungsrunde trifft eine reale Gefahr, aber am falschen Ort. Teuer wird Historie nicht
|
||||
durch das **Wie** des Schreibens, sondern durch das **Wo**: Wenn nach einem halben Jahr fünf Stellen im Code
|
||||
eine Notiz auf die Platte schreiben, muss man fünf Stellen anfassen. Wenn es genau eine gibt, ist Historie
|
||||
eine Änderung *innerhalb dieser einen Funktion* — und alles Weitere (Format, Ablageort, Aufbewahrung) eine
|
||||
Entscheidung, die man dann trifft, nicht heute.
|
||||
|
||||
Der Preis dafür, Historie möglich zu halten, beträgt also: **eine Funktion.** Keine Datei, kein Format, kein
|
||||
Byte auf der Platte, keine Dependency.
|
||||
|
||||
Und die Asymmetrie läuft genau andersherum als behauptet: Nicht das schlichte Schreiben ist die schwer
|
||||
umkehrbare Entscheidung, sondern **der Append-Log**. Von „Datei ist die Wahrheit" zu „Datei plus Sicherungen"
|
||||
ist eine Ergänzung. Von „Log ist die Wahrheit" zurück ist eine Migration. Wer die Option offenhalten will,
|
||||
hält sie gerade dadurch offen, dass er den Log **nicht** baut.
|
||||
|
||||
Dass der Log die Einbahnstraße ist, ist dabei nicht nur ein Architektur-Argument: Die Automerge-Maintainer
|
||||
schreiben selbst, dass sich Historie **nicht verwerfen lässt, ohne das Dokument zu ersetzen**, und Yjs
|
||||
begründet in seiner README, warum Tombstones konstruktionsbedingt liegenbleiben müssen (3.2).
|
||||
|
||||
---
|
||||
|
||||
## 1. Was Historie technisch überhaupt braucht
|
||||
|
||||
Bevor man Kosten schätzt, lohnt es, die Anforderung auf ihre Bestandteile zu zerlegen. Eine Historie —
|
||||
gleich welcher Bauart — braucht genau vier Dinge:
|
||||
|
||||
1. **Einen Auslöser.** Ein Zeitpunkt, an dem entschieden wird: jetzt entsteht eine neue Fassung.
|
||||
2. **Die Vorfassung.** Der Inhalt, der gleich ersetzt wird.
|
||||
3. **Einen Ablageort.** Irgendwo müssen die alten Fassungen hin.
|
||||
4. **Eine Aufräumregel.** Ohne sie wächst der Ablageort unbegrenzt.
|
||||
|
||||
Nur **Punkt 1 und 2 hängen am Schreibverfahren.** Punkt 3 und 4 sind reine Folgeentscheidungen und komplett
|
||||
unabhängig davon, wie die `.md` auf die Platte kommt.
|
||||
|
||||
Und beide, Auslöser und Vorfassung, fallen beim temp+rename-Verfahren **ohnehin an**:
|
||||
|
||||
- Der **Auslöser** ist der Aufruf der Schreibfunktion. Präziser geht es nicht.
|
||||
- Die **Vorfassung** liegt in dem Moment, in dem die temporäre Datei fertig geschrieben ist und das Umbenennen
|
||||
ansteht, noch vollständig und unangetastet unter ihrem Pfad. Man muss sie nur lesen, bevor man umbenennt.
|
||||
|
||||
Bei einem direkten `fs::write` auf die Zieldatei ist die Vorfassung dagegen nach dem ersten Byte weg. Wer dort
|
||||
später Historie nachrüsten will, muss die Datei **zusätzlich vorher lesen** — was möglich, aber ein
|
||||
Extra-Schritt ist, den das rename-Verfahren geschenkt bekommt.
|
||||
|
||||
> **Nebenbefund, der #19 unabhängig von Historie betrifft:** [#20](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/20)
|
||||
> (Zeitstempel-Konflikt) verlangt ohnehin, dass die Schreibfunktion vor dem Schreiben den Zustand der
|
||||
> Zieldatei prüft. Wer dort statt eines `stat` gleich den Inhalt liest, hat die Vorfassung ohne
|
||||
> zusätzlichen Syscall in der Hand. Das ist kein Argument, es heute zu tun — nur der Beleg, dass es
|
||||
> später auch nichts kostet.
|
||||
|
||||
---
|
||||
|
||||
## 2. Wie vergleichbare lokale Markdown-Editoren es lösen
|
||||
|
||||
Kurzüberblick, Details und Belege in den Unterabschnitten:
|
||||
|
||||
| Werkzeug | Wo liegt die Historie? | Format | Intervall | Aufbewahrung | Neben den `.md`? |
|
||||
|---|---|---|---|---|---|
|
||||
| **Obsidian** (File recovery) | App-Systemordner, `%APPDATA%\Obsidian\` — **außerhalb** des Vault | Volltext-Snapshots | 5 Min (Default) | 7 Tage (Default) | **Nein** |
|
||||
| **Obsidian Sync** (Version history) | Auf Obsidians Servern | serverseitig | — | 1 Monat (Standard) / 12 Monate (Plus) | **Nein**, kostenpflichtig |
|
||||
| **Joplin** (Note History) | Tabelle `revisions` in `database.sqlite` | Diff-Kette (`diff-match-patch`, JSON) | 10 Min | 90 Tage (Default) | **Nein** — es gibt gar keine `.md` |
|
||||
| **Logseq** (0.10.9) | `logseq/bak/<datei>/` **im** Graph-Ordner | Volltext-Kopien, Zeitstempel im Namen | **nur bei Konflikt oder Schreibfehler**, nicht bei jedem Speichern | 6 Fassungen pro Datei | **Ja** |
|
||||
| **Zettlr** | — es gibt keine | — | — | — | — |
|
||||
| **Syncthing** (kein Editor, aber das beste Vorbild) | `.stversions/` **im** Sync-Ordner | Volltext-Kopien mit Zeitstempel im Namen | pro Änderung | konfigurierbar, logarithmisch ausgedünnt | **Ja** |
|
||||
|
||||
**Das auffälligste Muster:** Von den untersuchten Editoren legt **keiner** seine Historie neben die
|
||||
Notizdateien. Sie liegt entweder im App-Ordner (Obsidian), in einer Datenbank (Joplin) oder auf einem Server
|
||||
(Obsidian Sync). Wer neben den `.md` versioniert, ist ein Sync-Werkzeug, kein Editor.
|
||||
|
||||
### 2.1 Obsidian — Historie als aufgesetztes Core-Plugin, komplett außerhalb des Vault
|
||||
|
||||
Obsidian ist der direkteste Vergleichspunkt: lose `.md`-Dateien in einem Ordner, dasselbe Versprechen an den
|
||||
Nutzer, dieselbe Ausgangslage.
|
||||
|
||||
**Belegt** (Zitate aus der offiziellen Help-Seite <https://obsidian.md/help/plugins/file-recovery>):
|
||||
|
||||
- „File recovery is a core plugin that protects your work from accidental deletions, file corruption, or
|
||||
unwanted changes by automatically saving **complete snapshots** of your notes at regular intervals."
|
||||
- „**Snapshots capture the full content of your files, not just changes**, allowing you to restore any
|
||||
previous version." — also **Volltext, keine Diffs.**
|
||||
- „**By default, snapshots are saved a minimum of 5 minutes from each other, and kept for 7 days.**"
|
||||
- „**Snapshots are kept in the Global settings, outside of the vault**, to account for vault-related data
|
||||
loss. This means that **snapshots are stored with the absolute path to the note**. If you've moved your
|
||||
vault recently, you may need to move it back to the location where it was when the snapshot was taken."
|
||||
- „If you are using Obsidian Sync or other syncing services, **File recovery snapshots will not sync between
|
||||
devices. Snapshots are device-specific and remain local to each device.**"
|
||||
- „File recovery is **not a complete backup solution**."
|
||||
- Nur `.md` und `.canvas` sind wiederherstellbar.
|
||||
- Aufräumen automatisch nach Ablauf der Tagesgrenze, manuell über *Clear history*.
|
||||
|
||||
Der Ablageort ist damit belegt als der Electron-userData-Ordner der App —
|
||||
**Windows: `%APPDATA%\Obsidian\`** (<https://obsidian.md/help/data-storage>), **nicht** `.obsidian/` im Vault
|
||||
und **nicht** neben den Notizen.
|
||||
|
||||
**Vermutung (starkes Indiz, nicht wörtlich belegt):** Die Snapshots liegen in Obsidians IndexedDB/LevelDB.
|
||||
Die Doku nennt bei IndexedDB nur Sync-State und Metadata-Cache, führt aber für File recovery **dieselbe**
|
||||
Apple-Lockdown-Mode-Einschränkung auf wie für IndexedDB. Ein Serialisierungsformat ist nirgends dokumentiert;
|
||||
Obsidian ist Closed Source.
|
||||
|
||||
**Widerspruch in der Doku selbst, den man kennen sollte:** Der Abschnitt „Storage and performance" derselben
|
||||
Seite behauptet, Snapshots „only store changed files", während der Haupttext „full content […] not just
|
||||
changes" sagt. Plausible Auflösung (**Vermutung**): Nur *geänderte Dateien* werden überhaupt gesnapshottet,
|
||||
jede davon aber als Volltext.
|
||||
|
||||
**Zum Schreibverfahren:** Es gibt **keine offizielle Aussage**, ob Obsidian atomar (temp+rename) oder
|
||||
in-place schreibt. Belegt ist nur die Plugin-API-Semantik: `Vault.process` ist dokumentiert als „**Atomically
|
||||
read, modify, and save the contents of a note**" und garantiert, „that the file doesn't change between
|
||||
reading the current content and writing the updated content"
|
||||
(<https://docs.obsidian.md/Plugins/Vault>). Das ist eine **Sperre gegen konkurrierende Plugins innerhalb des
|
||||
Obsidian-Prozesses**, keine dokumentierte Aussage über `rename()`-Atomarität gegenüber dem Dateisystem —
|
||||
diese Unterscheidung macht die Doku nirgends explizit (**Lesart, nicht wörtlich belegt**). Der Changelog zu
|
||||
1.13.4 (30.07.2026) enthält den Eintrag „Fixed a rare race condition where a file could get stuck and fail to
|
||||
save until the app was restarted" (<https://obsidian.md/changelog/2026-07-30-desktop-v1.13.4/>) — ein Beleg
|
||||
dafür, dass der Save-Pfad auch nach über zehn Jahren nicht trivial ist.
|
||||
|
||||
**Warum das für #22 der wichtigste Befund ist:** Obsidians Historie ist ein **nachträglich aufgesetztes
|
||||
Plugin, das mit dem Schreibverfahren des Kerns nichts zu tun hat**. Es beobachtet Dateien und legt Kopien
|
||||
woanders ab. Genau das ist das „Historie ist eine Schicht *über* dem Schreiben"-Modell — von dem am längsten
|
||||
etablierten Werkzeug dieser Kategorie in Produktion vorgeführt.
|
||||
|
||||
**Der Preis, den Obsidian für diesen Ort zahlt** (alles wörtlich aus der Doku oben): absolute Pfade, die nach
|
||||
einem Vault-Umzug brechen; keine Mitnahme beim Backup; kein Sync zwischen Geräten. Ein Mittelweg —
|
||||
Historie in einem versteckten Ordner *im* Vault — hat von den untersuchten Werkzeugen keiner gewählt; für
|
||||
Obsidian existiert dazu ein offener Feature-Request
|
||||
(<https://forum.obsidian.md/t/file-recovery-store-file-snapshots-in-the-vault-folder-instead-of-the-obsidians-system-folder/116677>,
|
||||
**Sekundärquelle, nur als Hinweis**).
|
||||
|
||||
### 2.2 Joplin — der Gegenentwurf: keine `.md`, dafür Diff-Ketten in SQLite
|
||||
|
||||
Joplin ist als Vergleich nur begrenzt tauglich, weil es das Versprechen, das DioNotes gibt, gar nicht erst
|
||||
macht — aber es zeigt sehr genau, was man mit einer Datenbank *dahinter* gewinnt.
|
||||
|
||||
**Belegt** (<https://joplinapp.org/help/dev/spec/user_profile/>): `database.sqlite` ist „The central SQLite
|
||||
database file where **all notes, notebooks, tags, and metadata are stored**". Es gibt keine `.md`-Dateien.
|
||||
Unter Windows: `%UserProfile%\.config\joplin-desktop`.
|
||||
|
||||
**Belegt** (<https://joplinapp.org/help/apps/note_history/>):
|
||||
|
||||
- „All the apps save a version of the modified notes **every 10 minutes**."
|
||||
- Aufbewahrung „by default, for **90 days**".
|
||||
- „These revisions are then **synced across all the devices**." — anders als bei Obsidian.
|
||||
- Wiederherstellen kopiert in einen Ordner „Restored Notes", ohne die aktuelle Version zu verändern.
|
||||
- Bei unterschiedlichen Einstellungen pro Gerät gewinnt die **kürzeste** Aufbewahrungsdauer.
|
||||
|
||||
**Belegt aus dem Quellcode** (<https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Revision.ts>,
|
||||
<https://github.com/laurent22/joplin/blob/dev/packages/lib/services/RevisionService.ts>,
|
||||
<https://github.com/laurent22/joplin/blob/dev/packages/lib/models/settings/builtInMetadata.ts>):
|
||||
|
||||
- Tabelle `revisions`, Felder u. a. `title_diff`, `body_diff`, `metadata_diff`, `item_id`, `parent_id`.
|
||||
- Diffs über **Google `diff-match-patch`**, serialisiert als JSON (`createTextPatch` →
|
||||
`JSON.stringify(dmp.patch_make(old, new))`).
|
||||
- Defaults: `revisionService.ttlDays` = **90**, `revisionService.intervalBetweenRevisions` = **10 Min**
|
||||
(nicht in der UI sichtbar), `revisionService.oldNoteInterval` = **7 Tage**.
|
||||
- Die erste Revision einer Notiz ist ein Volltext-Patch gegen den Leerstring; Folgerevisionen sind Diffs mit
|
||||
`parent_id` auf die Vorgängerin. `mergeDiffs()` läuft die Kette bis zur Wurzel zurück.
|
||||
- **Aufräumen mit Rebase am Kopf** — der interessanteste Teil. Kommentar wörtlich aus `Revision.ts`:
|
||||
|
||||
> „When deleting old revisions, we need to make sure that the oldest surviving revision is a 'merged' one
|
||||
> (as opposed to a diff from a now deleted revision). So every time we deleted a revision, we need to find
|
||||
> if there's a corresponding surviving revision and modify that revision into a 'merged' one."
|
||||
|
||||
Konkret: Die älteste überlebende Revision wird rekonstruiert und als Volltext-Patch gegen den Leerstring
|
||||
neu geschrieben, `parent_id` auf `''`. Damit bleibt die Kettenlänge auf das TTL-Fenster begrenzt.
|
||||
- Änderungen, die aus **Sync oder Entschlüsselung** stammen, werden ausgefiltert — nur echte lokale Edits
|
||||
erzeugen Revisionen.
|
||||
|
||||
**Die Lehre für DioNotes:** Diff-Ketten sind sparsam, aber nicht umsonst. Sie kosten eine Rekonstruktion in
|
||||
`O(Kettenlänge)` bei **jedem Anlegen und jedem Anzeigen** einer Revision, plus eine nicht triviale
|
||||
Rebase-Logik beim Aufräumen, plus einen dokumentierten Fehlerpfad, wenn eine zu löschende Revision noch
|
||||
verschlüsselt ist. Obsidian hat für dasselbe Feature Volltext gewählt und dafür die Aufbewahrung auf 7 statt
|
||||
90 Tage gesetzt. **Retention-Dauer und Speicherstrategie hängen direkt zusammen** — das ist die eigentliche
|
||||
Stellschraube, und sie liegt vollständig in der Zukunft.
|
||||
|
||||
### 2.3 Logseq — Sicherungskopien im Workspace, aber nur wenn es brenzlig wird
|
||||
|
||||
Logseq ist für DioNotes der **lehrreichste** Fall, weil es als einziges untersuchtes Werkzeug seine
|
||||
Sicherungskopien **neben die Notizen legt** — genau der Mittelweg aus der Ticket-Frage — und weil dabei
|
||||
sichtbar wird, wo dieser Weg scheuert.
|
||||
|
||||
> **Versionsvorbehalt:** Der `master`-Branch von Logseq ist inzwischen die DB-basierte Neuentwicklung ohne
|
||||
> `handler/file.cljs`. Alle Datei-relevanten Belege stammen deshalb aus **Tag `0.10.9`**, der letzten
|
||||
> dateibasierten Release-Linie.
|
||||
|
||||
**Belegt** — drei generierte Verzeichnisse im Graph-Ordner:
|
||||
|
||||
| Pfad | Zweck | Beleg |
|
||||
|---|---|---|
|
||||
| `logseq/bak` | Sicherung überschriebener Dateien | [`backup_file.cljs#L7`](https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/backup_file.cljs#L7) |
|
||||
| `logseq/version-files/local` | explizit angeforderte Versionen | [`backup_file.cljs#L8`](https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/backup_file.cljs#L8) |
|
||||
| `logseq/.recycle` | Papierkorb — Löschen ist ein `renameSync` dorthin | [`handler.cljs#L55-L67`](https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/handler.cljs#L55-L67) |
|
||||
|
||||
**Namenskonvention** — pro Datei ein eigener Unterordner, Zeitstempel im Dateinamen
|
||||
([`backup_file.cljs#L10-L51`](https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/backup_file.cljs#L10-L51)):
|
||||
|
||||
```
|
||||
<graph>/logseq/bak/pages/Foo/2024-01-15T10_23_45.678Z.Desktop.md
|
||||
└ relativer Pfad ┘└ ISO-8601, ":" → "_" ┘└ Plattform ┘
|
||||
```
|
||||
|
||||
**Der wichtigste Befund: Es wird NICHT bei jedem Speichern gesichert.** Es gibt genau drei Auslöser
|
||||
(**belegt**):
|
||||
|
||||
1. **Erkannte Abweichung zwischen Platte und DB beim Schreiben**
|
||||
([`fs/node.cljs#L54-L57`](https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/fs/node.cljs#L54-L57)) —
|
||||
und selbst dann filtert der Electron-Handler ein zweites Mal und sichert nur, wenn der Diff eine
|
||||
*Löschung* zeigt
|
||||
([`handler.cljs#L75-L80`](https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/handler.cljs#L75-L80)):
|
||||
```clojure
|
||||
(when (and (string? db-content) (string? new-content)
|
||||
(string-some-deleted? db-content new-content))
|
||||
(backup-file/backup-file repo :backup-dir path ...))
|
||||
```
|
||||
2. **Schreibfehler** (EBUSY, Lock, fehlende Rechte) — mit Notification „A backup file was saved to …"
|
||||
([`handler.cljs#L117-L140`](https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/handler.cljs#L117-L140)).
|
||||
3. **Externe Dateiänderung über den Watcher**, bevor sie in die DB übernommen wird — Quellcode-Kommentar
|
||||
wörtlich: *„save the previous content in a versioned bak file to avoid data overwritten."*
|
||||
([`watcher_handler.cljs#L44-L56`](https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/fs/watcher_handler.cljs#L44-L56)).
|
||||
|
||||
Maintainer `cnrpman` fasst es zusammen: *„Basically it writes to `bak` when there's a dangerous file
|
||||
operation… **It's a safe net.**"* ([Issue #3370](https://github.com/logseq/logseq/issues/3370#issuecomment-1556840114)).
|
||||
|
||||
**Das ist keine Historie, sondern ein Fangnetz** — und genau das macht es für DioNotes interessant: Der
|
||||
billigste denkbare Nutzen (Datenverlust bei Konflikten verhindern) wird mit dem billigsten denkbaren
|
||||
Mechanismus erkauft, ohne dass irgendetwas am Schreibverfahren hängt.
|
||||
|
||||
**Aufräumen — es gibt eine Grenze, sie greift nur zu lokal** (**belegt**,
|
||||
[`backup_file.cljs#L27-L34`](https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/backup_file.cljs#L27-L34)):
|
||||
|
||||
```clojure
|
||||
(defn- truncate-old-versioned-files!
|
||||
"reserve the latest 6 version files"
|
||||
[dir]
|
||||
... (drop 6 (reverse (sort files))) ... removeSync ...)
|
||||
```
|
||||
|
||||
Warum der Ordner trotzdem wächst (**Ableitung aus dem Code**, deckt sich mit den Nutzerberichten):
|
||||
|
||||
- Die Grenze gilt **pro Datei-Unterordner**; die **Zahl der Unterordner ist unbegrenzt**.
|
||||
- Aufgeräumt wird **nur beim Schreiben eines neuen Backups in genau diesen Ordner**. Kein Startup-Sweep,
|
||||
kein Hintergrundjob. Ordner von Dateien, die nie wieder konfligieren, bleiben ewig stehen.
|
||||
- Sortiert wird rein lexikografisch — funktioniert nur, weil ISO-8601 lexikografisch = chronologisch ist.
|
||||
|
||||
Die Retention-Zahl wanderte über die Jahre: nie getrunct (≤ 0.6.1) → 10
|
||||
([PR #4421](https://github.com/logseq/logseq/pull/4421), Commit-Text: *„Files in the logseq/bak folder are
|
||||
never truncated"*) → 3 → 6. Gründer `tiensonqin` empfiehlt in
|
||||
[Issue #4819](https://github.com/logseq/logseq/issues/4819): *„**I suggest to cleanup backups using some
|
||||
cron scripts.**"* Es gibt **keine offizielle Doku** zum bak-Ordner und **keinen Off-Schalter**
|
||||
([Forum, Aug 2025](https://discuss.logseq.com/t/add-option-to-disable-backups-logseq-bak-folder/33107),
|
||||
unbeantwortet).
|
||||
|
||||
**Korrektur einer verbreiteten Annahme:** Beschwerden über *Wachstum* gibt es reichlich
|
||||
([#4819](https://github.com/logseq/logseq/issues/4819), [#3588](https://github.com/logseq/logseq/issues/3588)
|
||||
„600 or more backup files", [#3370](https://github.com/logseq/logseq/issues/3370) offen seit 2021,
|
||||
[Discussion #8624](https://github.com/logseq/logseq/discussions/8624) seit drei Jahren unbeantwortet). Aber
|
||||
**keine Primärquelle nennt eine konkrete GB-Zahl.** Belegt ist unbegrenztes Ordner-Wachstum, nicht
|
||||
unbegrenztes Größenwachstum.
|
||||
|
||||
**Weitere belegte Details:**
|
||||
|
||||
- **Nicht atomar geschrieben:** `fs/writeFileSync path content`, mit vorherigem `chmodSync` auf `644`, falls
|
||||
die Datei nicht schreibbar ist
|
||||
([`handler.cljs#L117-L126`](https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/handler.cljs#L117-L126)).
|
||||
- **Watcher: chokidar**, `awaitWriteFinish true`
|
||||
([`fs_watcher.cljs`](https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/fs_watcher.cljs)).
|
||||
- **Konflikt:** Vergleich **vor** dem Schreiben; bei Abweichung wird **nicht geschrieben**, sondern ein Modal
|
||||
gezeigt mit zwei **editierbaren** Textareas („On disk:" / „In Logseq:"), je einem Button „Select this" —
|
||||
kein Merge, kein Abbrechen
|
||||
([`components/diff.cljs#L27-L90`](https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/components/diff.cljs#L27-L90)).
|
||||
- **Undo überlebt den Neustart nicht.** Der Stack ist ein reines In-Memory-Atom über
|
||||
DataScript-Datoms — `(def ^:private undo-redo-states (atom {}))`
|
||||
([`undo_redo.cljs#L16-L28`](https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/modules/editor/undo_redo.cljs#L16-L28)).
|
||||
Die offizielle Doku formuliert es vorsichtig: *„during their editing sessions"*
|
||||
([docs: Undo and Redo](https://github.com/logseq/docs/blob/master/pages/Undo%20and%20Redo.md)).
|
||||
- **Es gibt keine UI, die bak-Versionen anzeigt oder wiederherstellt** — nur „Open page backups directory",
|
||||
das den Explorer öffnet. Der Menüpunkt „View page history" ist dagegen **Git**-basiert
|
||||
(`shell/get-file-latest-git-log`) und setzt ein eigenes Git-Setup des Nutzers voraus
|
||||
([`page_menu.cljs#L93-L159`](https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/components/page_menu.cljs#L93-L159)).
|
||||
|
||||
**Die Lehre:** Selbst das Werkzeug, das im Workspace sichert, hat sich gegen „bei jedem Speichern" und für
|
||||
„nur wenn es brenzlig wird" entschieden — und hat trotzdem ein Aufräumproblem, weil die Grenze pro Datei
|
||||
statt global gilt. Beides sind Entwurfsfehler, die man **beim Nachrüsten** vermeiden kann, weil sie mit dem
|
||||
Schreibverfahren nichts zu tun haben.
|
||||
|
||||
### 2.4 Zettlr — gar keine Historie, und das ist die offizielle Position
|
||||
|
||||
**Belegt: Zettlr hat kein Snapshot-, History- oder Versions-Feature.** Die Doku delegiert das Thema
|
||||
ausdrücklich an den Nutzer
|
||||
(<https://docs.zettlr.com/en/first-time-users/workspaces.html>):
|
||||
|
||||
> „We strongly recommend that you regularly back up your workspaces. The simplest method is to use a cloud
|
||||
> storage provider (such as Dropbox, Google Drive, OneDrive, or iCloud)… you should also back up the files
|
||||
> onto an external storage device from time to time."
|
||||
|
||||
Vorhanden ist nur der System-Papierkorb via `shell.trashItem`
|
||||
([`safe-delete.ts`](https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/fsal/util/safe-delete.ts))
|
||||
und ein **überschreibendes** Autosave.
|
||||
|
||||
**Schreibverfahren — nicht atomar** (**belegt**,
|
||||
[`fsal/index.ts#L529-L533`](https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/fsal/index.ts#L529-L533)):
|
||||
|
||||
```ts
|
||||
public async writeTextFile (filePath: string, contents: string): Promise<void> {
|
||||
await this._cache.del(filePath)
|
||||
await fs.writeFile(filePath, contents, 'utf-8')
|
||||
}
|
||||
```
|
||||
|
||||
Drei Details des Save-Pfads, die #18 bis #20 direkt betreffen (**belegt**,
|
||||
[`documents/index.ts#L1527-L1603`](https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/documents/index.ts#L1527-L1603)):
|
||||
|
||||
- Der Descriptor wird **vor** dem Schreiben aktualisiert, mit explizitem Kommentar zur Race Condition
|
||||
„User ändert → Save startet → User ändert mehr → Save beendet und macht die Änderungen rückgängig".
|
||||
- **Self-Echo-Unterdrückung:** `this._ignoreChanges.push(filePath)` — der eigene Write wird beim
|
||||
Watcher-Event wieder herausgefiltert.
|
||||
- **BOM und Zeilenende werden rekonstruiert:** intern arbeitet Zettlr immer mit `\n`, beim Schreiben wird der
|
||||
ursprüngliche Zustand wiederhergestellt (`doc.descriptor.bom + content.split('\n').join(doc.descriptor.linefeed)`).
|
||||
|
||||
**Konfliktbehandlung, dreistufig** (**belegt**,
|
||||
[`documents/index.ts#L1070-L1130`](https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/documents/index.ts#L1070-L1130)) —
|
||||
für [#20](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/20) unmittelbar verwertbar:
|
||||
|
||||
1. **mtime-Filter**, bewusst `modtime <= ourModtime → return` statt Gleichheitsprüfung. Kommentar wörtlich:
|
||||
*„In response to issue #1621: We will not check for equal modtime but only for newer modtime to prevent
|
||||
sluggish cloud synchronization services (e.g. OneDrive and Box) from having text appear to 'jump'."*
|
||||
2. **Inhaltsvergleich:** `if (diskContents === doc.lastSavedContent) return` — fängt Cloud-Dienste ab, die
|
||||
nur Attribute anfassen.
|
||||
3. **Dialog** „File changed on disk" mit den Buttons **„Keep editor contents"** / **„Load changes from disk"**
|
||||
(kein Merge, kein Diff) und einer Checkbox „Always load changes from disk if there are no unsaved changes
|
||||
in the editor", die direkt die Einstellung `alwaysReloadFiles` schreibt.
|
||||
|
||||
**Zur Git-Frage: Es gibt kein offizielles Zettlr-Git-Plugin** (**belegt**). `Zettlr/Zettlr-Git-Plugin`
|
||||
liefert 404; die Organisation hat keine Plugin-Repos
|
||||
(<https://github.com/orgs/Zettlr/repositories>). Zettlr hat überhaupt **kein Plugin-System** — Maintainer
|
||||
`nathanlesage`: *„Writing a plugin system is incredibly difficult, and thus we should do that extremely
|
||||
limited."* ([Discussion #3108](https://github.com/Zettlr/Zettlr/discussions/3108)). Das einzige Git-Feature
|
||||
ist reine Erkennung: `dir.isGitRepository = (await fs.lstat(path.join(dir.path, '.git'))).isDirectory()`
|
||||
([`fsal-directory.ts#L103-L105`](https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/fsal/fsal-directory.ts#L103-L105)).
|
||||
[Issue #1050 „Integrate basic git functionality"](https://github.com/Zettlr/Zettlr/issues/1050) ist seit Juli
|
||||
2020 offen, Label „Priority: Low".
|
||||
|
||||
**Die Lehre:** Ein etablierter, ernstgemeinter Markdown-Editor kommt seit Jahren **ohne jede eingebaute
|
||||
Historie** aus und verweist stattdessen auf externe Werkzeuge. Das ist der empirische Beleg dafür, dass
|
||||
„Historie nicht bauen" eine tragfähige Produktentscheidung ist — nicht bloß ein Aufschub.
|
||||
|
||||
### 2.5 Syncthing — kein Editor, aber das beste Vorbild für Ablage und Aufräumen
|
||||
|
||||
Syncthing ist der einzige untersuchte Vertreter, der Versionen **neben** den Nutzdateien ablegt, und hat dafür
|
||||
die durchdachteste Aufräumstrategie. Alles hier **belegt** aus
|
||||
<https://docs.syncthing.net/users/versioning.html>:
|
||||
|
||||
- **Ablageort:** Standardmäßig der Ordner **`.stversions`** innerhalb des freigegebenen Verzeichnisses. Ein
|
||||
eigener Pfad ist konfigurierbar, aber die Doku warnt ausdrücklich: „please ensure that it's on the same
|
||||
partition or filesystem as the regular folder path."
|
||||
- **Namenskonvention:** zeitgestempelte Dateinamen.
|
||||
- **Vier Modi:** *Trash Can* (letzte Fassung, `cleanoutDays`), *Simple* (N Fassungen pro Datei mit
|
||||
Zeitstempel), *Staggered* (altersbasiertes Ausdünnen), *External* (delegiert an ein Kommando).
|
||||
- **Der Staggered-Versioner dünnt logarithmisch aus:**
|
||||
|
||||
| Alter | behalten wird |
|
||||
|---|---|
|
||||
| 1 Stunde | die älteste Version pro 30-Sekunden-Intervall |
|
||||
| 1 Tag | die älteste Version pro Stunde |
|
||||
| 30 Tage | die älteste Version pro Tag |
|
||||
| bis „Maximum Age" | die älteste Version pro Woche |
|
||||
|
||||
„Maximum Age" = 0 bedeutet: für immer behalten.
|
||||
|
||||
Das ist die Antwort auf die Aufräumfrage, falls DioNotes je Historie bekommt: **konstanter Overhead statt
|
||||
unbegrenztem Wachstum**, ohne Diff-Rekonstruktion, ohne Datenbank. Vier Zeilen Regel.
|
||||
|
||||
---
|
||||
|
||||
## 3. Was ein Append-Log konkret kosten würde
|
||||
|
||||
Der Vorschlag aus der Beratungsrunde: ein Append-Log neben jeder Notiz, aus dem die `.md` als lesbare Sicht
|
||||
materialisiert wird. Undo über Sitzungen, Historie und Konfliktauflösung fielen als Nebenprodukt an.
|
||||
|
||||
Der Vorschlag ist technisch seriös — das ist Event Sourcing, und es liefert tatsächlich alles drei. Die Frage
|
||||
ist nicht, ob er funktioniert, sondern was er kostet.
|
||||
|
||||
### 3.1 Größenwachstum — belegte Zahlen
|
||||
|
||||
Die maßgebliche Zahl stammt aus dem **offiziellen Benchmark-Repo von Kevin Jahns**, dem Yjs-Autor, das die
|
||||
Yjs-README selbst verlinkt (<https://github.com/dmonad/crdt-benchmarks>). Gemessen wird der bekannte
|
||||
B4-Editier-Trace: 182.315 Einfügungen, 77.463 Löschungen, **104.852 Zeichen im Endtext**.
|
||||
|
||||
| | Yjs 13.6.11 | Automerge 2.1.10 |
|
||||
|---|---|---|
|
||||
| **Dokumentgröße gemerged** (`docSize`) | 159.929 B ≈ **1,53× Rohtext** | 129.116 B ≈ **1,23× Rohtext** |
|
||||
| **Summe aller Einzel-Updates** (`updateSize`) | **7.306.126 B ≈ 70× Rohtext** | — |
|
||||
| Durchschnittliche Update-Größe | 29 B | — |
|
||||
| Parse-Zeit | 39 ms | 1.805 ms |
|
||||
|
||||
**Das ist die eigentliche Kostenaussage, und sie ist zweigeteilt** (**belegt**):
|
||||
|
||||
- **Ein *zusammengefasstes* CRDT-Dokument ist erstaunlich billig** — 1,2 bis 1,5× des Rohtextes, mit voller
|
||||
Historie. Das ist deutlich günstiger, als man intuitiv erwartet.
|
||||
- **Ein *ungekürzter Append-Log* — also genau das, was der Vorschlag beschreibt — kostet rund 70× den
|
||||
Rohtext.** Der Unterschied zwischen 1,5× und 70× ist die Compaction. Ein Log, der nur angehängt und nie
|
||||
zusammengefasst wird, liegt beim oberen Wert.
|
||||
|
||||
Bei der Skalierung B4×100 (10,5 Mio. Zeichen) bleibt die Dokumentgröße bei 1,53×, der **Arbeitsspeicher**
|
||||
steigt aber auf 327,1 MB — etwa 31× des Textes (**belegt**, dieselbe Quelle).
|
||||
|
||||
Die offizielle Yjs-FAQ ordnet das ein (<https://docs.yjs.dev/api/faq>): „**Size is very rarely a practical
|
||||
problem** as long as you deal with human-entered text input." Das ist ein fairer Einwand gegen eine reine
|
||||
Größen-Argumentation — und der Grund, warum der Ausschlussgrund in 3.4 steht und nicht hier.
|
||||
|
||||
**Was daraus für DioNotes folgt** (**Ableitung**):
|
||||
|
||||
- **Eine Sidecar-Datei pro Notiz.** Ein Workspace mit 500 Notizen enthält 1.000 Dateien statt 500. Der
|
||||
Notiz-Baum müsste die Hälfte davon ausblenden; jedes Backup-, Sync- und Suchwerkzeug des Nutzers sieht sie.
|
||||
- **Autosave koppelt an den Speicherplatz.** Die Auslöser-Entscheidung aus
|
||||
[#18](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/18) würde zur
|
||||
Speicherplatz-Entscheidung: „Autosave nach Tipppause" erzeugt Größenordnungen mehr Log-Einträge als
|
||||
„manuell mit Strg+S". Diese Kopplung existiert bei keiner anderen Option aus Abschnitt 4.
|
||||
|
||||
### 3.2 Aufräumen: bei CRDT-Logs nicht vorgesehen
|
||||
|
||||
Der wichtigste Befund dieses Abschnitts, und er ist aus Maintainer-Mund belegt.
|
||||
|
||||
**Automerge** — Peter van Hardenberg (Maintainer) in
|
||||
[Issue #799](https://github.com/automerge/automerge/issues/799), 24.12.2023, wörtlich:
|
||||
|
||||
> „'Compact' could mean a few different things here — one is whether you can reduce the on-disk size of an
|
||||
> Automerge document. **This happens automatically when you call `save()`.** It quite literally compacts the
|
||||
> history into a very efficient columnar representation.
|
||||
>
|
||||
> **Discarding the history is not currently possible without replacing the document: if you don't have the
|
||||
> same history, you can't merge the same changes.** That's because we use a git-like commit hash for each
|
||||
> change to ensure integrity."
|
||||
|
||||
„Shallow clone" und „cherry pick" nennt er als nützlich, verwandt und „**not yet implemented**"; nachgefasst
|
||||
am 23.09.2024: „I don't know *when* it will happen." Der Automerge-2.0-Blogpost kündigt dasselbe schon seit
|
||||
Januar 2023 an (<https://automerge.org/blog/automerge-2/>, Abschnitt „History Management"):
|
||||
|
||||
> „Today the best way to remove something from an Automerge document's history is to **recreate the document
|
||||
> from scratch** or to reset to a time before that change went in."
|
||||
|
||||
Orion Henry (Automerge-Core) in [Issue #348](https://github.com/automerge/automerge/issues/348), 25.04.2022:
|
||||
„the solutions really are to either have the nodes coordinate **a total document reset every so often —
|
||||
throwing out the old document and starting fresh**". Issue #348 wurde am 21.08.2026 geschlossen mit dem
|
||||
Hinweis auf `hexane`, den neuen Storage-Engine — der laut offiziellem Blogpost
|
||||
(<https://automerge.org/blog/2026-july/>) „around 2–9× faster" ist, also **Kompression und Performance
|
||||
liefert, keine History-Truncation**. Die Schließung bedeutet nicht, dass das Feature existiert.
|
||||
|
||||
**Yjs** — die README nennt den Grund formal
|
||||
(<https://github.com/yjs/yjs/blob/main/README.md>, Abschnitt „Yjs CRDT Algorithm"):
|
||||
|
||||
> „CRDTs that are suitable for shared text editing suffer from the fact that they **only grow in size**. […]
|
||||
> **We can't garbage collect deleted structs (tombstones) while ensuring a unique order of the structs.**"
|
||||
|
||||
Im Quellcode bestätigt: `Item.gc()` ersetzt beim normalen Löschen nur den `content` durch
|
||||
`ContentDeleted(len)` — **das komplette Item-Struct mit `id{client,clock}`, `origin`, `rightOrigin`, `parent`
|
||||
und der Reihenfolge-Verlinkung bleibt stehen**
|
||||
(<https://github.com/yjs/yjs/blob/main/src/structs/Item.js>). Und das Zusammenfassen von Updates hilft nicht:
|
||||
„**Note that this feature only merges document updates and doesn't garbage-collect deleted content**"
|
||||
(<https://docs.yjs.dev/api/document-updates>). Wer Snapshots will, muss GC ganz abschalten — der Code wirft
|
||||
sonst: `if (originDoc.gc) { throw new Error('Garbage-collection must be disabled in \`originDoc\`!') }`
|
||||
(<https://github.com/yjs/yjs/blob/main/src/utils/Snapshot.js>).
|
||||
|
||||
**Das Gegenargument, das fairerweise dazugehört.** Das offizielle Automerge-Cookbook
|
||||
(<https://automerge.org/docs/cookbook/modeling-data/>) schreibt:
|
||||
|
||||
> „Martin Kleppmann did some experiments with a benchmark document to see how much space would be saved by
|
||||
> discarding history […] **The savings are not all that great, which is why we haven't prioritised history
|
||||
> truncation so far.**"
|
||||
|
||||
Das ist ernst zu nehmen — es gilt für Kleppmanns Messfall: **ein großes, weitgehend linear getipptes
|
||||
Dokument mit wenigen Actor-IDs.** Eine Notiz-App hat das umgekehrte Profil: **viele kleine Dokumente, die
|
||||
über Jahre in vielen kurzen Sitzungen angefasst werden**, also viele Actor-IDs pro Dokument. Ob die Ökonomie
|
||||
dort kippt, ist für dieses Dokument **nicht belegt** — die 70×-Zahl aus 3.1 legt es nahe, ist aber am
|
||||
B4-Trace gemessen, nicht am Notiz-Profil.
|
||||
|
||||
**Fazit dieses Abschnitts:** Aufräumen heißt bei einem CRDT-Log entweder gar nichts (Yjs behält Tombstones
|
||||
konstruktionsbedingt) oder „Dokument wegwerfen und neu anfangen" (Automerge). Genau denselben Kompromiss
|
||||
fährt Joplin mit dem Rebase der ältesten überlebenden Revision (2.2) — nur innerhalb einer Datenbank, wo er
|
||||
billig ist. Neben jeder Notiz im Dateisystem ist er es nicht.
|
||||
|
||||
### 3.3 Sidecar-Dateien in Sync-Ordnern — belegte Risiken
|
||||
|
||||
Ob die bloße Verdopplung der Dateizahl schadet, ist **nicht belegt**; die harten Limits sprechen sogar
|
||||
dagegen (Dropbox: „Users shouldn't sync more than **300.000 files**" und „**1.000.000 write operations per
|
||||
namespace in a 24 hour period**", <https://help.dropbox.com/plans/large-deployments>; OneDrive: „no more than
|
||||
a total of **300.000 items**",
|
||||
<https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa>).
|
||||
Bei typischen Workspace-Größen ist das unkritisch.
|
||||
|
||||
**Belegt sind dagegen zwei andere Fallstricke**, und beide treffen ein Sidecar-Design härter als eine
|
||||
einzelne Datei:
|
||||
|
||||
- **Platzhalter-Dateien werden als gelöscht interpretiert.** Obsidians offizielle Sync-FAQ
|
||||
(<https://help.obsidian.md/sync/faq>): „Cloud storage services like OneDrive and Dropbox offer 'Files
|
||||
on-demand' or 'Online-only files' features […] However, since these files are not always available locally,
|
||||
**Obsidian Sync will interpret them as deleted, leading to their removal from your remote vault.**"
|
||||
Ergänzend (<https://help.obsidian.md/sync-notes>): „iCloud Drive on Windows may lead to **file duplication
|
||||
or corruption**." Und die architektonische Begründung: „**Obsidian requires access to the entire vault for
|
||||
its features** […] This makes it difficult for some services to function reliably with Obsidian."
|
||||
- **Das bloße Nachsehen, ob eine Datei existiert, kann die App einfrieren.** Apples Technote TN3150
|
||||
(<https://developer.apple.com/documentation/technotes/tn3150-getting-ready-for-data-less-files>):
|
||||
„Materializing a dataless file can take time […] **the app may become unresponsive.** If an app's main
|
||||
queue remains unresponsive for a long time, **the system may terminate the app and trigger a watchdog
|
||||
crash.**" Als auslösende Aktionen nennt Apple ausdrücklich „**enumerating a directory's contents and
|
||||
checking whether a file exists**", und: „`stat` and `getattrlist` both trigger the materialization of any
|
||||
intermediate folders." Für jede Notiz die zugehörige Log-Datei zu suchen oder zu statten ist genau dieses
|
||||
Muster. (Die Technote gilt für Apple-Plattformen; DioNotes ist Windows-only, aber der Mechanismus —
|
||||
Platzhalter-Dateien, die beim Zugriff nachgeladen werden — ist bei OneDrive Files On-Demand derselbe.
|
||||
**Übertragung: Vermutung.**)
|
||||
|
||||
Apples eigene Empfehlung für mehrteilige Dokumente ist bezeichnend: **File Packages statt nebeneinander\-
|
||||
liegender Dateien** („If your document data format consists of multiple distinct pieces, use a file
|
||||
package", <https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesigningForDocumentsIniCloud.html>).
|
||||
Für DioNotes untauglich — dann wäre die `.md` keine eigenständige Datei mehr, und genau das ist das
|
||||
Versprechen.
|
||||
|
||||
> **Nicht als Beleg verwendbar** (gegengeprüft und verworfen): Zotereos Warnung „extremely likely to corrupt
|
||||
> your database" betrifft SQLite und File Locking, nicht Sidecar-Strukturen. git-annex' Warnung gegen
|
||||
> Dropbox-Ordner beruht auf Case-Insensitivity und Symlink-Handling. Für DVC gibt es keine offizielle
|
||||
> Warnung, nur Community-Threads.
|
||||
|
||||
### 3.4 Das Zwei-Wahrheiten-Problem — der eigentliche Einwand
|
||||
|
||||
Dieser Punkt braucht keine externe Quelle, er folgt aus den Rahmenbedingungen des Projekts.
|
||||
|
||||
DioNotes hat **keinen Filewatcher** ([#16](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/16),
|
||||
Rahmenentscheidung) und rechnet **ausdrücklich damit, dass Dropbox, Syncthing oder Git die Dateien ändern,
|
||||
während die App läuft** ([#20](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/20)).
|
||||
|
||||
Damit gilt für einen Append-Log:
|
||||
|
||||
- Ändert jemand die `.md` von außen, weiß der Log nichts davon. Der Log sagt „Inhalt X", die Datei sagt
|
||||
„Inhalt Y". **Zwei Wahrheiten.**
|
||||
- Beim nächsten Materialisieren gewinnt der Log — und **überschreibt die externe Änderung stillschweigend.**
|
||||
Das ist kein Randfall, sondern der Normalfall bei jedem Sync-Konflikt.
|
||||
- Der Ausweg wäre, die externe Änderung als neuen Eintrag in den Log einzuspeisen (Diff gegen den zuletzt
|
||||
materialisierten Stand). Das ist machbar — aber es setzt genau die Konflikterkennung voraus, die #20 erst
|
||||
noch entscheiden muss, und macht sie von einer Kann- zu einer **Muss**-Anforderung.
|
||||
|
||||
Die entscheidende Beobachtung: **Beim „Datei ist die Wahrheit"-Modell gibt es dieses Problem nicht.** Eine
|
||||
Sicherungskopie, die veraltet ist, ist eine weniger nützliche Sicherungskopie — kein inkonsistenter Zustand.
|
||||
Die Datei ist immer richtig, weil sie per Definition die Wahrheit ist. Der Append-Log führt eine
|
||||
Korrektheitslücke ein, die das schlichte Verfahren gar nicht erst hat.
|
||||
|
||||
### 3.5 Bewertung am Versprechen
|
||||
|
||||
Der Bewertungsmaßstab aus dem Projektkontext: „Der Nutzer kann das Werkzeug jederzeit verlassen, seine
|
||||
Dateien sind mit jedem Editor lesbar."
|
||||
|
||||
Ein Append-Log, aus dem die `.md` materialisiert wird, degradiert die `.md` zur **Ausgabe**. Formal bleibt sie
|
||||
lesbar — praktisch ist sie es nicht mehr im Sinne des Versprechens, denn:
|
||||
|
||||
- Eine Änderung an ihr ist **wirkungslos oder wird verworfen**, sobald DioNotes das nächste Mal
|
||||
materialisiert. Der Nutzer *darf* seine Dateien dann nicht mehr mit einem anderen Editor bearbeiten,
|
||||
sondern nur noch lesen.
|
||||
- Das Werkzeug zu verlassen, hieße die Historie zurückzulassen — sie steckt im Log, nicht in der Datei.
|
||||
|
||||
Der Unterschied zu einer Sicherungskopie ist damit kein gradueller: Eine Sicherungskopie ist **zusätzliche**
|
||||
Information neben der Wahrheit. Ein Log ist **die** Wahrheit, und die Datei wird zur Kopie. Das ist genau die
|
||||
Umkehrung, die das Versprechen bricht.
|
||||
|
||||
---
|
||||
|
||||
## 4. Die billigeren Mittelwege
|
||||
|
||||
Zur Einordnung: Alle drei sind **Aufsätze auf ein unverändertes Schreibverfahren**. Keiner verlangt, dass
|
||||
#19 heute irgendetwas anders entscheidet. Sie sind hier aufgeführt, damit die Größenordnung des „später"
|
||||
bekannt ist — nicht als Empfehlung, heute etwas davon zu bauen.
|
||||
|
||||
### 4.1 Eine einzelne Sicherungskopie der Vorfassung
|
||||
|
||||
Der billigste Weg: Vor dem Umbenennen die Zieldatei nach `<name>.md.bak` (oder in `.dionotes/bak/`) kopieren.
|
||||
Eine Fassung, überschrieben bei jedem Speichern.
|
||||
|
||||
- **Kosten:** eine Datei pro je gespeicherter Notiz, Größe = Größe der Notiz. Bei 500 Notizen à 4 KB also
|
||||
~2 MB. Kein Wachstum über die Zeit, weil jede Sicherung die vorige ersetzt. **Aufräumen entfällt.**
|
||||
- **Nutzen:** deckt genau einen Fall ab — „ich habe gerade etwas kaputtgespeichert". Kein Undo über
|
||||
Sitzungen, keine Historie.
|
||||
- **Präzedenzfall:** Logseq macht im Kern nichts anderes, nur mit sechs statt einer Fassung und nur bei
|
||||
Konflikt (2.3).
|
||||
- **Fallstrick:** Wenn die Sicherung **neben** der Notiz liegt, sieht der Nutzer sie im Ordner und im Sync —
|
||||
und der Notiz-Baum müsste sie ausblenden, ohne die Punkt-Regel aufzuweichen. In `.dionotes/` liegt sie
|
||||
außer Sichtweite, wird aber beim Löschen einer Notiz zur Waise. Beides lösbar, beides eine Entscheidung
|
||||
für später.
|
||||
|
||||
### 4.2 Zeitstempel-Ordner mit logarithmischer Ausdünnung (das Syncthing-Modell)
|
||||
|
||||
Ein Ordner `.dionotes/versions/<relativer-pfad>/<zeitstempel>.md`, ausgedünnt nach dem Muster aus 2.5.
|
||||
|
||||
- **Kosten:** Volltext-Kopien, aber durch die Ausdünnung mit **konstantem Overhead statt unbegrenztem
|
||||
Wachstum**. Bei den Syncthing-Buckets landet man pro Notiz nach einem Jahr bei grob 50–100 Fassungen
|
||||
(**Abschätzung, nicht belegt**) — für eine 4-KB-Notiz also ein paar hundert Kilobyte.
|
||||
- **Nutzen:** echte Historie mit Zeitpunkten, Undo über Sitzungen, kein Diff-Algorithmus nötig.
|
||||
- **Belegtes Vorbild in Produktion:** Syncthings `.stversions` mit vier Modi und den Ausdünn-Buckets
|
||||
(2.5, <https://docs.syncthing.net/users/versioning.html>).
|
||||
- **Warum das die beste Nachrüst-Option wäre:** Es braucht keine Datenbank, kein Diff-Format, keine
|
||||
Kettenrekonstruktion wie bei Joplin (2.2) — und es vermeidet Logseqs Aufräumfehler, weil die Ausdünnung
|
||||
altersbasiert statt anzahlbasiert-pro-Datei ist.
|
||||
|
||||
### 4.3 Git-Repository im Workspace
|
||||
|
||||
Die Variante, die manche Nutzer heute schon von Hand fahren — und die Logseqs „View page history" bereits
|
||||
voraussetzt (2.3).
|
||||
|
||||
**Belegte Kosten:**
|
||||
|
||||
- Pro Commit entstehen **mindestens drei lose Objekte** (Blob + Root-Tree + Commit), pro Verzeichnisebene ein
|
||||
weiteres Tree-Objekt. Format: zlib-komprimierter Header + Inhalt unter
|
||||
`.git/objects/<2>/<38>` (<https://git-scm.com/book/en/v2/Git-Internals-Git-Objects>).
|
||||
- **Lose Objekte enthalten den vollen Blob, keine Deltas** — Delta-Kompression gibt es erst beim Packen. Eine
|
||||
Ein-Zeichen-Änderung an einer 5-KB-Notiz erzeugt einen kompletten neuen komprimierten Blob (**Ableitung**
|
||||
aus derselben Quelle).
|
||||
- `gc.auto` = **6700** lose Objekte, `gc.autoPackLimit` = **50** Packs, `gc.pruneExpire` = **2.weeks.ago**,
|
||||
`gc.autoDetach` = **true** (<https://git-scm.com/docs/git-config>, <https://git-scm.com/docs/git-gc>).
|
||||
- Sobald Housekeeping ausgelöst wird, laufen laut Doku **alle** Housekeeping-Aufgaben mit (rerere, worktrees,
|
||||
reflog).
|
||||
|
||||
**Ableitung für DioNotes:** 6700 ÷ ~3 Objekte je Commit ≈ **~2.200 Commits bis zum ersten automatischen
|
||||
Repack**. Bei „ein Commit pro Speichern" mit Autosave ist das eine Frage von Wochen, nicht von Jahren. Der
|
||||
Repack läuft dann im Hintergrund, schreibt große `.pack`-Dateien und löscht tausende lose Dateien auf einen
|
||||
Schlag — mitten in einem Ordner, den womöglich Dropbox oder Syncthing beobachtet. Für den Sync-Client sieht
|
||||
das aus wie ein Änderungssturm (**Ableitung, mittlere Konfidenz**). `.git/` gehörte damit zwingend aus dem
|
||||
Sync ausgenommen.
|
||||
|
||||
**Die Rust-Seite** (**belegt**, crates.io/docs.rs):
|
||||
|
||||
| Crate | Version | Downloads gesamt | C-Toolchain nötig? |
|
||||
|---|---|---|---|
|
||||
| `git2` (libgit2-Bindings) | 0.21.0 | ~110 Mio. | **Ja** — `libgit2-sys` kompiliert ~180k Zeilen C via `cc` (kein CMake) |
|
||||
| `gix` / gitoxide | 0.87.1 | ~44 Mio. | **Nein** — Feature-Doku wörtlich: „No C toolchain is involved." |
|
||||
|
||||
`gix` beschreibt sich als „pure Rust implementation of Git", testet Windows in CI („failures do prevent
|
||||
releases") und deckt commit, log, status, diff, reset ab. Der Haken ist der Reifegrad: Der Haupt-Einstiegs\-
|
||||
punkt `gix` steht laut eigenem Stability-Tier-Schema noch auf **„Initial Development"**, entsprechend
|
||||
API-Churn bei jedem Minor. Unter Windows nutzt libgit2 WinHTTP, OpenSSL ist für lokale Repo-Operationen nicht
|
||||
nötig (**Ableitung** aus der Windows-Verlinkung in `build.rs`).
|
||||
|
||||
**Bewertung:** Git ist der einzige Weg, der Historie *und* Diff *und* Branching liefert — und der einzige,
|
||||
der eine schwergewichtige Dependency plus unbegrenztes Wachstum plus periodisch teures Repacking mitbringt.
|
||||
Für „Undo über Sitzungen" ist es mit Abstand die teuerste Antwort. Bemerkenswert ist, was die untersuchten
|
||||
Editoren daraus machen: Zettlr empfiehlt Git **nicht** (2.4), Logseq benutzt es nur lesend und setzt voraus,
|
||||
dass der Nutzer es selbst eingerichtet hat (2.3). **Niemand richtet dem Nutzer ein Repo ein.**
|
||||
|
||||
### 4.4 Kostenvergleich
|
||||
|
||||
| Option | Zusätzliche Dateien | Wachstum | Aufräumen | Dependency | Bricht das Versprechen? |
|
||||
|---|---|---|---|---|---|
|
||||
| Nichts tun (heute) | 0 | — | — | keine | nein |
|
||||
| Eine Sicherungskopie (4.1) | 1 pro gespeicherter Notiz | keins | entfällt | keine | nein |
|
||||
| Zeitstempel-Ordner (4.2) | N pro Notiz, ausgedünnt | beschränkt | Ausdünn-Regel, ~1 Funktion | keine | nein |
|
||||
| Git im Workspace (4.3) | `.git/`, ≥3 Objekte pro Commit | unbeschränkt | `git gc`, teuer | `gix` oder `git2` | nein |
|
||||
| Append-Log (Abschnitt 3) | 1 Log pro Notiz | **≈ 70× Rohtext** ungekürzt, ≈ 1,2–1,5× gemerged | **von den Bibliotheken nicht vorgesehen** (Automerge: Dokument neu anlegen; Yjs: Tombstones bleiben) | CRDT-/Log-Bibliothek | **ja** — die `.md` wird zur Kopie |
|
||||
|
||||
---
|
||||
|
||||
## 5. Die eigentliche Frage: Schließt temp+rename eine spätere Historie aus?
|
||||
|
||||
**Nein.** Die Begründung in vier Schritten.
|
||||
|
||||
### 5.1 Das Schreibverfahren ist für Historie günstig, nicht hinderlich
|
||||
|
||||
Wie in Abschnitt 1 hergeleitet: Historie braucht Auslöser und Vorfassung. Beim temp+rename-Verfahren ist die
|
||||
Vorfassung im Moment vor dem Umbenennen noch vollständig auf der Platte. Der Einbau einer Historie wäre eine
|
||||
Einfügung von wenigen Zeilen **an einer Stelle**, an der man ohnehin schon steht:
|
||||
|
||||
```
|
||||
schreibe_notiz(pfad, inhalt):
|
||||
prüfe Konflikt (aus #20)
|
||||
── hier ließe sich später die Vorfassung wegsichern ──
|
||||
schreibe inhalt nach pfad.tmp
|
||||
sync
|
||||
benenne pfad.tmp nach pfad um
|
||||
```
|
||||
|
||||
Das Gegenteil gilt für den naiven Weg: `fs::write(pfad, inhalt)` zerstört die Vorfassung, bevor irgendjemand
|
||||
sie sichern könnte. Wer dort Historie nachrüstet, muss die Datei zusätzlich vorher lesen. Das ist immer noch
|
||||
billig — aber es ist der **teurere** der beiden Wege, und er ist zugleich der, den #19 aus Gründen der
|
||||
Absturzsicherheit ohnehin ausschließen will.
|
||||
|
||||
**Damit ist der Einwand in seiner konkreten Form widerlegt**: Die Entscheidung, die #19 zu treffen hat, macht
|
||||
Historie nicht teurer. Sie macht sie billiger.
|
||||
|
||||
### 5.2 Was Historie *tatsächlich* teuer machen würde
|
||||
|
||||
Damit das Ergebnis belastbar ist, hier die Gegenprobe — die Entscheidungen, die den Einwand berechtigt machen
|
||||
würden. Keine davon ist die Wahl zwischen `rename` und `write`:
|
||||
|
||||
| Falle | Warum sie teuer wird | Kosten sie zu vermeiden |
|
||||
|---|---|---|
|
||||
| **Mehrere Schreibstellen.** Autosave schreibt hier, „Notiz verlassen" dort, „Fenster schließen" an dritter Stelle. | Historie muss an jeder Stelle nachgezogen werden, und jede vergessene Stelle ist ein Loch in der Historie, das man erst durch einen Bug bemerkt. | **Eine** Funktion, die schreibt. Alle Auslöser aus [#18](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/18) rufen dieselbe. Kostet heute nichts. |
|
||||
| **Kein Notiz-Begriff, nur Pfade.** | Historie, die am Pfad hängt, reißt beim Umbenennen ab ([#23](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/23)). Obsidian hat genau dieses Problem, dokumentiert („snapshots are stored with the absolute path to the note"). | Entweder akzeptieren (Obsidian tut es) oder später eine stabile ID einführen. Beides nachrüstbar; die Kette bräche dann nur für Umbenennungen *vor* der Einführung. |
|
||||
| **Umbenennen als Löschen+Neuanlegen implementiert.** | Zerreißt jede Historie und erzeugt Waisen im Ablageort. | Umbenennen als Umbenennen implementieren. Ist in #23 ohnehin die naheliegende Wahl. |
|
||||
| **Der Log-Ansatz aus dem Vorschlag.** | Siehe Abschnitt 3 — er ist die einzige der hier betrachteten Optionen, die schwer umkehrbar ist. | Ihn nicht bauen. |
|
||||
|
||||
**Nur die erste Zeile ist eine echte Auflage an #19** — und sie kostet heute exakt nichts, weil es aktuell
|
||||
noch **überhaupt keine Schreibstelle im Code gibt** (verifiziert: keine Fundstelle für `fs::`/`write`/`File::`
|
||||
unter `src/`; der Editor nimmt Eingaben entgegen und verwirft sie, so beschrieben in
|
||||
[#16](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/16)).
|
||||
|
||||
### 5.3 Die Asymmetrie läuft andersherum
|
||||
|
||||
Der Vorschlag aus der Beratungsrunde begründet sich damit, dass die Schreib-Entscheidung schwer umkehrbar sei.
|
||||
Prüft man beide Richtungen, ergibt sich das Gegenteil:
|
||||
|
||||
| Richtung | Aufwand |
|
||||
|---|---|
|
||||
| „Datei ist die Wahrheit" → „Datei plus Sicherungen" | **Ergänzung.** Eine Funktion erweitern, ein Ordner, eine Aufräumregel. Bestehende Notizen funktionieren unverändert weiter, die Historie beginnt eben ab Einführung. Kein Datenmigrations-Schritt, kein Bruch für den Nutzer. |
|
||||
| „Log ist die Wahrheit" → „Datei ist die Wahrheit" | **Migration.** Jede Notiz aus dem Log materialisieren, die Logs entfernen oder verwaisen lassen, die gesamte gesammelte Historie wegwerfen oder in ein neues Format überführen — und den Nutzern erklären, warum ihre Historie weg ist. |
|
||||
|
||||
Wer sich die Option offenhalten will, hält sie also gerade dadurch offen, dass er den Log **nicht** baut. Der
|
||||
Log ist die Einbahnstraße, nicht das schlichte Schreiben.
|
||||
|
||||
Obsidian ist der empirische Beleg: Das Werkzeug wurde 2020 veröffentlicht, hat von Anfang an lose `.md`-Dateien
|
||||
geschrieben, und File recovery kam später als Core-Plugin obendrauf — vollständig außerhalb des Vault, ohne
|
||||
das Dateiformat oder das Schreibverfahren anzufassen (**belegt** durch den Aufbau des Plugins, s. 2.1;
|
||||
das genaue Erscheinungsdatum des Plugins ist **nicht belegt**).
|
||||
|
||||
### 5.4 Belastbarkeit dieser Antwort
|
||||
|
||||
Die Antwort steht auf zwei Beinen unterschiedlicher Härte:
|
||||
|
||||
- **Hart (belegt):** Obsidian betreibt eine produktive Historie für lose `.md`-Dateien als aufgesetzte
|
||||
Schicht, die vom Schreibverfahren des Kerns entkoppelt ist und außerhalb des Datenordners lebt. Ein
|
||||
Werkzeug mit derselben Ausgangslage wie DioNotes hat das Problem also nachweislich nachträglich gelöst.
|
||||
- **Weich (Argumentation, nicht durch Quelle belegt):** Dass genau *eine* Schreibstelle die einzige
|
||||
load-bearing Auflage ist, ist eine Schlussfolgerung aus der Zerlegung in Abschnitt 1, kein Zitat. Sie ist
|
||||
aber schwer zu bestreiten: Eine Schicht über N Aufrufstellen einzuziehen kostet N Änderungen, über einer
|
||||
Aufrufstelle eine.
|
||||
|
||||
**Fazit für #19: Der Einwand ist entkräftet. Die Schreib-Entscheidung darf allein nach den Kriterien aus #19
|
||||
fallen — Absturzsicherheit, Sichtbarkeit der temporären Datei, Dateirechte, Aufräumen. Historie legt ihr
|
||||
genau eine Auflage auf, und die ist heute gratis: eine einzige Funktion, durch die alles Schreiben läuft.**
|
||||
|
||||
---
|
||||
|
||||
## 6. Randbefunde für die Nachbartickets
|
||||
|
||||
Bei der Recherche fielen Belege an, die #22 nicht beantworten, aber #19 und #20 direkt betreffen. Sie stehen
|
||||
hier, damit sie nicht verlorengehen — **nicht** als Entscheidung.
|
||||
|
||||
**Für [#19](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/19) (Schreibverfahren):**
|
||||
|
||||
- **Weder Rust noch Microsoft dokumentieren `rename` als atomar.** Das Wort „atomic" kommt weder in
|
||||
<https://doc.rust-lang.org/std/fs/fn.rename.html> noch in der `MoveFileExW`-Doku vor. Rust garantiert nur
|
||||
„replacing the original file if `to` already exists" und nennt `MoveFileExW` mit Fallback auf
|
||||
`SetFileInformationByHandle`. Man verlässt sich auf undokumentiertes NTFS-Verhalten (**belegt durch
|
||||
Abwesenheit**).
|
||||
- **Microsoft empfiehlt für genau diesen Anwendungsfall `ReplaceFile`, nicht `MoveFileEx`.** Aus
|
||||
<https://learn.microsoft.com/en-us/windows/win32/fileio/deprecation-of-txf>, Abschnitt „Applications
|
||||
updating a single file with 'document-like' data": *„A common approach is to write the document to a new
|
||||
file, then replace the original file with the new one. **One method to do this is with the `ReplaceFile`
|
||||
API.**"*
|
||||
- **Temp+rename verliert Metadaten.** Die `ReplaceFileW`-Remarks führen als Vorteil auf, dass die Funktion
|
||||
*creation time, short file name, object identifier, DACLs, security resource attributes, encryption,
|
||||
compression* und *named streams* der Originaldatei erhält — was per Kontrast belegt, dass ein nacktes
|
||||
temp+rename genau diese verliert. Die **File ID ändert sich sogar bei `ReplaceFile`**. Kein untersuchtes
|
||||
Rust-Crate (`tempfile`, `atomicwrites`, `atomic-write-file`) nutzt `ReplaceFile`; `atomic-write-file`
|
||||
dokumentiert die Verluste immerhin explizit („No support for preserving timestamps, ACLs, extended
|
||||
attributes…").
|
||||
→ **Praktische Folge für #19:** Ein „erstellt am" gehört ins Frontmatter der `.md`, nicht ins Dateisystem.
|
||||
- **Das realistischste Fehlerbild ist `ERROR_SHARING_VIOLATION`**, wenn Defender oder ein Sync-Client die
|
||||
Zieldatei ohne `FILE_SHARE_DELETE` offen hält (<https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew>).
|
||||
`tempfile::NamedTempFile::persist` gibt die temporäre Datei im `PersistError` zurück — genau für den
|
||||
Retry-Fall gebaut. Ebenfalls belegt: `persist` synchronisiert weder Inhalt noch Verzeichnis.
|
||||
- **Die temporäre Datei muss im selben Verzeichnis liegen** — `fs::rename` scheitert über Volume-Grenzen
|
||||
(„This will not work if the new name is on a different mount point"), und Syncthing warnt bei seinem
|
||||
eigenen Versions-Ordner mit derselben Begründung.
|
||||
- **`fs-err` löst das Problem nicht** (es ist nur ein Drop-in-Ersatz für `std::fs` mit besseren
|
||||
Fehlermeldungen), ist aber die naheliegende Ergänzung, um überhaupt zu sehen, *welche* Datei die
|
||||
Sharing-Violation geworfen hat.
|
||||
|
||||
**Für [#20](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/20) (Zeitstempel-Konflikt):**
|
||||
|
||||
- **Zettlrs mtime-Regel ist bewusst `modtime <= ourModtime → ignorieren`, nicht Gleichheitsprüfung**, mit
|
||||
wörtlicher Begründung im Code: *„to prevent sluggish cloud synchronization services (e.g. OneDrive and Box)
|
||||
from having text appear to 'jump'"*. Danach folgt ein **Inhaltsvergleich**, weil Cloud-Dienste Dateien
|
||||
anfassen, ohne den Inhalt zu ändern. Ein reiner mtime-Vergleich fängt das nicht ab.
|
||||
- **Logseq vergleicht ebenfalls den Inhalt vor dem Schreiben** und schreibt bei Abweichung gar nicht.
|
||||
Beide untersuchten Open-Source-Editoren lösen den Konflikt über Inhalt, nicht über Zeitstempel allein.
|
||||
- **Beide brauchen einen Self-Echo-Filter**, damit der eigene Schreibvorgang nicht als externe Änderung
|
||||
zurückkommt (Zettlr: `_ignoreChanges`-Liste; Logseq: Inhaltsvergleich). Das ist erst relevant, falls je ein
|
||||
Filewatcher dazukommt — aber es erklärt, warum ein Watcher nicht „nur ein Watcher" ist.
|
||||
- **Dropbox nennt als dokumentierte Ursache Nr. 3 für Conflicted Copies wörtlich „applications with an
|
||||
auto-save feature"** (<https://help.dropbox.com/organize/conflicted-copy>). Das ist ein Argument für
|
||||
[#18](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/18), Autosave zu entprellen statt pro
|
||||
Tastendruck zu schreiben.
|
||||
- **Syncthings Konfliktnamen** sind `<name>.sync-conflict-<date>-<time>-<modifiedBy>.<ext>`, und diese
|
||||
Dateien werden „treated as normal files after they are created, so they are propagated between devices" —
|
||||
sie tauchen also im Notiz-Baum auf, wenn nichts sie ausfiltert.
|
||||
|
||||
---
|
||||
|
||||
## 7. Belegt vs. Vermutung — Übersicht
|
||||
|
||||
| Aussage | Status |
|
||||
|---|---|
|
||||
| Obsidian: Snapshots außerhalb des Vault, `%APPDATA%\Obsidian\`, Volltext, 5 Min / 7 Tage Default | **Belegt** (help.obsidian.md) |
|
||||
| Obsidian: Snapshots syncen nicht, absolute Pfade, brechen nach Vault-Umzug | **Belegt** (help.obsidian.md) |
|
||||
| Obsidian: Snapshots liegen konkret in IndexedDB/LevelDB | **Vermutung** — starkes Indiz über die Lockdown-Mode-Einschränkung, kein wörtlicher Beleg |
|
||||
| Obsidian: Serialisierungsformat der Snapshots | **Nicht belegbar** — Closed Source, nirgends dokumentiert |
|
||||
| Obsidian: Schreibverfahren des Kerns (atomar oder nicht) | **Nicht belegbar** — keine offizielle Aussage |
|
||||
| Obsidian: `Vault.process` ist „atomar" nur prozessintern, nicht auf FS-Ebene | **Lesart** — die Doku macht die Unterscheidung nicht explizit |
|
||||
| Joplin: SQLite statt `.md`, `revisions`-Tabelle, `diff-match-patch`, 10 Min / 90 Tage | **Belegt** (joplinapp.org + Quellcode) |
|
||||
| Joplin: Rebase der ältesten überlebenden Revision beim Aufräumen | **Belegt** (Quellcode-Kommentar + Implementierung) |
|
||||
| Joplin: konkrete Größe der Revisions-Tabelle in der Praxis | **Nicht belegbar** — keine offizielle Zahl |
|
||||
| Logseq: `logseq/bak/<datei>/<ISO>.md`, nur bei Konflikt/Schreibfehler/externer Änderung | **Belegt** (Quellcode 0.10.9) |
|
||||
| Logseq: „reserve the latest 6 version files", nur beim Schreiben in denselben Ordner | **Belegt** (Quellcode) |
|
||||
| Logseq: bak-Ordner werden GB-groß | **Nicht belegbar** — belegt ist unbegrenztes *Ordner*-Wachstum, keine Primärquelle nennt eine Größe |
|
||||
| Logseq: nicht-atomares `writeFileSync`, chokidar-Watcher, Diff-Modal bei Konflikt | **Belegt** (Quellcode) |
|
||||
| Logseq: Undo ist reines In-Memory-Atom, überlebt Neustart nicht | **Belegt** (Quellcode + Doku) |
|
||||
| Zettlr: keine eingebaute Historie, Doku verweist auf Cloud-Backup | **Belegt** (docs.zettlr.com) |
|
||||
| Zettlr: `fs.writeFile`, nicht atomar; mtime-Filter „nur neuer, nicht ungleich"; Konflikt-Dialog | **Belegt** (Quellcode master @ 86c6c56) |
|
||||
| Zettlr: kein offizielles Git-Plugin, kein Plugin-System | **Belegt** (404 + Maintainer-Aussage in Discussion #3108) |
|
||||
| Syncthing: `.stversions`, Zeitstempel-Namen, vier Modi, staggered-Buckets | **Belegt** (docs.syncthing.net) |
|
||||
| Git: `gc.auto` = 6700, `gc.autoPackLimit` = 50, ≥3 lose Objekte pro Commit | **Belegt** (git-scm.com) |
|
||||
| Git: „~2.200 Commits bis zum ersten Repack" | **Ableitung** aus den belegten Defaults |
|
||||
| `gix` braucht keinen C-Compiler, `git2` schon | **Belegt** (docs.rs Feature-Doku, `libgit2-sys/build.rs`) |
|
||||
| Ausgedünnte Zeitstempel-Historie ≈ 50–100 Fassungen pro Notiz nach einem Jahr | **Abschätzung**, nicht belegt |
|
||||
| „Historie ist eine Schicht über dem Schreiben" | **Belegt am Beispiel Obsidian**, im Übrigen **Argumentation** |
|
||||
| „Eine Schreibstelle ist die einzige echte Auflage" | **Argumentation**, nicht durch Quelle belegt — aber aus der Zerlegung in Abschnitt 1 zwingend |
|
||||
| Zwei-Wahrheiten-Problem des Append-Logs | **Argumentation** aus den Projekt-Rahmenbedingungen (kein Filewatcher, externer Sync erwartet) |
|
||||
| CRDT-Dokument **gemerged** ≈ 1,2–1,5× Rohtext; **Summe der Einzel-Updates ≈ 70× Rohtext** (B4-Trace) | **Belegt** (dmonad/crdt-benchmarks, Repo des Yjs-Autors) |
|
||||
| Automerge: „Discarding the history is not currently possible without replacing the document" | **Belegt** (Maintainer-Zitat, Issue #799) |
|
||||
| Automerge: shallow clone / history truncation seit 2023 angekündigt, bis heute nicht implementiert | **Belegt** (Blogpost + Issue-Verlauf + API-Oberfläche) |
|
||||
| Yjs: Tombstones sind konstruktionsbedingt nicht sammelbar, Update-Merge räumt nicht auf | **Belegt** (README, `Item.js`, docs.yjs.dev) |
|
||||
| Automerge-Cookbook: „The savings are not all that great" (Gegenargument) | **Belegt** — gilt für Kleppmanns Messfall (großes Dokument, wenige Actor-IDs) |
|
||||
| Ob die Ökonomie beim Notiz-Profil (viele kleine Dokumente, viele Sitzungen) kippt | **Nicht belegt** — die 70×-Zahl legt es nahe, ist aber am B4-Trace gemessen |
|
||||
| Obsidian Sync: „Files on-demand"-Platzhalter werden als gelöscht interpretiert | **Belegt** (help.obsidian.md) |
|
||||
| Apple TN3150: Existenzprüfung/Verzeichnis-Enumeration kann Materialisierung und Watchdog-Crash auslösen | **Belegt** — Übertragung auf OneDrive Files On-Demand unter Windows ist **Vermutung** |
|
||||
| Dass die bloße Verdopplung der Dateizahl in Sync-Ordnern schadet | **Nicht belegt** — die 300.000er-Limits sprechen eher dagegen |
|
||||
|
||||
---
|
||||
|
||||
## Quellenliste
|
||||
|
||||
**Obsidian**
|
||||
- File recovery (Core-Plugin): <https://obsidian.md/help/plugins/file-recovery>
|
||||
- How Obsidian stores data: <https://obsidian.md/help/data-storage>
|
||||
- Vault API Guide: <https://docs.obsidian.md/Plugins/Vault>
|
||||
- Vault TypeScript-Referenz: <https://docs.obsidian.md/Reference/TypeScript+API/Vault>
|
||||
- `DataAdapter.write`: <https://docs.obsidian.md/Reference/TypeScript+API/DataAdapter/write>
|
||||
- Sync Version history: <https://obsidian.md/help/sync/version-history>
|
||||
- Changelog 1.13.4 (Save-Race-Condition): <https://obsidian.md/changelog/2026-07-30-desktop-v1.13.4/>
|
||||
- Changelog 1.6.0 (externe Änderungen): <https://obsidian.md/changelog/2024-05-09-desktop-v1.6.0/>
|
||||
- Feature-Request „Snapshots im Vault" (*Sekundärquelle*): <https://forum.obsidian.md/t/file-recovery-store-file-snapshots-in-the-vault-folder-instead-of-the-obsidians-system-folder/116677>
|
||||
|
||||
**Joplin**
|
||||
- Note History: <https://joplinapp.org/help/apps/note_history/>
|
||||
- User profile / Datenablage: <https://joplinapp.org/help/dev/spec/user_profile/>
|
||||
- Import & Export: <https://joplinapp.org/help/apps/import_export/>
|
||||
- `Revision.ts`: <https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Revision.ts>
|
||||
- `RevisionService.ts`: <https://github.com/laurent22/joplin/blob/dev/packages/lib/services/RevisionService.ts>
|
||||
- `builtInMetadata.ts` (Defaults): <https://github.com/laurent22/joplin/blob/dev/packages/lib/models/settings/builtInMetadata.ts>
|
||||
|
||||
**Logseq** (alle Codebelege gegen Tag `0.10.9`)
|
||||
- `backup_file.cljs` (bak-Ordner, Namensschema, Truncate): <https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/backup_file.cljs>
|
||||
- `handler.cljs` (Schreiben, `.recycle`, `backupDbFile`): <https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/handler.cljs>
|
||||
- `fs/node.cljs` (Vergleich vor dem Schreiben): <https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/fs/node.cljs>
|
||||
- `watcher_handler.cljs` (Backup bei externer Änderung): <https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/fs/watcher_handler.cljs>
|
||||
- `fs_watcher.cljs` (chokidar): <https://github.com/logseq/logseq/blob/0.10.9/src/electron/electron/fs_watcher.cljs>
|
||||
- `components/diff.cljs` (Konflikt-Modal): <https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/components/diff.cljs>
|
||||
- `undo_redo.cljs` (In-Memory-Undo): <https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/modules/editor/undo_redo.cljs>
|
||||
- `page_menu.cljs` („View page history" via `git log`): <https://github.com/logseq/logseq/blob/0.10.9/src/main/frontend/components/page_menu.cljs>
|
||||
- Doku „Undo and Redo": <https://github.com/logseq/docs/blob/master/pages/Undo%20and%20Redo.md>
|
||||
- Issue #3370 (Maintainer: „It's a safe net"): <https://github.com/logseq/logseq/issues/3370>
|
||||
- Issue #4819 (Gründer: „cleanup backups using some cron scripts"): <https://github.com/logseq/logseq/issues/4819>
|
||||
- PR #4421 (Einführung des Truncate): <https://github.com/logseq/logseq/pull/4421>
|
||||
- Forum: Wunsch nach Off-Schalter (*Sekundärquelle*): <https://discuss.logseq.com/t/add-option-to-disable-backups-logseq-bak-folder/33107>
|
||||
|
||||
**Zettlr** (Codebelege gegen `master` @ `86c6c56`)
|
||||
- Workspaces / Backup-Empfehlung: <https://docs.zettlr.com/en/first-time-users/workspaces.html>
|
||||
- Settings (Autosave, „Always load remote changes"): <https://docs.zettlr.com/en/reference/settings.html>
|
||||
- `fsal/index.ts` (`writeTextFile`): <https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/fsal/index.ts>
|
||||
- `documents/index.ts` (`saveFile`, `handleRemoteChange`): <https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/documents/index.ts>
|
||||
- `fsal-watchdog.ts` (chokidar): <https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/fsal/fsal-watchdog.ts>
|
||||
- `fsal-directory.ts` (`isGitRepository`): <https://github.com/Zettlr/Zettlr/blob/86c6c56df1a94dfde4b1f05b13b365878ac1199a/source/app/service-providers/fsal/fsal-directory.ts>
|
||||
- Discussion #3108 (kein Plugin-System): <https://github.com/Zettlr/Zettlr/discussions/3108>
|
||||
- Issue #1050 (Git seit 2020 offen): <https://github.com/Zettlr/Zettlr/issues/1050>
|
||||
|
||||
**Append-Log / CRDT (Automerge, Yjs)**
|
||||
- Automerge Issue #799 (Maintainer pvh: „Discarding the history is not currently possible…"): <https://github.com/automerge/automerge/issues/799>
|
||||
- Automerge Issue #348 (Orion Henry: „a total document reset every so often"): <https://github.com/automerge/automerge/issues/348>
|
||||
- Automerge 2.0, Abschnitt „History Management": <https://automerge.org/blog/automerge-2/>
|
||||
- Automerge Cookbook, „Modeling Data → Performance" (Kleppmann: „The savings are not all that great"): <https://automerge.org/docs/cookbook/modeling-data/>
|
||||
- Automerge `hexane` Storage-Engine (Kompression, keine Truncation): <https://automerge.org/blog/2026-july/>
|
||||
- Yjs README, „Yjs CRDT Algorithm" (Tombstones): <https://github.com/yjs/yjs/blob/main/README.md>
|
||||
- Yjs `src/structs/Item.js` (`Item.gc()`): <https://github.com/yjs/yjs/blob/main/src/structs/Item.js>
|
||||
- Yjs `src/utils/Snapshot.js` (GC muss aus sein): <https://github.com/yjs/yjs/blob/main/src/utils/Snapshot.js>
|
||||
- Yjs Document Updates („doesn't garbage-collect deleted content"): <https://docs.yjs.dev/api/document-updates>
|
||||
- Yjs FAQ („Size is very rarely a practical problem"): <https://docs.yjs.dev/api/faq>
|
||||
- crdt-benchmarks (Kevin Jahns, Zahlen zu B4): <https://github.com/dmonad/crdt-benchmarks>
|
||||
|
||||
**Sidecar-Dateien in Sync-Ordnern**
|
||||
- Obsidian Sync FAQ („interpret them as deleted"): <https://help.obsidian.md/sync/faq>
|
||||
- Obsidian, Notizen zwischen Geräten synchronisieren: <https://help.obsidian.md/sync-notes>
|
||||
- Apple TN3150 „Getting ready for dataless files": <https://developer.apple.com/documentation/technotes/tn3150-getting-ready-for-data-less-files>
|
||||
- Apple iCloud Design Guide (File Packages statt Sidecars): <https://developer.apple.com/library/archive/documentation/General/Conceptual/iCloudDesignGuide/Chapters/DesigningForDocumentsIniCloud.html>
|
||||
- Dropbox-Limits (300.000 Dateien, 1 Mio. Writes/24 h): <https://help.dropbox.com/plans/large-deployments>
|
||||
- OneDrive-Limits (300.000 Items): <https://support.microsoft.com/en-us/office/restrictions-and-limitations-in-onedrive-and-sharepoint-64883a5d-228e-48f5-b3d2-eb39e07630fa>
|
||||
|
||||
**Syncthing**
|
||||
- File Versioning: <https://docs.syncthing.net/users/versioning.html>
|
||||
- Syncing / Conflict Handling: <https://docs.syncthing.net/users/syncing.html>
|
||||
- Ignoring Files (`.stignore`): <https://docs.syncthing.net/users/ignoring.html>
|
||||
|
||||
**Git und Rust-Anbindung**
|
||||
- Git Internals — Git Objects: <https://git-scm.com/book/en/v2/Git-Internals-Git-Objects>
|
||||
- `git-config` (`gc.auto`, `gc.autoPackLimit`, `gc.pruneExpire`): <https://git-scm.com/docs/git-config>
|
||||
- `git-gc`: <https://git-scm.com/docs/git-gc>
|
||||
- `git2`: <https://crates.io/crates/git2> · <https://github.com/rust-lang/git2-rs>
|
||||
- `gix` / gitoxide: <https://crates.io/crates/gix> · <https://github.com/GitoxideLabs/gitoxide> · <https://docs.rs/gix/>
|
||||
|
||||
**Windows-Schreibverfahren** (für [#19](https://gitlab.hive-brain.de/RDietermann/dionotes-client/issues/19), hier nur als Randbefund)
|
||||
- `std::fs::rename`: <https://doc.rust-lang.org/std/fs/fn.rename.html>
|
||||
- `MoveFileExW`: <https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-movefileexw>
|
||||
- `ReplaceFileW` (Liste der erhaltenen Attribute): <https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-replacefilew>
|
||||
- Deprecation of TxF (Microsofts Empfehlung für „document-like data"): <https://learn.microsoft.com/en-us/windows/win32/fileio/deprecation-of-txf>
|
||||
- `CreateFileW` (`ERROR_SHARING_VIOLATION`, `FILE_SHARE_DELETE`): <https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-createfilew>
|
||||
- `tempfile::NamedTempFile::persist`: <https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html#method.persist>
|
||||
- Dropbox „Conflicted copy" (Ursache: „applications with an auto-save feature"): <https://help.dropbox.com/organize/conflicted-copy>
|
||||
Reference in New Issue
Block a user