1use std::path::PathBuf;
2
3use iroh_blobs::Hash;
4
5use crate::{
6 Result, SyncwebError,
7 folder::SyncwebFolder,
8 node::{blob_store::BlobStore, docs_engine::DocsEngine},
9};
10
11#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
13#[non_exhaustive]
14pub struct CorruptionInfo {
15 pub path: PathBuf,
16 pub expected_hash: Hash,
17 pub actual_hash: Hash,
18}
19
20#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize)]
22#[non_exhaustive]
23pub struct VerifyResult {
24 pub total: u64,
25 pub verified: u64,
26 pub corrupted: Vec<CorruptionInfo>,
27 pub missing: Vec<PathBuf>,
28}
29
30impl VerifyResult {
31 #[must_use]
32 pub const fn is_valid(&self) -> bool {
33 self.corrupted.is_empty() && self.missing.is_empty()
34 }
35}
36
37#[derive(Clone)]
39pub struct IntegrityChecker {
40 blob_store: BlobStore,
41 docs_engine: DocsEngine,
42}
43
44impl IntegrityChecker {
45 #[must_use]
46 pub const fn new(blob_store: BlobStore, docs_engine: DocsEngine) -> Self {
47 Self {
48 blob_store,
49 docs_engine,
50 }
51 }
52
53 pub async fn verify_folder(&self, folder: &SyncwebFolder) -> Result<VerifyResult> {
59 let entries = self.docs_engine.list_latest(folder.doc()).await?;
60 let mut result = VerifyResult::default();
61 for entry in entries {
62 if entry.key().starts_with(b"sys/") {
63 continue;
64 }
65 result.total = result.total.saturating_add(1);
66 let path = String::from_utf8(entry.key().to_vec())
67 .map(PathBuf::from)
68 .map_err(|error| SyncwebError::operation("folder entry path is not UTF-8", error))?;
69 let expected = entry.content_hash();
70 if !self.blob_store.has(expected).await? {
71 result.missing.push(path);
72 continue;
73 }
74 let bytes = self.blob_store.get(expected).await?;
75 let actual = Hash::from_bytes(*blake3::hash(&bytes).as_bytes());
76 if actual == expected {
77 result.verified = result.verified.saturating_add(1);
78 } else {
79 result.corrupted.push(CorruptionInfo {
80 path,
81 expected_hash: expected,
82 actual_hash: actual,
83 });
84 }
85 }
86 Ok(result)
87 }
88}