1use crate::*;
2use alux_ext::ext;
3use ambassador::delegatable_trait;
4
5#[delegatable_trait]
7pub trait RsyncEndpointAlg: RsyncSorts {
8 fn endpoint(&self, user: Option<String>, host: Option<String>, path: impl Into<String>) -> Self::Endpoint;
12}
13
14#[delegatable_trait]
16pub trait RsyncEndpointViewAlg: RsyncSorts {
17 fn endpoint_user<'a>(&self, endpoint: &'a Self::Endpoint) -> Option<&'a str>;
19 fn endpoint_host<'a>(&self, endpoint: &'a Self::Endpoint) -> Option<&'a str>;
21 fn endpoint_path<'a>(&self, endpoint: &'a Self::Endpoint) -> &'a str;
23}
24
25pub const RSYNC_SCHEME: &str = "rsync://";
27
28pub const RSYNC_WORD: &str = "rsync";
30
31#[ext(name = RsyncEndpointExt)]
33pub impl<This> This
34where
35 This: RsyncEndpointAlg + RsyncEndpointViewAlg,
36{
37 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 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 ends.next().is_none().then(|| (self.read_endpoint(source), self.read_endpoint(destination)))
71 }
72
73 fn endpoint_remote(&self, endpoint: &This::Endpoint) -> bool {
75 self.endpoint_host(endpoint).is_some()
76 }
77
78 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 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
97fn split_account(authority: &str) -> (Option<String>, &str) {
99 authority.split_once('@').map_or((None, authority), |(user, host)| (Some(user.to_owned()), host))
100}