1use crate::*;
2use alux_ext::ext;
3use ambassador::delegatable_trait;
4
5#[delegatable_trait]
7pub trait RsyncEndpointAlg: RsyncSorts {
8 fn endpoint(
12 &self,
13 user: Option<String>,
14 host: Option<String>,
15 path: impl Into<String>,
16 ) -> Self::Endpoint;
17}
18
19#[delegatable_trait]
21pub trait RsyncEndpointViewAlg: RsyncSorts {
22 fn endpoint_user<'a>(&self, endpoint: &'a Self::Endpoint) -> Option<&'a str>;
24 fn endpoint_host<'a>(&self, endpoint: &'a Self::Endpoint) -> Option<&'a str>;
26 fn endpoint_path<'a>(&self, endpoint: &'a Self::Endpoint) -> &'a str;
28}
29
30pub const RSYNC_SCHEME: &str = "rsync://";
32
33pub const RSYNC_WORD: &str = "rsync";
35
36#[ext(name = RsyncEndpointExt)]
38pub impl<This> This
39where
40 This: RsyncEndpointAlg + RsyncEndpointViewAlg,
41{
42 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 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 ends.next()
78 .is_none()
79 .then(|| (self.read_endpoint(source), self.read_endpoint(destination)))
80 }
81
82 fn endpoint_remote(&self, endpoint: &This::Endpoint) -> bool {
84 self.endpoint_host(endpoint).is_some()
85 }
86
87 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 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
109fn 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}