//! ## SCP_Transfer
//!
//! `scps_transfer` is the module which provides the implementation for the SCP 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 regex;
extern crate ssh2;
// Locals
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::utils::lstime_to_systime;
// Includes
use regex::Regex;
use ssh2::{Channel, Session};
use std::io::{BufReader, BufWriter, Read, Write};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
/// ## ScpFileTransfer
///
/// SCP file transfer structure
pub struct ScpFileTransfer {
session: Option,
wrkdir: PathBuf,
}
impl ScpFileTransfer {
/// ### new
///
/// Instantiates a new ScpFileTransfer
pub fn new() -> ScpFileTransfer {
ScpFileTransfer {
session: None,
wrkdir: PathBuf::from("~"),
}
}
/// ### parse_ls_output
///
/// Parse a line of `ls -l` output and tokenize the output into a `FsEntry`
fn parse_ls_output(&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 = 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 = 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 = 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 lstime_to_systime(
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,
};
// Get link and name
let (file_name, symlink_path): (String, Option) = match is_symlink {
true => self.get_name_and_link(metadata.get(8).unwrap().as_str()),
false => (String::from(metadata.get(8).unwrap().as_str()), None),
};
// 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);
let extension: Option = match abs_path.as_path().extension() {
None => None,
Some(s) => Some(String::from(s.to_string_lossy())),
};
abs_path.push(file_name.as_str());
// Return
// Push to entries
Ok(match is_dir {
true => FsEntry::Directory(FsDirectory {
name: file_name,
abs_path: abs_path,
last_change_time: mtime,
last_access_time: mtime,
creation_time: mtime,
readonly: false,
symlink: symlink_path,
user: uid,
group: gid,
unix_pex: Some(unix_pex),
}),
false => FsEntry::File(FsFile {
name: file_name,
abs_path: abs_path,
last_change_time: mtime,
last_access_time: mtime,
creation_time: mtime,
size: filesize,
ftype: extension,
readonly: false,
symlink: symlink_path,
user: uid,
group: gid,
unix_pex: Some(unix_pex),
}),
})
}
None => Err(()),
}
}
/// ### get_name_and_link
///
/// Returns from a `ls -l` command output file name token, the name of the file and the symbolic link (if there is any)
fn get_name_and_link(&self, token: &str) -> (String, Option) {
let tokens: Vec<&str> = token.split(" -> ").collect();
let filename: String = String::from(*tokens.get(0).unwrap());
let symlink: Option = match tokens.get(1) {
Some(s) => Some(PathBuf::from(s)),
None => None,
};
(filename, symlink)
}
/// ### perform_shell_cmd_with
///
/// Perform a shell command, but change directory to specified path first
fn perform_shell_cmd_with_path(
&mut self,
path: &Path,
cmd: &str,
) -> Result {
self.perform_shell_cmd(format!("cd \"{}\"; {}", path.display(), cmd).as_str())
}
/// ### perform_shell_cmd
///
/// Perform a shell command and read the output from shell
/// This operation is, obviously, blocking.
fn perform_shell_cmd(&mut self, cmd: &str) -> Result {
match self.session.as_mut() {
Some(session) => {
// Create channel
let mut channel: Channel = match session.channel_session() {
Ok(ch) => ch,
Err(err) => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("Could not open channel: {}", err),
))
}
};
// Execute command
if let Err(err) = channel.exec(cmd) {
return Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("Could not execute command \"{}\": {}", cmd, err),
));
}
// Read output
let mut output: String = String::new();
match channel.read_to_string(&mut output) {
Ok(_) => {
// Wait close
let _ = channel.wait_close();
Ok(output)
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("Could not read output: {}", err),
)),
}
}
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
}
impl FileTransfer for ScpFileTransfer {
/// ### connect
///
/// Connect to the remote server
fn connect(
&mut self,
address: String,
port: u16,
username: Option,
password: Option,
) -> Result