lfsx_server/storage/
verify.rs1use std::path::Path;
2
3use futures_util::StreamExt;
4use serde::Serialize;
5use sha2::{Digest, Sha256};
6use tokio::fs;
7
8use super::LocalStore;
9use crate::error::Error;
10use crate::namespace::Namespace;
11
12#[derive(Debug, Default, Serialize, PartialEq, Eq)]
13pub struct VerifyReport {
14 pub checked: u64,
15 pub bytes: u64,
16 pub corrupt: Vec<String>,
17 pub unreadable: Vec<String>,
18 pub incomplete: bool,
22}
23
24impl LocalStore {
25 pub async fn verify(&self, ns: &Namespace) -> Result<VerifyReport, Error> {
31 let mut report = VerifyReport::default();
32
33 let Ok(mut prefixes) = fs::read_dir(self.root.join(ns.org()).join(ns.repo())).await else {
34 return Ok(report);
35 };
36
37 while let Some(prefix) = prefixes.next_entry().await? {
38 let mut fanouts = match fs::read_dir(prefix.path()).await {
39 Ok(fanouts) => fanouts,
40 Err(error) => {
41 tracing::warn!(path = ?prefix.path(), %error, "could not be listed");
42 report.incomplete = true;
43 continue;
44 }
45 };
46
47 loop {
48 match fanouts.next_entry().await {
49 Ok(Some(fanout)) => {
50 self.verify_directory(&fanout.path(), ns, &mut report).await;
51 }
52 Ok(None) => break,
53 Err(error) => {
54 tracing::warn!(path = ?prefix.path(), %error, "listing stopped early");
55 report.incomplete = true;
56 break;
57 }
58 }
59 }
60 }
61
62 Ok(report)
63 }
64
65 async fn verify_directory(&self, directory: &Path, ns: &Namespace, report: &mut VerifyReport) {
66 let mut entries = match fs::read_dir(directory).await {
67 Ok(entries) => entries,
68 Err(error) => {
69 tracing::warn!(?directory, %error, "could not be listed");
70 report.incomplete = true;
71 return;
72 }
73 };
74
75 loop {
76 let entry = match entries.next_entry().await {
77 Ok(Some(entry)) => entry,
78 Ok(None) => return,
79 Err(error) => {
80 tracing::warn!(?directory, %error, "listing stopped early");
81 report.incomplete = true;
82 return;
83 }
84 };
85
86 let oid = entry.file_name().to_string_lossy().into_owned();
87 if Self::validate_oid(&oid).is_err() {
88 continue;
89 }
90
91 report.checked += 1;
92
93 match self.digest_of(ns, &oid).await {
94 Ok((digest, read)) => {
95 report.bytes += read;
96
97 if digest != oid {
98 report.corrupt.push(oid);
99 }
100 }
101 Err(error) => {
105 tracing::warn!(oid, %error, "object could not be read");
106 report.unreadable.push(oid);
107 }
108 }
109 }
110 }
111
112 async fn digest_of(&self, ns: &Namespace, oid: &str) -> Result<(String, u64), Error> {
113 let object = self.open(ns, oid).await?;
114 let size = object.size();
115
116 let mut hasher = Sha256::new();
117 let mut read = 0u64;
118 let mut chunks = object.stream(0, size).await?;
119
120 while let Some(chunk) = chunks.next().await {
121 let chunk = chunk?;
122 read += chunk.len() as u64;
123 hasher.update(&chunk);
124 }
125
126 Ok((hex::encode(hasher.finalize()), read))
127 }
128}
129
130#[cfg(test)]
131mod tests;