Skip to main content

rsynko_rsync/
observation.rs

1use crate::*;
2use alux_ext::ext;
3use ambassador::delegatable_trait;
4use rsynko_manager::ChangeKind;
5
6/// Provides the carrier and constructor for one path a transfer would change.
7#[delegatable_trait]
8pub trait SyncChangeAlg: RsyncSorts {
9    /// Defines one change from the path it names, what happens to it, and what it moves.
10    fn sync_change(
11        &self,
12        path: impl Into<String>,
13        kind: ChangeKind,
14        size: Option<u64>,
15    ) -> Self::Change;
16}
17
18/// Provides the carriers and constructors for what a running transfer states.
19#[delegatable_trait]
20pub trait SyncObservationAlg: RsyncSorts {
21    /// Defines the observation of one path the transfer would change, or has changed.
22    fn observed_change(&self, change: Self::Change) -> Self::Observation;
23    /// Defines the observation of how far the whole transfer has come.
24    fn observed_progress(&self, transferred: u64, percent: u16) -> Self::Observation;
25    /// Defines the observation of a line naming nothing this specification reads.
26    fn observed_nothing(&self) -> Self::Observation;
27}
28
29/// Specifies what one observation of a running transfer states.
30#[delegatable_trait]
31pub trait SyncObservationViewAlg: RsyncSorts {
32    /// Observes the changed path, exactly when the observation states one.
33    fn observation_change<'a>(
34        &self,
35        observation: &'a Self::Observation,
36    ) -> Option<&'a Self::Change>;
37    /// Observes the bytes moved and the share completed, exactly when the observation states them.
38    fn observation_progress(&self, observation: &Self::Observation) -> Option<(u64, u16)>;
39}
40
41/// Names the mark a removal states instead of a change flag.
42pub const DELETION_MARK: &str = "*deleting";
43
44/// Names the field separator each stated path is written with.
45pub const FIELD_MARK: char = '|';
46
47/// Derives what one line a running transfer wrote states.
48#[ext(name = SyncReadExt)]
49pub impl<This> This
50where
51    This: SyncChangeAlg + SyncObservationAlg,
52{
53    /// Reads one line the transfer program wrote as what it states.
54    ///
55    /// A change is stated in three fields: the flags naming what happens, the byte count, and the
56    /// path. Anything else on the stream is progress, or is nothing this specification reads.
57    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    /// Reads one itemized line as the change it denotes, and states nothing for a line naming
69    /// only that a folder was opened on the way to what is inside it.
70    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        // A removal is stated without a size, because the transfer never looked at what it holds.
80        let size = (kind != ChangeKind::Delete)
81            .then(|| read_count(size))
82            .flatten();
83        Some(self.sync_change(path, kind, size))
84    }
85}
86
87/// Observes whether the flags name a folder that is only having its own attributes brought up
88/// to date.
89///
90/// Transferring a folder's contents restates the folder itself, which a reader has already been
91/// told about by the paths inside it. A folder that did not exist before is stated as it is.
92fn 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
101/// Reads the itemized flags as what would happen to the path they name.
102///
103/// A removal states itself in words. Everything else states eleven marks: what is happening, what
104/// kind of path it is, and then one mark per attribute that differs. All-new content states
105/// pluses; nothing differing states dots.
106fn 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
126/// Reads one progress line as the bytes it has moved and the share it has completed.
127fn 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
134/// Reads a count the transfer program wrote, which it groups for a reader.
135fn 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}