Skip to main content

syncweb_core/sync/
subscribe.rs

1use std::path::{Path, PathBuf};
2
3use iroh_blobs::Hash;
4use serde::{Deserialize, Serialize};
5use uuid::Uuid;
6
7use crate::schedule::BandwidthLimits;
8
9/// Restricts subscription events to a portion of a folder.
10#[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/// Controls which changes are delivered to a folder subscriber.
47#[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    /// Build subscription parameters from the persisted `join --subscribe` filters.
99    ///
100    /// # Errors
101    ///
102    /// Returns an error if the configured limits are already exhausted.
103    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/// A subscription area with aggregate entry and byte limits.
152#[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}