Skip to main content

rsynko_rsync/
endpoint.rs

1use crate::*;
2use alux_ext::ext;
3use ambassador::delegatable_trait;
4
5/// Provides the carrier and constructor for one end of a transfer.
6#[delegatable_trait]
7pub trait RsyncEndpointAlg: RsyncSorts {
8    /// Defines one endpoint from the account, the machine, and the path it names.
9    ///
10    /// An endpoint naming no machine names a path on this one.
11    fn endpoint(
12        &self,
13        user: Option<String>,
14        host: Option<String>,
15        path: impl Into<String>,
16    ) -> Self::Endpoint;
17}
18
19/// Specifies what one endpoint states about itself.
20#[delegatable_trait]
21pub trait RsyncEndpointViewAlg: RsyncSorts {
22    /// Observes the account the far end is reached as, when one was stated.
23    fn endpoint_user<'a>(&self, endpoint: &'a Self::Endpoint) -> Option<&'a str>;
24    /// Observes the machine the path rests on, and states nothing when the path is here.
25    fn endpoint_host<'a>(&self, endpoint: &'a Self::Endpoint) -> Option<&'a str>;
26    /// Observes the path itself.
27    fn endpoint_path<'a>(&self, endpoint: &'a Self::Endpoint) -> &'a str;
28}
29
30/// Names the scheme a daemon endpoint states itself with.
31pub const RSYNC_SCHEME: &str = "rsync://";
32
33/// Names the word a whole transfer command begins with.
34pub const RSYNC_WORD: &str = "rsync";
35
36/// Derives an endpoint from what somebody submitted, and states one back.
37#[ext(name = RsyncEndpointExt)]
38pub impl<This> This
39where
40    This: RsyncEndpointAlg + RsyncEndpointViewAlg,
41{
42    /// Reads one endpoint from the text somebody submitted.
43    ///
44    /// Three shapes name a path on another machine: a daemon URL, and a machine and a path
45    /// separated by a colon with nothing path-like before it. A text naming any other scheme
46    /// names a resource somewhere else, and a text naming neither names a path here.
47    fn read_endpoint(&self, input: &str) -> This::Endpoint {
48        let input = input.trim();
49        if let Some(rest) = input.strip_prefix(RSYNC_SCHEME) {
50            let (authority, path) = rest.split_once('/').unwrap_or((rest, ""));
51            let (user, host) = split_account(authority);
52            return self.endpoint(user, Some(host.to_owned()), format!("/{path}"));
53        }
54        if !input.contains("://")
55            && let Some((authority, path)) = input.split_once(':')
56            && !authority.contains('/')
57            && !authority.is_empty()
58        {
59            let (user, host) = split_account(authority);
60            return self.endpoint(user, Some(host.to_owned()), path);
61        }
62        self.endpoint(None, None, input)
63    }
64
65    /// Reads one submitted line as the two ends of a transfer, when it names two.
66    ///
67    /// A transfer is written the way it is run: what comes from, then what it comes to. Anything
68    /// naming how rather than what — the program itself, and the arguments given to it — is
69    /// already stated by the transfer, so a whole command can be submitted and still be read.
70    fn read_transfer(&self, line: &str) -> Option<(This::Endpoint, This::Endpoint)> {
71        let mut ends = line
72            .split_whitespace()
73            .filter(|word| !word.starts_with('-') && *word != RSYNC_WORD);
74        let source = ends.next()?;
75        let destination = ends.next()?;
76        // A line naming a third end names something this specification cannot read.
77        ends.next()
78            .is_none()
79            .then(|| (self.read_endpoint(source), self.read_endpoint(destination)))
80    }
81
82    /// Observes whether the path rests on another machine.
83    fn endpoint_remote(&self, endpoint: &This::Endpoint) -> bool {
84        self.endpoint_host(endpoint).is_some()
85    }
86
87    /// States the endpoint the way the transfer program is given it.
88    fn endpoint_text(&self, endpoint: &This::Endpoint) -> String {
89        let path = self.endpoint_path(endpoint);
90        let Some(host) = self.endpoint_host(endpoint) else {
91            return path.to_owned();
92        };
93        match self.endpoint_user(endpoint) {
94            Some(user) => format!("{user}@{host}:{path}"),
95            None => format!("{host}:{path}"),
96        }
97    }
98
99    /// Names what the path ends with, without the path that leads to it.
100    fn endpoint_name<'a>(&self, endpoint: &'a This::Endpoint) -> &'a str {
101        let path = self.endpoint_path(endpoint);
102        path.trim_end_matches('/')
103            .rsplit('/')
104            .find(|segment| !segment.is_empty())
105            .unwrap_or(path)
106    }
107}
108
109/// Splits an account from the machine it reaches.
110fn split_account(authority: &str) -> (Option<String>, &str) {
111    authority
112        .split_once('@')
113        .map_or((None, authority), |(user, host)| {
114            (Some(user.to_owned()), host)
115        })
116}