mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-13 14:22:25 +00:00
Apply Redactions replaces the recovery snapshot at once (#6454)
This commit is contained in:
1 parent
72311bc101
commit
3c26e2e3eb
5 files changed
+89
-2
No files matched your search
@@ -32,7 +32,8 @@ pub struct Document {
|
||||
pub dirty: bool,
|
||||
/// Bumped on every change to the working file (edit, undo, redo, save); autosave compares it.
|
||||
pub(crate) generation: u64,
|
||||
/// The generation last handed out by `autosave_snapshots`.
|
||||
/// The generation last handed out by `autosave_snapshots`, or another one when storing that
|
||||
/// snapshot failed (`autosave_failed`), so it is due again.
|
||||
pub(crate) snapshot_generation: u64,
|
||||
/// Why the document cannot be edited (e.g. encryption), if so.
|
||||
pub read_only_reason: Option<String>,
|
||||
|
||||
@@ -217,6 +217,15 @@ impl Session {
|
||||
out
|
||||
}
|
||||
|
||||
/// The snapshot [`Self::autosave_snapshots`] handed out for `id` could not be stored: the
|
||||
/// document is due again, so the next autosave tries it again.
|
||||
pub fn autosave_failed(&mut self, id: DocId) {
|
||||
if let Some(d) = self.docs.iter_mut().find(|d| d.id == id) {
|
||||
// Any generation but the current one leaves it due.
|
||||
d.snapshot_generation = d.generation.wrapping_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark a document opened from a recovery file: it has unsaved changes and belongs at
|
||||
/// `path` (where Save writes), as when the session ended.
|
||||
pub fn mark_recovered(&mut self, id: DocId, path: Option<String>) {
|
||||
|
||||
@@ -1846,6 +1846,9 @@ fn finish_redactions_and_fields(app: &mut PdfCraftApp, req: &mut Pending) {
|
||||
let which: Vec<pdfcraft_engine::Hidden> = app.hidden_draft.found.iter().filter(|f| f.2 && f.1 > 0).map(|f| f.0).collect();
|
||||
let n: usize = app.hidden_draft.found.iter().filter(|f| f.2).map(|f| f.1).sum();
|
||||
if app.apply_edit(Edit::RemoveHidden { which }) {
|
||||
if let Some((_, id)) = app.active_ids() {
|
||||
app.renew_recovery(id);
|
||||
}
|
||||
if n == 1 {
|
||||
app.notify_tr("Removed 1 hidden item. Save to remove them from the file.");
|
||||
} else {
|
||||
@@ -1854,11 +1857,17 @@ fn finish_redactions_and_fields(app: &mut PdfCraftApp, req: &mut Pending) {
|
||||
}
|
||||
}
|
||||
Some(Dialog::Sanitize) if app.apply_edit(Edit::Sanitize) => {
|
||||
if let Some((_, id)) = app.active_ids() {
|
||||
app.renew_recovery(id);
|
||||
}
|
||||
app.notify_tr("Document sanitized. Save to finish: saving rewrites the whole file.");
|
||||
}
|
||||
Some(Dialog::RedactApply) => {
|
||||
let marks = app.active_ids().and_then(|(_, id)| app.session.get(id)).map_or(0, |d| d.redaction_marks());
|
||||
if app.apply_edit(Edit::ApplyRedactions { pages: None }) {
|
||||
if let Some((_, id)) = app.active_ids() {
|
||||
app.renew_recovery(id);
|
||||
}
|
||||
if marks == 1 {
|
||||
app.notify_tr("Applied 1 redaction mark. Save to remove the content from the file.");
|
||||
} else {
|
||||
|
||||
@@ -320,6 +320,9 @@ impl PdfCraftApp {
|
||||
RecoveryMeta { key, name: snap.name, path: snap.path, saved_at: now_secs(), encrypted: snap.encrypted, instance: store.instance() };
|
||||
if let Err(e) = store.write(&meta, &snap.bytes) {
|
||||
log::warn!("autosave failed: {e}");
|
||||
// Still due: the next autosave writes it. After `renew_recovery` there is no
|
||||
// older snapshot to fall back on.
|
||||
self.session.autosave_failed(snap.doc);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,6 +334,14 @@ impl PdfCraftApp {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace a document's recovery snapshot with its current state now. After an edit that takes
|
||||
/// content out (Apply Redactions, Sanitize, Remove Hidden Information), the last snapshot still
|
||||
/// holds what went, until the next autosave, and for good if the app is killed before then.
|
||||
pub(crate) fn renew_recovery(&mut self, doc: pdfcraft_engine::DocId) {
|
||||
self.forget_recovery(doc);
|
||||
self.autosave_now();
|
||||
}
|
||||
|
||||
/// Autosave when the interval has passed (called by `logic`, which also runs while hidden).
|
||||
///
|
||||
/// The deadline is monotonic (`web_time::Instant`): egui's input clock does not advance
|
||||
|
||||
@@ -8,7 +8,7 @@ use egui::{Key, Modifiers};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::{NodeT, Queryable};
|
||||
use pdfcraft_engine::Edit;
|
||||
use pdfcraft_ui_egui::{PdfCraftApp, RecoveryStore};
|
||||
use pdfcraft_ui_egui::{Dialog, PdfCraftApp, RecoveryStore};
|
||||
|
||||
fn fixture(n: usize) -> Vec<u8> {
|
||||
let mut objs: Vec<String> = vec!["<< /Type /Catalog /Pages 2 0 R >>".into()];
|
||||
@@ -131,6 +131,63 @@ fn saving_or_closing_clears_the_entry_and_nothing_is_written_for_clean_documents
|
||||
let _ = std::fs::remove_file(out);
|
||||
}
|
||||
|
||||
/// Whether any recovery snapshot in `s` holds `text`, in its bytes or in a decoded stream.
|
||||
fn snapshot_holds(s: &RecoveryStore, text: &[u8]) -> bool {
|
||||
let found = |data: &[u8]| data.windows(text.len()).any(|w| w == text);
|
||||
s.list().iter().filter_map(|m| s.read(&m.key).ok()).any(|bytes| {
|
||||
found(&bytes)
|
||||
|| pdfcraft_cos::Document::open(std::sync::Arc::new(bytes)).is_ok_and(|doc| {
|
||||
doc.scan_objects().any(|(_, o)| matches!(&*o, pdfcraft_cos::Object::Stream(st) if st.decoded().is_ok_and(|d| found(&d))))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Applying redactions replaces the recovery snapshot at once: the one autosaved before still holds
|
||||
/// what the marks covered, until the next autosave, and for good after a crash.
|
||||
#[test]
|
||||
fn applying_redactions_replaces_the_recovery_snapshot_at_once() {
|
||||
let s = store("redact");
|
||||
let mut h = harness(s.clone());
|
||||
h.state_mut().open_bytes("a.pdf", None, fixture(1)).unwrap();
|
||||
assert!(h.state_mut().apply_edit(Edit::RotatePages { pages: vec![0], degrees: 90 }));
|
||||
h.state_mut().autosave_now();
|
||||
assert!(snapshot_holds(&s, b"Confidential"), "the autosave before the redaction holds the page's text");
|
||||
// Redact pages: the current page, then apply.
|
||||
assert!(h.state_mut().execute("redact.mark"));
|
||||
h.run_steps(2);
|
||||
h.state_mut().execute("redact.pages");
|
||||
h.run_steps(2);
|
||||
h.get_by_label("OK").click();
|
||||
h.run_steps(3);
|
||||
h.state_mut().dialog = Some(Dialog::RedactApply);
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Apply").click();
|
||||
h.run_steps(4);
|
||||
assert!(!snapshot_holds(&s, b"Confidential"), "a recovery snapshot still holds the redacted text");
|
||||
assert_eq!(s.list().len(), 1, "the redacted document is autosaved in its place");
|
||||
s.clear();
|
||||
}
|
||||
|
||||
/// An autosave that could not be written leaves the document due, so the next one writes it.
|
||||
/// Renewing the snapshot after a redaction removes the old one first, so nothing else is left.
|
||||
#[test]
|
||||
fn a_failed_autosave_is_written_by_the_next_one() {
|
||||
let s = store("retry");
|
||||
// A file where the folder belongs: the folder can't be made, so nothing can be written.
|
||||
std::fs::write(s.dir(), b"").unwrap();
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.enable_recovery(s.clone());
|
||||
app.open_bytes("a.pdf", None, fixture(1)).unwrap();
|
||||
assert!(app.apply_edit(Edit::RotatePages { pages: vec![0], degrees: 90 }));
|
||||
app.autosave_now();
|
||||
assert!(s.list().is_empty(), "the folder can't be made");
|
||||
std::fs::remove_file(s.dir()).unwrap();
|
||||
app.autosave_now();
|
||||
assert_eq!(s.list().len(), 1, "the snapshot that failed is written by the next autosave");
|
||||
s.clear();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quitting_cleanly_leaves_nothing_behind() {
|
||||
let s = store("quit");
|
||||
|
||||
Reference in new issue
Block a user