Skip to main content

lfsx_server/storage/
verify.rs

1use futures_util::StreamExt;
2use serde::Serialize;
3use sha2::{Digest, Sha256};
4
5use super::LocalStore;
6use crate::error::Error;
7use crate::namespace::Namespace;
8use crate::oid::Oid;
9
10#[derive(Debug, Default, Serialize, PartialEq, Eq)]
11pub struct VerifyReport {
12    pub checked: u64,
13    pub bytes: u64,
14    pub corrupt: Vec<String>,
15    pub unreadable: Vec<String>,
16    // An audit that could not see the whole repository must not read like one
17    // that found nothing wrong. Silence is the result here, so anything that
18    // makes the silence partial has to be said out loud.
19    pub incomplete: bool,
20}
21
22impl LocalStore {
23    // Every object is named after the digest of its own contents, so the store
24    // checks itself without a manifest: the property a restore is confirmed
25    // with. Compression at rest is what took it away from `sha256sum`: the file
26    // is no longer the bytes it is named after, so reading it back has to go
27    // through the same path a download does.
28    pub async fn verify(&self, ns: &Namespace) -> Result<VerifyReport, Error> {
29        let walk = self.objects_of(ns).await;
30        let mut report = VerifyReport {
31            incomplete: !walk.complete,
32            ..VerifyReport::default()
33        };
34
35        for found in walk.objects {
36            report.checked += 1;
37
38            match self.digest_of(ns, &found.oid).await {
39                Ok((digest, read)) => {
40                    report.bytes += read;
41
42                    if digest != found.oid.as_str() {
43                        report.corrupt.push(found.oid.to_string());
44                    }
45                }
46                // A file that cannot be read is its own kind of answer, and the
47                // one a failing disk gives first. Reporting it as corrupt would
48                // send an operator looking for the wrong problem.
49                Err(error) => {
50                    tracing::warn!(oid = %found.oid, %error, "object could not be read");
51                    report.unreadable.push(found.oid.to_string());
52                }
53            }
54        }
55
56        Ok(report)
57    }
58
59    async fn digest_of(&self, ns: &Namespace, oid: &Oid) -> Result<(String, u64), Error> {
60        let object = self.open(ns, oid).await?;
61        let size = object.size();
62
63        let mut hasher = Sha256::new();
64        let mut read = 0u64;
65        let mut chunks = object.stream(0, size).await?;
66
67        while let Some(chunk) = chunks.next().await {
68            let chunk = chunk?;
69            read += chunk.len() as u64;
70            hasher.update(&chunk);
71        }
72
73        Ok((hex::encode(hasher.finalize()), read))
74    }
75}
76
77#[cfg(test)]
78mod tests;