Skip to main content

syncweb_core/sync/
partial_fetch.rs

1use std::path::PathBuf;
2
3use iroh_blobs::Hash;
4use serde::{Deserialize, Serialize};
5
6/// A document blob considered for a partial fetch.
7#[derive(Clone, Debug, Eq, PartialEq)]
8#[non_exhaustive]
9pub struct FetchCandidate {
10    pub path: PathBuf,
11    pub hash: Hash,
12    pub size: u64,
13    pub peer_count: usize,
14    pub local: bool,
15}
16
17impl FetchCandidate {
18    #[must_use]
19    pub fn new(path: impl Into<PathBuf>, hash: Hash, size: u64, peer_count: usize, local: bool) -> Self {
20        Self {
21            path: path.into(),
22            hash,
23            size,
24            peer_count,
25            local,
26        }
27    }
28}
29
30/// Constraints for selecting a subset of folder content.
31#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
32#[non_exhaustive]
33pub struct FetchFilter {
34    pub paths: Option<Vec<PathBuf>>,
35    pub min_size: Option<u64>,
36    pub max_size: Option<u64>,
37    pub min_peers: Option<usize>,
38    pub max_peers: Option<usize>,
39    pub min_count: Option<usize>,
40    pub max_count: Option<usize>,
41}
42
43impl FetchFilter {
44    #[must_use]
45    pub const fn new() -> Self {
46        Self {
47            paths: None,
48            min_size: None,
49            max_size: None,
50            min_peers: None,
51            max_peers: None,
52            min_count: None,
53            max_count: None,
54        }
55    }
56
57    #[must_use]
58    pub fn with_paths(mut self, paths: Vec<PathBuf>) -> Self {
59        self.paths = Some(paths);
60        self
61    }
62
63    #[must_use]
64    pub const fn with_min_size(mut self, size: u64) -> Self {
65        self.min_size = Some(size);
66        self
67    }
68
69    #[must_use]
70    pub const fn with_max_size(mut self, size: u64) -> Self {
71        self.max_size = Some(size);
72        self
73    }
74
75    #[must_use]
76    pub const fn with_min_peers(mut self, peers: usize) -> Self {
77        self.min_peers = Some(peers);
78        self
79    }
80
81    #[must_use]
82    pub const fn with_max_peers(mut self, peers: usize) -> Self {
83        self.max_peers = Some(peers);
84        self
85    }
86
87    #[must_use]
88    pub const fn with_min_count(mut self, count: usize) -> Self {
89        self.min_count = Some(count);
90        self
91    }
92
93    #[must_use]
94    pub const fn with_max_count(mut self, count: usize) -> Self {
95        self.max_count = Some(count);
96        self
97    }
98
99    /// Return candidates satisfying this filter.
100    ///
101    /// Candidates with fewer peers are selected first so a capped fetch
102    /// improves folder seeding rather than repeatedly selecting common blobs.
103    #[must_use]
104    pub fn select(&self, candidates: &[FetchCandidate]) -> Vec<FetchCandidate> {
105        let mut selected = candidates
106            .iter()
107            .filter(|candidate| self.matches(candidate))
108            .cloned()
109            .collect::<Vec<_>>();
110        selected.sort_by(|left, right| {
111            left.peer_count
112                .cmp(&right.peer_count)
113                .then(left.path.cmp(&right.path))
114                .then(left.hash.cmp(&right.hash))
115        });
116        if let Some(max_count) = self.max_count {
117            selected.truncate(max_count);
118        }
119        selected
120    }
121
122    #[must_use]
123    pub fn matches(&self, candidate: &FetchCandidate) -> bool {
124        self.paths.as_ref().is_none_or(|paths| {
125            paths
126                .iter()
127                .any(|path| candidate.path == *path || candidate.path.starts_with(path))
128        }) && self.min_size.is_none_or(|size| candidate.size >= size)
129            && self.max_size.is_none_or(|size| candidate.size <= size)
130            && self.min_peers.is_none_or(|peers| candidate.peer_count >= peers)
131            && self.max_peers.is_none_or(|peers| candidate.peer_count <= peers)
132    }
133}
134
135/// Controls whether a fetch reconciles all content or only matching content.
136#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
137#[non_exhaustive]
138pub enum FetchStrategy {
139    #[default]
140    All,
141    Filter(FetchFilter),
142}
143
144impl FetchStrategy {
145    #[must_use]
146    pub const fn filter(filter: FetchFilter) -> Self {
147        Self::Filter(filter)
148    }
149
150    #[must_use]
151    pub fn select(&self, candidates: &[FetchCandidate]) -> Vec<FetchCandidate> {
152        match self {
153            Self::All => candidates.to_vec(),
154            Self::Filter(filter) => filter.select(candidates),
155        }
156    }
157}
158
159/// Per-blob availability information used by the health command.
160#[derive(Clone, Debug, Eq, PartialEq)]
161#[non_exhaustive]
162pub struct BlobHealth {
163    pub path: PathBuf,
164    pub hash: Hash,
165    pub size: u64,
166    pub peer_count: usize,
167    pub local: bool,
168}
169
170/// Aggregate availability counters for a folder.
171#[derive(Clone, Debug, Eq, PartialEq)]
172#[non_exhaustive]
173pub struct HealthReport {
174    pub total: usize,
175    pub well_seeded: usize,
176    pub under_seeded: usize,
177    pub unseeded: usize,
178    pub least_seeded: Vec<BlobHealth>,
179}
180
181impl HealthReport {
182    /// Build a report using `well_seeded_threshold` as the minimum healthy peer count.
183    #[must_use]
184    pub fn from_candidates(candidates: &[FetchCandidate], well_seeded_threshold: usize) -> Self {
185        let mut least_seeded = candidates
186            .iter()
187            .map(|candidate| BlobHealth {
188                path: candidate.path.clone(),
189                hash: candidate.hash,
190                size: candidate.size,
191                peer_count: candidate.peer_count,
192                local: candidate.local,
193            })
194            .collect::<Vec<_>>();
195        least_seeded.sort_by(|left, right| left.peer_count.cmp(&right.peer_count).then(left.path.cmp(&right.path)));
196        let total = candidates.len();
197        let well_seeded = candidates
198            .iter()
199            .filter(|candidate| candidate.peer_count >= well_seeded_threshold)
200            .count();
201        let unseeded = candidates.iter().filter(|candidate| candidate.peer_count == 0).count();
202        Self {
203            total,
204            well_seeded,
205            under_seeded: total.saturating_sub(well_seeded).saturating_sub(unseeded),
206            unseeded,
207            least_seeded,
208        }
209    }
210}