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}