1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use iroh_blobs::Hash;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Debug, Eq, PartialEq)]
9#[non_exhaustive]
10pub struct FetchCandidate {
11 pub path: PathBuf,
12 pub hash: Hash,
13 pub size: u64,
14 pub peer_count: usize,
15 pub local: bool,
16}
17
18impl FetchCandidate {
19 #[must_use]
20 pub fn new(path: impl Into<PathBuf>, hash: Hash, size: u64, peer_count: usize, local: bool) -> Self {
21 Self {
22 path: path.into(),
23 hash,
24 size,
25 peer_count,
26 local,
27 }
28 }
29}
30
31#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
33#[non_exhaustive]
34pub struct FetchFilter {
35 pub paths: Option<Vec<PathBuf>>,
36 pub min_size: Option<u64>,
37 pub max_size: Option<u64>,
38 pub min_peers: Option<usize>,
39 pub max_peers: Option<usize>,
40 pub min_count: Option<usize>,
41 pub max_count: Option<usize>,
42}
43
44impl FetchFilter {
45 #[must_use]
46 pub const fn new() -> Self {
47 Self {
48 paths: None,
49 min_size: None,
50 max_size: None,
51 min_peers: None,
52 max_peers: None,
53 min_count: None,
54 max_count: None,
55 }
56 }
57
58 #[must_use]
59 pub fn with_paths(mut self, paths: Vec<PathBuf>) -> Self {
60 self.paths = Some(paths);
61 self
62 }
63
64 #[must_use]
65 pub const fn with_min_size(mut self, size: u64) -> Self {
66 self.min_size = Some(size);
67 self
68 }
69
70 #[must_use]
71 pub const fn with_max_size(mut self, size: u64) -> Self {
72 self.max_size = Some(size);
73 self
74 }
75
76 #[must_use]
77 pub const fn with_min_peers(mut self, peers: usize) -> Self {
78 self.min_peers = Some(peers);
79 self
80 }
81
82 #[must_use]
83 pub const fn with_max_peers(mut self, peers: usize) -> Self {
84 self.max_peers = Some(peers);
85 self
86 }
87
88 #[must_use]
89 pub const fn with_min_count(mut self, count: usize) -> Self {
90 self.min_count = Some(count);
91 self
92 }
93
94 #[must_use]
95 pub const fn with_max_count(mut self, count: usize) -> Self {
96 self.max_count = Some(count);
97 self
98 }
99
100 #[must_use]
105 pub fn select(&self, candidates: &[FetchCandidate]) -> Vec<FetchCandidate> {
106 let mut selected = candidates
107 .iter()
108 .filter(|candidate| self.matches(candidate))
109 .cloned()
110 .collect::<Vec<_>>();
111 selected.sort_by(|left, right| {
112 left.peer_count
113 .cmp(&right.peer_count)
114 .then(left.path.cmp(&right.path))
115 .then(left.hash.cmp(&right.hash))
116 });
117 if let Some(max_count) = self.max_count {
118 selected.truncate(max_count);
119 }
120 selected
121 }
122
123 #[must_use]
124 pub fn matches(&self, candidate: &FetchCandidate) -> bool {
125 self.paths.as_ref().is_none_or(|paths| {
126 paths
127 .iter()
128 .any(|path| candidate.path == *path || candidate.path.starts_with(path))
129 }) && self.min_size.is_none_or(|size| candidate.size >= size)
130 && self.max_size.is_none_or(|size| candidate.size <= size)
131 && self.min_peers.is_none_or(|peers| candidate.peer_count >= peers)
132 && self.max_peers.is_none_or(|peers| candidate.peer_count <= peers)
133 }
134}
135
136#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
138#[non_exhaustive]
139pub enum FetchStrategy {
140 #[default]
141 All,
142 Filter(FetchFilter),
143}
144
145impl FetchStrategy {
146 #[must_use]
147 pub const fn filter(filter: FetchFilter) -> Self {
148 Self::Filter(filter)
149 }
150
151 #[must_use]
152 pub fn select(&self, candidates: &[FetchCandidate]) -> Vec<FetchCandidate> {
153 match self {
154 Self::All => candidates.to_vec(),
155 Self::Filter(filter) => filter.select(candidates),
156 }
157 }
158}
159
160#[derive(Clone, Debug, Eq, PartialEq)]
162#[non_exhaustive]
163pub struct BlobHealth {
164 pub path: PathBuf,
165 pub hash: Hash,
166 pub size: u64,
167 pub peer_count: usize,
168 pub local: bool,
169}
170
171#[derive(Clone, Debug, Eq, PartialEq)]
173#[non_exhaustive]
174pub struct HealthReport {
175 pub total: usize,
176 pub well_seeded: usize,
177 pub under_seeded: usize,
178 pub unseeded: usize,
179 pub least_seeded: Vec<BlobHealth>,
180}
181
182impl HealthReport {
183 #[must_use]
185 pub fn from_candidates(candidates: &[FetchCandidate], well_seeded_threshold: usize) -> Self {
186 let mut least_seeded = candidates
187 .iter()
188 .map(|candidate| BlobHealth {
189 path: candidate.path.clone(),
190 hash: candidate.hash,
191 size: candidate.size,
192 peer_count: candidate.peer_count,
193 local: candidate.local,
194 })
195 .collect::<Vec<_>>();
196 least_seeded.sort_by(|left, right| left.peer_count.cmp(&right.peer_count).then(left.path.cmp(&right.path)));
197 let total = candidates.len();
198 let well_seeded = candidates
199 .iter()
200 .filter(|candidate| candidate.peer_count >= well_seeded_threshold)
201 .count();
202 let unseeded = candidates.iter().filter(|candidate| candidate.peer_count == 0).count();
203 Self {
204 total,
205 well_seeded,
206 under_seeded: total.saturating_sub(well_seeded).saturating_sub(unseeded),
207 unseeded,
208 least_seeded,
209 }
210 }
211
212 #[must_use]
218 pub fn from_candidates_with_peer_counts(
219 candidates: &[FetchCandidate],
220 peer_counts: &HashMap<Hash, usize>,
221 well_seeded_threshold: usize,
222 ) -> Self {
223 let enriched: Vec<FetchCandidate> = candidates
224 .iter()
225 .map(|c| {
226 let peers = peer_counts.get(&c.hash).copied().unwrap_or(c.peer_count);
227 FetchCandidate::new(&c.path, c.hash, c.size, peers, c.local)
228 })
229 .collect();
230 Self::from_candidates(&enriched, well_seeded_threshold)
231 }
232}