Aws s3 support

This commit is contained in:
veeso 2021-08-26 11:24:13 +02:00
parent f31f58aa79
commit 1d09095ab9
37 changed files with 3458 additions and 973 deletions

View File

@ -9,6 +9,7 @@ ignore:
- src/main.rs
- src/lib.rs
- src/activity_manager.rs
- src/filetransfer/transfer/s3/mod.rs
- src/support.rs
- "src/ui/activities/*"
- src/ui/context.rs

View File

@ -27,6 +27,12 @@ Released on ??
> 🍁 Autumn update 🍇
- **Aws S3**
- Added support for the aws-s3 protocol
- Operate on your bucket directly from the file explorer
- You can also save your buckets as bookmarks
- Aws s3 reads credentials directly from your credentials file at `$HOME/.aws/credentials` or from environment. Read more in the user manual.
## 0.6.1
Released on 31/08/2021

270
Cargo.lock generated
View File

@ -33,6 +33,12 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "ahash"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
[[package]]
name = "aho-corasick"
version = "0.7.18"
@ -51,6 +57,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "anyhow"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
[[package]]
name = "argh"
version = "0.1.5"
@ -80,12 +92,78 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a61eb019cb8f415d162cb9f12130ee6bbe9168b7d953c17f4ad049e4051ca00"
[[package]]
name = "async-trait"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "attohttpc"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247"
dependencies = [
"http",
"log",
"native-tls",
"openssl",
"serde",
"serde_json",
"url",
"wildmatch 1.1.0",
]
[[package]]
name = "attohttpc"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8bda305457262b339322106c776e3fd21df860018e566eb6a5b1aa4b6ae02d"
dependencies = [
"http",
"log",
"native-tls",
"openssl",
"url",
"wildmatch 1.1.0",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "aws-creds"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1331d069460a674d42bd27c12b47ce578f789954c7bd7f239fd030771eca6616"
dependencies = [
"anyhow",
"attohttpc 0.16.3",
"dirs",
"rust-ini",
"serde",
"serde-xml-rs",
"serde_derive",
"url",
]
[[package]]
name = "aws-region"
version = "0.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2884b8f2aaeb4a4bf80b219b4fe1d340139ca9331679c57e0fd4a24f571a78bd"
dependencies = [
"anyhow",
]
[[package]]
name = "base64"
version = "0.13.0"
@ -136,6 +214,12 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]]
name = "bytesize"
version = "1.1.0"
@ -368,6 +452,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "dlv-list"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b"
dependencies = [
"rand 0.8.4",
]
[[package]]
name = "edit"
version = "0.1.3"
@ -384,6 +477,12 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
@ -409,6 +508,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures-core"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
[[package]]
name = "generic-array"
version = "0.14.4"
@ -441,6 +546,15 @@ dependencies = [
"wasi 0.10.0+wasi-snapshot-preview1",
]
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
dependencies = [
"ahash",
]
[[package]]
name = "heck"
version = "0.3.3"
@ -450,6 +564,12 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hkdf"
version = "0.10.0"
@ -481,6 +601,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "http"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "idna"
version = "0.2.3"
@ -622,6 +753,17 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "maybe-async"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6007f9dad048e0a224f27ca599d669fca8cfa0dac804725aab542b2eb032bce6"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "md-5"
version = "0.9.1"
@ -633,12 +775,27 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "minidom"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "332592c2149fc7dd40a64fc9ef6f0d65607284b474cef9817d1fc8c7e7b3608e"
dependencies = [
"quick-xml",
]
[[package]]
name = "mio"
version = "0.7.13"
@ -819,6 +976,16 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "ordered-multimap"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485"
dependencies = [
"dlv-list",
"hashbrown",
]
[[package]]
name = "output_vt100"
version = "0.1.2"
@ -895,6 +1062,12 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project-lite"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]]
name = "pkg-config"
version = "0.3.19"
@ -928,6 +1101,15 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "quick-xml"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26aab6b48e2590e4a64d1ed808749ba06257882b461d01ca71baeb747074a6dd"
dependencies = [
"memchr",
]
[[package]]
name = "quote"
version = "1.0.9"
@ -1094,6 +1276,46 @@ dependencies = [
"winapi",
]
[[package]]
name = "rust-ini"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55b134767a87e0b086f73a4ce569ac9ce7d202f39c8eab6caa266e2617e73ac6"
dependencies = [
"cfg-if 0.1.10",
"ordered-multimap",
]
[[package]]
name = "rust-s3"
version = "0.27.0-rc4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c93272c1d654d492f8ab30b94cd43d98f2700b1db55b2576aff7712ce40e3ef"
dependencies = [
"anyhow",
"async-trait",
"attohttpc 0.17.0",
"aws-creds",
"aws-region",
"base64",
"cfg-if 1.0.0",
"chrono",
"hex",
"hmac",
"http",
"log",
"maybe-async",
"md5",
"minidom",
"percent-encoding",
"serde",
"serde-xml-rs",
"serde_derive",
"sha2",
"tokio-stream",
"url",
]
[[package]]
name = "rustls"
version = "0.19.1"
@ -1210,6 +1432,18 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-xml-rs"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa"
dependencies = [
"log",
"serde",
"thiserror",
"xml-rs",
]
[[package]]
name = "serde_derive"
version = "1.0.127"
@ -1392,6 +1626,7 @@ dependencies = [
"rand 0.8.4",
"regex",
"rpassword",
"rust-s3",
"serde",
"simplelog",
"ssh2",
@ -1405,7 +1640,7 @@ dependencies = [
"ureq",
"users",
"whoami",
"wildmatch",
"wildmatch 2.1.0",
]
[[package]]
@ -1476,6 +1711,27 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5"
dependencies = [
"autocfg",
"pin-project-lite",
]
[[package]]
name = "tokio-stream"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "toml"
version = "0.5.8"
@ -1741,6 +1997,12 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wildmatch"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a"
[[package]]
name = "wildmatch"
version = "2.1.0"
@ -1777,3 +2039,9 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "xml-rs"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"

View File

@ -1,7 +1,7 @@
[package]
authors = ["Christian Visintin"]
categories = ["command-line-utilities"]
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP"
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/S3"
documentation = "https://docs.rs/termscp"
edition = "2018"
homepage = "https://veeso.github.io/termscp/"
@ -44,6 +44,7 @@ open = "2.0.1"
rand = "0.8.4"
regex = "1.5.4"
rpassword = "5.0.1"
rust-s3 = { version = "0.27.0-rc4", default-features = false, features = [ "sync-native-tls", "sync" ] }
serde = { version = "^1.0.0", features = [ "derive" ] }
simplelog = "0.10.0"
ssh2 = "0.9.0"
@ -63,7 +64,8 @@ pretty_assertions = "0.7.2"
[features]
default = [ "with-keyring" ]
github-actions = []
github-actions = [ ]
with-s3-ci = []
with-containers = []
with-keyring = [ "keyring" ]

View File

@ -24,7 +24,7 @@
## About termscp 🖥
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **BSD** and **Windows** compatible and supports SFTP, SCP, FTP and FTPS.
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/S3. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **BSD** and **Windows** compatible and supports SFTP, SCP, FTP, FTPS and S3.
![Explorer](assets/images/explorer.gif)
@ -36,6 +36,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
- SFTP
- SCP
- FTP and FTPS
- Aws S3
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
- Create, remove, rename, search, view and edit files
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
@ -160,6 +161,7 @@ termscp is powered by these aweseome projects:
- [keyring-rs](https://github.com/hwchen/keyring-rs)
- [open-rs](https://github.com/Byron/open-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-s3](https://github.com/durch/rust-s3)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
- [suppaftp](https://github.com/veeso/suppaftp)
- [textwrap](https://github.com/mgeisler/textwrap)

View File

@ -3,6 +3,7 @@
- [User manual 🎓](#user-manual-)
- [Usage ❓](#usage-)
- [Address argument 🌎](#address-argument-)
- [AWS S3 address argument](#aws-s3-address-argument)
- [How Password can be provided 🔐](#how-password-can-be-provided-)
- [File explorer 📂](#file-explorer-)
- [Keybindings ⌨](#keybindings-)
@ -13,6 +14,7 @@
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
- [Linux Keyring](#linux-keyring)
- [KeepassXC setup for termscp](#keepassxc-setup-for-termscp)
- [Aws S3 credentials 🦊](#aws-s3-credentials-)
- [Configuration ⚙️](#configuration-)
- [SSH Key Storage 🔐](#ssh-key-storage-)
- [File Explorer Format](#file-explorer-format)
@ -78,6 +80,20 @@ Let's see some example of this particular syntax, since it's very comfortable an
termscp scp://omar@192.168.1.31:4022:/tmp
```
#### AWS S3 address argument
Aws S3 has a different syntax for CLI address argument, for obvious reasons, but I managed to keep it the more similiar as possible to the generic address argument:
```txt
s3://<bucket-name>@<region>[:profile][:/wrkdir]
```
e.g.
```txt
s3://buckethead@eu-central-1:default:/assets
```
#### How Password can be provided 🔐
You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
@ -246,6 +262,30 @@ Follow these steps in order to setup keepassXC for termscp:
---
## Aws S3 credentials 🦊
In order to connect to an Aws S3 bucket you must obviously provide some credentials.
There are basically two ways to achieve this, and as you've probably already noticed you **can't** do that via the authentication form.
So these are the ways you can provide the credentials for s3:
1. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form.
2. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below:
These should always be mandatory:
- `AWS_ACCESS_KEY_ID`: aws access key ID (usually starts with `AKIA...`)
- `AWS_SECRET_ACCESS_KEY`: the secret access key
In case you've configured a stronger security, you *may* require these too:
- `AWS_SECURITY_TOKEN`: security token
- `AWS_SESSION_TOKEN`: session token
⚠️ Your credentials are safe: termscp won't manipulate these values directly! Your credentials are directly consumed by the **s3** crate.
In case you've got some concern regarding security, please contact the library author on [Github](https://github.com/durch/rust-s3) ⚠️
---
## Configuration ⚙️
termscp supports some user defined parameters, which can be defined in the configuration.

View File

@ -25,31 +25,57 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
use std::str::FromStr;
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserHosts
///
/// UserHosts contains all the hosts saved by the user in the data storage
/// It contains both `Bookmark`
#[derive(Deserialize, Serialize, Debug)]
pub struct UserHosts {
pub bookmarks: HashMap<String, Bookmark>,
pub recents: HashMap<String, Bookmark>,
}
#[derive(Deserialize, Serialize, std::fmt::Debug, PartialEq)]
/// ## Bookmark
///
/// Bookmark describes a single bookmark entry in the user hosts storage
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
pub struct Bookmark {
pub address: String,
pub port: u16,
pub protocol: String,
pub username: String,
pub password: Option<String>, // Password is optional; base64, aes-128 encrypted password
#[serde(
deserialize_with = "deserialize_protocol",
serialize_with = "serialize_protocol"
)]
pub protocol: FileTransferProtocol,
/// Address for generic parameters
pub address: Option<String>,
/// Port number for generic parameters
pub port: Option<u16>,
/// Username for generic parameters
pub username: Option<String>,
/// Password is optional; base64, aes-128 encrypted password
pub password: Option<String>,
/// S3 params; optional. When used other fields are empty for sure
pub s3: Option<S3Params>,
}
/// ## S3Params
///
/// Connection parameters for Aws s3 protocol
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Default)]
pub struct S3Params {
pub bucket: String,
pub region: String,
pub profile: Option<String>,
}
// -- impls
impl Default for UserHosts {
fn default() -> Self {
Self {
@ -59,6 +85,87 @@ impl Default for UserHosts {
}
}
impl From<FileTransferParams> for Bookmark {
fn from(params: FileTransferParams) -> Self {
let protocol: FileTransferProtocol = params.protocol;
// Create generic or others
match params.params {
ProtocolParams::Generic(params) => Self {
protocol,
address: Some(params.address),
port: Some(params.port),
username: params.username,
password: params.password,
s3: None,
},
ProtocolParams::AwsS3(params) => Self {
protocol,
address: None,
port: None,
username: None,
password: None,
s3: Some(S3Params::from(params)),
},
}
}
}
impl From<Bookmark> for FileTransferParams {
fn from(bookmark: Bookmark) -> Self {
// Create generic or others based on protocol
match bookmark.protocol {
FileTransferProtocol::AwsS3 => {
let params = bookmark.s3.unwrap_or_default();
let params = AwsS3Params::from(params);
Self::new(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params))
}
protocol => {
let params = GenericProtocolParams::default()
.address(bookmark.address.unwrap_or_default())
.port(bookmark.port.unwrap_or(22))
.username(bookmark.username)
.password(bookmark.password);
Self::new(protocol, ProtocolParams::Generic(params))
}
}
}
}
impl From<AwsS3Params> for S3Params {
fn from(params: AwsS3Params) -> Self {
S3Params {
bucket: params.bucket_name,
region: params.region,
profile: params.profile,
}
}
}
impl From<S3Params> for AwsS3Params {
fn from(params: S3Params) -> Self {
AwsS3Params::new(params.bucket, params.region, params.profile)
}
}
fn deserialize_protocol<'de, D>(deserializer: D) -> Result<FileTransferProtocol, D::Error>
where
D: Deserializer<'de>,
{
let s: &str = Deserialize::deserialize(deserializer)?;
// Parse color
match FileTransferProtocol::from_str(s) {
Err(err) => Err(DeError::custom(err)),
Ok(protocol) => Ok(protocol),
}
}
fn serialize_protocol<S>(protocol: &FileTransferProtocol, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(protocol.to_string().as_str())
}
// Tests
#[cfg(test)]
@ -77,48 +184,117 @@ mod tests {
#[test]
fn test_bookmarks_bookmark_new() {
let bookmark: Bookmark = Bookmark {
address: String::from("192.168.1.1"),
port: 22,
protocol: String::from("SFTP"),
username: String::from("root"),
address: Some(String::from("192.168.1.1")),
port: Some(22),
protocol: FileTransferProtocol::Sftp,
username: Some(String::from("root")),
password: Some(String::from("password")),
s3: None,
};
let recent: Bookmark = Bookmark {
address: String::from("192.168.1.2"),
port: 22,
protocol: String::from("SCP"),
username: String::from("admin"),
address: Some(String::from("192.168.1.2")),
port: Some(22),
protocol: FileTransferProtocol::Scp,
username: Some(String::from("admin")),
password: Some(String::from("password")),
s3: None,
};
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1);
bookmarks.insert(String::from("test"), bookmark);
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
recents.insert(String::from("ISO20201218T181432"), recent);
let hosts: UserHosts = UserHosts {
bookmarks: bookmarks,
recents: recents,
};
let hosts: UserHosts = UserHosts { bookmarks, recents };
// Verify
let bookmark: &Bookmark = hosts.bookmarks.get(&String::from("test")).unwrap();
assert_eq!(bookmark.address, String::from("192.168.1.1"));
assert_eq!(bookmark.port, 22);
assert_eq!(bookmark.protocol, String::from("SFTP"));
assert_eq!(bookmark.username, String::from("root"));
assert_eq!(
*bookmark.password.as_ref().unwrap(),
String::from("password")
);
assert_eq!(bookmark.address.as_deref().unwrap(), "192.168.1.1");
assert_eq!(bookmark.port.unwrap(), 22);
assert_eq!(bookmark.protocol, FileTransferProtocol::Sftp);
assert_eq!(bookmark.username.as_deref().unwrap(), "root");
assert_eq!(bookmark.password.as_deref().unwrap(), "password");
let bookmark: &Bookmark = hosts
.recents
.get(&String::from("ISO20201218T181432"))
.unwrap();
assert_eq!(bookmark.address, String::from("192.168.1.2"));
assert_eq!(bookmark.port, 22);
assert_eq!(bookmark.protocol, String::from("SCP"));
assert_eq!(bookmark.username, String::from("admin"));
assert_eq!(
*bookmark.password.as_ref().unwrap(),
String::from("password")
);
assert_eq!(bookmark.address.as_deref().unwrap(), "192.168.1.2");
assert_eq!(bookmark.port.unwrap(), 22);
assert_eq!(bookmark.protocol, FileTransferProtocol::Scp);
assert_eq!(bookmark.username.as_deref().unwrap(), "admin");
assert_eq!(bookmark.password.as_deref().unwrap(), "password");
}
#[test]
fn bookmark_from_generic_ftparams() {
let params = ProtocolParams::Generic(GenericProtocolParams {
address: "127.0.0.1".to_string(),
port: 10222,
username: Some(String::from("root")),
password: Some(String::from("omar")),
});
let params: FileTransferParams = FileTransferParams::new(FileTransferProtocol::Scp, params);
let bookmark = Bookmark::from(params);
assert_eq!(bookmark.protocol, FileTransferProtocol::Scp);
assert_eq!(bookmark.address.as_deref().unwrap(), "127.0.0.1");
assert_eq!(bookmark.port.unwrap(), 10222);
assert_eq!(bookmark.username.as_deref().unwrap(), "root");
assert_eq!(bookmark.password.as_deref().unwrap(), "omar");
assert!(bookmark.s3.is_none());
}
#[test]
fn bookmark_from_s3_ftparams() {
let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test")));
let params: FileTransferParams =
FileTransferParams::new(FileTransferProtocol::AwsS3, params);
let bookmark = Bookmark::from(params);
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
assert!(bookmark.address.is_none());
assert!(bookmark.port.is_none());
assert!(bookmark.username.is_none());
assert!(bookmark.password.is_none());
let s3: &S3Params = bookmark.s3.as_ref().unwrap();
assert_eq!(s3.bucket.as_str(), "omar");
assert_eq!(s3.region.as_str(), "eu-west-1");
assert_eq!(s3.profile.as_deref().unwrap(), "test");
}
#[test]
fn ftparams_from_generic_bookmark() {
let bookmark: Bookmark = Bookmark {
address: Some(String::from("192.168.1.1")),
port: Some(22),
protocol: FileTransferProtocol::Sftp,
username: Some(String::from("root")),
password: Some(String::from("password")),
s3: None,
};
let params = FileTransferParams::from(bookmark);
assert_eq!(params.protocol, FileTransferProtocol::Sftp);
let gparams = params.params.generic_params().unwrap();
assert_eq!(gparams.address.as_str(), "192.168.1.1");
assert_eq!(gparams.port, 22);
assert_eq!(gparams.username.as_deref().unwrap(), "root");
assert_eq!(gparams.password.as_deref().unwrap(), "password");
}
#[test]
fn ftparams_from_s3_bookmark() {
let bookmark: Bookmark = Bookmark {
protocol: FileTransferProtocol::AwsS3,
address: None,
port: None,
username: None,
password: None,
s3: Some(S3Params {
bucket: String::from("veeso"),
region: String::from("eu-west-1"),
profile: None,
}),
};
let params = FileTransferParams::from(bookmark);
assert_eq!(params.protocol, FileTransferProtocol::AwsS3);
let gparams = params.params.s3_params().unwrap();
assert_eq!(gparams.bucket_name.as_str(), "veeso");
assert_eq!(gparams.region.as_str(), "eu-west-1");
assert_eq!(gparams.profile, None);
}
}

View File

@ -141,17 +141,19 @@ where
mod tests {
use super::*;
use crate::config::bookmarks::{Bookmark, S3Params, UserHosts};
use crate::config::params::UserConfig;
use crate::config::themes::Theme;
use crate::filetransfer::FileTransferProtocol;
use crate::utils::test_helpers::create_file_ioers;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::io::{Seek, SeekFrom};
use std::path::PathBuf;
use tuirealm::tui::style::Color;
use crate::config::bookmarks::{Bookmark, UserHosts};
use crate::config::params::UserConfig;
use crate::config::themes::Theme;
use crate::utils::test_helpers::create_file_ioers;
#[test]
fn test_config_serialization_errors() {
let error: SerializerError = SerializerError::new(SerializerErrorKind::Syntax);
@ -373,31 +375,42 @@ mod tests {
// Verify recents
assert_eq!(hosts.recents.len(), 1);
let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap();
assert_eq!(host.address, String::from("172.16.104.10"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SCP"));
assert_eq!(host.username, String::from("root"));
assert_eq!(host.address.as_deref().unwrap(), "172.16.104.10");
assert_eq!(host.port.unwrap(), 22);
assert_eq!(host.protocol, FileTransferProtocol::Scp);
assert_eq!(host.username.as_deref().unwrap(), "root");
assert_eq!(host.password, None);
// Verify bookmarks
assert_eq!(hosts.bookmarks.len(), 3);
assert_eq!(hosts.bookmarks.len(), 4);
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
assert_eq!(host.address, String::from("192.168.1.31"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SFTP"));
assert_eq!(host.username, String::from("root"));
assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword"));
assert_eq!(host.address.as_deref().unwrap(), "192.168.1.31");
assert_eq!(host.port.unwrap(), 22);
assert_eq!(host.protocol, FileTransferProtocol::Sftp);
assert_eq!(host.username.as_deref().unwrap(), "root");
assert_eq!(host.password.as_deref().unwrap(), "mypassword");
let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap();
assert_eq!(host.address, String::from("192.168.1.30"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SFTP"));
assert_eq!(host.username, String::from("cvisintin"));
assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret"));
assert_eq!(host.address.as_deref().unwrap(), "192.168.1.30");
assert_eq!(host.port.unwrap(), 22);
assert_eq!(host.protocol, FileTransferProtocol::Sftp);
assert_eq!(host.username.as_deref().unwrap(), "cvisintin");
assert_eq!(host.password.as_deref().unwrap(), "mysecret");
let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap();
assert_eq!(host.address, String::from("51.23.67.12"));
assert_eq!(host.port, 21);
assert_eq!(host.protocol, String::from("FTPS"));
assert_eq!(host.username, String::from("aws001"));
assert_eq!(host.address.as_deref().unwrap(), "51.23.67.12");
assert_eq!(host.port.unwrap(), 21);
assert_eq!(host.protocol, FileTransferProtocol::Ftp(true));
assert_eq!(host.username.as_deref().unwrap(), "aws001");
assert_eq!(host.password, None);
// Aws s3 bucket
let host: &Bookmark = hosts.bookmarks.get("my-bucket").unwrap();
assert_eq!(host.address, None);
assert_eq!(host.port, None);
assert_eq!(host.username, None);
assert_eq!(host.password, None);
assert_eq!(host.protocol, FileTransferProtocol::AwsS3);
let s3 = host.s3.as_ref().unwrap();
assert_eq!(s3.bucket.as_str(), "veeso");
assert_eq!(s3.region.as_str(), "eu-west-1");
assert_eq!(s3.profile.as_deref().unwrap(), "default");
}
#[test]
@ -416,32 +429,50 @@ mod tests {
bookmarks.insert(
String::from("raspberrypi2"),
Bookmark {
address: String::from("192.168.1.31"),
port: 22,
protocol: String::from("SFTP"),
username: String::from("root"),
address: Some(String::from("192.168.1.31")),
port: Some(22),
protocol: FileTransferProtocol::Sftp,
username: Some(String::from("root")),
password: None,
s3: None,
},
);
bookmarks.insert(
String::from("msi-estrem"),
Bookmark {
address: String::from("192.168.1.30"),
port: 4022,
protocol: String::from("SFTP"),
username: String::from("cvisintin"),
address: Some(String::from("192.168.1.30")),
port: Some(4022),
protocol: FileTransferProtocol::Sftp,
username: Some(String::from("cvisintin")),
password: Some(String::from("password")),
s3: None,
},
);
bookmarks.insert(
String::from("my-bucket"),
Bookmark {
address: None,
port: None,
protocol: FileTransferProtocol::AwsS3,
username: None,
password: None,
s3: Some(S3Params {
bucket: "veeso".to_string(),
region: "eu-west-1".to_string(),
profile: None,
}),
},
);
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
recents.insert(
String::from("ISO20201215T094000Z"),
Bookmark {
address: String::from("192.168.1.254"),
port: 3022,
protocol: String::from("SCP"),
username: String::from("omar"),
address: Some(String::from("192.168.1.254")),
port: Some(3022),
protocol: FileTransferProtocol::Scp,
username: Some(String::from("omar")),
password: Some(String::from("aaa")),
s3: None,
},
);
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
@ -482,6 +513,14 @@ mod tests {
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" }
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" }
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[bookmarks.my-bucket]
protocol = "S3"
[bookmarks.my-bucket.s3]
bucket = "veeso"
region = "eu-west-1"
profile = "default"
[recents]
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
@ -497,7 +536,7 @@ mod tests {
let file_content: &str = r#"
[bookmarks]
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root"}
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP" }
msi-estrem = { address = "192.168.1.30", port = 22 }
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[recents]

View File

@ -28,27 +28,29 @@
// locals
use crate::fs::{FsEntry, FsFile};
// ext
use std::io::{Read, Write};
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use thiserror::Error;
use wildmatch::WildMatch;
// exports
pub mod ftp_transfer;
pub mod params;
pub mod scp_transfer;
pub mod sftp_transfer;
mod transfer;
pub use params::FileTransferParams;
// -- export types
pub use params::{FileTransferParams, ProtocolParams};
pub use transfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
/// ## FileTransferProtocol
///
/// This enum defines the different transfer protocol available in termscp
#[derive(PartialEq, Debug, std::clone::Clone, Copy)]
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum FileTransferProtocol {
Sftp,
Scp,
Ftp(bool), // Bool is for secure (true => ftps)
AwsS3,
}
/// ## FileTransferError
@ -130,25 +132,16 @@ impl std::fmt::Display for 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,
address: String,
port: u16,
username: Option<String>,
password: Option<String>,
) -> Result<Option<String>, FileTransferError>;
fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError>;
/// ### disconnect
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> Result<(), FileTransferError>;
/// ### is_connected
@ -210,18 +203,28 @@ pub trait FileTransfer {
/// 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
/// Returns file and its size.
/// By default returns unsupported feature
fn send_file(
&mut self,
local: &FsFile,
file_name: &Path,
) -> Result<Box<dyn Write>, FileTransferError>;
_local: &FsFile,
_file_name: &Path,
) -> Result<Box<dyn Write>, FileTransferError> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### recv_file
///
/// Receive file from remote with provided name
/// Returns file and its size
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError>;
/// By default returns unsupported feature
fn recv_file(&mut self, _file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### on_sent
///
@ -230,7 +233,10 @@ pub trait FileTransfer {
/// 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>) -> Result<(), FileTransferError>;
/// By default this function returns already `Ok(())`
fn on_sent(&mut self, _writable: Box<dyn Write>) -> Result<(), FileTransferError> {
Ok(())
}
/// ### on_recv
///
@ -239,7 +245,71 @@ pub trait FileTransfer {
/// 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>) -> Result<(), FileTransferError>;
/// By default this function returns already `Ok(())`
fn on_recv(&mut self, _readable: Box<dyn Read>) -> Result<(), FileTransferError> {
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>,
) -> Result<(), FileTransferError> {
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) -> Result<(), FileTransferError> {
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
///
@ -314,6 +384,7 @@ impl std::string::ToString for FileTransferProtocol {
},
FileTransferProtocol::Scp => "SCP",
FileTransferProtocol::Sftp => "SFTP",
FileTransferProtocol::AwsS3 => "S3",
})
}
}
@ -326,6 +397,7 @@ impl std::str::FromStr for FileTransferProtocol {
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
"SCP" => Ok(FileTransferProtocol::Scp),
"SFTP" => Ok(FileTransferProtocol::Sftp),
"S3" => Ok(FileTransferProtocol::AwsS3),
_ => Err(s.to_string()),
}
}
@ -385,6 +457,14 @@ mod tests {
FileTransferProtocol::from_str("scp").ok().unwrap(),
FileTransferProtocol::Scp
);
assert_eq!(
FileTransferProtocol::from_str("S3").ok().unwrap(),
FileTransferProtocol::AwsS3
);
assert_eq!(
FileTransferProtocol::from_str("s3").ok().unwrap(),
FileTransferProtocol::AwsS3
);
// Error
assert!(FileTransferProtocol::from_str("dummy").is_err());
// To String
@ -398,6 +478,7 @@ mod tests {
);
assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP"));
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
}
#[test]

View File

@ -32,44 +32,132 @@ use std::path::{Path, PathBuf};
/// ### FileTransferParams
///
/// Holds connection parameters for file transfers
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct FileTransferParams {
pub protocol: FileTransferProtocol,
pub params: ProtocolParams,
pub entry_directory: Option<PathBuf>,
}
/// ## ProtocolParams
///
/// Container for protocol params
#[derive(Debug, Clone)]
pub enum ProtocolParams {
Generic(GenericProtocolParams),
AwsS3(AwsS3Params),
}
/// ## GenericProtocolParams
///
/// Protocol params used by most common protocols
#[derive(Debug, Clone)]
pub struct GenericProtocolParams {
pub address: String,
pub port: u16,
pub protocol: FileTransferProtocol,
pub username: Option<String>,
pub password: Option<String>,
pub entry_directory: Option<PathBuf>,
}
/// ## AwsS3Params
///
/// Connection parameters for AWS S3 protocol
#[derive(Debug, Clone)]
pub struct AwsS3Params {
pub bucket_name: String,
pub region: String,
pub profile: Option<String>,
}
impl FileTransferParams {
/// ### new
///
/// Instantiates a new `FileTransferParams`
pub fn new<S: AsRef<str>>(address: S) -> Self {
pub fn new(protocol: FileTransferProtocol, params: ProtocolParams) -> Self {
Self {
address: address.as_ref().to_string(),
port: 22,
protocol: FileTransferProtocol::Sftp,
username: None,
password: None,
protocol,
params,
entry_directory: None,
}
}
/// ### port
/// ### entry_directory
///
/// Set port for params
pub fn port(mut self, port: u16) -> Self {
self.port = port;
/// Set entry directory
pub fn entry_directory<P: AsRef<Path>>(mut self, dir: Option<P>) -> Self {
self.entry_directory = dir.map(|x| x.as_ref().to_path_buf());
self
}
}
impl Default for FileTransferParams {
fn default() -> Self {
Self::new(FileTransferProtocol::Sftp, ProtocolParams::default())
}
}
impl Default for ProtocolParams {
fn default() -> Self {
Self::Generic(GenericProtocolParams::default())
}
}
impl ProtocolParams {
/// ### generic_params
///
/// Retrieve generic parameters from protocol params if any
pub fn generic_params(&self) -> Option<&GenericProtocolParams> {
match self {
ProtocolParams::Generic(params) => Some(params),
_ => None,
}
}
pub fn mut_generic_params(&mut self) -> Option<&mut GenericProtocolParams> {
match self {
ProtocolParams::Generic(params) => Some(params),
_ => None,
}
}
/// ### s3_params
///
/// Retrieve AWS S3 parameters if any
pub fn s3_params(&self) -> Option<&AwsS3Params> {
match self {
ProtocolParams::AwsS3(params) => Some(params),
_ => None,
}
}
}
// -- Generic protocol params
impl Default for GenericProtocolParams {
fn default() -> Self {
Self {
address: "localhost".to_string(),
port: 22,
username: None,
password: None,
}
}
}
impl GenericProtocolParams {
/// ### address
///
/// Set address to params
pub fn address<S: AsRef<str>>(mut self, address: S) -> Self {
self.address = address.as_ref().to_string();
self
}
/// ### protocol
/// ### port
///
/// Set protocol for params
pub fn protocol(mut self, protocol: FileTransferProtocol) -> Self {
self.protocol = protocol;
/// Set port to params
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
@ -88,19 +176,20 @@ impl FileTransferParams {
self.password = password.map(|x| x.as_ref().to_string());
self
}
/// ### entry_directory
///
/// Set entry directory
pub fn entry_directory<P: AsRef<Path>>(mut self, dir: Option<P>) -> Self {
self.entry_directory = dir.map(|x| x.as_ref().to_path_buf());
self
}
}
impl Default for FileTransferParams {
fn default() -> Self {
Self::new("localhost")
// -- S3 params
impl AwsS3Params {
/// ### new
///
/// Instantiates a new `AwsS3Params` struct
pub fn new<S: AsRef<str>>(bucket: S, region: S, profile: Option<S>) -> Self {
Self {
bucket_name: bucket.as_ref().to_string(),
region: region.as_ref().to_string(),
profile: profile.map(|x| x.as_ref().to_string()),
}
}
}
@ -112,26 +201,49 @@ mod test {
#[test]
fn test_filetransfer_params() {
let params: FileTransferParams = FileTransferParams::new("test.rebex.net")
.port(2222)
.protocol(FileTransferProtocol::Scp)
.username(Some("omar"))
.password(Some("foobar"))
.entry_directory(Some(&Path::new("/tmp")));
assert_eq!(params.address.as_str(), "test.rebex.net");
assert_eq!(params.port, 2222);
let params: FileTransferParams =
FileTransferParams::new(FileTransferProtocol::Scp, ProtocolParams::default())
.entry_directory(Some(&Path::new("/tmp")));
assert_eq!(
params.params.generic_params().unwrap().address.as_str(),
"localhost"
);
assert_eq!(params.protocol, FileTransferProtocol::Scp);
assert_eq!(params.username.as_ref().unwrap(), "omar");
assert_eq!(params.password.as_ref().unwrap(), "foobar");
assert_eq!(
params.entry_directory.as_deref().unwrap(),
Path::new("/tmp")
);
}
#[test]
fn test_filetransfer_params_default() {
let params: FileTransferParams = FileTransferParams::default();
fn params_default() {
let params: GenericProtocolParams = ProtocolParams::default()
.generic_params()
.unwrap()
.to_owned();
assert_eq!(params.address.as_str(), "localhost");
assert_eq!(params.port, 22);
assert_eq!(params.protocol, FileTransferProtocol::Sftp);
assert!(params.username.is_none());
assert!(params.password.is_none());
}
#[test]
fn params_aws_s3() {
let params: AwsS3Params = AwsS3Params::new("omar", "eu-west-1", Some("test"));
assert_eq!(params.bucket_name.as_str(), "omar");
assert_eq!(params.region.as_str(), "eu-west-1");
assert_eq!(params.profile.as_deref().unwrap(), "test");
}
#[test]
fn references() {
let mut params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test")));
assert!(params.s3_params().is_some());
assert!(params.generic_params().is_none());
assert!(params.mut_generic_params().is_none());
let mut params = ProtocolParams::default();
assert!(params.s3_params().is_none());
assert!(params.generic_params().is_some());
assert!(params.mut_generic_params().is_some());
}
}

View File

@ -1,4 +1,4 @@
//! ## Ftp_transfer
//! ## FTP transfer
//!
//! `ftp_transfer` is the module which provides the implementation for the FTP/FTPS file transfer
@ -25,7 +25,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use super::{FileTransfer, FileTransferError, FileTransferErrorType, ProtocolParams};
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use crate::utils::fmt::shadow_password;
use crate::utils::path;
@ -178,25 +178,24 @@ impl FileTransfer for FtpFileTransfer {
///
/// Connect to the remote server
fn connect(
&mut self,
address: String,
port: u16,
username: Option<String>,
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
// Get stream
info!("Connecting to {}:{}", address, port);
let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) {
Ok(stream) => stream,
Err(err) => {
error!("Failed to connect: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
));
}
fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError> {
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...");
@ -214,7 +213,7 @@ impl FileTransfer for FtpFileTransfer {
));
}
};
stream = match stream.into_secure(ctx, address.as_str()) {
stream = match stream.into_secure(ctx, params.address.as_str()) {
Ok(s) => s,
Err(err) => {
error!("Failed to setup TLS stream: {}", err);
@ -226,12 +225,12 @@ impl FileTransfer for FtpFileTransfer {
};
}
// Login (use anonymous if credentials are unspecified)
let username: String = match username {
Some(u) => u,
let username: String = match &params.username {
Some(u) => u.to_string(),
None => String::from("anonymous"),
};
let password: String = match password {
Some(pwd) => pwd,
let password: String = match &params.password {
Some(pwd) => pwd.to_string(),
None => String::new(),
};
info!(
@ -645,6 +644,7 @@ impl FileTransfer for FtpFileTransfer {
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;
@ -672,17 +672,15 @@ mod tests {
// Sample file
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect
#[cfg(not(feature = "github-actions"))]
let hostname: String = String::from("127.0.0.1");
#[cfg(feature = "github-actions")]
let hostname: String = String::from("127.0.0.1");
assert!(ftp
.connect(
hostname,
10021,
Some(String::from("test")),
Some(String::from("test")),
)
.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
@ -810,12 +808,13 @@ mod tests {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(
String::from("127.0.0.1"),
10021,
Some(String::from("omar")),
Some(String::from("ommlar")),
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10021)
.username(Some("omar"))
.password(Some("ommlar"))
))
.is_err());
}
@ -824,7 +823,13 @@ mod tests {
fn test_filetransfer_ftp_no_credentials() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
assert!(ftp
.connect(String::from("127.0.0.1"), 10021, None, None)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10021)
.username::<&str>(None)
.password::<&str>(None)
))
.is_err());
}
@ -833,12 +838,13 @@ mod tests {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(
String::from("mybadserver.veryverybad.awful"),
21,
Some(String::from("omar")),
Some(String::from("ommlar")),
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("mybad.veribad.server")
.port(21)
.username::<&str>(None)
.password::<&str>(None)
))
.is_err());
}
@ -890,12 +896,13 @@ mod tests {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(
String::from("test.rebex.net"),
21,
Some(String::from("demo")),
Some(String::from("password"))
)
.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("/"));

View File

@ -0,0 +1,18 @@
//! # transfer
//!
//! This module exposes all the file transfers supported by termscp
// -- import
use super::{FileTransfer, FileTransferError, FileTransferErrorType, 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;

View File

@ -0,0 +1,697 @@
//! ## 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, 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) -> Result<Vec<S3Object>, FileTransferError> {
// 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) -> Result<S3Object, FileTransferError> {
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,
) -> Result<Vec<S3Object>, FileTransferError> {
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) -> Result<Option<String>, FileTransferError> {
// 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) -> Result<(), FileTransferError> {
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) -> Result<PathBuf, FileTransferError> {
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) -> Result<PathBuf, FileTransferError> {
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) -> Result<(), FileTransferError> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### list_dir
///
/// List directory entries
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
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) -> Result<(), FileTransferError> {
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) -> Result<(), FileTransferError> {
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) -> Result<(), FileTransferError> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### stat
///
/// Stat file and return FsEntry
fn stat(&mut self, p: &Path) -> Result<FsEntry, FileTransferError> {
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) -> Result<String, FileTransferError> {
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>,
) -> Result<(), FileTransferError> {
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) -> Result<(), FileTransferError> {
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(&params).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))
}
}

View File

@ -0,0 +1,247 @@
//! ## 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");
}
}

View File

@ -1,4 +1,4 @@
//! ## SCP_Transfer
//! ## SCP transfer
//!
//! `scps_transfer` is the module which provides the implementation for the SCP file transfer
@ -26,7 +26,7 @@
* SOFTWARE.
*/
// Locals
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use super::{FileTransfer, FileTransferError, FileTransferErrorType, ProtocolParams};
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::{fmt_time, shadow_password};
@ -333,17 +333,15 @@ impl FileTransfer for ScpFileTransfer {
/// ### connect
///
/// Connect to the remote server
fn connect(
&mut self,
address: String,
port: u16,
username: Option<String>,
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError> {
let params = match params.generic_params() {
Some(params) => params,
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
};
// Setup tcp stream
info!("Connecting to {}:{}", address, port);
info!("Connecting to {}:{}", params.address, params.port);
let socket_addresses: Vec<SocketAddr> =
match format!("{}:{}", address, port).to_socket_addrs() {
match format!("{}:{}", params.address, params.port).to_socket_addrs() {
Ok(s) => s.collect(),
Err(err) => {
return Err(FileTransferError::new_ex(
@ -398,14 +396,14 @@ impl FileTransfer for ScpFileTransfer {
err.to_string(),
));
}
let username: String = match username {
Some(u) => u,
let username: String = match &params.username {
Some(u) => u.to_string(),
None => String::from(""),
};
// Check if it is possible to authenticate using a RSA key
match self
.key_storage
.resolve(address.as_str(), username.as_str())
.resolve(params.address.as_str(), username.as_str())
{
Some(rsa_key) => {
debug!(
@ -418,7 +416,7 @@ impl FileTransfer for ScpFileTransfer {
username.as_str(),
None,
rsa_key.as_path(),
password.as_deref(),
params.password.as_deref(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
@ -432,11 +430,16 @@ impl FileTransfer for ScpFileTransfer {
debug!(
"Authenticating with username {} and password {}",
username,
shadow_password(password.as_deref().unwrap_or(""))
shadow_password(params.password.as_deref().unwrap_or(""))
);
if let Err(err) = session.userauth_password(
username.as_str(),
password.unwrap_or_else(|| String::from("")).as_str(),
params
.password
.as_ref()
.cloned()
.unwrap_or_else(|| String::from(""))
.as_str(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
@ -942,36 +945,13 @@ impl FileTransfer for ScpFileTransfer {
)),
}
}
/// ### 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>) -> Result<(), FileTransferError> {
// Nothing to do
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.
fn on_recv(&mut self, _readable: Box<dyn Read>) -> Result<(), FileTransferError> {
// Nothing to do
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::utils::test_helpers::make_fsentry;
use pretty_assertions::assert_eq;
@ -993,12 +973,13 @@ mod tests {
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10222,
Some(String::from("sftp")),
Some(String::from("password"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10222)
.username(Some("sftp"))
.password(Some("password"))
))
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
@ -1180,12 +1161,13 @@ mod tests {
let mut client: ScpFileTransfer = ScpFileTransfer::new(storage);
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10222,
Some(String::from("sftp")),
None,
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10222)
.username(Some("sftp"))
.password::<&str>(None)
))
.is_ok());
assert_eq!(client.is_connected(), true);
assert!(client.disconnect().is_ok());
@ -1195,12 +1177,13 @@ mod tests {
fn test_filetransfer_scp_bad_auth() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("127.0.0.1"),
10222,
Some(String::from("demo")),
Some(String::from("badpassword"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10222)
.username(Some("sftp"))
.password(Some("badpassword"))
))
.is_err());
}
@ -1209,7 +1192,13 @@ mod tests {
fn test_filetransfer_scp_no_credentials() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(String::from("127.0.0.1"), 10222, None, None)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10222)
.username::<&str>(None)
.password::<&str>(None)
))
.is_err());
}
@ -1217,12 +1206,13 @@ mod tests {
fn test_filetransfer_scp_bad_server() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("mybadserver.veryverybad.awful"),
22,
None,
None
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("mybad.verybad.server")
.port(10222)
.username(Some("sftp"))
.password(Some("password"))
))
.is_err());
}

View File

@ -1,4 +1,4 @@
//! ## SFTP_Transfer
//! ## SFTP transfer
//!
//! `sftp_transfer` is the module which provides the implementation for the SFTP file transfer
@ -26,7 +26,7 @@
* SOFTWARE.
*/
// Locals
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use super::{FileTransfer, FileTransferError, FileTransferErrorType, ProtocolParams};
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::{fmt_time, shadow_password};
@ -257,17 +257,15 @@ impl FileTransfer for SftpFileTransfer {
/// ### connect
///
/// Connect to the remote server
fn connect(
&mut self,
address: String,
port: u16,
username: Option<String>,
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError> {
let params = match params.generic_params() {
Some(params) => params,
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
};
// Setup tcp stream
info!("Connecting to {}:{}", address, port);
info!("Connecting to {}:{}", params.address, params.port);
let socket_addresses: Vec<SocketAddr> =
match format!("{}:{}", address, port).to_socket_addrs() {
match format!("{}:{}", params.address, params.port).to_socket_addrs() {
Ok(s) => s.collect(),
Err(err) => {
return Err(FileTransferError::new_ex(
@ -321,14 +319,14 @@ impl FileTransfer for SftpFileTransfer {
err.to_string(),
));
}
let username: String = match username {
Some(u) => u,
let username: String = match &params.username {
Some(u) => u.to_string(),
None => String::from(""),
};
// Check if it is possible to authenticate using a RSA key
match self
.key_storage
.resolve(address.as_str(), username.as_str())
.resolve(params.address.as_str(), username.as_str())
{
Some(rsa_key) => {
debug!(
@ -341,7 +339,7 @@ impl FileTransfer for SftpFileTransfer {
username.as_str(),
None,
rsa_key.as_path(),
password.as_deref(),
params.password.as_deref(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
@ -355,11 +353,16 @@ impl FileTransfer for SftpFileTransfer {
debug!(
"Authenticating with username {} and password {}",
username,
shadow_password(password.as_deref().unwrap_or(""))
shadow_password(params.password.as_deref().unwrap_or(""))
);
if let Err(err) = session.userauth_password(
username.as_str(),
password.unwrap_or_else(|| String::from("")).as_str(),
params
.password
.as_ref()
.cloned()
.unwrap_or_else(|| String::from(""))
.as_str(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
@ -766,36 +769,21 @@ impl FileTransfer for SftpFileTransfer {
}
}
}
/// ### on_sent
///
/// Finalize send method. This method must be implemented only if necessary.
/// 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>) -> Result<(), FileTransferError> {
Ok(())
}
/// ### on_recv
///
/// Finalize recv method. This method must be implemented only if necessary.
/// 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>) -> Result<(), FileTransferError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::utils::test_helpers::make_fsentry;
#[cfg(feature = "with-containers")]
use crate::utils::test_helpers::{create_sample_file_entry, write_file, write_ssh_key};
use crate::utils::test_helpers::{
create_sample_file, create_sample_file_entry, write_file, write_ssh_key,
};
use pretty_assertions::assert_eq;
#[cfg(feature = "with-containers")]
use std::fs::File;
#[test]
fn test_filetransfer_sftp_new() {
@ -814,12 +802,13 @@ mod tests {
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10022,
Some(String::from("sftp")),
Some(String::from("password"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10022)
.username(Some("sftp"))
.password(Some("password"))
))
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
@ -889,6 +878,11 @@ mod tests {
.unwrap();
write_file(&file, &mut writable);
assert!(client.on_sent(writable).is_ok());
// Upload file without stream
let reader = Box::new(File::open(entry.abs_path.as_path()).ok().unwrap());
assert!(client
.send_file_wno_stream(&entry, PathBuf::from("README2.md").as_path(), reader)
.is_ok());
// Upload file (err)
assert!(client
.send_file(&entry, PathBuf::from("/ommlar/omarone").as_path())
@ -898,10 +892,10 @@ mod tests {
.list_dir(PathBuf::from("/tmp/omar").as_path())
.ok()
.unwrap();
assert_eq!(list.len(), 2);
assert_eq!(list.len(), 3);
// Find
assert_eq!(client.find("*.txt").ok().unwrap().len(), 1);
assert_eq!(client.find("*.md").ok().unwrap().len(), 1);
assert_eq!(client.find("*.md").ok().unwrap().len(), 2);
assert_eq!(client.find("*.jpeg").ok().unwrap().len(), 0);
// Rename
assert!(client
@ -955,6 +949,9 @@ mod tests {
let mut data: Vec<u8> = vec![0; 1024];
assert!(readable.read(&mut data).is_ok());
assert!(client.on_recv(readable).is_ok());
let dest_file = create_sample_file();
// Receive file wno stream
assert!(client.recv_file_wno_stream(&file, dest_file.path()).is_ok());
// Receive file (err)
assert!(client.recv_file(&entry).is_err());
// Cleanup
@ -979,12 +976,13 @@ mod tests {
let mut client: SftpFileTransfer = SftpFileTransfer::new(storage);
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10022,
Some(String::from("sftp")),
None,
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10022)
.username(Some("sftp"))
.password::<&str>(None)
))
.is_ok());
assert_eq!(client.is_connected(), true);
assert!(client.disconnect().is_ok());
@ -994,12 +992,13 @@ mod tests {
fn test_filetransfer_sftp_bad_auth() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("127.0.0.1"),
10022,
Some(String::from("demo")),
Some(String::from("badpassword"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10022)
.username(Some("sftp"))
.password(Some("badpassword"))
))
.is_err());
}
@ -1008,7 +1007,13 @@ mod tests {
fn test_filetransfer_sftp_no_credentials() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(String::from("127.0.0.1"), 10022, None, None)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10022)
.username::<&str>(None)
.password::<&str>(None)
))
.is_err());
}
@ -1018,12 +1023,13 @@ mod tests {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10022,
Some(String::from("sftp")),
Some(String::from("password"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10022)
.username(Some("sftp"))
.password(Some("password"))
))
.is_ok());
// get realpath
assert!(client
@ -1054,12 +1060,13 @@ mod tests {
fn test_filetransfer_sftp_bad_server() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("mybadserver.veryverybad.awful"),
22,
None,
None
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("myverybad.verybad.server")
.port(10022)
.username(Some("sftp"))
.password(Some("password"))
))
.is_err());
}

View File

@ -38,7 +38,9 @@ use std::fs::set_permissions;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
// Locals
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
#[cfg(target_family = "unix")]
use crate::fs::UnixPex;
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::utils::path;
/// ## HostErrorType

View File

@ -52,6 +52,7 @@ extern crate open;
extern crate path_slash;
extern crate rand;
extern crate regex;
extern crate s3;
extern crate ssh2;
extern crate suppaftp;
extern crate tempfile;

View File

@ -66,7 +66,12 @@ enum Task {
#[derive(FromArgs)]
#[argh(description = "
where positional can be: [protocol://user@address:port:wrkdir] [local-wrkdir]
where positional can be: [address] [local-wrkdir]
Address syntax can be:
- `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp
- `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol
Please, report issues to <https://github.com/veeso/termscp>
Please, consider supporting the author <https://www.buymeacoffee.com/veeso>")]
@ -180,7 +185,9 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
Ok(mut remote) => {
// If password is provided, set password
if let Some(passwd) = args.password {
remote = remote.password(Some(passwd));
if let Some(mut params) = remote.params.mut_generic_params() {
params.password = Some(passwd);
}
}
// Set params
run_opts.remote = Some(remote);
@ -209,25 +216,26 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
fn read_password(run_opts: &mut RunOpts) -> Result<(), String> {
// Initialize client if necessary
if let Some(remote) = run_opts.remote.as_mut() {
debug!("User has specified remote options: address: {:?}, port: {:?}, protocol: {:?}, user: {:?}, password: {}", remote.address, remote.port, remote.protocol, remote.username, utils::fmt::shadow_password(remote.password.as_deref().unwrap_or("")));
if remote.password.is_none() {
// Ask password if unspecified
remote.password = match rpassword::read_password_from_tty(Some("Password: ")) {
Ok(p) => {
if p.is_empty() {
None
} else {
debug!(
"Read password from tty: {}",
utils::fmt::shadow_password(p.as_str())
);
Some(p)
if let Some(mut params) = remote.params.mut_generic_params() {
if params.password.is_none() {
// Ask password if unspecified
params.password = match rpassword::read_password_from_tty(Some("Password: ")) {
Ok(p) => {
if p.is_empty() {
None
} else {
debug!(
"Read password from tty: {}",
utils::fmt::shadow_password(p.as_str())
);
Some(p)
}
}
}
Err(_) => {
return Err("Could not read password from prompt".to_string());
}
};
Err(_) => {
return Err("Could not read password from prompt".to_string());
}
};
}
}
}
Ok(())

View File

@ -34,14 +34,13 @@ use crate::config::{
bookmarks::{Bookmark, UserHosts},
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
};
use crate::filetransfer::FileTransferProtocol;
use crate::filetransfer::FileTransferParams;
use crate::utils::crypto;
use crate::utils::fmt::fmt_time;
use crate::utils::random::random_alphanumeric_with_len;
// Ext
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
use std::time::SystemTime;
@ -166,59 +165,45 @@ impl BookmarksClient {
/// ### get_bookmark
///
/// Get bookmark associated to key
pub fn get_bookmark(
&self,
key: &str,
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
pub fn get_bookmark(&self, key: &str) -> Option<FileTransferParams> {
debug!("Getting bookmark {}", key);
Some((
entry.address.clone(),
entry.port,
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
Ok(proto) => proto,
Err(err) => {
error!(
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
err
);
FileTransferProtocol::Sftp // Default
let mut entry: Bookmark = self.hosts.bookmarks.get(key).cloned()?;
// Decrypt password first
if let Some(pwd) = entry.password.as_mut() {
match self.decrypt_str(pwd.as_str()) {
Ok(decrypted_pwd) => {
*pwd = decrypted_pwd;
}
},
entry.username.clone(),
match &entry.password {
// Decrypted password if Some; if decryption fails return None
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
Ok(decrypted_pwd) => Some(decrypted_pwd),
Err(err) => {
error!("Failed to decrypt password for bookmark: {}", err);
None
}
},
None => None,
},
))
Err(err) => {
error!("Failed to decrypt password for bookmark: {}", err);
}
}
}
// Then convert into
Some(FileTransferParams::from(entry))
}
/// ### add_recent
///
/// Add a new recent to bookmarks
pub fn add_bookmark(
pub fn add_bookmark<S: AsRef<str>>(
&mut self,
name: String,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
name: S,
params: FileTransferParams,
save_password: bool,
) {
let name: String = name.as_ref().to_string();
if name.is_empty() {
error!("Fatal error; bookmark name is empty");
panic!("Bookmark name can't be empty");
}
// Make bookmark
info!("Added bookmark {} with address {}", name, addr);
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
info!("Added bookmark {}", name);
let mut host: Bookmark = self.make_bookmark(params);
// If not save_password, set password to `None`
if !save_password {
host.password = None;
}
self.hosts.bookmarks.insert(name, host);
}
@ -239,43 +224,25 @@ impl BookmarksClient {
/// ### get_recent
///
/// Get recent associated to key
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
pub fn get_recent(&self, key: &str) -> Option<FileTransferParams> {
// NOTE: password is not decrypted; recents will never have password
info!("Getting bookmark {}", key);
let entry: &Bookmark = self.hosts.recents.get(key)?;
Some((
entry.address.clone(),
entry.port,
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
Ok(proto) => proto,
Err(err) => {
error!(
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
err
);
FileTransferProtocol::Sftp // Default
}
},
entry.username.clone(),
))
let entry: Bookmark = self.hosts.recents.get(key).cloned()?;
Some(FileTransferParams::from(entry))
}
/// ### add_recent
///
/// Add a new recent to bookmarks
pub fn add_recent(
&mut self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
) {
pub fn add_recent(&mut self, params: FileTransferParams) {
// Make bookmark
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, None);
let mut host: Bookmark = self.make_bookmark(params);
// Null password for recents
host.password = None;
// Check if duplicated
for recent_host in self.hosts.recents.values() {
if *recent_host == host {
debug!("Discarding recent since duplicated ({})", host.address);
for (key, value) in &self.hosts.recents {
if *value == host {
debug!("Discarding recent since duplicated ({})", key);
// Don't save duplicates
return;
}
@ -300,7 +267,7 @@ impl BookmarksClient {
}
}
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
info!("Saved recent host {} ({})", name, host.address);
info!("Saved recent host {}", name);
self.hosts.recents.insert(name, host);
}
@ -376,21 +343,13 @@ impl BookmarksClient {
/// ### make_bookmark
///
/// Make bookmark from credentials
fn make_bookmark(
&self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
) -> Bookmark {
Bookmark {
address: addr,
port,
username,
protocol: protocol.to_string(),
password: password.map(|p| self.encrypt_str(p.as_str())),
fn make_bookmark(&self, params: FileTransferParams) -> Bookmark {
let mut bookmark: Bookmark = Bookmark::from(params);
// Encrypt password
if let Some(pwd) = bookmark.password {
bookmark.password = Some(self.encrypt_str(pwd.as_str()));
}
bookmark
}
/// ### encrypt_str
@ -419,6 +378,8 @@ impl BookmarksClient {
mod tests {
use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::filetransfer::{FileTransferProtocol, ProtocolParams};
use pretty_assertions::assert_eq;
use std::thread::sleep;
@ -473,19 +434,23 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add some bookmarks
client.add_bookmark(
String::from("raspberry"),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
"raspberry",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword"),
),
true,
);
client.add_recent(
String::from("192.168.1.31"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.31",
22,
"pi",
Some("mypassword"),
));
let recent_key: String = String::from(client.iter_recents().next().unwrap());
assert!(client.write_bookmarks().is_ok());
let key: String = client.key.clone();
@ -494,19 +459,18 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Verify it loaded parameters correctly
assert_eq!(client.key, key);
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) =
client.get_bookmark(&String::from("raspberry")).unwrap();
let bookmark = ftparams_to_tup(client.get_bookmark("raspberry").unwrap());
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword"));
let bookmark: (String, u16, FileTransferProtocol, String) =
client.get_recent(&recent_key).unwrap();
let bookmark = ftparams_to_tup(client.get_recent(&recent_key).unwrap());
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(bookmark.4, None);
}
#[test]
@ -519,26 +483,31 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
String::from("raspberry"),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
"raspberry",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword"),
),
true,
);
client.add_bookmark(
String::from("raspberry2"),
String::from("192.168.1.32"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword2")),
"raspberry2",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword2"),
),
true,
);
// Iter
assert_eq!(client.iter_bookmarks().count(), 2);
// Get bookmark
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) =
client.get_bookmark(&String::from("raspberry")).unwrap();
let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
@ -565,15 +534,45 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
String::from(""),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
"",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword"),
),
true,
);
}
#[test]
fn save_bookmark_wno_password() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
"raspberry",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword"),
),
false,
);
let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(bookmark.4, None);
}
#[test]
fn test_system_bookmarks_manipulate_recents() {
@ -583,22 +582,23 @@ mod tests {
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_recent(
String::from("192.168.1.31"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.31",
22,
"pi",
Some("mypassword"),
));
// Iter
assert_eq!(client.iter_recents().count(), 1);
let key: String = String::from(client.iter_recents().next().unwrap());
// Get bookmark
let bookmark: (String, u16, FileTransferProtocol, String) =
client.get_recent(&key).unwrap();
let bookmark = ftparams_to_tup(client.get_recent(&key).unwrap());
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(bookmark.4, None);
// Write bookmarks
assert!(client.write_bookmarks().is_ok());
// Delete bookmark
@ -618,18 +618,20 @@ mod tests {
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_recent(
String::from("192.168.1.31"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
client.add_recent(
String::from("192.168.1.31"),
"192.168.1.31",
22,
"pi",
Some("mypassword"),
));
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.31",
22,
"pi",
Some("mypassword"),
));
// There should be only one recent
assert_eq!(client.iter_recents().count(), 1);
}
@ -644,39 +646,60 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2).unwrap();
// Add recent, wait 1 second for each one (cause the name depends on time)
// 1
client.add_recent(
String::from("192.168.1.1"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.1",
22,
"pi",
Some("mypassword"),
));
sleep(Duration::from_secs(1));
// 2
client.add_recent(
String::from("192.168.1.2"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.2",
22,
"pi",
Some("mypassword"),
));
sleep(Duration::from_secs(1));
// 3
client.add_recent(
String::from("192.168.1.3"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.3",
22,
"pi",
Some("mypassword"),
));
// Limit is 2
assert_eq!(client.iter_recents().count(), 2);
// Check that 192.168.1.1 has been removed
let key: String = client.iter_recents().nth(0).unwrap().to_string();
assert!(matches!(
client.hosts.recents.get(&key).unwrap().address.as_str(),
client
.hosts
.recents
.get(&key)
.unwrap()
.address
.as_ref()
.cloned()
.unwrap_or_default()
.as_str(),
"192.168.1.2" | "192.168.1.3"
));
let key: String = client.iter_recents().nth(1).unwrap().to_string();
assert!(matches!(
client.hosts.recents.get(&key).unwrap().address.as_str(),
client
.hosts
.recents
.get(&key)
.unwrap()
.address
.as_ref()
.cloned()
.unwrap_or_default()
.as_str(),
"192.168.1.2" | "192.168.1.3"
));
}
@ -691,12 +714,15 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
String::from(""),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
"",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword"),
),
true,
);
}
@ -724,4 +750,35 @@ mod tests {
c.push("bookmarks.toml");
(c, k)
}
fn make_generic_ftparams(
protocol: FileTransferProtocol,
address: &str,
port: u16,
username: &str,
password: Option<&str>,
) -> FileTransferParams {
let params = ProtocolParams::Generic(
GenericProtocolParams::default()
.address(address)
.port(port)
.username(Some(username))
.password(password),
);
FileTransferParams::new(protocol, params)
}
fn ftparams_to_tup(
params: FileTransferParams,
) -> (String, u16, FileTransferProtocol, String, Option<String>) {
let protocol = params.protocol;
let p = params.params.generic_params().unwrap();
(
p.address.to_string(),
p.port,
protocol,
p.username.as_ref().cloned().unwrap_or_default(),
p.password.as_ref().cloned(),
)
}
}

View File

@ -26,14 +26,15 @@
* SOFTWARE.
*/
// Locals
use super::{AuthActivity, FileTransferProtocol};
use super::{AuthActivity, FileTransferParams};
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::environment;
// Ext
use std::path::PathBuf;
use tui_realm_stdlib::{input::InputPropsBuilder, radio::RadioPropsBuilder};
use tuirealm::{Payload, PropsBuilder, Value};
use tuirealm::PropsBuilder;
impl AuthActivity {
/// ### del_bookmark
@ -62,9 +63,7 @@ impl AuthActivity {
if let Some(key) = self.bookmarks_list.get(idx) {
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
// Load parameters into components
self.load_bookmark_into_gui(
bookmark.0, bookmark.1, bookmark.2, bookmark.3, bookmark.4,
);
self.load_bookmark_into_gui(bookmark);
}
}
}
@ -74,20 +73,15 @@ impl AuthActivity {
///
/// Save current input fields as a bookmark
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) {
let (address, port, protocol, username, password) = self.get_input();
let params = match self.collect_host_params() {
Ok(p) => p,
Err(e) => {
self.mount_error(e);
return;
}
};
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
// Check if password must be saved
let password: Option<String> = match save_password {
true => match self
.view
.get_state(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
{
Some(Payload::One(Value::Usize(0))) => Some(password), // Yes
_ => None, // No such component / No
},
false => None,
};
bookmarks_cli.add_bookmark(name.clone(), address, port, protocol, username, password);
bookmarks_cli.add_bookmark(name.clone(), params, save_password);
// Save bookmarks
self.write_bookmarks();
// Remove `name` from bookmarks if exists
@ -122,9 +116,7 @@ impl AuthActivity {
if let Some(key) = self.recents_list.get(idx) {
if let Some(bookmark) = client.get_recent(key) {
// Load parameters
self.load_bookmark_into_gui(
bookmark.0, bookmark.1, bookmark.2, bookmark.3, None,
);
self.load_bookmark_into_gui(bookmark);
}
}
}
@ -134,9 +126,15 @@ impl AuthActivity {
///
/// Save current input fields as a "recent"
pub(super) fn save_recent(&mut self) {
let (address, port, protocol, username, _password) = self.get_input();
let params = match self.collect_host_params() {
Ok(p) => p,
Err(e) => {
self.mount_error(e);
return;
}
};
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
bookmarks_cli.add_recent(address, port, protocol, username);
bookmarks_cli.add_recent(params);
// Save bookmarks
self.write_bookmarks();
}
@ -234,40 +232,66 @@ impl AuthActivity {
/// ### load_bookmark_into_gui
///
/// Load bookmark data into the gui components
fn load_bookmark_into_gui(
&mut self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
) {
fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
// Load parameters into components
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = RadioPropsBuilder::from(props)
.with_value(Self::protocol_enum_to_opt(bookmark.protocol))
.build();
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
}
match bookmark.params {
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params),
ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params),
}
}
fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
let props = InputPropsBuilder::from(props).with_value(addr).build();
let props = InputPropsBuilder::from(props)
.with_value(params.address.clone())
.build();
self.view.update(super::COMPONENT_INPUT_ADDR, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
let props = InputPropsBuilder::from(props)
.with_value(port.to_string())
.with_value(params.port.to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PORT, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = RadioPropsBuilder::from(props)
.with_value(Self::protocol_enum_to_opt(protocol))
.build();
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
let props = InputPropsBuilder::from(props).with_value(username).build();
let props = InputPropsBuilder::from(props)
.with_value(params.username.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
}
if let Some(password) = password {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = InputPropsBuilder::from(props).with_value(password).build();
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = InputPropsBuilder::from(props)
.with_value(params.password.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
}
}
fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_BUCKET) {
let props = InputPropsBuilder::from(props)
.with_value(params.bucket_name.clone())
.build();
self.view.update(super::COMPONENT_INPUT_S3_BUCKET, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_REGION) {
let props = InputPropsBuilder::from(props)
.with_value(params.region.clone())
.build();
self.view.update(super::COMPONENT_INPUT_S3_REGION, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_PROFILE) {
let props = InputPropsBuilder::from(props)
.with_value(params.profile.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_S3_PROFILE, props);
}
}
}

View File

@ -26,6 +26,7 @@
* SOFTWARE.
*/
use super::{AuthActivity, FileTransferParams, FileTransferProtocol};
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
impl AuthActivity {
/// ### protocol_opt_to_enum
@ -36,6 +37,7 @@ impl AuthActivity {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp,
}
}
@ -49,6 +51,7 @@ impl AuthActivity {
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
}
}
@ -59,6 +62,7 @@ impl AuthActivity {
match protocol {
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
FileTransferProtocol::Ftp(_) => 21,
FileTransferProtocol::AwsS3 => 22, // Doesn't matter, since not used
}
}
@ -83,15 +87,24 @@ impl AuthActivity {
/// ### collect_host_params
///
/// Get input values from fields or return an error if fields are invalid
/// Collect host params as `FileTransferParams`
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
let (address, port, protocol, username, password): (
String,
u16,
FileTransferProtocol,
String,
String,
) = self.get_input();
let protocol: FileTransferProtocol = self.get_protocol();
match protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(protocol),
protocol => self.collect_generic_host_params(protocol),
}
}
/// ### collect_generic_host_params
///
/// Get input values from fields or return an error if fields are invalid to work as generic
pub(super) fn collect_generic_host_params(
&self,
protocol: FileTransferProtocol,
) -> Result<FileTransferParams, &'static str> {
let (address, port, username, password): (String, u16, String, String) =
self.get_generic_params_input();
if address.is_empty() {
return Err("Invalid host");
}
@ -99,17 +112,42 @@ impl AuthActivity {
return Err("Invalid port");
}
Ok(FileTransferParams {
address,
port,
protocol,
username: match username.is_empty() {
true => None,
false => Some(username),
},
password: match password.is_empty() {
true => None,
false => Some(password),
},
params: ProtocolParams::Generic(
GenericProtocolParams::default()
.address(address)
.port(port)
.username(match username.is_empty() {
true => None,
false => Some(username),
})
.password(match password.is_empty() {
true => None,
false => Some(password),
}),
),
entry_directory: None,
})
}
/// ### collect_s3_host_params
///
/// Get input values from fields or return an error if fields are invalid to work as aws s3
pub(super) fn collect_s3_host_params(
&self,
protocol: FileTransferProtocol,
) -> Result<FileTransferParams, &'static str> {
let (bucket, region, profile): (String, String, Option<String>) =
self.get_s3_params_input();
if bucket.is_empty() {
return Err("Invalid bucket");
}
if region.is_empty() {
return Err("Invalid region");
}
Ok(FileTransferParams {
protocol,
params: ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)),
entry_directory: None,
})
}

View File

@ -57,6 +57,9 @@ const COMPONENT_INPUT_PORT: &str = "INPUT_PORT";
const COMPONENT_INPUT_USERNAME: &str = "INPUT_USERNAME";
const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD";
const COMPONENT_INPUT_BOOKMARK_NAME: &str = "INPUT_BOOKMARK_NAME";
const COMPONENT_INPUT_S3_BUCKET: &str = "INPUT_S3_BUCKET";
const COMPONENT_INPUT_S3_REGION: &str = "INPUT_S3_REGION";
const COMPONENT_INPUT_S3_PROFILE: &str = "INPUT_S3_PROFILE";
const COMPONENT_RADIO_PROTOCOL: &str = "RADIO_PROTOCOL";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK";
@ -163,6 +166,16 @@ impl AuthActivity {
fn theme(&self) -> &Theme {
self.context().theme_provider().theme()
}
/// ### input_mask
///
/// Get current input mask to show
fn input_mask(&self) -> InputMask {
match self.get_protocol() {
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
_ => InputMask::Generic,
}
}
}
impl Activity for AuthActivity {
@ -261,3 +274,12 @@ impl Activity for AuthActivity {
}
}
}
/// ## InputMask
///
/// Auth form input mask
#[derive(Eq, PartialEq)]
enum InputMask {
Generic,
AwsS3,
}

View File

@ -27,8 +27,9 @@
*/
// locals
use super::{
AuthActivity, FileTransferProtocol, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
AuthActivity, FileTransferProtocol, InputMask, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT,
COMPONENT_INPUT_S3_BUCKET, COMPONENT_INPUT_S3_PROFILE, COMPONENT_INPUT_S3_REGION,
COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR,
@ -53,54 +54,80 @@ impl Update for AuthActivity {
Some(msg) => match msg {
// Focus ( DOWN )
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_ADDR);
// Give focus based on current mask
match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_ADDR),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_BUCKET),
};
None
}
// -- generic mask (DOWN)
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PASSWORD);
None
}
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// -- s3 mask (DOWN)
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_S3_REGION);
None
}
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_S3_PROFILE);
None
}
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Focus ( UP )
// -- generic (UP)
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_ADDR);
None
}
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// -- s3 (UP)
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_S3_BUCKET);
None
}
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_S3_REGION);
None
}
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PASSWORD);
// Give focus based on current mask
match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_PASSWORD),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_PROFILE),
};
None
}
// Protocol - On Change
@ -144,14 +171,20 @@ impl Update for AuthActivity {
// Enter
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_bookmark(*idx);
// Give focus to input password
self.view.active(COMPONENT_INPUT_PASSWORD);
// Give focus to input password (or to protocol if not generic)
self.view.active(match self.input_mask() {
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
});
None
}
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_recent(*idx);
// Give focus to input password
self.view.active(COMPONENT_INPUT_PASSWORD);
self.view.active(match self.input_mask() {
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
});
None
}
// Bookmark radio
@ -320,7 +353,7 @@ impl Update for AuthActivity {
if key == &MSG_KEY_TAB =>
{
// Give focus to address
self.view.active(COMPONENT_INPUT_ADDR);
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Any <TAB>, go to bookmarks

View File

@ -26,7 +26,9 @@
* SOFTWARE.
*/
// Locals
use super::{AuthActivity, Context, FileTransferProtocol};
use super::{AuthActivity, Context, FileTransferProtocol, InputMask};
use crate::filetransfer::params::ProtocolParams;
use crate::filetransfer::FileTransferParams;
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
use crate::utils::ui::draw_area_in;
// Ext
@ -109,7 +111,7 @@ impl AuthActivity {
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, protocol_color)
.with_title("Protocol", Alignment::Left)
.with_options(&["SFTP", "SCP", "FTP", "FTPS"])
.with_options(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
.with_value(Self::protocol_enum_to_opt(default_protocol))
.rewind(true)
.build(),
@ -163,6 +165,39 @@ impl AuthActivity {
.build(),
)),
);
// Bucket
self.view.mount(
super::COMPONENT_INPUT_S3_BUCKET,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(addr_color)
.with_borders(Borders::ALL, BorderType::Rounded, addr_color)
.with_label("Bucket name", Alignment::Left)
.build(),
)),
);
// Region
self.view.mount(
super::COMPONENT_INPUT_S3_REGION,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(port_color)
.with_borders(Borders::ALL, BorderType::Rounded, port_color)
.with_label("Region", Alignment::Left)
.build(),
)),
);
// Profile
self.view.mount(
super::COMPONENT_INPUT_S3_PROFILE,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(username_color)
.with_borders(Borders::ALL, BorderType::Rounded, username_color)
.with_label("Profile", Alignment::Left)
.build(),
)),
);
// Version notice
if let Some(version) = self
.context()
@ -240,20 +275,43 @@ impl AuthActivity {
let auth_chunks = Layout::default()
.constraints(
[
Constraint::Length(1), // h1
Constraint::Length(1), // h2
Constraint::Length(1), // Version
Constraint::Length(3), // protocol
Constraint::Length(3), // host
Constraint::Length(3), // port
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // footer
Constraint::Length(1), // h1
Constraint::Length(1), // h2
Constraint::Length(1), // Version
Constraint::Length(3), // protocol
Constraint::Length(self.input_mask_size()), // Input mask
Constraint::Length(3), // footer
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(chunks[0]);
// Input mask chunks
let input_mask = match self.input_mask() {
InputMask::AwsS3 => Layout::default()
.constraints(
[
Constraint::Length(3), // bucket
Constraint::Length(3), // region
Constraint::Length(3), // profile
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
InputMask::Generic => Layout::default()
.constraints(
[
Constraint::Length(3), // host
Constraint::Length(3), // port
Constraint::Length(3), // username
Constraint::Length(3), // password
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
};
// Create bookmark chunks
let bookmark_chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
@ -269,16 +327,29 @@ impl AuthActivity {
.render(super::COMPONENT_TEXT_NEW_VERSION, f, auth_chunks[2]);
self.view
.render(super::COMPONENT_RADIO_PROTOCOL, f, auth_chunks[3]);
// Render input mask
match self.input_mask() {
InputMask::AwsS3 => {
self.view
.render(super::COMPONENT_INPUT_S3_BUCKET, f, input_mask[0]);
self.view
.render(super::COMPONENT_INPUT_S3_REGION, f, input_mask[1]);
self.view
.render(super::COMPONENT_INPUT_S3_PROFILE, f, input_mask[2]);
}
InputMask::Generic => {
self.view
.render(super::COMPONENT_INPUT_ADDR, f, input_mask[0]);
self.view
.render(super::COMPONENT_INPUT_PORT, f, input_mask[1]);
self.view
.render(super::COMPONENT_INPUT_USERNAME, f, input_mask[2]);
self.view
.render(super::COMPONENT_INPUT_PASSWORD, f, input_mask[3]);
}
}
self.view
.render(super::COMPONENT_INPUT_ADDR, f, auth_chunks[4]);
self.view
.render(super::COMPONENT_INPUT_PORT, f, auth_chunks[5]);
self.view
.render(super::COMPONENT_INPUT_USERNAME, f, auth_chunks[6]);
self.view
.render(super::COMPONENT_INPUT_PASSWORD, f, auth_chunks[7]);
self.view
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[8]);
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[5]);
// Bookmark chunks
self.view
.render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]);
@ -388,19 +459,13 @@ impl AuthActivity {
.bookmarks_list
.iter()
.map(|x| {
let entry: (String, u16, FileTransferProtocol, String, _) = self
.bookmarks_client
.as_ref()
.unwrap()
.get_bookmark(x)
.unwrap();
format!(
"{} ({}://{}@{}:{})",
Self::fmt_bookmark(
x,
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
self.bookmarks_client
.as_ref()
.unwrap()
.get_bookmark(x)
.unwrap(),
)
})
.collect();
@ -426,19 +491,12 @@ impl AuthActivity {
.recents_list
.iter()
.map(|x| {
let entry: (String, u16, FileTransferProtocol, String) = self
.bookmarks_client
.as_ref()
.unwrap()
.get_recent(x)
.unwrap();
format!(
"{}://{}@{}:{}",
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
Self::fmt_recent(
self.bookmarks_client
.as_ref()
.unwrap()
.get_recent(x)
.unwrap(),
)
})
.collect();
@ -743,16 +801,32 @@ impl AuthActivity {
self.view.umount(super::COMPONENT_TEXT_NEW_VERSION_NOTES);
}
/// ### get_input
/// ### get_protocol
///
/// Get protocol from view
pub(super) fn get_protocol(&self) -> FileTransferProtocol {
self.get_input_protocol()
}
/// ### get_generic_params
///
/// Collect input values from view
pub(super) fn get_input(&self) -> (String, u16, FileTransferProtocol, String, String) {
pub(super) fn get_generic_params_input(&self) -> (String, u16, String, String) {
let addr: String = self.get_input_addr();
let port: u16 = self.get_input_port();
let protocol: FileTransferProtocol = self.get_input_protocol();
let username: String = self.get_input_username();
let password: String = self.get_input_password();
(addr, port, protocol, username, password)
(addr, port, username, password)
}
/// ### get_s3_params_input
///
/// Collect s3 input values from view
pub(super) fn get_s3_params_input(&self) -> (String, String, Option<String>) {
let bucket: String = self.get_input_s3_bucket();
let region: String = self.get_input_s3_region();
let profile: Option<String> = self.get_input_s3_profile();
(bucket, region, profile)
}
pub(super) fn get_input_addr(&self) -> String {
@ -792,4 +866,75 @@ impl AuthActivity {
_ => String::new(),
}
}
pub(super) fn get_input_s3_bucket(&self) -> String {
match self.view.get_state(super::COMPONENT_INPUT_S3_BUCKET) {
Some(Payload::One(Value::Str(x))) => x,
_ => String::new(),
}
}
pub(super) fn get_input_s3_region(&self) -> String {
match self.view.get_state(super::COMPONENT_INPUT_S3_REGION) {
Some(Payload::One(Value::Str(x))) => x,
_ => String::new(),
}
}
pub(super) fn get_input_s3_profile(&self) -> Option<String> {
match self.view.get_state(super::COMPONENT_INPUT_S3_PROFILE) {
Some(Payload::One(Value::Str(x))) => match x.is_empty() {
true => None,
false => Some(x),
},
_ => None,
}
}
/// ### input_mask_size
///
/// Returns the input mask size based on current input mask
pub(super) fn input_mask_size(&self) -> u16 {
match self.input_mask() {
InputMask::AwsS3 => 9,
InputMask::Generic => 12,
}
}
/// ### fmt_bookmark
///
/// Format bookmark to display on ui
fn fmt_bookmark(name: &str, b: FileTransferParams) -> String {
let addr: String = Self::fmt_recent(b);
format!("{} ({})", name, addr)
}
/// ### fmt_recent
///
/// Format recent connection to display on ui
fn fmt_recent(b: FileTransferParams) -> String {
let protocol: String = b.protocol.to_string().to_lowercase();
match b.params {
ProtocolParams::AwsS3(s3) => {
let profile: String = match s3.profile {
Some(p) => format!("[{}]", p),
None => String::default(),
};
format!(
"{}://{} ({}) {}",
protocol, s3.bucket_name, s3.region, profile
)
}
ProtocolParams::Generic(params) => {
let username: String = match params.username {
None => String::default(),
Some(u) => format!("{}@", u),
};
format!(
"{}://{}{}:{}",
protocol, username, params.address, params.port
)
}
}
}
}

View File

@ -125,7 +125,7 @@ impl FileTransferActivity {
Err(err) => match err.kind() {
FileTransferErrorType::UnsupportedFeature => {
// If copy is not supported, perform the tricky copy
self.tricky_copy(entry, dest);
let _ = self.tricky_copy(entry, dest);
}
_ => self.log_and_alert(
LogLevel::Error,
@ -143,7 +143,7 @@ impl FileTransferActivity {
/// ### tricky_copy
///
/// Tricky copy will be used whenever copy command is not available on remote host
fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) {
pub(super) fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) -> Result<(), String> {
// NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen
self.umount_wait();
// match entry
@ -157,7 +157,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: could not create temporary file: {}", err),
);
return;
return Err(String::from("Could not create temporary file"));
}
};
// Download file
@ -170,7 +170,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: could not download to temporary file: {}", err),
);
return;
return Err(err);
}
// Get local fs entry
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) {
@ -184,7 +184,7 @@ impl FileTransferActivity {
err
),
);
return;
return Err(err.to_string());
}
};
// Upload file to destination
@ -202,8 +202,9 @@ impl FileTransferActivity {
err
),
);
return;
return Err(err);
}
Ok(())
}
FsEntry::Directory(_) => {
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
@ -213,7 +214,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: could not create temporary directory: {}", err),
);
return;
return Err(err.to_string());
}
};
// Get path of dest
@ -227,7 +228,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: failed to download file: {}", err),
);
return;
return Err(err);
}
// Stat dir
let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) {
@ -241,7 +242,7 @@ impl FileTransferActivity {
err
),
);
return;
return Err(err.to_string());
}
};
// Upload to destination
@ -255,8 +256,9 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: failed to send file: {}", err),
);
return;
return Err(err);
}
Ok(())
}
}
}

View File

@ -27,6 +27,7 @@
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel};
use std::fs::File;
use std::path::PathBuf;
impl FileTransferActivity {
@ -99,24 +100,29 @@ impl FileTransferActivity {
};
if let FsEntry::File(local_file) = local_file {
// Create file
match self.client.send_file(&local_file, file_path.as_path()) {
let reader = Box::new(match File::open(tfile.path()) {
Ok(f) => f,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not open tempfile: {}", err),
);
return;
}
});
match self
.client
.send_file_wno_stream(&local_file, file_path.as_path(), reader)
{
Err(err) => self.log_and_alert(
LogLevel::Error,
format!("Could not create file \"{}\": {}", file_path.display(), err),
),
Ok(writer) => {
// Finalize write
if let Err(err) = self.client.on_sent(writer) {
self.log_and_alert(
LogLevel::Warn,
format!("Could not finalize file: {}", err),
);
} else {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
}
Ok(_) => {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
// Reload files
self.reload_remote_dir();
}

View File

@ -27,6 +27,7 @@
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use crate::filetransfer::FileTransferErrorType;
use std::path::{Path, PathBuf};
impl FileTransferActivity {
@ -114,6 +115,9 @@ impl FileTransferActivity {
),
);
}
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
self.tricky_move(entry, dest);
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
@ -125,4 +129,41 @@ impl FileTransferActivity {
),
}
}
/// ### tricky_move
///
/// Tricky move will be used whenever copy command is not available on remote host.
/// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`)
fn tricky_move(&mut self, entry: &FsEntry, dest: &Path) {
debug!(
"Using tricky-move to move entry {} to {}",
entry.get_abs_path().display(),
dest.display()
);
if self.tricky_copy(entry.clone(), dest).is_ok() {
// Delete remote existing entry
debug!("Tricky-copy worked; removing existing remote entry");
match self.client.remove(entry) {
Ok(_) => self.log(
LogLevel::Info,
format!(
"Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest.display()
),
),
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Copied \"{}\" to \"{}\"; but failed to remove src: {}",
entry.get_abs_path().display(),
dest.display(),
err
),
),
}
} else {
error!("Tricky move aborted due to tricky-copy failure");
}
}
}

View File

@ -140,6 +140,10 @@ impl ProgressStates {
///
/// Calculate progress in a range between 0.0 to 1.0
pub fn calc_progress(&self) -> f64 {
// Prevent dividing by 0
if self.total == 0 {
return 0.0;
}
let prog: f64 = (self.written as f64) / (self.total as f64);
match prog > 1.0 {
true => 1.0,
@ -238,6 +242,11 @@ mod test {
// Check if terminated at started
states.started = Instant::now();
assert_eq!(states.calc_bytes_per_second(), 1024);
// Divide by zero
let states: ProgressStates = ProgressStates::default();
assert_eq!(states.total, 0);
assert_eq!(states.written, 0);
assert_eq!(states.calc_progress(), 0.0);
}
#[test]

View File

@ -23,6 +23,7 @@
*/
// Locals
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
use crate::filetransfer::ProtocolParams;
use crate::system::environment;
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::path;
@ -134,4 +135,15 @@ impl FileTransferActivity {
pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf {
path::absolutize(self.remote().wrkdir.as_path(), path)
}
/// ### get_remote_hostname
///
/// Get remote hostname
pub(super) fn get_remote_hostname(&self) -> String {
let ft_params = self.context().ft_params().unwrap();
match &ft_params.params {
ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
}
}
}

View File

@ -36,10 +36,8 @@ pub(self) mod view;
// locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::ftp_transfer::FtpFileTransfer;
use crate::filetransfer::scp_transfer::ScpFileTransfer;
use crate::filetransfer::sftp_transfer::SftpFileTransfer;
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
use crate::filetransfer::{FileTransfer, FileTransferProtocol, ProtocolParams};
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
use crate::fs::explorer::FileExplorer;
use crate::fs::FsEntry;
use crate::host::Localhost;
@ -155,6 +153,7 @@ impl FileTransferActivity {
FileTransferProtocol::Scp => {
Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client)))
}
FileTransferProtocol::AwsS3 => Box::new(S3FileTransfer::default()),
},
browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
@ -237,6 +236,28 @@ impl FileTransferActivity {
fn theme(&self) -> &Theme {
self.context().theme_provider().theme()
}
/// ### get_connection_msg
///
/// Get connection message to show to client
fn get_connection_msg(params: &ProtocolParams) -> String {
match params {
ProtocolParams::Generic(params) => {
info!(
"Client is not connected to remote; connecting to {}:{}",
params.address, params.port
);
format!("Connecting to {}:{}", params.address, params.port)
}
ProtocolParams::AwsS3(params) => {
info!(
"Client is not connected to remote; connecting to {} ({})",
params.bucket_name, params.region
);
format!("Connecting to {}", params.bucket_name)
}
}
}
}
/**
@ -290,12 +311,9 @@ impl Activity for FileTransferActivity {
}
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
let params = self.context().ft_params().unwrap();
info!(
"Client is not connected to remote; connecting to {}:{}",
params.address, params.port
);
let msg: String = format!("Connecting to {}:{}", params.address, params.port);
let ftparams = self.context().ft_params().unwrap();
// print params
let msg: String = Self::get_connection_msg(&ftparams.params);
// Set init state to connecting popup
self.mount_wait(msg.as_str());
// Force ui draw

View File

@ -34,6 +34,7 @@ use crate::utils::fmt::fmt_millis;
// Ext
use bytesize::ByteSize;
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use std::time::Instant;
@ -76,22 +77,20 @@ impl FileTransferActivity {
///
/// Connect to remote
pub(super) fn connect(&mut self) {
let params = self.context().ft_params().unwrap().clone();
let addr: String = params.address.clone();
let entry_dir: Option<PathBuf> = params.entry_directory.clone();
let ft_params = self.context().ft_params().unwrap().clone();
let entry_dir: Option<PathBuf> = ft_params.entry_directory.clone();
// Connect to remote
match self.client.connect(
params.address,
params.port,
params.username,
params.password,
) {
match self.client.connect(&ft_params.params) {
Ok(welcome) => {
if let Some(banner) = welcome {
// Log welcome
self.log(
LogLevel::Info,
format!("Established connection with '{}': \"{}\"", addr, banner),
format!(
"Established connection with '{}': \"{}\"",
self.get_remote_hostname(),
banner
),
);
}
// Try to change directory to entry directory
@ -121,8 +120,7 @@ impl FileTransferActivity {
///
/// disconnect from remote
pub(super) fn disconnect(&mut self) {
let params = self.context().ft_params().unwrap();
let msg: String = format!("Disconnecting from {}", params.address);
let msg: String = format!("Disconnecting from {}", self.get_remote_hostname());
// Show popup disconnecting
self.mount_wait(msg.as_str());
// Disconnect
@ -442,103 +440,165 @@ impl FileTransferActivity {
// Upload file
// Try to open local file
match self.host.open_file_read(local.abs_path.as_path()) {
Ok(mut fhnd) => match self.client.send_file(local, remote) {
Ok(mut rhnd) => {
// Write file
let file_size: usize =
fhnd.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer
self.transfer.partial.init(file_size);
// rewind
if let Err(err) = fhnd.seek(std::io::SeekFrom::Start(0)) {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Write remote file
let mut total_bytes_written: usize = 0;
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely written,
// Or filetransfer has been aborted
while total_bytes_written < file_size && !self.transfer.aborted() {
// Handle input events (each 500ms) or if never fetched before
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match fhnd.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match rhnd.write(&buffer[delta..bytes_read]) {
Ok(bytes) => {
delta += bytes;
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(
err,
));
}
}
}
delta
Ok(fhnd) => match self.client.send_file(local, remote) {
Ok(rhnd) => {
self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd)
}
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd)
}
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
},
Err(err) => Err(TransferErrorReason::HostError(err)),
}
}
/// ### filetransfer_send_one_with_stream
///
/// Send file to remote using stream
fn filetransfer_send_one_with_stream(
&mut self,
local: &FsFile,
remote: &Path,
file_name: String,
mut reader: File,
mut writer: Box<dyn Write>,
) -> Result<(), TransferErrorReason> {
// Write file
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer
self.transfer.partial.init(file_size);
// rewind
if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Write remote file
let mut total_bytes_written: usize = 0;
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely written,
// Or filetransfer has been aborted
while total_bytes_written < file_size && !self.transfer.aborted() {
// Handle input events (each 500ms) or if never fetched before
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match reader.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match writer.write(&buffer[delta..bytes_read]) {
Ok(bytes) => {
delta += bytes;
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
}
Err(err) => {
return Err(TransferErrorReason::LocalIoError(err));
}
};
// Increase progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
delta
}
// Finalize stream
if let Err(err) = self.client.on_sent(rhnd) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// if upload was abrupted, return error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
}
Err(err) => return Err(TransferErrorReason::FileTransferError(err)),
},
Err(err) => return Err(TransferErrorReason::HostError(err)),
Err(err) => {
return Err(TransferErrorReason::LocalIoError(err));
}
};
// Increase progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
}
// Finalize stream
if let Err(err) = self.client.on_sent(writer) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// if upload was abrupted, return error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}
/// ### filetransfer_send_one_wno_stream
///
/// Send an `FsFile` to remote without using streams.
fn filetransfer_send_one_wno_stream(
&mut self,
local: &FsFile,
remote: &Path,
file_name: String,
mut reader: File,
) -> Result<(), TransferErrorReason> {
// Write file
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer
self.transfer.partial.init(file_size);
// rewind
if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Draw before
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
// Send file
if let Err(err) = self
.client
.send_file_wno_stream(local, remote, Box::new(reader))
{
return Err(TransferErrorReason::FileTransferError(err));
}
// Set transfer size ok
self.transfer.partial.update_progress(file_size);
self.transfer.full.update_progress(file_size);
// Draw again after
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
// log and return Ok
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}
@ -796,120 +856,187 @@ impl FileTransferActivity {
) -> Result<(), TransferErrorReason> {
// Try to open local file
match self.host.open_file_write(local) {
Ok(mut local_file) => {
Ok(local_file) => {
// Download file from remote
match self.client.recv_file(remote) {
Ok(mut rhnd) => {
let mut total_bytes_written: usize = 0;
// Init transfer
self.transfer.partial.init(remote.size);
// Write local file
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely read,
// Or filetransfer has been aborted
while total_bytes_written < remote.size && !self.transfer.aborted() {
// Handle input events (each 500 ms) or is None
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match rhnd.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match local_file.write(&buffer[delta..bytes_read]) {
Ok(bytes) => delta += bytes,
Err(err) => {
return Err(TransferErrorReason::LocalIoError(
err,
));
}
}
}
delta
}
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
};
// Set progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
}
// Finalize stream
if let Err(err) = self.client.on_recv(rhnd) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// If download was abrupted, return Error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
// Apply file mode to file
#[cfg(any(
target_family = "unix",
target_os = "macos",
target_os = "linux"
))]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
local.display(),
err
),
);
}
}
// Log
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(rhnd) => self.filetransfer_recv_one_with_stream(
local, remote, file_name, rhnd, local_file,
),
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
self.filetransfer_recv_one_wno_stream(local, remote, file_name)
}
Err(err) => return Err(TransferErrorReason::FileTransferError(err)),
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
}
}
Err(err) => return Err(TransferErrorReason::HostError(err)),
Err(err) => Err(TransferErrorReason::HostError(err)),
}
}
/// ### filetransfer_recv_one_with_stream
///
/// Receive an `FsEntry` from remote using stream
fn filetransfer_recv_one_with_stream(
&mut self,
local: &Path,
remote: &FsFile,
file_name: String,
mut reader: Box<dyn Read>,
mut writer: File,
) -> Result<(), TransferErrorReason> {
let mut total_bytes_written: usize = 0;
// Init transfer
self.transfer.partial.init(remote.size);
// Write local file
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely read,
// Or filetransfer has been aborted
while total_bytes_written < remote.size && !self.transfer.aborted() {
// Handle input events (each 500 ms) or is None
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match reader.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match writer.write(&buffer[delta..bytes_read]) {
Ok(bytes) => delta += bytes,
Err(err) => {
return Err(TransferErrorReason::LocalIoError(err));
}
}
}
delta
}
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
};
// Set progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
}
// Finalize stream
if let Err(err) = self.client.on_recv(reader) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// If download was abrupted, return Error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
// Apply file mode to file
#[cfg(target_family = "unix")]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
local.display(),
err
),
);
}
}
// Log
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}
/// ### filetransfer_recv_one_with_stream
///
/// Receive an `FsEntry` from remote without using stream
fn filetransfer_recv_one_wno_stream(
&mut self,
local: &Path,
remote: &FsFile,
file_name: String,
) -> Result<(), TransferErrorReason> {
// Init transfer
self.transfer.partial.init(remote.size);
// Draw before transfer
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
// recv wno stream
if let Err(err) = self.client.recv_file_wno_stream(remote, local) {
return Err(TransferErrorReason::FileTransferError(err));
}
// Update progress at the end
self.transfer.partial.update_progress(remote.size);
self.transfer.full.update_progress(remote.size);
// Draw after transfer
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
// Apply file mode to file
#[cfg(target_family = "unix")]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
local.display(),
err
),
);
}
}
// Log
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}

View File

@ -810,14 +810,14 @@ impl FileTransferActivity {
.store()
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
.unwrap_or(256);
let params = self.context().ft_params().unwrap();
let hostname = self.get_remote_hostname();
let hostname: String = format!(
"{}:{} ",
params.address,
hostname,
fmt_path_elide_ex(
self.remote().wrkdir.as_path(),
width,
params.address.len() + 3 // 3 because of '/…/'
hostname.len() + 3 // 3 because of '/…/'
)
);
let files: Vec<String> = self

View File

@ -81,12 +81,7 @@ impl SetupActivity {
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
.with_title("Default file transfer protocol", Alignment::Left)
.with_options(&[
String::from("SFTP"),
String::from("SCP"),
String::from("FTP"),
String::from("FTPS"),
])
.with_options(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
.rewind(true)
.build(),
)),
@ -265,6 +260,7 @@ impl SetupActivity {
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
};
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
let _ = self
@ -334,6 +330,7 @@ impl SetupActivity {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp,
};
self.config_mut().set_default_protocol(protocol);

View File

@ -26,7 +26,10 @@
* SOFTWARE.
*/
// Locals
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
use crate::filetransfer::{
params::{AwsS3Params, GenericProtocolParams, ProtocolParams},
FileTransferParams, FileTransferProtocol,
};
#[cfg(not(test))] // NOTE: don't use configuration during tests
use crate::system::config_client::ConfigClient;
#[cfg(not(test))] // NOTE: don't use configuration during tests
@ -43,15 +46,34 @@ use tuirealm::tui::style::Color;
// Regex
lazy_static! {
/**
* This regex matches the protocol used as option
* Regex matches:
* - group 1: Some(protocol) | None
* - group 2: Some(other args)
*/
static ref REMOTE_OPT_PROTOCOL_REGEX: Regex = Regex::new(r"(?:([a-z0-9]+)://)?(?:(.+))").unwrap();
/**
* Regex matches:
* - group 1: Some(protocol) | None
* - group 2: Some(user) | None
* - group 3: Address
* - group 4: Some(port) | None
* - group 5: Some(path) | None
* - group 1: Some(user) | None
* - group 2: Address
* - group 3: Some(port) | None
* - group 4: Some(path) | None
*/
static ref REMOTE_OPT_REGEX: Regex = Regex::new(r"(?:([a-z]+)://)?(?:([^@]+)@)?(?:([^:]+))(?::((?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])))?(?::([^:]+))?").ok().unwrap();
static ref REMOTE_GENERIC_OPT_REGEX: Regex = Regex::new(r"(?:([^@]+)@)?(?:([^:]+))(?::((?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])))?(?::([^:]+))?").ok().unwrap();
/**
* Regex matches:
* - group 1: Bucket
* - group 2: Region
* - group 3: Some(profile) | None
* - group 4: Some(path) | None
*/
static ref REMOTE_S3_OPT_REGEX: Regex = Regex::new(r"(?:([^@]+)@)(?:([^:]+))(?::([a-zA-Z0-9][^:]+))?(?::([^:]+))?").unwrap();
/**
* Regex matches:
* - group 1: Version
@ -75,6 +97,8 @@ lazy_static! {
static ref COLOR_RGB_REGEX: Regex = Regex::new(r"^(rgb)?\(?([01]?\d\d?|2[0-4]\d|25[0-5])(\W+)([01]?\d\d?|2[0-4]\d|25[0-5])\W+(([01]?\d\d?|2[0-4]\d|25[0-5])\)?)").unwrap();
}
// -- remote opts
/// ### parse_remote_opt
///
/// Parse remote option string. Returns in case of success a RemoteOptions struct
@ -93,10 +117,10 @@ lazy_static! {
/// - sftp://172.26.104.1:4022
/// - sftp://172.26.104.1
/// - ...
pub fn parse_remote_opt(remote: &str) -> Result<FileTransferParams, String> {
pub fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
// Set protocol to default protocol
#[cfg(not(test))] // NOTE: don't use configuration during tests
let mut protocol: FileTransferProtocol = match environment::init_config_dir() {
let default_protocol: FileTransferProtocol = match environment::init_config_dir() {
Ok(p) => match p {
Some(p) => {
// Create config client
@ -111,28 +135,60 @@ pub fn parse_remote_opt(remote: &str) -> Result<FileTransferParams, String> {
Err(_) => FileTransferProtocol::Sftp,
};
#[cfg(test)] // NOTE: during test set protocol just to Sftp
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
// Match against regex
match REMOTE_OPT_REGEX.captures(remote) {
let default_protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
// Get protocol
let (protocol, s): (FileTransferProtocol, String) =
parse_remote_opt_protocol(s, default_protocol)?;
// Match against regex for protocol type
match protocol {
FileTransferProtocol::AwsS3 => parse_s3_remote_opt(s.as_str()),
protocol => parse_generic_remote_opt(s.as_str(), protocol),
}
}
/// ### parse_remote_opt_protocol
///
/// Parse protocol from CLI option. In case of success, return the protocol to be used and the remaining arguments
fn parse_remote_opt_protocol(
s: &str,
default: FileTransferProtocol,
) -> Result<(FileTransferProtocol, String), String> {
match REMOTE_OPT_PROTOCOL_REGEX.captures(s) {
Some(groups) => {
// Parse protocol or use default
let protocol = groups.get(1).map(|x| {
FileTransferProtocol::from_str(x.as_str())
.map_err(|_| format!("Unknown protocol \"{}\"", x.as_str()))
});
let protocol = match protocol {
Some(Ok(protocol)) => protocol,
Some(Err(err)) => return Err(err),
None => default,
};
// Return protocol and remaining arguments
Ok((
protocol,
groups
.get(2)
.map(|x| x.as_str().to_string())
.unwrap_or_default(),
))
}
None => Err("Invalid args".to_string()),
}
}
/// ### parse_generic_remote_opt
///
/// Parse generic remote options
fn parse_generic_remote_opt(
s: &str,
protocol: FileTransferProtocol,
) -> Result<FileTransferParams, String> {
match REMOTE_GENERIC_OPT_REGEX.captures(s) {
Some(groups) => {
// Match protocol
let mut port: u16 = 22;
if let Some(group) = groups.get(1) {
// Set protocol from group
let (m_protocol, m_port) = match FileTransferProtocol::from_str(group.as_str()) {
Ok(proto) => match proto {
FileTransferProtocol::Ftp(_) => (proto, 21),
FileTransferProtocol::Scp => (proto, 22),
FileTransferProtocol::Sftp => (proto, 22),
},
Err(_) => return Err(format!("Unknown protocol \"{}\"", group.as_str())),
};
// NOTE: tuple destructuring assignment is not supported yet :(
protocol = m_protocol;
port = m_port;
}
// Match user
let username: Option<String> = match groups.get(2) {
let username: Option<String> = match groups.get(1) {
Some(group) => Some(group.as_str().to_string()),
None => match protocol {
// If group is empty, set to current user
@ -143,25 +199,62 @@ pub fn parse_remote_opt(remote: &str) -> Result<FileTransferParams, String> {
},
};
// Get address
let address: String = match groups.get(3) {
let address: String = match groups.get(2) {
Some(group) => group.as_str().to_string(),
None => return Err(String::from("Missing address")),
};
// Get port
if let Some(group) = groups.get(4) {
port = match group.as_str().parse::<u16>() {
let port: u16 = match groups.get(3) {
Some(port) => match port.as_str().parse::<u16>() {
// Try to parse port
Ok(p) => p,
Err(err) => return Err(format!("Bad port \"{}\": {}", group.as_str(), err)),
};
}
Err(err) => return Err(format!("Bad port \"{}\": {}", port.as_str(), err)),
},
None => match protocol {
// Set port based on protocol
FileTransferProtocol::Ftp(_) => 21,
FileTransferProtocol::Scp => 22,
FileTransferProtocol::Sftp => 22,
_ => 22, // Doesn't matter
},
};
// Get workdir
let entry_directory: Option<PathBuf> =
groups.get(5).map(|group| PathBuf::from(group.as_str()));
Ok(FileTransferParams::new(address)
.port(port)
.protocol(protocol)
.username(username)
.entry_directory(entry_directory))
groups.get(4).map(|group| PathBuf::from(group.as_str()));
let params: ProtocolParams = ProtocolParams::Generic(
GenericProtocolParams::default()
.address(address)
.port(port)
.username(username),
);
Ok(FileTransferParams::new(protocol, params).entry_directory(entry_directory))
}
None => Err(String::from("Bad remote host syntax!")),
}
}
/// ### parse_s3_remote_opt
///
/// Parse remote options for s3 protocol
fn parse_s3_remote_opt(s: &str) -> Result<FileTransferParams, String> {
match REMOTE_S3_OPT_REGEX.captures(s) {
Some(groups) => {
let bucket: String = groups
.get(1)
.map(|x| x.as_str().to_string())
.unwrap_or_default();
let region: String = groups
.get(2)
.map(|x| x.as_str().to_string())
.unwrap_or_default();
let profile: Option<String> = groups.get(3).map(|x| x.as_str().to_string());
let entry_directory: Option<PathBuf> =
groups.get(4).map(|group| PathBuf::from(group.as_str()));
Ok(FileTransferParams::new(
FileTransferProtocol::AwsS3,
ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)),
)
.entry_directory(entry_directory))
}
None => Err(String::from("Bad remote host syntax!")),
}
@ -470,101 +563,127 @@ mod tests {
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 22);
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some());
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 22);
assert!(params.username.is_some());
// User case
let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 22);
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(result.username.unwrap(), String::from("root"));
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 22);
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("root")
);
assert!(result.entry_directory.is_none());
// User + port
let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 8022);
let params = result.params.generic_params().unwrap();
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 8022);
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("root")
);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(result.username.unwrap(), String::from("root"));
assert!(result.entry_directory.is_none());
// Port only
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:4022"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 4022);
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some());
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 4022);
assert!(params.username.is_some());
assert!(result.entry_directory.is_none());
// Protocol
let result: FileTransferParams = parse_remote_opt(&String::from("ftp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 21); // Fallback to ftp default
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
assert!(result.username.is_none()); // Doesn't fall back
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 21); // Fallback to ftp default
assert!(params.username.is_none()); // Doesn't fall back
assert!(result.entry_directory.is_none());
// Protocol
let result: FileTransferParams = parse_remote_opt(&String::from("sftp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 22); // Fallback to sftp default
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some()); // Doesn't fall back
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 22); // Fallback to sftp default
assert!(params.username.is_some()); // Doesn't fall back
assert!(result.entry_directory.is_none());
let result: FileTransferParams = parse_remote_opt(&String::from("scp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 22); // Fallback to scp default
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Scp);
assert!(result.username.is_some()); // Doesn't fall back
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 22); // Fallback to scp default
assert!(params.username.is_some()); // Doesn't fall back
assert!(result.entry_directory.is_none());
// Protocol + user
let result: FileTransferParams =
parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 21); // Fallback to ftp default
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Ftp(true));
assert_eq!(result.username.unwrap(), String::from("anon"));
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 21); // Fallback to ftp default
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("anon")
);
assert!(result.entry_directory.is_none());
// Path
let result: FileTransferParams =
parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 8022);
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(result.username.unwrap(), String::from("root"));
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 8022);
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("root")
);
assert_eq!(result.entry_directory.unwrap(), PathBuf::from("/var"));
// Port only
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:home"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 22);
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some());
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 22);
assert!(params.username.is_some());
assert_eq!(result.entry_directory.unwrap(), PathBuf::from("home"));
// All together now
let result: FileTransferParams =
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp"))
.ok()
.unwrap();
assert_eq!(result.address, String::from("172.26.104.1"));
assert_eq!(result.port, 8021); // Fallback to ftp default
let params = result.params.generic_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
assert_eq!(result.username.unwrap(), String::from("anon"));
assert_eq!(params.address, String::from("172.26.104.1"));
assert_eq!(params.port, 8021); // Fallback to ftp default
assert_eq!(
params.username.as_deref().unwrap().to_string(),
String::from("anon")
);
assert_eq!(result.entry_directory.unwrap(), PathBuf::from("/tmp"));
// bad syntax
// Bad protocol
@ -573,6 +692,56 @@ mod tests {
assert!(parse_remote_opt(&String::from("scp://172.26.104.1:650000")).is_err());
}
#[test]
fn parse_aws_s3_opt() {
// Simple
let result: FileTransferParams =
parse_remote_opt(&String::from("s3://mybucket@eu-central-1"))
.ok()
.unwrap();
let params = result.params.s3_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
assert_eq!(result.entry_directory, None);
assert_eq!(params.bucket_name.as_str(), "mybucket");
assert_eq!(params.region.as_str(), "eu-central-1");
assert_eq!(params.profile, None);
// With profile
let result: FileTransferParams =
parse_remote_opt(&String::from("s3://mybucket@eu-central-1:default"))
.ok()
.unwrap();
let params = result.params.s3_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
assert_eq!(result.entry_directory, None);
assert_eq!(params.bucket_name.as_str(), "mybucket");
assert_eq!(params.region.as_str(), "eu-central-1");
assert_eq!(params.profile.as_deref(), Some("default"));
// With wrkdir only
let result: FileTransferParams =
parse_remote_opt(&String::from("s3://mybucket@eu-central-1:/foobar"))
.ok()
.unwrap();
let params = result.params.s3_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
assert_eq!(result.entry_directory, Some(PathBuf::from("/foobar")));
assert_eq!(params.bucket_name.as_str(), "mybucket");
assert_eq!(params.region.as_str(), "eu-central-1");
assert_eq!(params.profile, None);
// With all arguments
let result: FileTransferParams =
parse_remote_opt(&String::from("s3://mybucket@eu-central-1:default:/foobar"))
.ok()
.unwrap();
let params = result.params.s3_params().unwrap();
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
assert_eq!(result.entry_directory, Some(PathBuf::from("/foobar")));
assert_eq!(params.bucket_name.as_str(), "mybucket");
assert_eq!(params.region.as_str(), "eu-central-1");
assert_eq!(params.profile.as_deref(), Some("default"));
// -- bad args
assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err());
}
#[test]
fn test_utils_parse_lstime() {
// Good cases

View File

@ -2,7 +2,7 @@
//!
//! Path related utilities
use std::path::{Path, PathBuf};
use std::path::{Component, Path, PathBuf};
/// ### absolutize
///
@ -24,6 +24,64 @@ pub fn absolutize(wrkdir: &Path, target: &Path) -> PathBuf {
}
}
/// ### diff_paths
///
/// This function will get the difference from path `path` to `base`. Basically will remove `base` from `path`
///
/// For example:
///
/// ```rust
/// assert_eq!(diff_paths(&Path::new("/foo/bar"), &Path::new("/")).as_path(), Path::new("foo/bar"));
/// assert_eq!(diff_paths(&Path::new("/foo/bar"), &Path::new("/foo")).as_path(), Path::new("bar"));
/// ```
///
/// This function has been written by <https://github.com/Manishearth>
/// and is licensed under the APACHE-2/MIT license <https://github.com/Manishearth/pathdiff>
pub fn diff_paths<P, B>(path: P, base: B) -> Option<PathBuf>
where
P: AsRef<Path>,
B: AsRef<Path>,
{
let path = path.as_ref();
let base = base.as_ref();
if path.is_absolute() != base.is_absolute() {
if path.is_absolute() {
Some(PathBuf::from(path))
} else {
None
}
} else {
let mut ita = path.components();
let mut itb = base.components();
let mut comps: Vec<Component> = vec![];
loop {
match (ita.next(), itb.next()) {
(None, None) => break,
(Some(a), None) => {
comps.push(a);
comps.extend(ita.by_ref());
break;
}
(None, _) => comps.push(Component::ParentDir),
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
(Some(a), Some(b)) if b == Component::CurDir => comps.push(a),
(Some(_), Some(b)) if b == Component::ParentDir => return None,
(Some(a), Some(_)) => {
comps.push(Component::ParentDir);
for _ in itb {
comps.push(Component::ParentDir);
}
comps.push(a);
comps.extend(ita.by_ref());
break;
}
}
}
Some(comps.iter().map(|c| c.as_os_str()).collect())
}
}
#[cfg(test)]
mod test {
@ -40,4 +98,26 @@ mod test {
Path::new("/tmp/readme.txt")
);
}
#[test]
fn calc_diff_paths() {
assert_eq!(
diff_paths(&Path::new("/foo/bar"), &Path::new("/"))
.unwrap()
.as_path(),
Path::new("foo/bar")
);
assert_eq!(
diff_paths(&Path::new("/foo/bar"), &Path::new("/foo"))
.unwrap()
.as_path(),
Path::new("bar")
);
assert_eq!(
diff_paths(&Path::new("/foo/bar/chiedo.gif"), &Path::new("/"))
.unwrap()
.as_path(),
Path::new("foo/bar/chiedo.gif")
);
}
}

View File

@ -28,9 +28,9 @@
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
// ext
use std::fs::File;
#[cfg(feature = "with-containers")]
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
use std::fs::OpenOptions;
#[cfg(feature = "with-containers")]
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
use std::io::Read;
use std::io::Write;
use std::path::{Path, PathBuf};
@ -97,7 +97,7 @@ pub fn make_dir_at(dir: &Path, dirname: &str) -> std::io::Result<()> {
std::fs::create_dir(p.as_path())
}
#[cfg(feature = "with-containers")]
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
pub fn write_file(file: &NamedTempFile, writable: &mut Box<dyn Write>) {
let mut fhnd = OpenOptions::new()
.create(false)
@ -153,7 +153,8 @@ RorU9FCmS/654wAAABFyb290QDhjNTBmZDRjMzQ1YQECAw==
/// ### make_fsentry
///
/// Create a FsEntry at specified path
pub fn make_fsentry(path: PathBuf, is_dir: bool) -> FsEntry {
pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> FsEntry {
let path: PathBuf = path.as_ref().to_path_buf();
match is_dir {
true => FsEntry::Directory(FsDirectory {
name: path.file_name().unwrap().to_string_lossy().to_string(),