fix(sync): compare small files by content to detect quick rewrites
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 27m40s

File times are only as fine as the kernel clock tick. A manifest rewritten
right after a check with the same length (one page replaced by another)
looked unchanged, so the change was never picked up. Files up to 64 KiB
(manifest, open marker, small pages) now also carry a content hash.

Showed up as a flaky test_page_still_being_synced_is_tried_again in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
haraldandClaude Opus 5.5 committed 2026-10-27 20:23:20 +01:00
1 parent c95cde8ec8
commit 4ca21483e5
1 file changed
+22 -3
+22 -3
View File
@@ -19,7 +19,21 @@ use crate::storage::{self, NotebookManifest, OpenMarker, PageData};
/// Time between two checks. /// Time between two checks.
pub const INTERVAL: Duration = Duration::from_secs(3); pub const INTERVAL: Duration = Duration::from_secs(3);
type Fingerprint = (Option<SystemTime>, u64); /// Modification time, size and, for small files, a hash of the content.
type Fingerprint = (Option<SystemTime>, u64, Option<u64>);
/// Files up to this size are also compared by content: file times are only as fine as the
/// kernel clock tick, so a file rewritten right after a check with the same length (e.g. a
/// manifest with one page replaced by another) would otherwise look unchanged.
const HASHED_FILE_SIZE: u64 = 64 * 1024;
fn content_hash(path: &Path) -> Option<u64> {
use std::hash::{Hash, Hasher};
let bytes = fs::read(path).ok()?;
let mut hasher = std::collections::hash_map::DefaultHasher::new();
bytes.hash(&mut hasher);
Some(hasher.finish())
}
/// What a check found and took over. /// What a check found and took over.
#[derive(Debug, Default, Clone, PartialEq)] #[derive(Debug, Default, Clone, PartialEq)]
@@ -61,8 +75,13 @@ fn scan(dir: &Path) -> HashMap<String, Fingerprint> {
return None; return None;
} }
let meta = entry.metadata().ok()?; let meta = entry.metadata().ok()?;
meta.is_file() if !meta.is_file() {
.then(|| (name, (meta.modified().ok(), meta.len()))) return None;
}
let hash = (meta.len() <= HASHED_FILE_SIZE)
.then(|| content_hash(&entry.path()))
.flatten();
Some((name, (meta.modified().ok(), meta.len(), hash)))
}) })
.collect() .collect()
} }