fix(storage): Datenverlust bei Ladefehlern, neuen Seiten und Löschen verhindern
- Beschädigte Seiten werden als page_<id>.json.corrupt-<zeit> gesichert statt leer überschrieben; bei I/O-Fehlern wird die Ersatzseite als load_failed markiert und nie gespeichert - Autosave gleicht das Manifest (page_ids, Zähler) mit dem Notizbuch ab, damit neu angelegte Seiten nach einem Neustart nicht verloren gehen - Start: nicht ladbares Notizbuch bleibt unverändert, stattdessen wird ein neues angelegt - Aktuelles Notizbuch löschen: ausstehende Schreibvorgänge vorher abarbeiten, damit der Ordner nicht wieder angelegt wird - PDF-Export lädt nicht geöffnete Seiten nach und verweigert unvollständige Exporte; Ergebnis wird per Kanal an die UI gemeldet - Strg+S speichert alle geladenen Seiten und meldet Schreibfehler; flush() liefert Fehler zurück statt immer "Gespeichert" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f9b39c6fe3
commit
d61e1819ed
5 files changed
+527
-172
No files matched your search
+85
-14
@@ -11,7 +11,7 @@ use std::sync::Arc;
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::document::Page;
|
||||
use crate::document::{Notebook, Page};
|
||||
use crate::storage::{self, NotebookManifest, PageData};
|
||||
|
||||
/// Aktueller Status des Autosave-Controllers für OSD und UI.
|
||||
@@ -138,14 +138,53 @@ impl AutosaveController {
|
||||
}
|
||||
|
||||
/// Markiert eine Seite als geändert und startet/erneuert das Debounce-Fenster.
|
||||
///
|
||||
/// Nicht geladene oder fehlgeschlagene Ersatzseiten werden nie gespeichert, damit ihre
|
||||
/// Originaldatei auf der Festplatte nicht überschrieben wird.
|
||||
pub fn mark_page_dirty(&mut self, page: &Page, manifest: &NotebookManifest) {
|
||||
let page_data = PageData::from(page);
|
||||
self.pending_pages.insert(page.id, page_data);
|
||||
if page.is_loaded && !page.load_failed {
|
||||
self.pending_pages.insert(page.id, PageData::from(page));
|
||||
}
|
||||
self.pending_manifest = Some(manifest.clone());
|
||||
self.last_change_time = Some(Instant::now());
|
||||
self.status = AutosaveStatus::Pending;
|
||||
}
|
||||
|
||||
/// Markiert die aktuelle Seite als geändert und gleicht das Manifest (Seitenliste,
|
||||
/// aktuelle Seite, ID-Zähler) mit dem Notizbuch ab.
|
||||
pub fn mark_dirty(&mut self, notebook: &Notebook, manifest: &NotebookManifest) {
|
||||
let mut synced = manifest.clone();
|
||||
synced.sync_from_notebook(notebook);
|
||||
self.mark_page_dirty(notebook.current_page(), &synced);
|
||||
}
|
||||
|
||||
/// Sichert sofort alle geladenen Seiten und das Manifest (Strg+S) und meldet,
|
||||
/// ob wirklich alles geschrieben wurde.
|
||||
pub fn save_now(
|
||||
&mut self,
|
||||
notebook: &Notebook,
|
||||
manifest: &NotebookManifest,
|
||||
) -> Result<(), String> {
|
||||
for page in ¬ebook.pages {
|
||||
if page.is_loaded && !page.load_failed {
|
||||
self.pending_pages.insert(page.id, PageData::from(page));
|
||||
}
|
||||
}
|
||||
let mut synced = manifest.clone();
|
||||
synced.sync_from_notebook(notebook);
|
||||
self.pending_manifest = Some(synced);
|
||||
|
||||
let errors = self.flush_collect_errors();
|
||||
if errors.is_empty() {
|
||||
// Der komplette Zustand liegt jetzt auf der Festplatte – frühere Fehler sind überholt
|
||||
self.status = AutosaveStatus::Saved;
|
||||
Ok(())
|
||||
} else {
|
||||
self.status = AutosaveStatus::Error;
|
||||
Err(errors.join("; "))
|
||||
}
|
||||
}
|
||||
|
||||
/// Prüft den Worker-Kanal und stößt fällige Schreibvorgänge nach Ablauf des Debounce-Timers an.
|
||||
///
|
||||
/// Sollte regelmäßig im Event-Loop aufgerufen werden.
|
||||
@@ -220,40 +259,72 @@ impl AutosaveController {
|
||||
}
|
||||
|
||||
/// Führt einen synchronen Flush aller noch ausstehenden Änderungen durch (z. B. vor Beenden).
|
||||
pub fn flush(&mut self) {
|
||||
pub fn flush(&mut self) -> Result<(), String> {
|
||||
let errors = self.flush_collect_errors();
|
||||
if errors.is_empty() {
|
||||
if self.status != AutosaveStatus::Error {
|
||||
self.status = AutosaveStatus::Saved;
|
||||
}
|
||||
Ok(())
|
||||
} else {
|
||||
self.status = AutosaveStatus::Error;
|
||||
Err(errors.join("; "))
|
||||
}
|
||||
}
|
||||
|
||||
/// Schickt alle anstehenden Änderungen los, wartet, bis der Worker sie abgearbeitet hat,
|
||||
/// und liefert alle dabei aufgetretenen Schreibfehler.
|
||||
fn flush_collect_errors(&mut self) -> Vec<String> {
|
||||
// Falls noch Änderungen im Debounce-Puffer liegen, sofort losschicken
|
||||
if !self.pending_pages.is_empty() || self.pending_manifest.is_some() {
|
||||
self.dispatch_pending();
|
||||
}
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let (ack_tx, ack_rx) = mpsc::channel();
|
||||
if self.cmd_tx.send(AutosaveCommand::Flush(ack_tx)).is_ok() {
|
||||
let _ = ack_rx.recv();
|
||||
if self.cmd_tx.send(AutosaveCommand::Flush(ack_tx)).is_err() || ack_rx.recv().is_err() {
|
||||
errors.push("Autosave-Worker ist nicht erreichbar".to_string());
|
||||
}
|
||||
|
||||
// Restliche Antworten leeren
|
||||
// Restliche Antworten leeren (der Worker hat alle vor dem Flush-Befehl abgearbeitet)
|
||||
while let Ok(response) = self.resp_rx.try_recv() {
|
||||
if self.in_flight_count > 0 {
|
||||
self.in_flight_count -= 1;
|
||||
}
|
||||
if let AutosaveResponse::PageSaved { result: Err(e), .. } = response {
|
||||
eprintln!("[Autosave-Flush] Fehler: {e}");
|
||||
match response {
|
||||
AutosaveResponse::PageSaved {
|
||||
page_id,
|
||||
result: Err(e),
|
||||
} => {
|
||||
eprintln!("[Autosave-Flush] Seite {page_id}: {e}");
|
||||
errors.push(format!("Seite {page_id}: {e}"));
|
||||
}
|
||||
AutosaveResponse::ManifestSaved { result: Err(e) } => {
|
||||
eprintln!("[Autosave-Flush] Manifest: {e}");
|
||||
errors.push(format!("Manifest: {e}"));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.status = AutosaveStatus::Saved;
|
||||
errors
|
||||
}
|
||||
|
||||
/// Wechselt das Zielverzeichnis für zukünftige Speicheroperationen (z. B. beim Dokumentenwechsel),
|
||||
/// nachdem alle noch anstehenden Schreibvorgänge des vorherigen Notizbuchs sicher geflusht wurden.
|
||||
pub fn switch_notebook_dir(&mut self, new_notebook_dir: PathBuf) {
|
||||
self.flush();
|
||||
/// Liefert die Schreibfehler des vorherigen Notizbuchs zurück; der Status gilt danach
|
||||
/// für das neue Notizbuch.
|
||||
pub fn switch_notebook_dir(&mut self, new_notebook_dir: PathBuf) -> Result<(), String> {
|
||||
let result = self.flush();
|
||||
self.notebook_dir = new_notebook_dir;
|
||||
self.status = AutosaveStatus::Saved;
|
||||
result
|
||||
}
|
||||
|
||||
/// Beendet den Hintergrund-Worker ordnungsgemäß nach einem Flush.
|
||||
pub fn shutdown(&mut self) {
|
||||
self.flush();
|
||||
if let Err(e) = self.flush() {
|
||||
eprintln!("[Shutdown] Nicht alle Änderungen konnten gespeichert werden: {e}");
|
||||
}
|
||||
let _ = self.cmd_tx.send(AutosaveCommand::Shutdown);
|
||||
if let Some(handle) = self.worker_handle.take() {
|
||||
let _ = handle.join();
|
||||
|
||||
Reference in new issue
Block a user