use crate::{
endpoint::FtpEndpoint,
error::Error,
list::{TreeItem, TreeItemKind, TreeItems, TreeList, join_path, strip_relative_path},
};
use async_trait::async_trait;
use chrono::{DateTime, Datelike, NaiveDateTime, Utc};
use regex::{Captures, Regex};
use std::{convert::TryFrom, str::FromStr, time::SystemTime};
const PATH_SEPARATOR: char = '/';
#[async_trait]
impl TreeList for FtpEndpoint {
async fn list(&self, root_path: &str, prefix: Option<&str>) -> Result<TreeItems, Error> {
let absolute_path = Self::get_absolute_path(root_path, prefix, PATH_SEPARATOR);
let ftp_stream = self.connection();
let list_result = ftp_stream
.lock()
.unwrap()
.list(Some(&absolute_path))
.map(|entries| {
entries
.iter()
.filter_map(|entry| {
let dir_entry = FtpDirEntry::new(root_path, &absolute_path, entry);
let item = TreeItem::try_from(dir_entry);
if let Err(error) = &item {
log::warn!("Cannot access entry in {absolute_path}: {error:?}");
}
item.ok()
})
.collect()
});
let items = list_result.unwrap_or_else(|error| {
log::warn!("Cannot access {absolute_path} content: {error:?}");
TreeItems::default()
});
Ok(items)
}
}
#[derive(Debug)]
struct FtpDirEntry {
root_path: String,
directory: String,
line: String,
}
impl FtpDirEntry {
fn new(root_path: &str, directory: &str, entry: &str) -> Self {
Self {
root_path: root_path.to_string(),
directory: directory.to_string(),
line: entry.to_string(),
}
}
}
const DIR_ENTRY_REGEX: &str = r"^(?P<type>[-dl])(?P<permissions>[-rwxs]{9}) *(?P<nb_links>\d*) *(?P<user>[^ ]*) *(?P<group>[^ ]*) *(?P<size>\d*) *(?P<month>[^ ]*) *(?P<day>[^ ]*) *((?P<year>\d{4})|(?P<time>\d{2}:\d{2})) *(?P<entry_name>.*)$";
impl TryFrom<FtpDirEntry> for TreeItem {
type Error = Error;
fn try_from(dir_entry: FtpDirEntry) -> Result<Self, Self::Error> {
let regex = Regex::new(DIR_ENTRY_REGEX)?;
let line = &dir_entry.line;
let captures = regex
.captures(line)
.ok_or_else(|| Error::Other(format!("Could not parse entry from: {line}")))?;
let kind = extract_from_captures(&captures, "type", line)?;
let kind = match kind.as_str() {
"-" => Ok(TreeItemKind::File),
"d" => Ok(TreeItemKind::Folder),
"l" => Ok(TreeItemKind::Link),
other => Err(Error::Other(format!(
"Unsupported '{other}' entry type: {line}"
))),
}?;
let size = extract_from_captures(&captures, "size", line)?;
let size = u64::from_str(size.as_str()).map_err(|error| Error::from((size.as_str(), error)))?;
let entry_name = extract_from_captures(&captures, "entry_name", line)?;
let directory_path =
strip_relative_path(&dir_entry.root_path, &dir_entry.directory, PATH_SEPARATOR);
let relative_path = join_path(&directory_path, &entry_name, PATH_SEPARATOR);
let year = extract_from_captures(&captures, "year", line)
.unwrap_or_else(|_| chrono::Utc::now().year().to_string());
let month = extract_from_captures(&captures, "month", line)?;
let day = extract_from_captures(&captures, "day", line)?;
let time =
extract_from_captures(&captures, "time", line).unwrap_or_else(|_| "00:00".to_string());
let last_update =
NaiveDateTime::parse_from_str(&format!("{year} {month} {day} {time}"), "%Y %b %d %H:%M")
.map_err(|error| Error::from((line.as_str(), error)))?;
let last_update =
SystemTime::from(DateTime::<Utc>::from_naive_utc_and_offset(last_update, Utc));
Ok(Self::new(
kind,
&relative_path,
&dir_entry.root_path,
size,
last_update,
None,
PATH_SEPARATOR,
))
}
}
fn extract_from_captures(
captures: &Captures,
group_name: &str,
line: &str,
) -> Result<String, Error> {
let capture = captures
.name(group_name)
.ok_or_else(|| Error::Other(format!("Could not parse entry {group_name} from: {line}")))?;
Ok(capture.as_str().to_string())
}