1use std::collections::HashMap;
2use std::path::PathBuf;
3
4use iroh_blobs::Hash;
5use serde::{Deserialize, Serialize};
6
7use crate::error::Result;
8use crate::folder::SyncwebFolder;
9use crate::node::iroh_node::IrohNode;
10
11pub async fn fetch_selected_content(node: &IrohNode, folder: &SyncwebFolder, strategy: &FetchStrategy) -> Result<u64> {
23 let mut candidates = Vec::new();
24 for entry in folder.content_entries().await? {
25 let hash = entry.content_hash();
26 let local = folder.has_local(hash).await?;
27 let path = PathBuf::from(String::from_utf8_lossy(entry.key()).into_owned());
28 candidates.push(FetchCandidate::new(path, hash, entry.content_len(), 0, local));
29 }
30 let selected = strategy.select(&candidates);
31 let peers = node
32 .topic_tracker()
33 .find_peers(folder.namespace_id())
34 .await
35 .unwrap_or_default();
36 let mut bytes = 0_u64;
37 for candidate in selected {
38 if candidate.local {
39 continue;
40 }
41 for peer in &peers {
42 if node
43 .blob_store()
44 .force_fetch_from_peer(node.endpoint(), peer, candidate.hash)
45 .await
46 .is_ok()
47 {
48 bytes = bytes.saturating_add(candidate.size);
49 break;
50 }
51 }
52 }
53 Ok(bytes)
54}
55
56#[derive(Clone, Debug, Eq, PartialEq)]
58#[non_exhaustive]
59pub struct FetchCandidate {
60 pub path: PathBuf,
61 pub hash: Hash,
62 pub size: u64,
63 pub peer_count: usize,
64 pub local: bool,
65}
66
67impl FetchCandidate {
68 #[must_use]
69 pub fn new(path: impl Into<PathBuf>, hash: Hash, size: u64, peer_count: usize, local: bool) -> Self {
70 Self {
71 path: path.into(),
72 hash,
73 size,
74 peer_count,
75 local,
76 }
77 }
78}
79
80#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
82#[non_exhaustive]
83pub struct FetchFilter {
84 pub paths: Option<Vec<PathBuf>>,
85 pub min_size: Option<u64>,
86 pub max_size: Option<u64>,
87 pub min_peers: Option<usize>,
88 pub max_peers: Option<usize>,
89 pub min_count: Option<usize>,
90 pub max_count: Option<usize>,
91}
92
93impl FetchFilter {
94 #[must_use]
95 pub const fn new() -> Self {
96 Self {
97 paths: None,
98 min_size: None,
99 max_size: None,
100 min_peers: None,
101 max_peers: None,
102 min_count: None,
103 max_count: None,
104 }
105 }
106
107 #[must_use]
108 pub fn with_paths(mut self, paths: Vec<PathBuf>) -> Self {
109 self.paths = Some(paths);
110 self
111 }
112
113 #[must_use]
114 pub const fn with_min_size(mut self, size: u64) -> Self {
115 self.min_size = Some(size);
116 self
117 }
118
119 #[must_use]
120 pub const fn with_max_size(mut self, size: u64) -> Self {
121 self.max_size = Some(size);
122 self
123 }
124
125 #[must_use]
126 pub const fn with_min_peers(mut self, peers: usize) -> Self {
127 self.min_peers = Some(peers);
128 self
129 }
130
131 #[must_use]
132 pub const fn with_max_peers(mut self, peers: usize) -> Self {
133 self.max_peers = Some(peers);
134 self
135 }
136
137 #[must_use]
138 pub const fn with_min_count(mut self, count: usize) -> Self {
139 self.min_count = Some(count);
140 self
141 }
142
143 #[must_use]
144 pub const fn with_max_count(mut self, count: usize) -> Self {
145 self.max_count = Some(count);
146 self
147 }
148
149 #[must_use]
154 pub fn select(&self, candidates: &[FetchCandidate]) -> Vec<FetchCandidate> {
155 let mut selected = candidates
156 .iter()
157 .filter(|candidate| self.matches(candidate))
158 .cloned()
159 .collect::<Vec<_>>();
160 selected.sort_by(|left, right| {
161 left.peer_count
162 .cmp(&right.peer_count)
163 .then(left.path.cmp(&right.path))
164 .then(left.hash.cmp(&right.hash))
165 });
166 if let Some(max_count) = self.max_count {
167 selected.truncate(max_count);
168 }
169 selected
170 }
171
172 #[must_use]
173 pub fn matches(&self, candidate: &FetchCandidate) -> bool {
174 self.paths.as_ref().is_none_or(|paths| {
175 paths
176 .iter()
177 .any(|path| candidate.path == *path || candidate.path.starts_with(path))
178 }) && self.min_size.is_none_or(|size| candidate.size >= size)
179 && self.max_size.is_none_or(|size| candidate.size <= size)
180 && self.min_peers.is_none_or(|peers| candidate.peer_count >= peers)
181 && self.max_peers.is_none_or(|peers| candidate.peer_count <= peers)
182 }
183}
184
185#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
187#[non_exhaustive]
188pub enum FetchStrategy {
189 #[default]
190 All,
191 Filter(FetchFilter),
192}
193
194impl FetchStrategy {
195 #[must_use]
196 pub const fn filter(filter: FetchFilter) -> Self {
197 Self::Filter(filter)
198 }
199
200 #[must_use]
201 pub fn select(&self, candidates: &[FetchCandidate]) -> Vec<FetchCandidate> {
202 match self {
203 Self::All => candidates.to_vec(),
204 Self::Filter(filter) => filter.select(candidates),
205 }
206 }
207}
208
209#[derive(Clone, Debug, Eq, PartialEq)]
211#[non_exhaustive]
212pub struct BlobHealth {
213 pub path: PathBuf,
214 pub hash: Hash,
215 pub size: u64,
216 pub peer_count: usize,
217 pub local: bool,
218}
219
220#[derive(Clone, Debug, Eq, PartialEq)]
222#[non_exhaustive]
223pub struct HealthReport {
224 pub total: usize,
225 pub well_seeded: usize,
226 pub under_seeded: usize,
227 pub unseeded: usize,
228 pub least_seeded: Vec<BlobHealth>,
229}
230
231impl HealthReport {
232 #[must_use]
234 pub fn from_candidates(candidates: &[FetchCandidate], well_seeded_threshold: usize) -> Self {
235 let mut least_seeded = candidates
236 .iter()
237 .map(|candidate| BlobHealth {
238 path: candidate.path.clone(),
239 hash: candidate.hash,
240 size: candidate.size,
241 peer_count: candidate.peer_count,
242 local: candidate.local,
243 })
244 .collect::<Vec<_>>();
245 least_seeded.sort_by(|left, right| left.peer_count.cmp(&right.peer_count).then(left.path.cmp(&right.path)));
246 let total = candidates.len();
247 let well_seeded = candidates
248 .iter()
249 .filter(|candidate| candidate.peer_count >= well_seeded_threshold)
250 .count();
251 let unseeded = candidates.iter().filter(|candidate| candidate.peer_count == 0).count();
252 Self {
253 total,
254 well_seeded,
255 under_seeded: total.saturating_sub(well_seeded).saturating_sub(unseeded),
256 unseeded,
257 least_seeded,
258 }
259 }
260
261 #[must_use]
267 pub fn from_candidates_with_peers_per_hash(
268 candidates: &[FetchCandidate],
269 peers_per_hash: &HashMap<Hash, usize>,
270 well_seeded_threshold: usize,
271 ) -> Self {
272 let enriched: Vec<FetchCandidate> = candidates
273 .iter()
274 .map(|c| {
275 let peers = peers_per_hash.get(&c.hash).copied().unwrap_or(c.peer_count);
276 FetchCandidate::new(&c.path, c.hash, c.size, peers, c.local)
277 })
278 .collect();
279 Self::from_candidates(&enriched, well_seeded_threshold)
280 }
281}