ftp_cmd_list_parse/entry/
unix.rs

1use std::convert::{TryFrom, TryInto};
2
3use ::regex::Regex;
4
5use super::*;
6
7lazy_static! {
8    static ref RELIST: Regex = Regex::new(
9        r"(?x)
10        ^(?P<type>[bcdelfmpSs-])
11        (?P<permission>((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-])))
12        (?P<acl>([\+|@]))?\s+
13        (?P<inodes>\d+)\s+
14        (?P<owner>\d+|[A-Z]{1}\w+\s+[A-Z]{1}\w+|\w+|\S+)\s+
15        (?P<group>\d+|[A-Z]{1}[\w\\]+\s+[A-Z]{1}\w+|\w+|\S+)\s+
16        (?P<size>\d+(?:,\s*\d*)?)\s+
17        (?P<timestamp>((?P<month1>\w{3})\s+
18            (?P<date1>\d{1,2})\s+
19            (?P<hour>\d{1,2}):(?P<minute>\d{2}))|
20            ((?P<month2>\w{3})\s+
21                (?P<date2>\d{1,2})\s+
22                (?P<year>\d{4})))\s+
23        (?P<name>.+)$
24    "
25    )
26    .unwrap();
27}
28
29/// Represents entry from Unix-like FTP server.
30#[derive(Debug)]
31pub struct FtpEntryUnix {
32    kind: FtpEntryKind,
33    name: String,
34    size: usize,
35    // pub date: NaiveDateTime,
36    date_str: String,
37    /// For symlink entries, this is the symlink's target.
38    pub target: Option<String>,
39    /// True if the sticky bit is set for this entry.
40    pub sticky: bool,
41    /// The various permissions for this entry.
42    pub permissions: FtpEntryPermissions,
43    /// Marks extra ACL permission for this entry.
44    pub acl: bool,
45    /// The user name or ID that this entry belongs to.
46    pub owner: String,
47    /// The group name or ID that this entry belongs to.
48    pub group: String,
49    pub pointer: Option<String>,
50}
51
52impl FtpEntryUnix {
53    /// Represents parsed string as entry of an Unix-type FTP server.
54    pub fn new(string: &str) -> Option<Self> {
55        FtpEntryUnix::try_from(string).ok()
56    }
57}
58
59impl FtpEntryInfo for FtpEntryUnix {
60    fn kind(&self) -> FtpEntryKind {
61        self.kind
62    }
63
64    fn name(&self) -> &str {
65        &self.name
66    }
67
68    fn size(&self) -> usize {
69        self.size
70    }
71
72    // fn date(&self) -> NaiveDateTime {
73    //     self.date
74    // }
75
76    fn date_str(&self) -> &str {
77        &self.date_str
78    }
79}
80
81impl TryFrom<&str> for FtpEntryUnix {
82    type Error = ();
83
84    fn try_from(value: &str) -> Result<Self, Self::Error> {
85        if let Some(caps) = RELIST.captures(&value) {
86            let kind: FtpEntryKind = caps
87                .name("type")
88                .ok_or("this is not a list unix format")
89                .map_or_else(|_| Err(()), |v| v.as_str().try_into().map_err(|_| ()))?;
90
91            let (sticky, permissions) = caps
92                .name("permission")
93                .ok_or("this is not a list unix format")
94                .map_or_else(
95                    |_| Err(()),
96                    |v| {
97                        let mut permission = v.as_str().to_string();
98                        Ok((
99                            match permission.chars().last() {
100                                Some(t) if t == 't' || t == 'T' => {
101                                    permission.pop();
102                                    permission.push(if t == 't' { 'x' } else { '-' });
103                                    true
104                                }
105                                _ => false,
106                            },
107                            permission,
108                        ))
109                    },
110                )?;
111
112            let acl = caps.name("acl").map(|v| v.as_str() == "+").unwrap_or(false);
113            let owner = caps.name("owner").unwrap().as_str().to_string();
114            let group = caps.name("group").unwrap().as_str().to_string();
115
116            let (size, pointer) = caps.name("size").map_or((0, None), |v| {
117                if v.as_str().chars().any(|c| c == ',') {
118                    (
119                        0,
120                        Some(v.as_str().chars().filter(|c| !c.is_whitespace()).collect()),
121                    )
122                } else {
123                    (v.as_str().parse().unwrap_or(0), None)
124                }
125            });
126
127            let date_str = caps
128                .name("timestamp")
129                .unwrap()
130                .as_str()
131                .split_whitespace()
132                .collect::<Vec<_>>()
133                .join(" ");
134
135            // let date = None
136            //     .or_else(|| {
137            //         let date = vec![
138            //             caps.name("month1"),
139            //             caps.name("date1"),
140            //             caps.name("hour"),
141            //             caps.name("minute"),
142            //         ];
143
144            //         let date = if date.iter().any(|m| m.is_none()) {
145            //             return None;
146            //         } else {
147            //             date.iter().map(|m| m.unwrap().as_str()).collect::<Vec<_>>()
148            //         };
149
150            //         let now = ::chrono::Utc::now();
151            //         let year = format!("{}", now.format("%Y"));
152
153            //         let date = format!("{}-{}-{}T{}:{}", year, date[0], date[1], date[2], date[3]);
154            //         let date = match NaiveDateTime::parse_from_str(&date, "%Y-%b-%_dT%_H:%_M") {
155            //             Ok(mut date) => {
156            //                 // If the date is in the past but no more than 6 months old, year
157            //                 // isn't displayed and doesn't have to be the current year.
158            //                 //
159            //                 // If the date is in the future (less than an hour from now), year
160            //                 // isn't displayed and doesn't have to be the current year.
161            //                 // That second case is much more rare than the first and less annoying.
162            //                 // It's impossible to fix without knowing about the server's timezone,
163            //                 // so we just don't do anything about it.
164            //                 //
165            //                 // If we're here with a time that is more than 28 hours into the
166            //                 // future (1 hour + maximum timezone offset which is 27 hours),
167            //                 // there is a problem -- we should be in the second conditional block
168            //                 if date.timestamp() - now.timestamp() > 100_800_000 {
169            //                     date = date.with_year(date.year() - 1).unwrap();
170            //                 }
171
172            //                 // If we're here with a time that is more than 6 months old, there's
173            //                 // a problem as well.
174            //                 // Maybe local & remote servers aren't on the same timezone (with remote
175            //                 // ahead of local)
176            //                 // For instance, remote is in 2014 while local is still in 2013. In
177            //                 // this case, a date like 01/01/13 02:23 could be detected instead of
178            //                 // 01/01/14 02:23
179            //                 // Our trigger point will be 3600*24*31*6 (since we already use 31
180            //                 // as an upper bound, no need to add the 27 hours timezone offset)
181            //                 if now.timestamp() - date.timestamp() > 16_070_400_000 {
182            //                     date = date.with_year(date.year() - 1).unwrap();
183            //                 }
184
185            //                 Some(date)
186            //             }
187            //             Err(_) => None,
188            //         };
189
190            //         date
191            //     })
192            //     .or_else(|| {
193            //         let date = vec![caps.name("year"), caps.name("month2"), caps.name("date2")];
194
195            //         let date = if date.iter().any(|m| m.is_none()) {
196            //             return None;
197            //         } else {
198            //             date.iter().map(|m| m.unwrap().as_str()).collect::<Vec<_>>()
199            //         };
200
201            //         let date = format!("{}-{}-{}", date[0], date[1], date[2]);
202            //         NaiveDate::parse_from_str(&date, "%Y-%b-%_d")
203            //             .map(|date| date.and_hms(0, 0, 0))
204            //             .ok()
205            //     });
206
207            // let date = match date {
208            //     Some(date) => date,
209            //     _ => return Err(()),
210            // };
211
212            let (name, target) = {
213                let name = caps.name("name").unwrap().as_str();
214                if kind == FtpEntryKind::Symlink {
215                    let mut s1 = name.split(" -> ");
216                    (
217                        s1.next().unwrap().to_string(),
218                        s1.next().map(|v| v.to_string()),
219                    )
220                } else {
221                    (name.to_string(), None)
222                }
223            };
224
225            return Ok(Self {
226                kind,
227                name,
228                target,
229                sticky,
230                permissions: FtpEntryPermissions(permissions),
231                acl,
232                owner,
233                group,
234                size,
235                pointer,
236                date_str,
237            });
238        }
239
240        Err(())
241    }
242}