1use std::path::PathBuf;
2
3use iroh_blobs::Hash;
4use serde::{Deserialize, Serialize};
5
6#[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#[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 #[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#[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#[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#[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 #[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}