syncweb_core/sync/
subscribe.rs1use std::path::{Path, PathBuf};
2
3use iroh_blobs::Hash;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::schedule::BandwidthLimits;
8
9#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
11#[non_exhaustive]
12pub enum AreaFilter {
13 All,
14 Prefix(PathBuf),
15 Glob(String),
16 HashRange([u8; 32], [u8; 32]),
17}
18
19impl AreaFilter {
20 #[must_use]
21 pub fn matches_path(&self, path: &Path) -> bool {
22 match self {
23 Self::Prefix(prefix) => path.starts_with(prefix),
24 Self::Glob(pattern) => globset::Glob::new(pattern).is_ok_and(|glob| glob.compile_matcher().is_match(path)),
25 Self::All | Self::HashRange(_, _) => true,
26 }
27 }
28
29 #[must_use]
30 pub fn matches_hash(&self, hash: &Hash) -> bool {
31 match self {
32 Self::All | Self::Prefix(_) | Self::Glob(_) => true,
33 Self::HashRange(start, end) => {
34 let bytes = hash.as_bytes();
35 bytes >= start && bytes < end
36 }
37 }
38 }
39
40 #[must_use]
41 pub fn matches_entry(&self, path: &Path, hash: &Hash) -> bool {
42 self.matches_path(path) && self.matches_hash(hash)
43 }
44}
45
46#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
48#[non_exhaustive]
49pub struct SubscribeParams {
50 pub ingest_only: bool,
51 pub ignore_session: Option<Uuid>,
52 pub area_filter: Option<AreaFilter>,
53 pub area_of_interest: Option<AreaOfInterest>,
54 pub bandwidth: Option<BandwidthLimits>,
55}
56
57impl SubscribeParams {
58 #[must_use]
59 pub const fn ingest_only() -> Self {
60 Self {
61 ingest_only: true,
62 ignore_session: None,
63 area_filter: None,
64 area_of_interest: None,
65 bandwidth: None,
66 }
67 }
68
69 #[must_use]
70 pub const fn ignore_session(session_id: Uuid) -> Self {
71 Self {
72 ingest_only: false,
73 ignore_session: Some(session_id),
74 area_filter: None,
75 area_of_interest: None,
76 bandwidth: None,
77 }
78 }
79
80 #[must_use]
81 pub fn with_area(mut self, area_filter: AreaFilter) -> Self {
82 self.area_filter = Some(area_filter);
83 self
84 }
85
86 #[must_use]
87 pub fn with_limits(mut self, area: AreaOfInterest) -> Self {
88 self.area_of_interest = Some(area);
89 self
90 }
91
92 #[must_use]
93 pub const fn with_bandwidth_limits(mut self, bandwidth: BandwidthLimits) -> Self {
94 self.bandwidth = Some(bandwidth);
95 self
96 }
97
98 pub fn from_filters(filters: &crate::storage::config::SubscribeFilters) -> crate::error::Result<Self> {
104 let mut params = if filters.ingest_only {
105 Self::ingest_only()
106 } else {
107 Self::default()
108 };
109 if filters.ignore_self {
110 params.ignore_session = Some(Uuid::new_v4());
111 }
112 let area = filters
113 .sync_prefix
114 .clone()
115 .map(AreaFilter::Prefix)
116 .or_else(|| filters.glob.clone().map(AreaFilter::Glob));
117 if let Some(filter) = area.clone() {
118 params = params.with_area(filter);
119 }
120 if filters.max_size.is_some() || filters.max_count.is_some() {
121 let limit_area = area.unwrap_or(AreaFilter::All);
122 let limits = AreaOfInterest::with_limits(
123 limit_area,
124 filters.max_size.unwrap_or(0),
125 filters.max_count.unwrap_or(0),
126 );
127 if limits.is_limit_reached(0, 0) {
128 return Err(crate::error::SyncwebError::InvalidConfig(
129 "subscription limits are already exhausted".to_owned(),
130 ));
131 }
132 params = params.with_limits(limits);
133 }
134 Ok(params)
135 }
136
137 #[must_use]
138 pub fn accepts(&self, path: &Path, hash: &Hash) -> bool {
139 let area_matches = self
140 .area_of_interest
141 .as_ref()
142 .is_none_or(|area| area.area.matches_entry(path, hash));
143 let filter_matches = self
144 .area_filter
145 .as_ref()
146 .is_none_or(|filter| filter.matches_entry(path, hash));
147 area_matches && filter_matches
148 }
149}
150
151#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
153#[non_exhaustive]
154pub struct AreaOfInterest {
155 pub area: AreaFilter,
156 pub max_size: u64,
157 pub max_count: u64,
158}
159
160impl AreaOfInterest {
161 #[must_use]
162 pub const fn unlimited(area: AreaFilter) -> Self {
163 Self {
164 area,
165 max_size: 0,
166 max_count: 0,
167 }
168 }
169
170 #[must_use]
171 pub const fn with_count_limit(area: AreaFilter, max_count: u64) -> Self {
172 Self {
173 area,
174 max_size: 0,
175 max_count,
176 }
177 }
178
179 #[must_use]
180 pub const fn with_size_limit(area: AreaFilter, max_size: u64) -> Self {
181 Self {
182 area,
183 max_size,
184 max_count: 0,
185 }
186 }
187
188 #[must_use]
189 pub const fn with_limits(area: AreaFilter, max_size: u64, max_count: u64) -> Self {
190 Self {
191 area,
192 max_size,
193 max_count,
194 }
195 }
196
197 #[must_use]
198 pub const fn is_limit_reached(&self, synced_count: u64, synced_size: u64) -> bool {
199 (self.max_count > 0 && synced_count >= self.max_count) || (self.max_size > 0 && synced_size >= self.max_size)
200 }
201
202 #[must_use]
203 pub const fn permits(&self, synced_count: u64, synced_size: u64, next_size: u64) -> bool {
204 let next_count = synced_count.saturating_add(1);
205 let next_total_size = synced_size.saturating_add(next_size);
206 (self.max_count == 0 || next_count <= self.max_count)
207 && (self.max_size == 0 || next_total_size <= self.max_size)
208 }
209}