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    #[must_use]
99    pub fn accepts(&self, path: &Path, hash: &Hash) -> bool {
100        let area_matches = self
101            .area_of_interest
102            .as_ref()
103            .is_none_or(|area| area.area.matches_entry(path, hash));
104        let filter_matches = self
105            .area_filter
106            .as_ref()
107            .is_none_or(|filter| filter.matches_entry(path, hash));
108        area_matches && filter_matches
109    }
110}
111
112/// A subscription area with aggregate entry and byte limits.
113#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
114#[non_exhaustive]
115pub struct AreaOfInterest {
116    pub area: AreaFilter,
117    pub max_size: u64,
118    pub max_count: u64,
119}
120
121impl AreaOfInterest {
122    #[must_use]
123    pub const fn unlimited(area: AreaFilter) -> Self {
124        Self {
125            area,
126            max_size: 0,
127            max_count: 0,
128        }
129    }
130
131    #[must_use]
132    pub const fn with_count_limit(area: AreaFilter, max_count: u64) -> Self {
133        Self {
134            area,
135            max_size: 0,
136            max_count,
137        }
138    }
139
140    #[must_use]
141    pub const fn with_size_limit(area: AreaFilter, max_size: u64) -> Self {
142        Self {
143            area,
144            max_size,
145            max_count: 0,
146        }
147    }
148
149    #[must_use]
150    pub const fn with_limits(area: AreaFilter, max_size: u64, max_count: u64) -> Self {
151        Self {
152            area,
153            max_size,
154            max_count,
155        }
156    }
157
158    #[must_use]
159    pub const fn is_limit_reached(&self, synced_count: u64, synced_size: u64) -> bool {
160        (self.max_count > 0 && synced_count >= self.max_count) || (self.max_size > 0 && synced_size >= self.max_size)
161    }
162
163    #[must_use]
164    pub const fn permits(&self, synced_count: u64, synced_size: u64, next_size: u64) -> bool {
165        let next_count = synced_count.saturating_add(1);
166        let next_total_size = synced_size.saturating_add(next_size);
167        (self.max_count == 0 || next_count <= self.max_count)
168            && (self.max_size == 0 || next_total_size <= self.max_size)
169    }
170}