Add PDF archive store, raw-body IPC and export file helpers

The searchable PDF can be archived content-addressed under the local data
dir with its sha256 and layout fingerprint; a different PDF for an archived
invoice is refused and reads verify the hash. Export writes go through a
raw-body command that remembers the last folder; reveal-in-folder and open
fall back to xdg-open / explorer. Adds a Windows-safe export file name
builder. Migration M4 adds the archive columns and last export dir.
This commit is contained in:
2026-10-04 05:54:26 +05:30
parent f6c81a7e7f
commit 2ade618fb5
17 changed files with 891 additions and 36 deletions
+30 -2
View File
@@ -10,7 +10,7 @@ const MAX_BACKUPS: usize = 10;
const BACKUP_PREFIX: &str = "voiced-pre-v";
/// Highest schema version, i.e. the number of entries in `migrations()`.
const LATEST_VERSION: i64 = 3;
const LATEST_VERSION: i64 = 4;
#[derive(Debug, thiserror::Error)]
pub enum DbError {
@@ -174,8 +174,15 @@ const M3: &str = r#"
ALTER TABLE app_settings ADD COLUMN render_prefs TEXT NOT NULL DEFAULT '';
"#;
/// Version 4: the archived PDF's layout fingerprint and time, and the last export folder.
const M4: &str = r#"
ALTER TABLE invoices ADD COLUMN archived_fingerprint TEXT;
ALTER TABLE invoices ADD COLUMN archived_at TEXT;
ALTER TABLE app_settings ADD COLUMN last_export_dir TEXT NOT NULL DEFAULT '';
"#;
fn migrations() -> Migrations<'static> {
Migrations::new(vec![M::up(SCHEMA), M::up(M2), M::up(M3)])
Migrations::new(vec![M::up(SCHEMA), M::up(M2), M::up(M3), M::up(M4)])
}
/// Open (creating if needed) the database at `path` and bring it to the latest schema.
@@ -541,6 +548,27 @@ CREATE INDEX IF NOT EXISTS idx_invoices_created ON invoices(created_at DESC);
assert!(backups_in(&backups).is_empty());
}
#[test]
fn m4_adds_archive_and_export_dir_columns() {
let conn = open_in_memory().unwrap();
let dir: String = conn
.query_row("SELECT last_export_dir FROM app_settings WHERE id = 1", [], |r| r.get(0))
.unwrap();
assert_eq!(dir, "");
// The columns exist and are nullable on invoices (no rows needed to prove it).
conn.prepare("SELECT archived_pdf_sha256, archived_fingerprint, archived_at FROM invoices")
.unwrap();
let notnull: i64 = conn
.query_row(
"SELECT SUM(\"notnull\") FROM pragma_table_info('invoices')
WHERE name IN ('archived_fingerprint', 'archived_at')",
[],
|r| r.get(0),
)
.unwrap();
assert_eq!(notnull, 0);
}
#[test]
fn render_prefs_column_defaults_to_empty_and_round_trips() {
let conn = open_in_memory().unwrap();