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