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(
11 &self,
12 path: impl Into<String>,
13 kind: ChangeKind,
14 size: Option<u64>,
15 ) -> Self::Change;
16}
17
18#[delegatable_trait]
20pub trait SyncObservationAlg: RsyncSorts {
21 fn observed_change(&self, change: Self::Change) -> Self::Observation;
23 fn observed_progress(&self, transferred: u64, percent: u16) -> Self::Observation;
25 fn observed_nothing(&self) -> Self::Observation;
27}
28
29#[delegatable_trait]
31pub trait SyncObservationViewAlg: RsyncSorts {
32 fn observation_change<'a>(
34 &self,
35 observation: &'a Self::Observation,
36 ) -> Option<&'a Self::Change>;
37 fn observation_progress(&self, observation: &Self::Observation) -> Option<(u64, u16)>;
39}
40
41pub const DELETION_MARK: &str = "*deleting";
43
44pub const FIELD_MARK: char = '|';
46
47#[ext(name = SyncReadExt)]
49pub impl<This> This
50where
51 This: SyncChangeAlg + SyncObservationAlg,
52{
53 fn read_sync_line(&self, line: &str) -> This::Observation {
58 let line = line.trim_end();
59 if let Some(change) = self.read_sync_change(line) {
60 return self.observed_change(change);
61 }
62 read_advance(line).map_or_else(
63 || self.observed_nothing(),
64 |(transferred, percent)| self.observed_progress(transferred, percent),
65 )
66 }
67
68 fn read_sync_change(&self, line: &str) -> Option<This::Change> {
71 let mut fields = line.splitn(3, FIELD_MARK);
72 let flags = fields.next()?.trim();
73 let size = fields.next()?.trim();
74 let path = fields.next()?.trim();
75 if path.is_empty() || touches_folder(flags) {
76 return None;
77 }
78 let kind = read_kind(flags)?;
79 let size = (kind != ChangeKind::Delete)
81 .then(|| read_count(size))
82 .flatten();
83 Some(self.sync_change(path, kind, size))
84 }
85}
86
87fn touches_folder(flags: &str) -> bool {
93 let mut marks = flags.chars();
94 let happening = marks.next();
95 let kind_of_path = marks.next();
96 matches!(kind_of_path, Some('d'))
97 && matches!(happening, Some('.' | 'c' | '>' | '<'))
98 && !marks.as_str().starts_with("+++")
99}
100
101fn read_kind(flags: &str) -> Option<ChangeKind> {
107 if flags.starts_with(DELETION_MARK) {
108 return Some(ChangeKind::Delete);
109 }
110 let mut marks = flags.chars();
111 let happening = marks.next()?;
112 if !matches!(happening, '<' | '>' | 'c' | 'h' | '.') {
113 return None;
114 }
115 let _kind_of_path = marks.next()?;
116 let attributes = marks.as_str();
117 if attributes.starts_with("+++") {
118 return Some(ChangeKind::Create);
119 }
120 if happening == '.' && attributes.chars().all(|mark| mark == '.' || mark == ' ') {
121 return Some(ChangeKind::Unchanged);
122 }
123 Some(ChangeKind::Update)
124}
125
126fn read_advance(line: &str) -> Option<(u64, u16)> {
128 let mut fields = line.split_whitespace();
129 let transferred = read_count(fields.next()?)?;
130 let percent = fields.next()?.strip_suffix('%')?.parse::<u16>().ok()?;
131 Some((transferred, percent.min(100)))
132}
133
134fn read_count(text: &str) -> Option<u64> {
136 let digits = text.replace([',', '.', '\u{a0}'], "");
137 (!digits.is_empty() && digits.chars().all(|digit| digit.is_ascii_digit()))
138 .then(|| digits.parse().ok())
139 .flatten()
140}