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