feat: end-to-end integrity checks with self-healing; website; installer and Windows fixes
Store::verify compares the whole SQLite projection, every table row for row, against a fresh projection of the journal; verify_and_heal rebuilds a drifted database (keeping the old file) at every server start and via the new books.verify tool, which also re-hashes every blob. The GUI checks the server's database read-only and every stored file, and shows the result. rebuild() also drops stale WAL/SHM files. site/: the wanderbooks website (just site, just publish). install.sh: OS detection (Windows points to WSL2), C compiler and git checks, WANDERBOOKS_REPO. Windows: no directory fsync off Unix; .exe-aware lookup. Repo justfile: check, build, install, demo, site. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MtYEaR6M6Ybdhm6X98BWNu
This commit is contained in:
25 files changed
+808
-41
No files matched your search
@@ -143,3 +143,45 @@ fn walk(dir: &std::path::Path) -> Vec<(String, u64)> {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_snapshot_checks_the_writers_database_and_files() {
|
||||
use ledger_store::DiskCheck;
|
||||
let dir = test_dir("disk");
|
||||
let mut store = Store::create(&dir, Currency::USD).unwrap();
|
||||
let cash = store
|
||||
.add_account("1000", "Cash", AccountType::Asset, None, false)
|
||||
.unwrap();
|
||||
let equity = store
|
||||
.add_account("3000", "Equity", AccountType::Equity, None, false)
|
||||
.unwrap();
|
||||
let e = post(&mut store, 1, cash, equity, 100);
|
||||
store
|
||||
.attach_receipt(Some(e), "r.png", "image/png", b"receipt-bytes", None)
|
||||
.unwrap();
|
||||
drop(store);
|
||||
let snap = Snapshot::load(&dir).unwrap();
|
||||
assert_eq!(snap.check_disk(), DiskCheck::Matches);
|
||||
assert_eq!(snap.verify_blobs(), (1, vec![]));
|
||||
// Someone edits the database behind the journal's back.
|
||||
let conn = rusqlite::Connection::open(dir.join(ledger_store::DB_FILE)).unwrap();
|
||||
conn.execute(
|
||||
"UPDATE accounts SET name = 'Petty cash' WHERE code = '1000'",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
drop(conn);
|
||||
match snap.check_disk() {
|
||||
DiskCheck::Differs(why) => assert!(why.contains("accounts"), "{why}"),
|
||||
other => panic!("drift not seen: {other:?}"),
|
||||
}
|
||||
// A damaged file is caught too.
|
||||
let blob = walk(&dir.join("blobs"))
|
||||
.into_iter()
|
||||
.map(|(p, _)| p)
|
||||
.find(|p| !p.ends_with(".tmp"))
|
||||
.unwrap();
|
||||
std::fs::write(&blob, b"tampered").unwrap();
|
||||
let (ok, bad) = snap.verify_blobs();
|
||||
assert_eq!((ok, bad.len()), (0, 1));
|
||||
}
|
||||
Reference in new issue
Block a user