//! ## Ftp_transfer
//!
//! `ftp_transfer` is the module which provides the implementation for the FTP/FTPS file transfer
/*
*
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see .
*
*/
// Dependencies
extern crate chrono;
extern crate ftp4;
extern crate regex;
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::utils::parser::{parse_datetime, parse_lstime};
// Includes
use ftp4::native_tls::TlsConnector;
use ftp4::FtpStream;
use regex::Regex;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::SystemTime;
/// ## FtpFileTransfer
///
/// Ftp file transfer struct
pub struct FtpFileTransfer {
stream: Option,
ftps: bool,
}
impl FtpFileTransfer {
/// ### new
///
/// Instantiates a new `FtpFileTransfer`
pub fn new(ftps: bool) -> FtpFileTransfer {
FtpFileTransfer { stream: None, ftps }
}
/// ### parse_list_line
///
/// Parse a line of LIST command output and instantiates an FsEntry from it
fn parse_list_line(&self, path: &Path, line: &str) -> Result {
// Try to parse using UNIX syntax
match self.parse_unix_list_line(path, line) {
Ok(entry) => Ok(entry),
Err(_) => match self.parse_dos_list_line(path, line) {
// If UNIX parsing fails, try DOS
Ok(entry) => Ok(entry),
Err(_) => Err(()),
},
}
}
/// ### parse_unix_list_line
///
/// Try to parse a "LIST" output command line in UNIX format.
/// Returns error if syntax is not UNIX compliant.
/// UNIX syntax has the following syntax:
/// {FILE_TYPE}{UNIX_PEX} {HARD_LINKS} {USER} {GROUP} {SIZE} {DATE} {FILENAME}
/// -rw-r--r-- 1 cvisintin staff 4968 27 Dic 10:46 CHANGELOG.md
fn parse_unix_list_line(&self, path: &Path, line: &str) -> Result {
// Prepare list regex
// NOTE: about this damn regex
lazy_static! {
static ref LS_RE: Regex = Regex::new(r#"^([\-ld])([\-rwxs]{9})\s+(\d+)\s+(\w+)\s+(\w+)\s+(\d+)\s+(\w{3}\s+\d{1,2}\s+(?:\d{1,2}:\d{1,2}|\d{4}))\s+(.+)$"#).unwrap();
}
// Apply regex to result
match LS_RE.captures(line) {
// String matches regex
Some(metadata) => {
// NOTE: metadata fmt: (regex, file_type, permissions, link_count, uid, gid, filesize, mtime, filename)
// Expected 7 + 1 (8) values: + 1 cause regex is repeated at 0
if metadata.len() < 8 {
return Err(());
}
// Collect metadata
// Get if is directory and if is symlink
let (is_dir, _is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str() {
"-" => (false, false),
"l" => (false, true),
"d" => (true, false),
_ => return Err(()), // Ignore special files
};
// Check string length (unix pex)
if metadata.get(2).unwrap().as_str().len() < 9 {
return Err(());
}
// Get unix pex
let unix_pex: (u8, u8, u8) = {
let owner_pex: u8 = {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[0..3].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
}
}
}
count
};
let group_pex: u8 = {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[3..6].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
}
}
}
count
};
let others_pex: u8 = {
let mut count: u8 = 0;
for (i, c) in metadata.get(2).unwrap().as_str()[6..9].chars().enumerate() {
match c {
'-' => {}
_ => {
count += match i {
0 => 4,
1 => 2,
2 => 1,
_ => 0,
}
}
}
}
count
};
(owner_pex, group_pex, others_pex)
};
// Parse mtime and convert to SystemTime
let mtime: SystemTime = match parse_lstime(
metadata.get(7).unwrap().as_str(),
"%b %d %Y",
"%b %d %H:%M",
) {
Ok(t) => t,
Err(_) => SystemTime::UNIX_EPOCH,
};
// Get uid
let uid: Option = match metadata.get(4).unwrap().as_str().parse::() {
Ok(uid) => Some(uid),
Err(_) => None,
};
// Get gid
let gid: Option = match metadata.get(5).unwrap().as_str().parse::() {
Ok(gid) => Some(gid),
Err(_) => None,
};
// Get filesize
let filesize: usize = match metadata.get(6).unwrap().as_str().parse::() {
Ok(sz) => sz,
Err(_) => 0,
};
let file_name: String = String::from(metadata.get(8).unwrap().as_str());
// Check if file_name is '.' or '..'
if file_name.as_str() == "." || file_name.as_str() == ".." {
return Err(());
}
let mut abs_path: PathBuf = PathBuf::from(path);
abs_path.push(file_name.as_str());
// get extension
let extension: Option = match abs_path.as_path().extension() {
None => None,
Some(s) => Some(String::from(s.to_string_lossy())),
};
// Return
// Push to entries
Ok(match is_dir {
true => FsEntry::Directory(FsDirectory {
name: file_name,
abs_path,
last_change_time: mtime,
last_access_time: mtime,
creation_time: mtime,
readonly: false,
symlink: None,
user: uid,
group: gid,
unix_pex: Some(unix_pex),
}),
false => FsEntry::File(FsFile {
name: file_name,
abs_path,
last_change_time: mtime,
last_access_time: mtime,
creation_time: mtime,
size: filesize,
ftype: extension,
readonly: false,
symlink: None,
user: uid,
group: gid,
unix_pex: Some(unix_pex),
}),
})
}
None => Err(()),
}
}
/// ### parse_dos_list_line
///
/// Try to parse a "LIST" output command line in DOS format.
/// Returns error if syntax is not DOS compliant.
/// DOS syntax has the following syntax:
/// {DATE} {TIME} { | SIZE} {FILENAME}
/// 10-19-20 03:19PM pub
/// 04-08-14 03:09PM 403 readme.txt
fn parse_dos_list_line(&self, path: &Path, line: &str) -> Result {
// Prepare list regex
// NOTE: you won't find this regex on the internet. It seems I'm the only person in the world who needs this
lazy_static! {
static ref DOS_RE: Regex = Regex::new(
r#"^(\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}\s*[AP]M)\s+()?([\d,]*)\s+(.+)$"#
)
.unwrap();
}
// Apply regex to result
match DOS_RE.captures(line) {
// String matches regex
Some(metadata) => {
// NOTE: metadata fmt: (regex, date_time, is_dir?, file_size?, file_name)
// Expected 4 + 1 (5) values: + 1 cause regex is repeated at 0
if metadata.len() < 5 {
return Err(());
}
// Parse date time
let time: SystemTime =
match parse_datetime(metadata.get(1).unwrap().as_str(), "%d-%m-%y %I:%M%p") {
Ok(t) => t,
Err(_) => SystemTime::UNIX_EPOCH, // Don't return error
};
// Get if is a directory
let is_dir: bool = metadata.get(2).is_some();
// Get file size
let file_size: usize = match is_dir {
true => 0, // If is directory, filesize is 0
false => match metadata.get(3) {
// If is file, parse arg 3
Some(val) => match val.as_str().parse::() {
Ok(sz) => sz,
Err(_) => 0,
},
None => 0, // Should not happen
},
};
// Get file name
let file_name: String = String::from(metadata.get(4).unwrap().as_str());
// Get absolute path
let mut abs_path: PathBuf = PathBuf::from(path);
abs_path.push(file_name.as_str());
// Get extension
let extension: Option = match abs_path.as_path().extension() {
None => None,
Some(s) => Some(String::from(s.to_string_lossy())),
};
// Return entry
Ok(match is_dir {
true => FsEntry::Directory(FsDirectory {
name: file_name,
abs_path,
last_change_time: time,
last_access_time: time,
creation_time: time,
readonly: false,
symlink: None,
user: None,
group: None,
unix_pex: None,
}),
false => FsEntry::File(FsFile {
name: file_name,
abs_path,
last_change_time: time,
last_access_time: time,
creation_time: time,
size: file_size,
ftype: extension,
readonly: false,
symlink: None,
user: None,
group: None,
unix_pex: None,
}),
})
}
None => Err(()), // Invalid syntax
}
}
}
impl FileTransfer for FtpFileTransfer {
/// ### connect
///
/// Connect to the remote server
fn connect(
&mut self,
address: String,
port: u16,
username: Option,
password: Option,
) -> Result