mirror of
https://github.com/veeso/termscp.git
synced 2026-07-14 12:03:52 +02:00
Removed filetransfer module; migrated to remotefs crate
This commit is contained in:
committed by
Christian Visintin
parent
25dd1b9b0a
commit
df7a4381c4
213
src/filetransfer/builder.rs
Normal file
213
src/filetransfer/builder.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
//! ## builder
|
||||
//!
|
||||
//! Remotefs client builder
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::params::{AwsS3Params, GenericProtocolParams};
|
||||
use super::{FileTransferProtocol, ProtocolParams};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
|
||||
use remotefs::client::{
|
||||
aws_s3::AwsS3Fs,
|
||||
ftp::FtpFs,
|
||||
ssh::{ScpFs, SftpFs, SshOpts},
|
||||
};
|
||||
use remotefs::RemoteFs;
|
||||
|
||||
/// Remotefs builder
|
||||
pub struct Builder;
|
||||
|
||||
impl Builder {
|
||||
/// Build RemoteFs client from protocol and params.
|
||||
///
|
||||
/// if protocol and parameters are inconsistent, the function will panic.
|
||||
pub fn build(
|
||||
protocol: FileTransferProtocol,
|
||||
params: ProtocolParams,
|
||||
config_client: &ConfigClient,
|
||||
) -> Box<dyn RemoteFs> {
|
||||
match (protocol, params) {
|
||||
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => {
|
||||
Box::new(Self::aws_s3_client(params))
|
||||
}
|
||||
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
|
||||
Box::new(Self::ftp_client(params, secure))
|
||||
}
|
||||
(FileTransferProtocol::Scp, ProtocolParams::Generic(params)) => {
|
||||
Box::new(Self::scp_client(params, config_client))
|
||||
}
|
||||
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
|
||||
Box::new(Self::sftp_client(params, config_client))
|
||||
}
|
||||
(protocol, params) => {
|
||||
error!("Invalid params for protocol '{:?}'", protocol);
|
||||
panic!(
|
||||
"Invalid protocol '{:?}' with parameters of type {:?}",
|
||||
protocol, params
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build aws s3 client from parameters
|
||||
fn aws_s3_client(params: AwsS3Params) -> AwsS3Fs {
|
||||
let mut client = AwsS3Fs::new(params.bucket_name, params.region);
|
||||
if let Some(profile) = params.profile {
|
||||
client = client.profile(profile);
|
||||
}
|
||||
client
|
||||
}
|
||||
|
||||
/// Build ftp client from parameters
|
||||
fn ftp_client(params: GenericProtocolParams, secure: bool) -> FtpFs {
|
||||
let mut client = FtpFs::new(params.address, params.port).passive_mode();
|
||||
if let Some(username) = params.username {
|
||||
client = client.username(username);
|
||||
}
|
||||
if let Some(password) = params.password {
|
||||
client = client.password(password);
|
||||
}
|
||||
if secure {
|
||||
client = client.secure(true, true);
|
||||
}
|
||||
client
|
||||
}
|
||||
|
||||
/// Build scp client
|
||||
fn scp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> ScpFs {
|
||||
Self::build_ssh_opts(params, config_client).into()
|
||||
}
|
||||
|
||||
/// Build sftp client
|
||||
fn sftp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> SftpFs {
|
||||
Self::build_ssh_opts(params, config_client).into()
|
||||
}
|
||||
|
||||
/// Build ssh options from generic protocol params and client configuration
|
||||
fn build_ssh_opts(params: GenericProtocolParams, config_client: &ConfigClient) -> SshOpts {
|
||||
let mut opts = SshOpts::new(params.address)
|
||||
.key_storage(Box::new(Self::make_ssh_storage(config_client)))
|
||||
.port(params.port);
|
||||
if let Some(username) = params.username {
|
||||
opts = opts.username(username);
|
||||
}
|
||||
if let Some(password) = params.password {
|
||||
opts = opts.password(password);
|
||||
}
|
||||
opts
|
||||
}
|
||||
|
||||
/// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded)
|
||||
fn make_ssh_storage(config_client: &ConfigClient) -> SshKeyStorage {
|
||||
SshKeyStorage::storage_from_config(config_client)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn should_build_aws_s3_fs() {
|
||||
let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test")));
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_build_ftp_fs() {
|
||||
let params = ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(21)
|
||||
.username(Some("omar"))
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Ftp(true), params, &config_client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_build_scp_fs() {
|
||||
let params = ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(22)
|
||||
.username(Some("omar"))
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Scp, params, &config_client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_build_sftp_fs() {
|
||||
let params = ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(22)
|
||||
.username(Some("omar"))
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn should_not_build_fs() {
|
||||
let params = ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(22)
|
||||
.username(Some("omar"))
|
||||
.password(Some("qwerty123")),
|
||||
);
|
||||
let config_client = get_config_client();
|
||||
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||
}
|
||||
|
||||
fn get_config_client() -> ConfigClient {
|
||||
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
|
||||
let (cfg_path, ssh_keys_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
ConfigClient::new(cfg_path.as_path(), ssh_keys_path.as_path())
|
||||
.ok()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Get paths for configuration and keys directory
|
||||
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||
let mut k: PathBuf = PathBuf::from(dir);
|
||||
let mut c: PathBuf = k.clone();
|
||||
k.push("ssh-keys/");
|
||||
c.push("config.toml");
|
||||
(c, k)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
//! ## FileTransfer
|
||||
//!
|
||||
//! `filetransfer` is the module which provides the trait file transfers must implement and the different file transfers
|
||||
//! `filetransfer` is the module which provides the file transfer protocols and remotefs builders
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
@@ -25,21 +25,12 @@
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// locals
|
||||
use crate::fs::{FsEntry, FsFile};
|
||||
// ext
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use wildmatch::WildMatch;
|
||||
// exports
|
||||
mod builder;
|
||||
pub mod params;
|
||||
mod transfer;
|
||||
|
||||
// -- export types
|
||||
pub use builder::Builder;
|
||||
pub use params::{FileTransferParams, ProtocolParams};
|
||||
pub use transfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
|
||||
|
||||
/// ## FileTransferProtocol
|
||||
///
|
||||
@@ -53,325 +44,6 @@ pub enum FileTransferProtocol {
|
||||
AwsS3,
|
||||
}
|
||||
|
||||
/// ## FileTransferError
|
||||
///
|
||||
/// FileTransferError defines the possible errors available for a file transfer
|
||||
#[derive(Debug)]
|
||||
pub struct FileTransferError {
|
||||
code: FileTransferErrorType,
|
||||
msg: Option<String>,
|
||||
}
|
||||
|
||||
/// ## FileTransferErrorType
|
||||
///
|
||||
/// FileTransferErrorType defines the possible errors available for a file transfer
|
||||
#[derive(Error, Debug, Clone, Copy, PartialEq)]
|
||||
pub enum FileTransferErrorType {
|
||||
#[error("Authentication failed")]
|
||||
AuthenticationFailed,
|
||||
#[error("Bad address syntax")]
|
||||
BadAddress,
|
||||
#[error("Connection error")]
|
||||
ConnectionError,
|
||||
#[error("SSL error")]
|
||||
SslError,
|
||||
#[error("Could not stat directory")]
|
||||
DirStatFailed,
|
||||
#[error("Directory already exists")]
|
||||
DirectoryAlreadyExists,
|
||||
#[error("Failed to create file")]
|
||||
FileCreateDenied,
|
||||
#[error("No such file or directory")]
|
||||
NoSuchFileOrDirectory,
|
||||
#[error("Not enough permissions")]
|
||||
PexError,
|
||||
#[error("Protocol error")]
|
||||
ProtocolError,
|
||||
#[error("Uninitialized session")]
|
||||
UninitializedSession,
|
||||
#[error("Unsupported feature")]
|
||||
UnsupportedFeature,
|
||||
}
|
||||
|
||||
impl FileTransferError {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new FileTransferError
|
||||
pub fn new(code: FileTransferErrorType) -> FileTransferError {
|
||||
FileTransferError { code, msg: None }
|
||||
}
|
||||
|
||||
/// ### new_ex
|
||||
///
|
||||
/// Instantiates a new FileTransferError with message
|
||||
pub fn new_ex(code: FileTransferErrorType, msg: String) -> FileTransferError {
|
||||
let mut err: FileTransferError = FileTransferError::new(code);
|
||||
err.msg = Some(msg);
|
||||
err
|
||||
}
|
||||
|
||||
/// ### kind
|
||||
///
|
||||
/// Returns the error kind
|
||||
pub fn kind(&self) -> FileTransferErrorType {
|
||||
self.code
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FileTransferError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match &self.msg {
|
||||
Some(msg) => write!(f, "{} ({})", self.code, msg),
|
||||
None => write!(f, "{}", self.code),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## FileTransferResult
|
||||
///
|
||||
/// Result type returned by a `FileTransfer` implementation
|
||||
pub type FileTransferResult<T> = Result<T, FileTransferError>;
|
||||
|
||||
/// ## FileTransfer
|
||||
///
|
||||
/// File transfer trait must be implemented by all the file transfers and defines the method used by a generic file transfer
|
||||
pub trait FileTransfer {
|
||||
/// ### connect
|
||||
///
|
||||
/// Connect to the remote server
|
||||
/// Can return banner / welcome message on success
|
||||
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>>;
|
||||
|
||||
/// ### disconnect
|
||||
///
|
||||
/// Disconnect from the remote server
|
||||
fn disconnect(&mut self) -> FileTransferResult<()>;
|
||||
|
||||
/// ### is_connected
|
||||
///
|
||||
/// Indicates whether the client is connected to remote
|
||||
fn is_connected(&self) -> bool;
|
||||
|
||||
/// ### pwd
|
||||
///
|
||||
/// Print working directory
|
||||
|
||||
fn pwd(&mut self) -> FileTransferResult<PathBuf>;
|
||||
|
||||
/// ### change_dir
|
||||
///
|
||||
/// Change working directory
|
||||
|
||||
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf>;
|
||||
|
||||
/// ### copy
|
||||
///
|
||||
/// Copy file to destination
|
||||
fn copy(&mut self, src: &FsEntry, dst: &Path) -> FileTransferResult<()>;
|
||||
|
||||
/// ### list_dir
|
||||
///
|
||||
/// List directory entries
|
||||
|
||||
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>>;
|
||||
|
||||
/// ### mkdir
|
||||
///
|
||||
/// Make directory
|
||||
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
|
||||
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()>;
|
||||
|
||||
/// ### remove
|
||||
///
|
||||
/// Remove a file or a directory
|
||||
fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()>;
|
||||
|
||||
/// ### rename
|
||||
///
|
||||
/// Rename file or a directory
|
||||
fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()>;
|
||||
|
||||
/// ### stat
|
||||
///
|
||||
/// Stat file and return FsEntry
|
||||
fn stat(&mut self, path: &Path) -> FileTransferResult<FsEntry>;
|
||||
|
||||
/// ### exec
|
||||
///
|
||||
/// Execute a command on remote host
|
||||
fn exec(&mut self, cmd: &str) -> FileTransferResult<String>;
|
||||
|
||||
/// ### send_file
|
||||
///
|
||||
/// Send file to remote
|
||||
/// File name is referred to the name of the file as it will be saved
|
||||
/// Data contains the file data
|
||||
/// Returns file and its size.
|
||||
/// By default returns unsupported feature
|
||||
fn send_file(
|
||||
&mut self,
|
||||
_local: &FsFile,
|
||||
_file_name: &Path,
|
||||
) -> FileTransferResult<Box<dyn Write>> {
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### recv_file
|
||||
///
|
||||
/// Receive file from remote with provided name
|
||||
/// Returns file and its size
|
||||
/// By default returns unsupported feature
|
||||
fn recv_file(&mut self, _file: &FsFile) -> FileTransferResult<Box<dyn Read>> {
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### on_sent
|
||||
///
|
||||
/// Finalize send method.
|
||||
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
|
||||
/// The purpose of this method is to finalize the connection with the peer when writing data.
|
||||
/// This is necessary for some protocols such as FTP.
|
||||
/// You must call this method each time you want to finalize the write of the remote file.
|
||||
/// By default this function returns already `Ok(())`
|
||||
fn on_sent(&mut self, _writable: Box<dyn Write>) -> FileTransferResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### on_recv
|
||||
///
|
||||
/// Finalize recv method.
|
||||
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
|
||||
/// The purpose of this method is to finalize the connection with the peer when reading data.
|
||||
/// This mighe be necessary for some protocols.
|
||||
/// You must call this method each time you want to finalize the read of the remote file.
|
||||
/// By default this function returns already `Ok(())`
|
||||
fn on_recv(&mut self, _readable: Box<dyn Read>) -> FileTransferResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### send_file_wno_stream
|
||||
///
|
||||
/// Send a file to remote WITHOUT using streams.
|
||||
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
|
||||
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
|
||||
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
|
||||
/// By default this function uses the streams function to copy content from reader to writer
|
||||
fn send_file_wno_stream(
|
||||
&mut self,
|
||||
src: &FsFile,
|
||||
dest: &Path,
|
||||
mut reader: Box<dyn Read>,
|
||||
) -> FileTransferResult<()> {
|
||||
match self.is_connected() {
|
||||
true => {
|
||||
let mut stream = self.send_file(src, dest)?;
|
||||
io::copy(&mut reader, &mut stream).map_err(|e| {
|
||||
FileTransferError::new_ex(FileTransferErrorType::ProtocolError, e.to_string())
|
||||
})?;
|
||||
self.on_sent(stream)
|
||||
}
|
||||
false => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### recv_file_wno_stream
|
||||
///
|
||||
/// Receive a file from remote WITHOUT using streams.
|
||||
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
|
||||
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
|
||||
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
|
||||
/// For safety reasons this function doesn't accept the `Write` trait, but the destination path.
|
||||
/// By default this function uses the streams function to copy content from reader to writer
|
||||
fn recv_file_wno_stream(&mut self, src: &FsFile, dest: &Path) -> FileTransferResult<()> {
|
||||
match self.is_connected() {
|
||||
true => {
|
||||
let mut writer = File::create(dest).map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::FileCreateDenied,
|
||||
format!("Could not open local file: {}", e),
|
||||
)
|
||||
})?;
|
||||
let mut stream = self.recv_file(src)?;
|
||||
io::copy(&mut stream, &mut writer)
|
||||
.map(|_| ())
|
||||
.map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
self.on_recv(stream)
|
||||
}
|
||||
false => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### find
|
||||
///
|
||||
/// Find files from current directory (in all subdirectories) whose name matches the provided search
|
||||
/// Search supports wildcards ('?', '*')
|
||||
fn find(&mut self, search: &str) -> FileTransferResult<Vec<FsEntry>> {
|
||||
match self.is_connected() {
|
||||
true => {
|
||||
// Starting from current directory, iter dir
|
||||
match self.pwd() {
|
||||
Ok(p) => self.iter_search(p.as_path(), &WildMatch::new(search)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
false => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### iter_search
|
||||
///
|
||||
/// Search recursively in `dir` for file matching the wildcard.
|
||||
/// NOTE: DON'T RE-IMPLEMENT THIS FUNCTION, unless the file transfer provides a faster way to do so
|
||||
/// NOTE: don't call this method from outside; consider it as private
|
||||
fn iter_search(&mut self, dir: &Path, filter: &WildMatch) -> FileTransferResult<Vec<FsEntry>> {
|
||||
let mut drained: Vec<FsEntry> = Vec::new();
|
||||
// Scan directory
|
||||
match self.list_dir(dir) {
|
||||
Ok(entries) => {
|
||||
/* For each entry:
|
||||
- if is dir: call iter_search with `dir`
|
||||
- push `iter_search` result to `drained`
|
||||
- if is file: check if it matches `filter`
|
||||
- if it matches `filter`: push to to filter
|
||||
*/
|
||||
for entry in entries.iter() {
|
||||
match entry {
|
||||
FsEntry::Directory(dir) => {
|
||||
// If directory name, matches wildcard, push it to drained
|
||||
if filter.matches(dir.name.as_str()) {
|
||||
drained.push(FsEntry::Directory(dir.clone()));
|
||||
}
|
||||
drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?);
|
||||
}
|
||||
FsEntry::File(file) => {
|
||||
if filter.matches(file.name.as_str()) {
|
||||
drained.push(FsEntry::File(file.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(drained)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traits
|
||||
|
||||
impl std::string::ToString for FileTransferProtocol {
|
||||
@@ -479,96 +151,4 @@ mod tests {
|
||||
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
|
||||
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_mod_error() {
|
||||
let err: FileTransferError = FileTransferError::new_ex(
|
||||
FileTransferErrorType::NoSuchFileOrDirectory,
|
||||
String::from("non va una mazza"),
|
||||
);
|
||||
assert_eq!(*err.msg.as_ref().unwrap(), String::from("non va una mazza"));
|
||||
assert_eq!(
|
||||
format!("{}", err),
|
||||
String::from("No such file or directory (non va una mazza)")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::AuthenticationFailed)
|
||||
),
|
||||
String::from("Authentication failed")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::BadAddress)
|
||||
),
|
||||
String::from("Bad address syntax")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::ConnectionError)
|
||||
),
|
||||
String::from("Connection error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::DirStatFailed)
|
||||
),
|
||||
String::from("Could not stat directory")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::FileCreateDenied)
|
||||
),
|
||||
String::from("Failed to create file")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::NoSuchFileOrDirectory)
|
||||
),
|
||||
String::from("No such file or directory")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::PexError)
|
||||
),
|
||||
String::from("Not enough permissions")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::ProtocolError)
|
||||
),
|
||||
String::from("Protocol error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::SslError)
|
||||
),
|
||||
String::from("SSL error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::UninitializedSession)
|
||||
),
|
||||
String::from("Uninitialized session")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::UnsupportedFeature)
|
||||
),
|
||||
String::from("Unsupported feature")
|
||||
);
|
||||
let err = FileTransferError::new(FileTransferErrorType::UnsupportedFeature);
|
||||
assert_eq!(err.kind(), FileTransferErrorType::UnsupportedFeature);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,8 +103,7 @@ impl Default for ProtocolParams {
|
||||
}
|
||||
|
||||
impl ProtocolParams {
|
||||
/// ### generic_params
|
||||
///
|
||||
#[cfg(test)]
|
||||
/// Retrieve generic parameters from protocol params if any
|
||||
pub fn generic_params(&self) -> Option<&GenericProtocolParams> {
|
||||
match self {
|
||||
@@ -120,8 +119,7 @@ impl ProtocolParams {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### s3_params
|
||||
///
|
||||
#[cfg(test)]
|
||||
/// Retrieve AWS S3 parameters if any
|
||||
pub fn s3_params(&self) -> Option<&AwsS3Params> {
|
||||
match self {
|
||||
|
||||
@@ -1,960 +0,0 @@
|
||||
//! ## FTP transfer
|
||||
//!
|
||||
//! `ftp_transfer` is the module which provides the implementation for the FTP/FTPS file transfer
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{
|
||||
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
|
||||
};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
|
||||
use crate::utils::fmt::shadow_password;
|
||||
use crate::utils::path;
|
||||
|
||||
// Includes
|
||||
use std::convert::TryFrom;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::UNIX_EPOCH;
|
||||
use suppaftp::native_tls::TlsConnector;
|
||||
use suppaftp::{
|
||||
list::{File, PosixPexQuery},
|
||||
status::FILE_UNAVAILABLE,
|
||||
types::{FileType, Response},
|
||||
FtpError, FtpStream,
|
||||
};
|
||||
|
||||
/// ## FtpFileTransfer
|
||||
///
|
||||
/// Ftp file transfer struct
|
||||
pub struct FtpFileTransfer {
|
||||
stream: Option<FtpStream>,
|
||||
ftps: bool,
|
||||
}
|
||||
|
||||
impl FtpFileTransfer {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new `FtpFileTransfer`
|
||||
pub fn new(ftps: bool) -> FtpFileTransfer {
|
||||
FtpFileTransfer { stream: None, ftps }
|
||||
}
|
||||
|
||||
/// ### resolve
|
||||
///
|
||||
/// Fix provided path; on Windows fixes the backslashes, converting them to slashes
|
||||
/// While on POSIX does nothing
|
||||
#[cfg(target_os = "windows")]
|
||||
fn resolve(p: &Path) -> PathBuf {
|
||||
PathBuf::from(path_slash::PathExt::to_slash_lossy(p).as_str())
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
fn resolve(p: &Path) -> PathBuf {
|
||||
p.to_path_buf()
|
||||
}
|
||||
|
||||
/// ### parse_list_lines
|
||||
///
|
||||
/// Parse all lines of LIST command output and instantiates a vector of FsEntry from it.
|
||||
/// This function also converts from `suppaftp::list::File` to `FsEntry`
|
||||
fn parse_list_lines(&mut self, path: &Path, lines: Vec<String>) -> Vec<FsEntry> {
|
||||
// Iter and collect
|
||||
lines
|
||||
.into_iter()
|
||||
.map(File::try_from) // Try to convert to file
|
||||
.flatten() // Remove errors
|
||||
.map(|x| {
|
||||
let mut abs_path: PathBuf = path.to_path_buf();
|
||||
abs_path.push(x.name());
|
||||
match x.is_directory() {
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
name: x.name().to_string(),
|
||||
abs_path,
|
||||
last_access_time: x.modified(),
|
||||
last_change_time: x.modified(),
|
||||
creation_time: x.modified(),
|
||||
symlink: None,
|
||||
user: x.uid(),
|
||||
group: x.gid(),
|
||||
unix_pex: Some(Self::query_unix_pex(&x)),
|
||||
}),
|
||||
false => FsEntry::File(FsFile {
|
||||
name: x.name().to_string(),
|
||||
size: x.size(),
|
||||
ftype: abs_path
|
||||
.extension()
|
||||
.map(|ext| String::from(ext.to_str().unwrap_or(""))),
|
||||
last_access_time: x.modified(),
|
||||
last_change_time: x.modified(),
|
||||
creation_time: x.modified(),
|
||||
user: x.uid(),
|
||||
group: x.gid(),
|
||||
symlink: Self::get_symlink_entry(path, x.symlink()),
|
||||
abs_path,
|
||||
unix_pex: Some(Self::query_unix_pex(&x)),
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// ### get_symlink_entry
|
||||
///
|
||||
/// Get FsEntry from symlink
|
||||
fn get_symlink_entry(wrkdir: &Path, link: Option<&Path>) -> Option<Box<FsEntry>> {
|
||||
match link {
|
||||
None => None,
|
||||
Some(p) => {
|
||||
// Make abs path
|
||||
let abs_path: PathBuf = path::absolutize(wrkdir, p);
|
||||
Some(Box::new(FsEntry::File(FsFile {
|
||||
name: p
|
||||
.file_name()
|
||||
.map(|x| x.to_str().unwrap_or("").to_string())
|
||||
.unwrap_or_default(),
|
||||
ftype: abs_path
|
||||
.extension()
|
||||
.map(|ext| String::from(ext.to_str().unwrap_or(""))),
|
||||
size: 0,
|
||||
last_access_time: UNIX_EPOCH,
|
||||
last_change_time: UNIX_EPOCH,
|
||||
creation_time: UNIX_EPOCH,
|
||||
user: None,
|
||||
group: None,
|
||||
symlink: None,
|
||||
unix_pex: None,
|
||||
abs_path,
|
||||
})))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### query_unix_pex
|
||||
///
|
||||
/// Returns unix pex in tuple of values
|
||||
fn query_unix_pex(f: &File) -> (UnixPex, UnixPex, UnixPex) {
|
||||
(
|
||||
UnixPex::new(
|
||||
f.can_read(PosixPexQuery::Owner),
|
||||
f.can_write(PosixPexQuery::Owner),
|
||||
f.can_execute(PosixPexQuery::Owner),
|
||||
),
|
||||
UnixPex::new(
|
||||
f.can_read(PosixPexQuery::Group),
|
||||
f.can_write(PosixPexQuery::Group),
|
||||
f.can_execute(PosixPexQuery::Group),
|
||||
),
|
||||
UnixPex::new(
|
||||
f.can_read(PosixPexQuery::Others),
|
||||
f.can_write(PosixPexQuery::Others),
|
||||
f.can_execute(PosixPexQuery::Others),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FileTransfer for FtpFileTransfer {
|
||||
/// ### connect
|
||||
///
|
||||
/// Connect to the remote server
|
||||
|
||||
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>> {
|
||||
let params = match params.generic_params() {
|
||||
Some(params) => params,
|
||||
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
|
||||
};
|
||||
// Get stream
|
||||
info!("Connecting to {}:{}", params.address, params.port);
|
||||
let mut stream: FtpStream =
|
||||
match FtpStream::connect(format!("{}:{}", params.address, params.port)) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
error!("Failed to connect: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
// If SSL, open secure session
|
||||
if self.ftps {
|
||||
info!("Setting up TLS stream...");
|
||||
let ctx = match TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.danger_accept_invalid_hostnames(true)
|
||||
.build()
|
||||
{
|
||||
Ok(tls) => tls,
|
||||
Err(err) => {
|
||||
error!("Failed to setup TLS stream: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::SslError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
stream = match stream.into_secure(ctx, params.address.as_str()) {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
error!("Failed to setup TLS stream: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::SslError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
// Login (use anonymous if credentials are unspecified)
|
||||
let username: String = match ¶ms.username {
|
||||
Some(u) => u.to_string(),
|
||||
None => String::from("anonymous"),
|
||||
};
|
||||
let password: String = match ¶ms.password {
|
||||
Some(pwd) => pwd.to_string(),
|
||||
None => String::new(),
|
||||
};
|
||||
info!(
|
||||
"Signin in with username: {}, password: {}",
|
||||
username,
|
||||
shadow_password(password.as_str())
|
||||
);
|
||||
if let Err(err) = stream.login(username.as_str(), password.as_str()) {
|
||||
error!("Login failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
debug!("Setting transfer type to Binary");
|
||||
// Initialize file type
|
||||
if let Err(err) = stream.transfer_type(FileType::Binary) {
|
||||
error!("Failed to set transfer type to binary: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
// Set stream
|
||||
self.stream = Some(stream);
|
||||
info!("Connection successfully established");
|
||||
// Return OK
|
||||
Ok(self
|
||||
.stream
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_welcome_msg()
|
||||
.map(|x| x.to_string()))
|
||||
}
|
||||
|
||||
/// ### disconnect
|
||||
///
|
||||
/// Disconnect from the remote server
|
||||
|
||||
fn disconnect(&mut self) -> FileTransferResult<()> {
|
||||
info!("Disconnecting from FTP server...");
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.quit() {
|
||||
Ok(_) => {
|
||||
self.stream = None;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### is_connected
|
||||
///
|
||||
/// Indicates whether the client is connected to remote
|
||||
fn is_connected(&self) -> bool {
|
||||
self.stream.is_some()
|
||||
}
|
||||
|
||||
/// ### pwd
|
||||
///
|
||||
/// Print working directory
|
||||
|
||||
fn pwd(&mut self) -> FileTransferResult<PathBuf> {
|
||||
info!("PWD");
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.pwd() {
|
||||
Ok(path) => Ok(PathBuf::from(path.as_str())),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### change_dir
|
||||
///
|
||||
/// Change working directory
|
||||
|
||||
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf> {
|
||||
let dir: PathBuf = Self::resolve(dir);
|
||||
info!("Changing directory to {}", dir.display());
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) {
|
||||
Ok(_) => Ok(dir),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### copy
|
||||
///
|
||||
/// Copy file to destination
|
||||
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> FileTransferResult<()> {
|
||||
// FTP doesn't support file copy
|
||||
debug!("COPY issues (will fail, since unsupported)");
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### list_dir
|
||||
///
|
||||
/// List directory entries
|
||||
|
||||
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>> {
|
||||
let dir: PathBuf = Self::resolve(path);
|
||||
info!("LIST dir {}", dir.display());
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) {
|
||||
Ok(lines) => {
|
||||
debug!("Got {} lines in LIST result", lines.len());
|
||||
// Iterate over entries
|
||||
Ok(self.parse_list_lines(path, lines))
|
||||
}
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::DirStatFailed,
|
||||
err.to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### mkdir
|
||||
///
|
||||
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
|
||||
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> {
|
||||
let dir: PathBuf = Self::resolve(dir);
|
||||
info!("MKDIR {}", dir.display());
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(FtpError::UnexpectedResponse(Response {
|
||||
// Directory already exists
|
||||
code: FILE_UNAVAILABLE,
|
||||
body: _,
|
||||
})) => {
|
||||
error!("Directory {} already exists", dir.display());
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::DirectoryAlreadyExists,
|
||||
))
|
||||
}
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::FileCreateDenied,
|
||||
err.to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### remove
|
||||
///
|
||||
/// Remove a file or a directory
|
||||
fn remove(&mut self, fsentry: &FsEntry) -> FileTransferResult<()> {
|
||||
if self.stream.is_none() {
|
||||
return Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
));
|
||||
}
|
||||
info!("Removing entry {}", fsentry.get_abs_path().display());
|
||||
let wrkdir: PathBuf = self.pwd()?;
|
||||
match fsentry {
|
||||
// Match fs entry...
|
||||
FsEntry::File(file) => {
|
||||
// Go to parent directory
|
||||
if let Some(parent_dir) = file.abs_path.parent() {
|
||||
debug!("Changing wrkdir to {}", parent_dir.display());
|
||||
self.change_dir(parent_dir)?;
|
||||
}
|
||||
debug!("entry is a file; removing file {}", file.abs_path.display());
|
||||
// Remove file directly
|
||||
let result = self
|
||||
.stream
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.rm(file.name.as_ref())
|
||||
.map(|_| ())
|
||||
.map_err(|e| {
|
||||
FileTransferError::new_ex(FileTransferErrorType::PexError, e.to_string())
|
||||
});
|
||||
// Go to source directory
|
||||
match self.change_dir(wrkdir.as_path()) {
|
||||
Err(err) => Err(err),
|
||||
Ok(_) => result,
|
||||
}
|
||||
}
|
||||
FsEntry::Directory(dir) => {
|
||||
// Get directory files
|
||||
debug!("Entry is a directory; iterating directory entries");
|
||||
let result = match self.list_dir(dir.abs_path.as_path()) {
|
||||
Ok(files) => {
|
||||
// Remove recursively files
|
||||
debug!("Removing {} entries from directory...", files.len());
|
||||
for file in files.iter() {
|
||||
if let Err(err) = self.remove(file) {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::PexError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Once all files in directory have been deleted, remove directory
|
||||
debug!("Finally removing directory {}...", dir.name);
|
||||
// Enter parent directory
|
||||
if let Some(parent_dir) = dir.abs_path.parent() {
|
||||
debug!(
|
||||
"Changing wrkdir to {} to delete directory {}",
|
||||
parent_dir.display(),
|
||||
dir.name
|
||||
);
|
||||
self.change_dir(parent_dir)?;
|
||||
}
|
||||
match self.stream.as_mut().unwrap().rmdir(dir.name.as_str()) {
|
||||
Ok(_) => {
|
||||
debug!("Removed {}", dir.abs_path.display());
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::PexError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::DirStatFailed,
|
||||
err.to_string(),
|
||||
)),
|
||||
};
|
||||
// Restore directory
|
||||
match self.change_dir(wrkdir.as_path()) {
|
||||
Err(err) => Err(err),
|
||||
Ok(_) => result,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### rename
|
||||
///
|
||||
/// Rename file or a directory
|
||||
fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()> {
|
||||
let dst: PathBuf = Self::resolve(dst);
|
||||
info!(
|
||||
"Renaming {} to {}",
|
||||
file.get_abs_path().display(),
|
||||
dst.display()
|
||||
);
|
||||
match &mut self.stream {
|
||||
Some(stream) => {
|
||||
// Get name
|
||||
let src_name: String = match file {
|
||||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
};
|
||||
// Only names are supported
|
||||
match stream.rename(src_name.as_str(), &dst.as_path().to_string_lossy()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::FileCreateDenied,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### stat
|
||||
///
|
||||
/// Stat file and return FsEntry
|
||||
fn stat(&mut self, _path: &Path) -> FileTransferResult<FsEntry> {
|
||||
match &mut self.stream {
|
||||
Some(_) => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
)),
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### exec
|
||||
///
|
||||
/// Execute a command on remote host
|
||||
fn exec(&mut self, _cmd: &str) -> FileTransferResult<String> {
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### send_file
|
||||
///
|
||||
/// Send file to remote
|
||||
/// File name is referred to the name of the file as it will be saved
|
||||
/// Data contains the file data
|
||||
/// Returns file and its size
|
||||
fn send_file(
|
||||
&mut self,
|
||||
_local: &FsFile,
|
||||
file_name: &Path,
|
||||
) -> FileTransferResult<Box<dyn Write>> {
|
||||
let file_name: PathBuf = Self::resolve(file_name);
|
||||
info!("Sending file {}", file_name.display());
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.put_with_stream(&file_name.as_path().to_string_lossy()) {
|
||||
Ok(writer) => Ok(Box::new(writer)), // NOTE: don't use BufWriter here, since already returned by the library
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::FileCreateDenied,
|
||||
err.to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### recv_file
|
||||
///
|
||||
/// Receive file from remote with provided name
|
||||
/// Returns file and its size
|
||||
fn recv_file(&mut self, file: &FsFile) -> FileTransferResult<Box<dyn Read>> {
|
||||
info!("Receiving file {}", file.abs_path.display());
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.retr_as_stream(&file.abs_path.as_path().to_string_lossy())
|
||||
{
|
||||
Ok(reader) => Ok(Box::new(reader)), // NOTE: don't use BufReader here, since already returned by the library
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::NoSuchFileOrDirectory,
|
||||
err.to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_sent
|
||||
///
|
||||
/// Finalize send method.
|
||||
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
|
||||
/// The purpose of this method is to finalize the connection with the peer when writing data.
|
||||
/// This is necessary for some protocols such as FTP.
|
||||
/// You must call this method each time you want to finalize the write of the remote file.
|
||||
fn on_sent(&mut self, writable: Box<dyn Write>) -> FileTransferResult<()> {
|
||||
info!("Finalizing put stream");
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.finalize_put_stream(writable) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
err.to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_recv
|
||||
///
|
||||
/// Finalize recv method.
|
||||
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
|
||||
/// The purpose of this method is to finalize the connection with the peer when reading data.
|
||||
/// This mighe be necessary for some protocols.
|
||||
/// You must call this method each time you want to finalize the read of the remote file.
|
||||
fn on_recv(&mut self, readable: Box<dyn Read>) -> FileTransferResult<()> {
|
||||
info!("Finalizing get");
|
||||
match &mut self.stream {
|
||||
Some(stream) => match stream.finalize_retr_stream(readable) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
err.to_string(),
|
||||
)),
|
||||
},
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::filetransfer::params::GenericProtocolParams;
|
||||
use crate::utils::file::open_file;
|
||||
#[cfg(feature = "with-containers")]
|
||||
use crate::utils::test_helpers::write_file;
|
||||
use crate::utils::test_helpers::{create_sample_file_entry, make_fsentry};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::io::{Read, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_new() {
|
||||
let ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
assert_eq!(ftp.ftps, false);
|
||||
assert!(ftp.stream.is_none());
|
||||
// FTPS
|
||||
let ftp: FtpFileTransfer = FtpFileTransfer::new(true);
|
||||
assert_eq!(ftp.ftps, true);
|
||||
assert!(ftp.stream.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with-containers")]
|
||||
fn test_filetransfer_ftp_server() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Sample file
|
||||
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
|
||||
// Connect
|
||||
let hostname: String = String::from("127.0.0.1");
|
||||
assert!(ftp
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address(hostname)
|
||||
.port(10021)
|
||||
.username(Some("test"))
|
||||
.password(Some("test"))
|
||||
))
|
||||
.is_ok());
|
||||
assert_eq!(ftp.is_connected(), true);
|
||||
// Get pwd
|
||||
assert_eq!(ftp.pwd().unwrap(), PathBuf::from("/"));
|
||||
// List dir (dir is empty)
|
||||
assert_eq!(ftp.list_dir(&Path::new("/")).unwrap().len(), 0);
|
||||
// Make directory
|
||||
assert!(ftp.mkdir(PathBuf::from("/home").as_path()).is_ok());
|
||||
// Remake directory (should report already exists)
|
||||
assert_eq!(
|
||||
ftp.mkdir(PathBuf::from("/home").as_path())
|
||||
.err()
|
||||
.unwrap()
|
||||
.kind(),
|
||||
FileTransferErrorType::DirectoryAlreadyExists
|
||||
);
|
||||
// Make directory (err)
|
||||
assert!(ftp.mkdir(PathBuf::from("/root/pommlar").as_path()).is_err());
|
||||
// Change directory
|
||||
assert!(ftp.change_dir(PathBuf::from("/home").as_path()).is_ok());
|
||||
// Change directory (err)
|
||||
assert!(ftp
|
||||
.change_dir(PathBuf::from("/tmp/oooo/aaaa/eee").as_path())
|
||||
.is_err());
|
||||
// Copy (not supported)
|
||||
assert!(ftp
|
||||
.copy(&FsEntry::File(entry.clone()), PathBuf::from("/").as_path())
|
||||
.is_err());
|
||||
// Exec (not supported)
|
||||
assert!(ftp.exec("echo 1;").is_err());
|
||||
// Upload 2 files
|
||||
let mut writable = ftp
|
||||
.send_file(&entry, PathBuf::from("omar.txt").as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
write_file(&file, &mut writable);
|
||||
assert!(ftp.on_sent(writable).is_ok());
|
||||
let mut writable = ftp
|
||||
.send_file(&entry, PathBuf::from("README.md").as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
write_file(&file, &mut writable);
|
||||
assert!(ftp.on_sent(writable).is_ok());
|
||||
// Upload file (err)
|
||||
assert!(ftp
|
||||
.send_file(&entry, PathBuf::from("/ommlar/omarone").as_path())
|
||||
.is_err());
|
||||
// List dir
|
||||
let list: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/home").as_path()).ok().unwrap();
|
||||
assert_eq!(list.len(), 2);
|
||||
// Find
|
||||
assert!(ftp.change_dir(PathBuf::from("/").as_path()).is_ok());
|
||||
assert_eq!(ftp.find("*.txt").ok().unwrap().len(), 1);
|
||||
assert_eq!(ftp.find("*.md").ok().unwrap().len(), 1);
|
||||
assert_eq!(ftp.find("*.jpeg").ok().unwrap().len(), 0);
|
||||
assert!(ftp.change_dir(PathBuf::from("/home").as_path()).is_ok());
|
||||
// Rename
|
||||
assert!(ftp.mkdir(PathBuf::from("/uploads").as_path()).is_ok());
|
||||
assert!(ftp
|
||||
.rename(
|
||||
list.get(0).unwrap(),
|
||||
PathBuf::from("/uploads/README.txt").as_path()
|
||||
)
|
||||
.is_ok());
|
||||
// Rename (err)
|
||||
assert!(ftp
|
||||
.rename(list.get(0).unwrap(), PathBuf::from("OMARONE").as_path())
|
||||
.is_err());
|
||||
let dummy: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("cucumber.txt"),
|
||||
abs_path: PathBuf::from("/cucumber.txt"),
|
||||
last_change_time: UNIX_EPOCH,
|
||||
last_access_time: UNIX_EPOCH,
|
||||
creation_time: UNIX_EPOCH,
|
||||
size: 0,
|
||||
ftype: Some(String::from("txt")), // File type
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
||||
});
|
||||
assert!(ftp
|
||||
.rename(&dummy, PathBuf::from("/a/b/c").as_path())
|
||||
.is_err());
|
||||
// Remove
|
||||
assert!(ftp.remove(list.get(1).unwrap()).is_ok());
|
||||
assert!(ftp.remove(list.get(1).unwrap()).is_err());
|
||||
// Receive file
|
||||
let mut writable = ftp
|
||||
.send_file(&entry, PathBuf::from("/uploads/README.txt").as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
write_file(&file, &mut writable);
|
||||
assert!(ftp.on_sent(writable).is_ok());
|
||||
let file: FsFile = ftp
|
||||
.list_dir(PathBuf::from("/uploads").as_path())
|
||||
.ok()
|
||||
.unwrap()
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_file();
|
||||
let mut readable = ftp.recv_file(&file).ok().unwrap();
|
||||
let mut data: Vec<u8> = vec![0; 1024];
|
||||
assert!(readable.read(&mut data).is_ok());
|
||||
assert!(ftp.on_recv(readable).is_ok());
|
||||
// Receive file (err)
|
||||
assert!(ftp.recv_file(&entry).is_err());
|
||||
// Cleanup
|
||||
assert!(ftp.change_dir(PathBuf::from("/").as_path()).is_ok());
|
||||
assert!(ftp
|
||||
.remove(&make_fsentry(PathBuf::from("/home"), true))
|
||||
.is_ok());
|
||||
assert!(ftp
|
||||
.remove(&make_fsentry(PathBuf::from("/uploads"), true))
|
||||
.is_ok());
|
||||
// Disconnect
|
||||
assert!(ftp.disconnect().is_ok());
|
||||
assert_eq!(ftp.is_connected(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with-containers")]
|
||||
fn test_filetransfer_ftp_server_bad_auth() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Connect
|
||||
assert!(ftp
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10021)
|
||||
.username(Some("omar"))
|
||||
.password(Some("ommlar"))
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "with-containers")]
|
||||
fn test_filetransfer_ftp_no_credentials() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
assert!(ftp
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10021)
|
||||
.username::<&str>(None)
|
||||
.password::<&str>(None)
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_server_bad_server() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Connect
|
||||
assert!(ftp
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("mybad.veribad.server")
|
||||
.port(21)
|
||||
.username::<&str>(None)
|
||||
.password::<&str>(None)
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_parse_list_line_unix() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Simple file
|
||||
let file: FsFile = ftp
|
||||
.parse_list_lines(
|
||||
PathBuf::from("/tmp").as_path(),
|
||||
vec!["-rw-rw-r-- 1 root dialout 8192 Nov 5 2018 omar.txt".to_string()],
|
||||
)
|
||||
.get(0)
|
||||
.unwrap()
|
||||
.clone()
|
||||
.unwrap_file();
|
||||
assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt"));
|
||||
assert_eq!(file.name, String::from("omar.txt"));
|
||||
assert_eq!(file.size, 8192);
|
||||
assert!(file.symlink.is_none());
|
||||
assert_eq!(file.user, None);
|
||||
assert_eq!(file.group, None);
|
||||
assert_eq!(
|
||||
file.unix_pex.unwrap(),
|
||||
(UnixPex::from(6), UnixPex::from(6), UnixPex::from(4))
|
||||
);
|
||||
assert_eq!(
|
||||
file.last_access_time
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1541376000)
|
||||
);
|
||||
assert_eq!(
|
||||
file.last_change_time
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1541376000)
|
||||
);
|
||||
assert_eq!(
|
||||
file.creation_time.duration_since(UNIX_EPOCH).ok().unwrap(),
|
||||
Duration::from_secs(1541376000)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_list_dir_dos_syntax() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Connect
|
||||
assert!(ftp
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("test.rebex.net")
|
||||
.port(21)
|
||||
.username(Some("demo"))
|
||||
.password(Some("password"))
|
||||
))
|
||||
.is_ok());
|
||||
// Pwd
|
||||
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
|
||||
// List dir
|
||||
let files: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap();
|
||||
// There should be at least 1 file
|
||||
assert!(files.len() > 0);
|
||||
// Disconnect
|
||||
assert!(ftp.disconnect().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_uninitialized() {
|
||||
let file: FsFile = FsFile {
|
||||
name: String::from("omar.txt"),
|
||||
abs_path: PathBuf::from("/omar.txt"),
|
||||
last_change_time: UNIX_EPOCH,
|
||||
last_access_time: UNIX_EPOCH,
|
||||
creation_time: UNIX_EPOCH,
|
||||
size: 0,
|
||||
ftype: Some(String::from("txt")), // File type
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
||||
};
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
assert!(ftp.change_dir(Path::new("/tmp")).is_err());
|
||||
assert!(ftp.disconnect().is_err());
|
||||
assert!(ftp.list_dir(Path::new("/tmp")).is_err());
|
||||
assert!(ftp.mkdir(Path::new("/tmp")).is_err());
|
||||
assert!(ftp
|
||||
.remove(&make_fsentry(PathBuf::from("/nowhere"), false))
|
||||
.is_err());
|
||||
assert!(ftp
|
||||
.rename(
|
||||
&make_fsentry(PathBuf::from("/nowhere"), false),
|
||||
PathBuf::from("/culonia").as_path()
|
||||
)
|
||||
.is_err());
|
||||
assert!(ftp.pwd().is_err());
|
||||
assert!(ftp.stat(Path::new("/tmp")).is_err());
|
||||
assert!(ftp.recv_file(&file).is_err());
|
||||
assert!(ftp.send_file(&file, Path::new("/tmp/omar.txt")).is_err());
|
||||
let (_, temp): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
|
||||
let readable: Box<dyn Read> = Box::new(std::fs::File::open(temp.path()).unwrap());
|
||||
assert!(ftp.on_recv(readable).is_err());
|
||||
let (_, temp): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
|
||||
let writable: Box<dyn Write> =
|
||||
Box::new(open_file(temp.path(), true, true, true).ok().unwrap());
|
||||
assert!(ftp.on_sent(writable).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
//! # transfer
|
||||
//!
|
||||
//! This module exposes all the file transfers supported by termscp
|
||||
|
||||
// -- import
|
||||
use super::{
|
||||
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
|
||||
};
|
||||
|
||||
// -- modules
|
||||
mod ftp;
|
||||
mod s3;
|
||||
mod scp;
|
||||
mod sftp;
|
||||
|
||||
// -- export
|
||||
pub use self::s3::S3FileTransfer;
|
||||
pub use ftp::FtpFileTransfer;
|
||||
pub use scp::ScpFileTransfer;
|
||||
pub use sftp::SftpFileTransfer;
|
||||
@@ -1,699 +0,0 @@
|
||||
//! ## S3 transfer
|
||||
//!
|
||||
//! S3 file transfer module
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
// -- mod
|
||||
mod object;
|
||||
|
||||
// Locals
|
||||
use super::{
|
||||
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
|
||||
};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::utils::path;
|
||||
use object::S3Object;
|
||||
|
||||
// ext
|
||||
use s3::creds::Credentials;
|
||||
use s3::serde_types::Object;
|
||||
use s3::{Bucket, Region};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
/// ## S3FileTransfer
|
||||
///
|
||||
/// Aws s3 file transfer
|
||||
pub struct S3FileTransfer {
|
||||
bucket: Option<Bucket>,
|
||||
wrkdir: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for S3FileTransfer {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
bucket: None,
|
||||
wrkdir: PathBuf::from("/"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl S3FileTransfer {
|
||||
/// ### list_objects
|
||||
///
|
||||
/// List objects contained in `p` path
|
||||
fn list_objects(&self, p: &Path, list_dir: bool) -> FileTransferResult<Vec<S3Object>> {
|
||||
// Make path relative
|
||||
let key: String = Self::fmt_path(p, list_dir);
|
||||
debug!("Query list directory {}; key: {}", p.display(), key);
|
||||
self.query_objects(key, true)
|
||||
}
|
||||
|
||||
/// ### stat_object
|
||||
///
|
||||
/// Stat an s3 object
|
||||
fn stat_object(&self, p: &Path) -> FileTransferResult<S3Object> {
|
||||
let key: String = Self::fmt_path(p, false);
|
||||
debug!("Query stat object {}; key: {}", p.display(), key);
|
||||
let objects = self.query_objects(key, false)?;
|
||||
// Absolutize path
|
||||
let absol: PathBuf = path::absolutize(Path::new("/"), p);
|
||||
// Find associated object
|
||||
match objects
|
||||
.into_iter()
|
||||
.find(|x| x.path.as_path() == absol.as_path())
|
||||
{
|
||||
Some(obj) => Ok(obj),
|
||||
None => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::NoSuchFileOrDirectory,
|
||||
format!("{}: No such file or directory", p.display()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### query_objects
|
||||
///
|
||||
/// Query objects at key
|
||||
fn query_objects(
|
||||
&self,
|
||||
key: String,
|
||||
only_direct_children: bool,
|
||||
) -> FileTransferResult<Vec<S3Object>> {
|
||||
let results = self.bucket.as_ref().unwrap().list(key.clone(), None);
|
||||
match results {
|
||||
Ok(entries) => {
|
||||
let mut objects: Vec<S3Object> = Vec::new();
|
||||
entries.iter().for_each(|x| {
|
||||
x.contents
|
||||
.iter()
|
||||
.filter(|x| {
|
||||
if only_direct_children {
|
||||
Self::list_object_should_be_kept(x, key.as_str())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.for_each(|x| objects.push(S3Object::from(x)))
|
||||
});
|
||||
debug!("Found objects: {:?}", objects);
|
||||
Ok(objects)
|
||||
}
|
||||
Err(e) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::DirStatFailed,
|
||||
e.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### list_object_should_be_kept
|
||||
///
|
||||
/// Returns whether object should be kept after list command.
|
||||
/// The object won't be kept if:
|
||||
///
|
||||
/// 1. is not a direct child of provided dir
|
||||
fn list_object_should_be_kept(obj: &Object, dir: &str) -> bool {
|
||||
Self::is_direct_child(obj.key.as_str(), dir)
|
||||
}
|
||||
|
||||
/// ### is_direct_child
|
||||
///
|
||||
/// Checks whether Object's key is direct child of `parent` path.
|
||||
fn is_direct_child(key: &str, parent: &str) -> bool {
|
||||
key == format!("{}{}", parent, S3Object::object_name(key))
|
||||
|| key == format!("{}{}/", parent, S3Object::object_name(key))
|
||||
}
|
||||
|
||||
/// ### resolve
|
||||
///
|
||||
/// Make s3 absolute path from a given path
|
||||
fn resolve(&self, p: &Path) -> PathBuf {
|
||||
path::diff_paths(path::absolutize(self.wrkdir.as_path(), p), &Path::new("/"))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// ### fmt_fs_entry_path
|
||||
///
|
||||
/// fmt path for fsentry according to format expected by s3
|
||||
fn fmt_fs_file_path(f: &FsFile) -> String {
|
||||
Self::fmt_path(f.abs_path.as_path(), false)
|
||||
}
|
||||
|
||||
/// ### fmt_path
|
||||
///
|
||||
/// fmt path for fsentry according to format expected by s3
|
||||
fn fmt_path(p: &Path, is_dir: bool) -> String {
|
||||
// prevent root as slash
|
||||
if p == Path::new("/") {
|
||||
return "".to_string();
|
||||
}
|
||||
// Remove root only if absolute
|
||||
#[cfg(target_family = "unix")]
|
||||
let is_absolute: bool = p.is_absolute();
|
||||
// NOTE: don't use is_absolute: on windows won't work
|
||||
#[cfg(target_family = "windows")]
|
||||
let is_absolute: bool = p.display().to_string().starts_with('/');
|
||||
let p: PathBuf = match is_absolute {
|
||||
true => path::diff_paths(p, &Path::new("/")).unwrap_or_default(),
|
||||
false => p.to_path_buf(),
|
||||
};
|
||||
// NOTE: windows only: resolve paths
|
||||
#[cfg(target_family = "windows")]
|
||||
let p: PathBuf = PathBuf::from(path_slash::PathExt::to_slash_lossy(p.as_path()).as_str());
|
||||
// Fmt
|
||||
match is_dir {
|
||||
true => {
|
||||
let mut p: String = p.display().to_string();
|
||||
if !p.ends_with('/') {
|
||||
p.push('/');
|
||||
}
|
||||
p
|
||||
}
|
||||
false => p.to_string_lossy().to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileTransfer for S3FileTransfer {
|
||||
/// ### connect
|
||||
///
|
||||
/// Connect to the remote server
|
||||
/// Can return banner / welcome message on success
|
||||
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>> {
|
||||
// Verify parameters are S3
|
||||
let params = match params.s3_params() {
|
||||
Some(params) => params,
|
||||
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
|
||||
};
|
||||
// Load credentials
|
||||
debug!("Loading credentials... (profile {:?})", params.profile);
|
||||
let credentials: Credentials =
|
||||
Credentials::new(None, None, None, None, params.profile.as_deref()).map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
format!("Could not load s3 credentials: {}", e),
|
||||
)
|
||||
})?;
|
||||
// Parse region
|
||||
debug!("Parsing region {}", params.region);
|
||||
let region: Region = Region::from_str(params.region.as_str()).map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
format!("Could not parse s3 region: {}", e),
|
||||
)
|
||||
})?;
|
||||
debug!(
|
||||
"Credentials loaded! Connecting to bucket {}...",
|
||||
params.bucket_name
|
||||
);
|
||||
self.bucket = Some(
|
||||
Bucket::new(params.bucket_name.as_str(), region, credentials).map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
format!("Could not connect to bucket {}: {}", params.bucket_name, e),
|
||||
)
|
||||
})?,
|
||||
);
|
||||
info!("Connection successfully established");
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// ### disconnect
|
||||
///
|
||||
/// Disconnect from the remote server
|
||||
fn disconnect(&mut self) -> FileTransferResult<()> {
|
||||
info!("Disconnecting from S3 bucket...");
|
||||
match self.bucket.take() {
|
||||
Some(bucket) => {
|
||||
drop(bucket);
|
||||
Ok(())
|
||||
}
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### is_connected
|
||||
///
|
||||
/// Indicates whether the client is connected to remote
|
||||
fn is_connected(&self) -> bool {
|
||||
self.bucket.is_some()
|
||||
}
|
||||
|
||||
/// ### pwd
|
||||
///
|
||||
/// Print working directory
|
||||
fn pwd(&mut self) -> FileTransferResult<PathBuf> {
|
||||
info!("PWD");
|
||||
match self.is_connected() {
|
||||
true => Ok(self.wrkdir.clone()),
|
||||
false => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### change_dir
|
||||
///
|
||||
/// Change working directory
|
||||
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf> {
|
||||
match &self.bucket.is_some() {
|
||||
true => {
|
||||
// Always allow entering root
|
||||
if dir == Path::new("/") {
|
||||
self.wrkdir = dir.to_path_buf();
|
||||
info!("New working directory: {}", self.wrkdir.display());
|
||||
return Ok(self.wrkdir.clone());
|
||||
}
|
||||
// Check if directory exists
|
||||
debug!("Entering directory {}...", dir.display());
|
||||
let dir_p: PathBuf = self.resolve(dir);
|
||||
let dir_s: String = Self::fmt_path(dir_p.as_path(), true);
|
||||
debug!("Searching for key {} (path: {})...", dir_s, dir_p.display());
|
||||
// Check if directory already exists
|
||||
if self
|
||||
.stat_object(PathBuf::from(dir_s.as_str()).as_path())
|
||||
.is_ok()
|
||||
{
|
||||
self.wrkdir = path::absolutize(Path::new("/"), dir_p.as_path());
|
||||
info!("New working directory: {}", self.wrkdir.display());
|
||||
Ok(self.wrkdir.clone())
|
||||
} else {
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::NoSuchFileOrDirectory,
|
||||
))
|
||||
}
|
||||
}
|
||||
false => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### copy
|
||||
///
|
||||
/// Copy file to destination
|
||||
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> FileTransferResult<()> {
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### list_dir
|
||||
///
|
||||
/// List directory entries
|
||||
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>> {
|
||||
match self.is_connected() {
|
||||
true => self
|
||||
.list_objects(path, true)
|
||||
.map(|x| x.into_iter().map(|x| x.into()).collect()),
|
||||
false => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### mkdir
|
||||
///
|
||||
/// Make directory
|
||||
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
|
||||
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> {
|
||||
match &self.bucket {
|
||||
Some(bucket) => {
|
||||
let dir: String = Self::fmt_path(self.resolve(dir).as_path(), true);
|
||||
debug!("Making directory {}...", dir);
|
||||
// Check if directory already exists
|
||||
if self
|
||||
.stat_object(PathBuf::from(dir.as_str()).as_path())
|
||||
.is_ok()
|
||||
{
|
||||
error!("Directory {} already exists", dir);
|
||||
return Err(FileTransferError::new(
|
||||
FileTransferErrorType::DirectoryAlreadyExists,
|
||||
));
|
||||
}
|
||||
bucket
|
||||
.put_object(dir.as_str(), &[])
|
||||
.map(|_| ())
|
||||
.map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::FileCreateDenied,
|
||||
format!("Could not make directory: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### remove
|
||||
///
|
||||
/// Remove a file or a directory
|
||||
fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()> {
|
||||
let path = Self::fmt_path(
|
||||
path::diff_paths(file.get_abs_path(), &Path::new("/"))
|
||||
.unwrap_or_default()
|
||||
.as_path(),
|
||||
file.is_dir(),
|
||||
);
|
||||
info!("Removing object {}...", path);
|
||||
match &self.bucket {
|
||||
Some(bucket) => bucket.delete_object(path).map(|_| ()).map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
format!("Could not remove file: {}", e),
|
||||
)
|
||||
}),
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### rename
|
||||
///
|
||||
/// Rename file or a directory
|
||||
fn rename(&mut self, _file: &FsEntry, _dst: &Path) -> FileTransferResult<()> {
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### stat
|
||||
///
|
||||
/// Stat file and return FsEntry
|
||||
fn stat(&mut self, p: &Path) -> FileTransferResult<FsEntry> {
|
||||
match self.is_connected() {
|
||||
true => {
|
||||
// First try as a "file"
|
||||
let path: PathBuf = self.resolve(p);
|
||||
if let Ok(obj) = self.stat_object(path.as_path()) {
|
||||
return Ok(obj.into());
|
||||
}
|
||||
// Try as a "directory"
|
||||
debug!("Failed to stat object as file; trying as a directory...");
|
||||
let path: PathBuf = PathBuf::from(Self::fmt_path(path.as_path(), true));
|
||||
self.stat_object(path.as_path()).map(|x| x.into())
|
||||
}
|
||||
false => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### exec
|
||||
///
|
||||
/// Execute a command on remote host
|
||||
fn exec(&mut self, _cmd: &str) -> FileTransferResult<String> {
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### send_file_wno_stream
|
||||
///
|
||||
/// Send a file to remote WITHOUT using streams.
|
||||
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
|
||||
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
|
||||
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
|
||||
/// By default this function uses the streams function to copy content from reader to writer
|
||||
fn send_file_wno_stream(
|
||||
&mut self,
|
||||
_src: &FsFile,
|
||||
dest: &Path,
|
||||
mut reader: Box<dyn Read>,
|
||||
) -> FileTransferResult<()> {
|
||||
match &mut self.bucket {
|
||||
Some(bucket) => {
|
||||
let key = Self::fmt_path(dest, false);
|
||||
info!("Query PUT for key '{}'", key);
|
||||
bucket
|
||||
.put_object_stream(&mut reader, key.as_str())
|
||||
.map(|_| ())
|
||||
.map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
format!("Could not put file: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### recv_file_wno_stream
|
||||
///
|
||||
/// Receive a file from remote WITHOUT using streams.
|
||||
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
|
||||
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
|
||||
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
|
||||
/// By default this function uses the streams function to copy content from reader to writer
|
||||
fn recv_file_wno_stream(&mut self, src: &FsFile, dest: &Path) -> FileTransferResult<()> {
|
||||
match &mut self.bucket {
|
||||
Some(bucket) => {
|
||||
let mut writer = File::create(dest).map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::FileCreateDenied,
|
||||
format!("Could not open local file: {}", e),
|
||||
)
|
||||
})?;
|
||||
let key = Self::fmt_fs_file_path(src);
|
||||
info!("Query GET for key '{}'", key);
|
||||
bucket
|
||||
.get_object_stream(key.as_str(), &mut writer)
|
||||
.map(|_| ())
|
||||
.map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
format!("Could not get file: {}", e),
|
||||
)
|
||||
})
|
||||
}
|
||||
None => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
#[cfg(feature = "with-s3-ci")]
|
||||
use crate::filetransfer::params::AwsS3Params;
|
||||
#[cfg(feature = "with-s3-ci")]
|
||||
use crate::utils::random;
|
||||
use crate::utils::test_helpers;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(feature = "with-s3-ci")]
|
||||
use std::env;
|
||||
#[cfg(feature = "with-s3-ci")]
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn s3_new() {
|
||||
let s3: S3FileTransfer = S3FileTransfer::default();
|
||||
assert_eq!(s3.wrkdir.as_path(), Path::new("/"));
|
||||
assert!(s3.bucket.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_is_direct_child() {
|
||||
assert_eq!(S3FileTransfer::is_direct_child("pippo/", ""), true);
|
||||
assert_eq!(
|
||||
S3FileTransfer::is_direct_child("pippo/sottocartella/", ""),
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
S3FileTransfer::is_direct_child("pippo/sottocartella/", "pippo/"),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
S3FileTransfer::is_direct_child("pippo/sottocartella/", "pippo"), // This case must be handled indeed
|
||||
false
|
||||
);
|
||||
assert_eq!(
|
||||
S3FileTransfer::is_direct_child(
|
||||
"pippo/sottocartella/readme.md",
|
||||
"pippo/sottocartella/"
|
||||
),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
S3FileTransfer::is_direct_child(
|
||||
"pippo/sottocartella/readme.md",
|
||||
"pippo/sottocartella/"
|
||||
),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_resolve() {
|
||||
let mut s3: S3FileTransfer = S3FileTransfer::default();
|
||||
s3.wrkdir = PathBuf::from("/tmp");
|
||||
// Absolute
|
||||
assert_eq!(
|
||||
s3.resolve(&Path::new("/tmp/sottocartella/")).as_path(),
|
||||
Path::new("tmp/sottocartella")
|
||||
);
|
||||
// Relative
|
||||
assert_eq!(
|
||||
s3.resolve(&Path::new("subfolder/")).as_path(),
|
||||
Path::new("tmp/subfolder")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_fmt_fs_file_path() {
|
||||
let f: FsFile =
|
||||
test_helpers::make_fsentry(&Path::new("/tmp/omar.txt"), false).unwrap_file();
|
||||
assert_eq!(
|
||||
S3FileTransfer::fmt_fs_file_path(&f).as_str(),
|
||||
"tmp/omar.txt"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_fmt_path() {
|
||||
assert_eq!(
|
||||
S3FileTransfer::fmt_path(&Path::new("/tmp/omar.txt"), false).as_str(),
|
||||
"tmp/omar.txt"
|
||||
);
|
||||
assert_eq!(
|
||||
S3FileTransfer::fmt_path(&Path::new("omar.txt"), false).as_str(),
|
||||
"omar.txt"
|
||||
);
|
||||
assert_eq!(
|
||||
S3FileTransfer::fmt_path(&Path::new("/tmp/subfolder"), true).as_str(),
|
||||
"tmp/subfolder/"
|
||||
);
|
||||
assert_eq!(
|
||||
S3FileTransfer::fmt_path(&Path::new("tmp/subfolder"), true).as_str(),
|
||||
"tmp/subfolder/"
|
||||
);
|
||||
assert_eq!(
|
||||
S3FileTransfer::fmt_path(&Path::new("tmp"), true).as_str(),
|
||||
"tmp/"
|
||||
);
|
||||
assert_eq!(
|
||||
S3FileTransfer::fmt_path(&Path::new("tmp/"), true).as_str(),
|
||||
"tmp/"
|
||||
);
|
||||
assert_eq!(S3FileTransfer::fmt_path(&Path::new("/"), true).as_str(), "");
|
||||
}
|
||||
|
||||
// -- test transfer
|
||||
#[cfg(feature = "with-s3-ci")]
|
||||
#[test]
|
||||
fn s3_filetransfer() {
|
||||
// Gather s3 environment args
|
||||
let bucket: String = env::var("AWS_S3_BUCKET").ok().unwrap();
|
||||
let region: String = env::var("AWS_S3_REGION").ok().unwrap();
|
||||
let params = get_ftparams(bucket, region);
|
||||
// Get transfer
|
||||
let mut s3 = S3FileTransfer::default();
|
||||
// Connect
|
||||
assert!(s3.connect(¶ms).is_ok());
|
||||
// Check is connected
|
||||
assert_eq!(s3.is_connected(), true);
|
||||
// Pwd
|
||||
assert_eq!(s3.pwd().ok().unwrap(), PathBuf::from("/"));
|
||||
// Go to github-ci directory
|
||||
assert!(s3.change_dir(&Path::new("/github-ci")).is_ok());
|
||||
assert_eq!(s3.pwd().ok().unwrap(), PathBuf::from("/github-ci"));
|
||||
// Find
|
||||
assert_eq!(s3.find("*.jpg").ok().unwrap().len(), 1);
|
||||
// List directory (3 entries)
|
||||
assert_eq!(s3.list_dir(&Path::new("/github-ci")).ok().unwrap().len(), 3);
|
||||
// Go to playground
|
||||
assert!(s3.change_dir(&Path::new("/github-ci/playground")).is_ok());
|
||||
assert_eq!(
|
||||
s3.pwd().ok().unwrap(),
|
||||
PathBuf::from("/github-ci/playground")
|
||||
);
|
||||
// Create directory
|
||||
let dir_name: String = format!("{}/", random::random_alphanumeric_with_len(8));
|
||||
let mut dir_path: PathBuf = PathBuf::from("/github-ci/playground");
|
||||
dir_path.push(dir_name.as_str());
|
||||
let dir_entry = test_helpers::make_fsentry(dir_path.as_path(), true);
|
||||
assert!(s3.mkdir(dir_path.as_path()).is_ok());
|
||||
assert!(s3.change_dir(dir_path.as_path()).is_ok());
|
||||
// Copy/rename file is unsupported
|
||||
assert!(s3.copy(&dir_entry, &Path::new("/copia")).is_err());
|
||||
assert!(s3.rename(&dir_entry, &Path::new("/copia")).is_err());
|
||||
// Exec is unsupported
|
||||
assert!(s3.exec("omar!").is_err());
|
||||
// Stat file
|
||||
let entry = s3
|
||||
.stat(&Path::new("/github-ci/avril_lavigne.jpg"))
|
||||
.ok()
|
||||
.unwrap()
|
||||
.unwrap_file();
|
||||
assert_eq!(entry.name.as_str(), "avril_lavigne.jpg");
|
||||
assert_eq!(
|
||||
entry.abs_path.as_path(),
|
||||
Path::new("/github-ci/avril_lavigne.jpg")
|
||||
);
|
||||
assert_eq!(entry.ftype.as_deref().unwrap(), "jpg");
|
||||
assert_eq!(entry.size, 101738);
|
||||
assert_eq!(entry.user, None);
|
||||
assert_eq!(entry.group, None);
|
||||
assert_eq!(entry.unix_pex, None);
|
||||
// Download file
|
||||
let (local_file_entry, local_file): (FsFile, NamedTempFile) =
|
||||
test_helpers::create_sample_file_entry();
|
||||
let remote_entry =
|
||||
test_helpers::make_fsentry(&Path::new("/github-ci/avril_lavigne.jpg"), false)
|
||||
.unwrap_file();
|
||||
assert!(s3
|
||||
.recv_file_wno_stream(&remote_entry, local_file.path())
|
||||
.is_ok());
|
||||
// Upload file
|
||||
let mut dest_path = dir_path.clone();
|
||||
dest_path.push("aurellia_lavagna.jpg");
|
||||
let reader = Box::new(File::open(local_file.path()).ok().unwrap());
|
||||
assert!(s3
|
||||
.send_file_wno_stream(&local_file_entry, dest_path.as_path(), reader)
|
||||
.is_ok());
|
||||
// Remove temp dir
|
||||
assert!(s3.remove(&dir_entry).is_ok());
|
||||
// Disconnect
|
||||
assert!(s3.disconnect().is_ok());
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-s3-ci")]
|
||||
fn get_ftparams(bucket: String, region: String) -> ProtocolParams {
|
||||
ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, None))
|
||||
}
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
//! ## S3 object
|
||||
//!
|
||||
//! This module exposes the S3Object structure, which is an intermediate structure to work with
|
||||
//! S3 objects. Easy to be converted into a FsEntry.
|
||||
|
||||
/**
|
||||
* MIT License
|
||||
*
|
||||
* termscp - Copyright (c) 2021 Christian Visintin
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{FsDirectory, FsEntry, FsFile, Object};
|
||||
use crate::utils::parser::parse_datetime;
|
||||
use crate::utils::path;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
/// ## S3Object
|
||||
///
|
||||
/// An intermediate struct to work with s3 `Object`.
|
||||
/// Really easy to be converted into a `FsEntry`
|
||||
#[derive(Debug)]
|
||||
pub struct S3Object {
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
pub size: usize,
|
||||
pub last_modified: SystemTime,
|
||||
/// Whether or not represents a directory. I already know directories don't exist in s3!
|
||||
pub is_dir: bool,
|
||||
}
|
||||
|
||||
impl From<&Object> for S3Object {
|
||||
fn from(obj: &Object) -> Self {
|
||||
let is_dir: bool = obj.key.ends_with('/');
|
||||
let abs_path: PathBuf = path::absolutize(
|
||||
PathBuf::from("/").as_path(),
|
||||
PathBuf::from(obj.key.as_str()).as_path(),
|
||||
);
|
||||
let last_modified: SystemTime =
|
||||
match parse_datetime(obj.last_modified.as_str(), "%Y-%m-%dT%H:%M:%S%Z") {
|
||||
Ok(dt) => dt,
|
||||
Err(_) => UNIX_EPOCH,
|
||||
};
|
||||
Self {
|
||||
name: Self::object_name(obj.key.as_str()),
|
||||
path: abs_path,
|
||||
size: obj.size as usize,
|
||||
last_modified,
|
||||
is_dir,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<S3Object> for FsEntry {
|
||||
fn from(obj: S3Object) -> Self {
|
||||
let abs_path: PathBuf = path::absolutize(Path::new("/"), obj.path.as_path());
|
||||
match obj.is_dir {
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
name: obj.name,
|
||||
abs_path,
|
||||
last_change_time: obj.last_modified,
|
||||
last_access_time: obj.last_modified,
|
||||
creation_time: obj.last_modified,
|
||||
symlink: None,
|
||||
user: None,
|
||||
group: None,
|
||||
unix_pex: None,
|
||||
}),
|
||||
false => FsEntry::File(FsFile {
|
||||
name: obj.name,
|
||||
ftype: obj
|
||||
.path
|
||||
.extension()
|
||||
.map(|x| x.to_string_lossy().to_string()),
|
||||
abs_path,
|
||||
size: obj.size,
|
||||
last_change_time: obj.last_modified,
|
||||
last_access_time: obj.last_modified,
|
||||
creation_time: obj.last_modified,
|
||||
symlink: None,
|
||||
user: None,
|
||||
group: None,
|
||||
unix_pex: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl S3Object {
|
||||
/// ### object_name
|
||||
///
|
||||
/// Get object name from key
|
||||
pub fn object_name(key: &str) -> String {
|
||||
let mut tokens = key.split('/');
|
||||
let count = tokens.clone().count();
|
||||
let demi_last: String = match count > 1 {
|
||||
true => tokens.nth(count - 2).unwrap().to_string(),
|
||||
false => String::new(),
|
||||
};
|
||||
if let Some(last) = tokens.last() {
|
||||
// If last is not empty, return last one
|
||||
if !last.is_empty() {
|
||||
return last.to_string();
|
||||
}
|
||||
}
|
||||
// Return demi last
|
||||
demi_last
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn object_to_s3object_file() {
|
||||
let obj: Object = Object {
|
||||
key: String::from("pippo/sottocartella/chiedo.gif"),
|
||||
e_tag: String::default(),
|
||||
size: 1516966,
|
||||
owner: None,
|
||||
storage_class: String::default(),
|
||||
last_modified: String::from("2021-08-28T10:20:37.000Z"),
|
||||
};
|
||||
let s3_obj: S3Object = S3Object::from(&obj);
|
||||
assert_eq!(s3_obj.name.as_str(), "chiedo.gif");
|
||||
assert_eq!(
|
||||
s3_obj.path.as_path(),
|
||||
Path::new("/pippo/sottocartella/chiedo.gif")
|
||||
);
|
||||
assert_eq!(s3_obj.size, 1516966);
|
||||
assert_eq!(s3_obj.is_dir, false);
|
||||
assert_eq!(
|
||||
s3_obj
|
||||
.last_modified
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1630146037)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_to_s3object_dir() {
|
||||
let obj: Object = Object {
|
||||
key: String::from("temp/"),
|
||||
e_tag: String::default(),
|
||||
size: 0,
|
||||
owner: None,
|
||||
storage_class: String::default(),
|
||||
last_modified: String::from("2021-08-28T10:20:37.000Z"),
|
||||
};
|
||||
let s3_obj: S3Object = S3Object::from(&obj);
|
||||
assert_eq!(s3_obj.name.as_str(), "temp");
|
||||
assert_eq!(s3_obj.path.as_path(), Path::new("/temp"));
|
||||
assert_eq!(s3_obj.size, 0);
|
||||
assert_eq!(s3_obj.is_dir, true);
|
||||
assert_eq!(
|
||||
s3_obj
|
||||
.last_modified
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1630146037)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fsentry_from_s3obj_file() {
|
||||
let obj: S3Object = S3Object {
|
||||
name: String::from("chiedo.gif"),
|
||||
path: PathBuf::from("/pippo/sottocartella/chiedo.gif"),
|
||||
size: 1516966,
|
||||
is_dir: false,
|
||||
last_modified: UNIX_EPOCH,
|
||||
};
|
||||
let entry: FsFile = FsEntry::from(obj).unwrap_file();
|
||||
assert_eq!(entry.name.as_str(), "chiedo.gif");
|
||||
assert_eq!(
|
||||
entry.abs_path.as_path(),
|
||||
Path::new("/pippo/sottocartella/chiedo.gif")
|
||||
);
|
||||
assert_eq!(entry.creation_time, UNIX_EPOCH);
|
||||
assert_eq!(entry.last_change_time, UNIX_EPOCH);
|
||||
assert_eq!(entry.last_access_time, UNIX_EPOCH);
|
||||
assert_eq!(entry.size, 1516966);
|
||||
assert_eq!(entry.ftype.unwrap().as_str(), "gif");
|
||||
assert_eq!(entry.user, None);
|
||||
assert_eq!(entry.group, None);
|
||||
assert_eq!(entry.unix_pex, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fsentry_from_s3obj_directory() {
|
||||
let obj: S3Object = S3Object {
|
||||
name: String::from("temp"),
|
||||
path: PathBuf::from("/temp"),
|
||||
size: 0,
|
||||
is_dir: true,
|
||||
last_modified: UNIX_EPOCH,
|
||||
};
|
||||
let entry: FsDirectory = FsEntry::from(obj).unwrap_dir();
|
||||
assert_eq!(entry.name.as_str(), "temp");
|
||||
assert_eq!(entry.abs_path.as_path(), Path::new("/temp"));
|
||||
assert_eq!(entry.creation_time, UNIX_EPOCH);
|
||||
assert_eq!(entry.last_change_time, UNIX_EPOCH);
|
||||
assert_eq!(entry.last_access_time, UNIX_EPOCH);
|
||||
assert_eq!(entry.user, None);
|
||||
assert_eq!(entry.group, None);
|
||||
assert_eq!(entry.unix_pex, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_name() {
|
||||
assert_eq!(
|
||||
S3Object::object_name("pippo/sottocartella/chiedo.gif").as_str(),
|
||||
"chiedo.gif"
|
||||
);
|
||||
assert_eq!(
|
||||
S3Object::object_name("pippo/sottocartella/").as_str(),
|
||||
"sottocartella"
|
||||
);
|
||||
assert_eq!(S3Object::object_name("pippo/").as_str(), "pippo");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user