rs_transfer 8.0.0

A simple crate to handle downloads and uploads on multiple providers
Documentation
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())
}