rsynko_rsync/
observation.rs1use crate::*;
2use alux_ext::ext;
3use ambassador::delegatable_trait;
4use rsynko_manager::ChangeKind;
5
6#[delegatable_trait]
8pub trait SyncChangeAlg: RsyncSorts {
9 fn sync_change(&self, path: impl Into<String>, kind: ChangeKind, size: Option<u64>) -> Self::Change;
11}
12
13#[delegatable_trait]
15pub trait SyncObservationAlg: RsyncSorts {
16 fn observed_change(&self, change: Self::Change) -> Self::Observation;
18 fn observed_progress(&self, transferred: u64, percent: u16) -> Self::Observation;
20 fn observed_nothing(&self) -> Self::Observation;
22}
23
24#[delegatable_trait]
26pub trait SyncObservationViewAlg: RsyncSorts {
27 fn observation_change<'a>(&self, observation: &'a Self::Observation) -> Option<&'a Self::Change>;
29 fn observation_progress(&self, observation: &Self::Observation) -> Option<(u64, u16)>;
31}
32
33pub const DELETION_MARK: &str = "*deleting";
35
36pub const FIELD_MARK: char = '|';
38
39#[ext(name = SyncReadExt)]
41pub impl<This> This
42where
43 This: SyncChangeAlg + SyncObservationAlg,
44{
45 fn read_sync_line(&self, line: &str) -> This::Observation {
50 let line = line.trim_end();
51 if let Some(change) = self.read_sync_change(line) {
52 return self.observed_change(change);
53 }
54 read_advance(line).map_or_else(
55 || self.observed_nothing(),
56 |(transferred, percent)| self.observed_progress(transferred, percent),
57 )
58 }
59
60 fn read_sync_change(&self, line: &str) -> Option<This::Change> {
63 let mut fields = line.splitn(3, FIELD_MARK);
64 let flags = fields.next()?.trim();
65 let size = fields.next()?.trim();
66 let path = fields.next()?.trim();
67 if path.is_empty() || touches_folder(flags) {
68 return None;
69 }
70 let kind = read_kind(flags)?;
71 let size = (kind != ChangeKind::Delete).then(|| read_count(size)).flatten();
73 Some(self.sync_change(path, kind, size))
74 }
75}
76
77fn touches_folder(flags: &str) -> bool {
83 let mut marks = flags.chars();
84 let happening = marks.next();
85 let kind_of_path = marks.next();
86 matches!(kind_of_path, Some('d'))
87 && matches!(happening, Some('.' | 'c' | '>' | '<'))
88 && !marks.as_str().starts_with("+++")
89}
90
91fn read_kind(flags: &str) -> Option<ChangeKind> {
97 if flags.starts_with(DELETION_MARK) {
98 return Some(ChangeKind::Delete);
99 }
100 let mut marks = flags.chars();
101 let happening = marks.next()?;
102 if !matches!(happening, '<' | '>' | 'c' | 'h' | '.') {
103 return None;
104 }
105 let _kind_of_path = marks.next()?;
106 let attributes = marks.as_str();
107 if attributes.starts_with("+++") {
108 return Some(ChangeKind::Create);
109 }
110 if happening == '.' && attributes.chars().all(|mark| mark == '.' || mark == ' ') {
111 return Some(ChangeKind::Unchanged);
112 }
113 Some(ChangeKind::Update)
114}
115
116fn read_advance(line: &str) -> Option<(u64, u16)> {
118 let mut fields = line.split_whitespace();
119 let transferred = read_count(fields.next()?)?;
120 let percent = fields.next()?.strip_suffix('%')?.parse::<u16>().ok()?;
121 Some((transferred, percent.min(100)))
122}
123
124fn read_count(text: &str) -> Option<u64> {
126 let digits = text.replace([',', '.', '\u{a0}'], "");
127 (!digits.is_empty() && digits.chars().all(|digit| digit.is_ascii_digit())).then(|| digits.parse().ok()).flatten()
128}