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/main.rs
- src/lib.rs - src/lib.rs
- src/activity_manager.rs - src/activity_manager.rs
- src/filetransfer/transfer/s3/mod.rs
- src/support.rs - src/support.rs
- "src/ui/activities/*" - "src/ui/activities/*"
- src/ui/context.rs - src/ui/context.rs

View File

@@ -27,6 +27,12 @@ Released on ??
> 🍁 Autumn update 🍇 > 🍁 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 ## 0.6.1
Released on 31/08/2021 Released on 31/08/2021

270
Cargo.lock generated
View File

@@ -33,6 +33,12 @@ dependencies = [
"opaque-debug", "opaque-debug",
] ]
[[package]]
name = "ahash"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.18" version = "0.7.18"
@@ -51,6 +57,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "anyhow"
version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
[[package]] [[package]]
name = "argh" name = "argh"
version = "0.1.5" version = "0.1.5"
@@ -80,12 +92,78 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a61eb019cb8f415d162cb9f12130ee6bbe9168b7d953c17f4ad049e4051ca00" 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]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.0.1" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" 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]] [[package]]
name = "base64" name = "base64"
version = "0.13.0" version = "0.13.0"
@@ -136,6 +214,12 @@ version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]] [[package]]
name = "bytesize" name = "bytesize"
version = "1.1.0" version = "1.1.0"
@@ -368,6 +452,15 @@ dependencies = [
"winapi", "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]] [[package]]
name = "edit" name = "edit"
version = "0.1.3" version = "0.1.3"
@@ -384,6 +477,12 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "foreign-types" name = "foreign-types"
version = "0.3.2" version = "0.3.2"
@@ -409,6 +508,12 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures-core"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.4" version = "0.14.4"
@@ -441,6 +546,15 @@ dependencies = [
"wasi 0.10.0+wasi-snapshot-preview1", "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]] [[package]]
name = "heck" name = "heck"
version = "0.3.3" version = "0.3.3"
@@ -450,6 +564,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "hex"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]] [[package]]
name = "hkdf" name = "hkdf"
version = "0.10.0" version = "0.10.0"
@@ -481,6 +601,17 @@ dependencies = [
"winapi", "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]] [[package]]
name = "idna" name = "idna"
version = "0.2.3" version = "0.2.3"
@@ -622,6 +753,17 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" 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]] [[package]]
name = "md-5" name = "md-5"
version = "0.9.1" version = "0.9.1"
@@ -633,12 +775,27 @@ dependencies = [
"opaque-debug", "opaque-debug",
] ]
[[package]]
name = "md5"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.4.1" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" 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]] [[package]]
name = "mio" name = "mio"
version = "0.7.13" version = "0.7.13"
@@ -819,6 +976,16 @@ dependencies = [
"vcpkg", "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]] [[package]]
name = "output_vt100" name = "output_vt100"
version = "0.1.2" version = "0.1.2"
@@ -895,6 +1062,12 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project-lite"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.19" version = "0.3.19"
@@ -928,6 +1101,15 @@ dependencies = [
"unicode-xid", "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]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.9"
@@ -1094,6 +1276,46 @@ dependencies = [
"winapi", "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]] [[package]]
name = "rustls" name = "rustls"
version = "0.19.1" version = "0.19.1"
@@ -1210,6 +1432,18 @@ dependencies = [
"serde_derive", "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]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.127" version = "1.0.127"
@@ -1392,6 +1626,7 @@ dependencies = [
"rand 0.8.4", "rand 0.8.4",
"regex", "regex",
"rpassword", "rpassword",
"rust-s3",
"serde", "serde",
"simplelog", "simplelog",
"ssh2", "ssh2",
@@ -1405,7 +1640,7 @@ dependencies = [
"ureq", "ureq",
"users", "users",
"whoami", "whoami",
"wildmatch", "wildmatch 2.1.0",
] ]
[[package]] [[package]]
@@ -1476,6 +1711,27 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" 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]] [[package]]
name = "toml" name = "toml"
version = "0.5.8" version = "0.5.8"
@@ -1741,6 +1997,12 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "wildmatch"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a"
[[package]] [[package]]
name = "wildmatch" name = "wildmatch"
version = "2.1.0" version = "2.1.0"
@@ -1777,3 +2039,9 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 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] [package]
authors = ["Christian Visintin"] authors = ["Christian Visintin"]
categories = ["command-line-utilities"] 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" documentation = "https://docs.rs/termscp"
edition = "2018" edition = "2018"
homepage = "https://veeso.github.io/termscp/" homepage = "https://veeso.github.io/termscp/"
@@ -44,6 +44,7 @@ open = "2.0.1"
rand = "0.8.4" rand = "0.8.4"
regex = "1.5.4" regex = "1.5.4"
rpassword = "5.0.1" 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" ] } serde = { version = "^1.0.0", features = [ "derive" ] }
simplelog = "0.10.0" simplelog = "0.10.0"
ssh2 = "0.9.0" ssh2 = "0.9.0"
@@ -63,7 +64,8 @@ pretty_assertions = "0.7.2"
[features] [features]
default = [ "with-keyring" ] default = [ "with-keyring" ]
github-actions = [] github-actions = [ ]
with-s3-ci = []
with-containers = [] with-containers = []
with-keyring = [ "keyring" ] with-keyring = [ "keyring" ]

View File

@@ -24,7 +24,7 @@
## About termscp 🖥 ## 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) ![Explorer](assets/images/explorer.gif)
@@ -36,6 +36,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
- SFTP - SFTP
- SCP - SCP
- FTP and FTPS - FTP and FTPS
- Aws S3
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI - 🖥 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 - Create, remove, rename, search, view and edit files
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections - ⭐ 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) - [keyring-rs](https://github.com/hwchen/keyring-rs)
- [open-rs](https://github.com/Byron/open-rs) - [open-rs](https://github.com/Byron/open-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword) - [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-s3](https://github.com/durch/rust-s3)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs) - [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
- [suppaftp](https://github.com/veeso/suppaftp) - [suppaftp](https://github.com/veeso/suppaftp)
- [textwrap](https://github.com/mgeisler/textwrap) - [textwrap](https://github.com/mgeisler/textwrap)

View File

@@ -3,6 +3,7 @@
- [User manual 🎓](#user-manual-) - [User manual 🎓](#user-manual-)
- [Usage ❓](#usage-) - [Usage ❓](#usage-)
- [Address argument 🌎](#address-argument-) - [Address argument 🌎](#address-argument-)
- [AWS S3 address argument](#aws-s3-address-argument)
- [How Password can be provided 🔐](#how-password-can-be-provided-) - [How Password can be provided 🔐](#how-password-can-be-provided-)
- [File explorer 📂](#file-explorer-) - [File explorer 📂](#file-explorer-)
- [Keybindings ⌨](#keybindings-) - [Keybindings ⌨](#keybindings-)
@@ -13,6 +14,7 @@
- [Are my passwords Safe 😈](#are-my-passwords-safe-) - [Are my passwords Safe 😈](#are-my-passwords-safe-)
- [Linux Keyring](#linux-keyring) - [Linux Keyring](#linux-keyring)
- [KeepassXC setup for termscp](#keepassxc-setup-for-termscp) - [KeepassXC setup for termscp](#keepassxc-setup-for-termscp)
- [Aws S3 credentials 🦊](#aws-s3-credentials-)
- [Configuration ⚙️](#configuration-) - [Configuration ⚙️](#configuration-)
- [SSH Key Storage 🔐](#ssh-key-storage-) - [SSH Key Storage 🔐](#ssh-key-storage-)
- [File Explorer Format](#file-explorer-format) - [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 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 🔐 #### How Password can be provided 🔐
You have probably noticed, that, when providing the address as argument, there's no way to provide the password. 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 ⚙️ ## Configuration ⚙️
termscp supports some user defined parameters, which can be defined in the 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 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
use serde::{Deserialize, Serialize}; use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
use std::collections::HashMap; 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
/// ///
/// UserHosts contains all the hosts saved by the user in the data storage /// UserHosts contains all the hosts saved by the user in the data storage
/// It contains both `Bookmark` /// It contains both `Bookmark`
#[derive(Deserialize, Serialize, Debug)]
pub struct UserHosts { pub struct UserHosts {
pub bookmarks: HashMap<String, Bookmark>, pub bookmarks: HashMap<String, Bookmark>,
pub recents: HashMap<String, Bookmark>, pub recents: HashMap<String, Bookmark>,
} }
#[derive(Deserialize, Serialize, std::fmt::Debug, PartialEq)]
/// ## Bookmark /// ## Bookmark
/// ///
/// Bookmark describes a single bookmark entry in the user hosts storage /// Bookmark describes a single bookmark entry in the user hosts storage
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
pub struct Bookmark { pub struct Bookmark {
pub address: String, #[serde(
pub port: u16, deserialize_with = "deserialize_protocol",
pub protocol: String, serialize_with = "serialize_protocol"
pub username: String, )]
pub password: Option<String>, // Password is optional; base64, aes-128 encrypted password 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 { impl Default for UserHosts {
fn default() -> Self { fn default() -> Self {
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 // Tests
#[cfg(test)] #[cfg(test)]
@@ -77,48 +184,117 @@ mod tests {
#[test] #[test]
fn test_bookmarks_bookmark_new() { fn test_bookmarks_bookmark_new() {
let bookmark: Bookmark = Bookmark { let bookmark: Bookmark = Bookmark {
address: String::from("192.168.1.1"), address: Some(String::from("192.168.1.1")),
port: 22, port: Some(22),
protocol: String::from("SFTP"), protocol: FileTransferProtocol::Sftp,
username: String::from("root"), username: Some(String::from("root")),
password: Some(String::from("password")), password: Some(String::from("password")),
s3: None,
}; };
let recent: Bookmark = Bookmark { let recent: Bookmark = Bookmark {
address: String::from("192.168.1.2"), address: Some(String::from("192.168.1.2")),
port: 22, port: Some(22),
protocol: String::from("SCP"), protocol: FileTransferProtocol::Scp,
username: String::from("admin"), username: Some(String::from("admin")),
password: Some(String::from("password")), password: Some(String::from("password")),
s3: None,
}; };
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1); let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1);
bookmarks.insert(String::from("test"), bookmark); bookmarks.insert(String::from("test"), bookmark);
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1); let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
recents.insert(String::from("ISO20201218T181432"), recent); recents.insert(String::from("ISO20201218T181432"), recent);
let hosts: UserHosts = UserHosts { let hosts: UserHosts = UserHosts { bookmarks, recents };
bookmarks: bookmarks,
recents: recents,
};
// Verify // Verify
let bookmark: &Bookmark = hosts.bookmarks.get(&String::from("test")).unwrap(); let bookmark: &Bookmark = hosts.bookmarks.get(&String::from("test")).unwrap();
assert_eq!(bookmark.address, String::from("192.168.1.1")); assert_eq!(bookmark.address.as_deref().unwrap(), "192.168.1.1");
assert_eq!(bookmark.port, 22); assert_eq!(bookmark.port.unwrap(), 22);
assert_eq!(bookmark.protocol, String::from("SFTP")); assert_eq!(bookmark.protocol, FileTransferProtocol::Sftp);
assert_eq!(bookmark.username, String::from("root")); assert_eq!(bookmark.username.as_deref().unwrap(), "root");
assert_eq!( assert_eq!(bookmark.password.as_deref().unwrap(), "password");
*bookmark.password.as_ref().unwrap(),
String::from("password")
);
let bookmark: &Bookmark = hosts let bookmark: &Bookmark = hosts
.recents .recents
.get(&String::from("ISO20201218T181432")) .get(&String::from("ISO20201218T181432"))
.unwrap(); .unwrap();
assert_eq!(bookmark.address, String::from("192.168.1.2")); assert_eq!(bookmark.address.as_deref().unwrap(), "192.168.1.2");
assert_eq!(bookmark.port, 22); assert_eq!(bookmark.port.unwrap(), 22);
assert_eq!(bookmark.protocol, String::from("SCP")); assert_eq!(bookmark.protocol, FileTransferProtocol::Scp);
assert_eq!(bookmark.username, String::from("admin")); assert_eq!(bookmark.username.as_deref().unwrap(), "admin");
assert_eq!( assert_eq!(bookmark.password.as_deref().unwrap(), "password");
*bookmark.password.as_ref().unwrap(), }
String::from("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 { mod tests {
use super::*; 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 pretty_assertions::assert_eq;
use std::collections::HashMap; use std::collections::HashMap;
use std::io::{Seek, SeekFrom}; use std::io::{Seek, SeekFrom};
use std::path::PathBuf; use std::path::PathBuf;
use tuirealm::tui::style::Color; 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] #[test]
fn test_config_serialization_errors() { fn test_config_serialization_errors() {
let error: SerializerError = SerializerError::new(SerializerErrorKind::Syntax); let error: SerializerError = SerializerError::new(SerializerErrorKind::Syntax);
@@ -373,31 +375,42 @@ mod tests {
// Verify recents // Verify recents
assert_eq!(hosts.recents.len(), 1); assert_eq!(hosts.recents.len(), 1);
let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap(); let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap();
assert_eq!(host.address, String::from("172.16.104.10")); assert_eq!(host.address.as_deref().unwrap(), "172.16.104.10");
assert_eq!(host.port, 22); assert_eq!(host.port.unwrap(), 22);
assert_eq!(host.protocol, String::from("SCP")); assert_eq!(host.protocol, FileTransferProtocol::Scp);
assert_eq!(host.username, String::from("root")); assert_eq!(host.username.as_deref().unwrap(), "root");
assert_eq!(host.password, None); assert_eq!(host.password, None);
// Verify bookmarks // Verify bookmarks
assert_eq!(hosts.bookmarks.len(), 3); assert_eq!(hosts.bookmarks.len(), 4);
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap(); let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
assert_eq!(host.address, String::from("192.168.1.31")); assert_eq!(host.address.as_deref().unwrap(), "192.168.1.31");
assert_eq!(host.port, 22); assert_eq!(host.port.unwrap(), 22);
assert_eq!(host.protocol, String::from("SFTP")); assert_eq!(host.protocol, FileTransferProtocol::Sftp);
assert_eq!(host.username, String::from("root")); assert_eq!(host.username.as_deref().unwrap(), "root");
assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword")); assert_eq!(host.password.as_deref().unwrap(), "mypassword");
let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap(); let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap();
assert_eq!(host.address, String::from("192.168.1.30")); assert_eq!(host.address.as_deref().unwrap(), "192.168.1.30");
assert_eq!(host.port, 22); assert_eq!(host.port.unwrap(), 22);
assert_eq!(host.protocol, String::from("SFTP")); assert_eq!(host.protocol, FileTransferProtocol::Sftp);
assert_eq!(host.username, String::from("cvisintin")); assert_eq!(host.username.as_deref().unwrap(), "cvisintin");
assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret")); assert_eq!(host.password.as_deref().unwrap(), "mysecret");
let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap(); let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap();
assert_eq!(host.address, String::from("51.23.67.12")); assert_eq!(host.address.as_deref().unwrap(), "51.23.67.12");
assert_eq!(host.port, 21); assert_eq!(host.port.unwrap(), 21);
assert_eq!(host.protocol, String::from("FTPS")); assert_eq!(host.protocol, FileTransferProtocol::Ftp(true));
assert_eq!(host.username, String::from("aws001")); assert_eq!(host.username.as_deref().unwrap(), "aws001");
assert_eq!(host.password, None); 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] #[test]
@@ -416,32 +429,50 @@ mod tests {
bookmarks.insert( bookmarks.insert(
String::from("raspberrypi2"), String::from("raspberrypi2"),
Bookmark { Bookmark {
address: String::from("192.168.1.31"), address: Some(String::from("192.168.1.31")),
port: 22, port: Some(22),
protocol: String::from("SFTP"), protocol: FileTransferProtocol::Sftp,
username: String::from("root"), username: Some(String::from("root")),
password: None, password: None,
s3: None,
}, },
); );
bookmarks.insert( bookmarks.insert(
String::from("msi-estrem"), String::from("msi-estrem"),
Bookmark { Bookmark {
address: String::from("192.168.1.30"), address: Some(String::from("192.168.1.30")),
port: 4022, port: Some(4022),
protocol: String::from("SFTP"), protocol: FileTransferProtocol::Sftp,
username: String::from("cvisintin"), username: Some(String::from("cvisintin")),
password: Some(String::from("password")), 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); let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
recents.insert( recents.insert(
String::from("ISO20201215T094000Z"), String::from("ISO20201215T094000Z"),
Bookmark { Bookmark {
address: String::from("192.168.1.254"), address: Some(String::from("192.168.1.254")),
port: 3022, port: Some(3022),
protocol: String::from("SCP"), protocol: FileTransferProtocol::Scp,
username: String::from("omar"), username: Some(String::from("omar")),
password: Some(String::from("aaa")), password: Some(String::from("aaa")),
s3: None,
}, },
); );
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap(); 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" } 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" } 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" } 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] [recents]
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" } ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
@@ -497,7 +536,7 @@ mod tests {
let file_content: &str = r#" let file_content: &str = r#"
[bookmarks] [bookmarks]
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root"} 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" } aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[recents] [recents]

View File

@@ -28,27 +28,29 @@
// locals // locals
use crate::fs::{FsEntry, FsFile}; use crate::fs::{FsEntry, FsFile};
// ext // ext
use std::io::{Read, Write}; use std::fs::File;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
use wildmatch::WildMatch; use wildmatch::WildMatch;
// exports // exports
pub mod ftp_transfer;
pub mod params; pub mod params;
pub mod scp_transfer; mod transfer;
pub mod sftp_transfer;
pub use params::FileTransferParams; // -- export types
pub use params::{FileTransferParams, ProtocolParams};
pub use transfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
/// ## FileTransferProtocol /// ## FileTransferProtocol
/// ///
/// This enum defines the different transfer protocol available in termscp /// 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 { pub enum FileTransferProtocol {
Sftp, Sftp,
Scp, Scp,
Ftp(bool), // Bool is for secure (true => ftps) Ftp(bool), // Bool is for secure (true => ftps)
AwsS3,
} }
/// ## FileTransferError /// ## FileTransferError
@@ -130,25 +132,16 @@ impl std::fmt::Display for FileTransferError {
/// ## FileTransfer /// ## FileTransfer
/// ///
/// File transfer trait must be implemented by all the file transfers and defines the method used by a generic file transfer /// File transfer trait must be implemented by all the file transfers and defines the method used by a generic file transfer
pub trait FileTransfer { pub trait FileTransfer {
/// ### connect /// ### connect
/// ///
/// Connect to the remote server /// Connect to the remote server
/// Can return banner / welcome message on success /// Can return banner / welcome message on success
fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError>;
fn connect(
&mut self,
address: String,
port: u16,
username: Option<String>,
password: Option<String>,
) -> Result<Option<String>, FileTransferError>;
/// ### disconnect /// ### disconnect
/// ///
/// Disconnect from the remote server /// Disconnect from the remote server
fn disconnect(&mut self) -> Result<(), FileTransferError>; fn disconnect(&mut self) -> Result<(), FileTransferError>;
/// ### is_connected /// ### is_connected
@@ -210,18 +203,28 @@ pub trait FileTransfer {
/// Send file to remote /// Send file to remote
/// File name is referred to the name of the file as it will be saved /// File name is referred to the name of the file as it will be saved
/// Data contains the file data /// Data contains the file data
/// Returns file and its size /// Returns file and its size.
/// By default returns unsupported feature
fn send_file( fn send_file(
&mut self, &mut self,
local: &FsFile, _local: &FsFile,
file_name: &Path, _file_name: &Path,
) -> Result<Box<dyn Write>, FileTransferError>; ) -> Result<Box<dyn Write>, FileTransferError> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### recv_file /// ### recv_file
/// ///
/// Receive file from remote with provided name /// Receive file from remote with provided name
/// Returns file and its size /// 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 /// ### 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. /// 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. /// 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. /// 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 /// ### 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. /// The purpose of this method is to finalize the connection with the peer when reading data.
/// This mighe be necessary for some protocols. /// This mighe be necessary for some protocols.
/// You must call this method each time you want to finalize the read of the remote file. /// 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 /// ### find
/// ///
@@ -314,6 +384,7 @@ impl std::string::ToString for FileTransferProtocol {
}, },
FileTransferProtocol::Scp => "SCP", FileTransferProtocol::Scp => "SCP",
FileTransferProtocol::Sftp => "SFTP", FileTransferProtocol::Sftp => "SFTP",
FileTransferProtocol::AwsS3 => "S3",
}) })
} }
} }
@@ -326,6 +397,7 @@ impl std::str::FromStr for FileTransferProtocol {
"FTPS" => Ok(FileTransferProtocol::Ftp(true)), "FTPS" => Ok(FileTransferProtocol::Ftp(true)),
"SCP" => Ok(FileTransferProtocol::Scp), "SCP" => Ok(FileTransferProtocol::Scp),
"SFTP" => Ok(FileTransferProtocol::Sftp), "SFTP" => Ok(FileTransferProtocol::Sftp),
"S3" => Ok(FileTransferProtocol::AwsS3),
_ => Err(s.to_string()), _ => Err(s.to_string()),
} }
} }
@@ -385,6 +457,14 @@ mod tests {
FileTransferProtocol::from_str("scp").ok().unwrap(), FileTransferProtocol::from_str("scp").ok().unwrap(),
FileTransferProtocol::Scp FileTransferProtocol::Scp
); );
assert_eq!(
FileTransferProtocol::from_str("S3").ok().unwrap(),
FileTransferProtocol::AwsS3
);
assert_eq!(
FileTransferProtocol::from_str("s3").ok().unwrap(),
FileTransferProtocol::AwsS3
);
// Error // Error
assert!(FileTransferProtocol::from_str("dummy").is_err()); assert!(FileTransferProtocol::from_str("dummy").is_err());
// To String // To String
@@ -398,6 +478,7 @@ mod tests {
); );
assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP")); assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP"));
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP")); assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
} }
#[test] #[test]

View File

@@ -32,44 +32,132 @@ use std::path::{Path, PathBuf};
/// ### FileTransferParams /// ### FileTransferParams
/// ///
/// Holds connection parameters for file transfers /// Holds connection parameters for file transfers
#[derive(Clone)] #[derive(Debug, Clone)]
pub struct FileTransferParams { 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 address: String,
pub port: u16, pub port: u16,
pub protocol: FileTransferProtocol,
pub username: Option<String>, pub username: Option<String>,
pub password: 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 { impl FileTransferParams {
/// ### new /// ### new
/// ///
/// Instantiates a new `FileTransferParams` /// Instantiates a new `FileTransferParams`
pub fn new<S: AsRef<str>>(address: S) -> Self { pub fn new(protocol: FileTransferProtocol, params: ProtocolParams) -> Self {
Self { Self {
address: address.as_ref().to_string(), protocol,
port: 22, params,
protocol: FileTransferProtocol::Sftp,
username: None,
password: None,
entry_directory: None, entry_directory: None,
} }
} }
/// ### port /// ### entry_directory
/// ///
/// Set port for params /// Set entry directory
pub fn port(mut self, port: u16) -> Self { pub fn entry_directory<P: AsRef<Path>>(mut self, dir: Option<P>) -> Self {
self.port = port; 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 self
} }
/// ### protocol /// ### port
/// ///
/// Set protocol for params /// Set port to params
pub fn protocol(mut self, protocol: FileTransferProtocol) -> Self { pub fn port(mut self, port: u16) -> Self {
self.protocol = protocol; self.port = port;
self self
} }
@@ -88,19 +176,20 @@ impl FileTransferParams {
self.password = password.map(|x| x.as_ref().to_string()); self.password = password.map(|x| x.as_ref().to_string());
self 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 { // -- S3 params
fn default() -> Self {
Self::new("localhost") 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] #[test]
fn test_filetransfer_params() { fn test_filetransfer_params() {
let params: FileTransferParams = FileTransferParams::new("test.rebex.net") let params: FileTransferParams =
.port(2222) FileTransferParams::new(FileTransferProtocol::Scp, ProtocolParams::default())
.protocol(FileTransferProtocol::Scp) .entry_directory(Some(&Path::new("/tmp")));
.username(Some("omar")) assert_eq!(
.password(Some("foobar")) params.params.generic_params().unwrap().address.as_str(),
.entry_directory(Some(&Path::new("/tmp"))); "localhost"
assert_eq!(params.address.as_str(), "test.rebex.net"); );
assert_eq!(params.port, 2222);
assert_eq!(params.protocol, FileTransferProtocol::Scp); assert_eq!(params.protocol, FileTransferProtocol::Scp);
assert_eq!(params.username.as_ref().unwrap(), "omar"); assert_eq!(
assert_eq!(params.password.as_ref().unwrap(), "foobar"); params.entry_directory.as_deref().unwrap(),
Path::new("/tmp")
);
} }
#[test] #[test]
fn test_filetransfer_params_default() { fn params_default() {
let params: FileTransferParams = FileTransferParams::default(); let params: GenericProtocolParams = ProtocolParams::default()
.generic_params()
.unwrap()
.to_owned();
assert_eq!(params.address.as_str(), "localhost"); assert_eq!(params.address.as_str(), "localhost");
assert_eq!(params.port, 22); assert_eq!(params.port, 22);
assert_eq!(params.protocol, FileTransferProtocol::Sftp);
assert!(params.username.is_none()); assert!(params.username.is_none());
assert!(params.password.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 //! `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 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
use super::{FileTransfer, FileTransferError, FileTransferErrorType}; use super::{FileTransfer, FileTransferError, FileTransferErrorType, ProtocolParams};
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use crate::utils::fmt::shadow_password; use crate::utils::fmt::shadow_password;
use crate::utils::path; use crate::utils::path;
@@ -178,25 +178,24 @@ impl FileTransfer for FtpFileTransfer {
/// ///
/// Connect to the remote server /// Connect to the remote server
fn connect( fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError> {
&mut self, let params = match params.generic_params() {
address: String, Some(params) => params,
port: u16, None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
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(),
));
}
}; };
// 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 SSL, open secure session
if self.ftps { if self.ftps {
info!("Setting up TLS stream..."); 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, Ok(s) => s,
Err(err) => { Err(err) => {
error!("Failed to setup TLS stream: {}", err); error!("Failed to setup TLS stream: {}", err);
@@ -226,12 +225,12 @@ impl FileTransfer for FtpFileTransfer {
}; };
} }
// Login (use anonymous if credentials are unspecified) // Login (use anonymous if credentials are unspecified)
let username: String = match username { let username: String = match &params.username {
Some(u) => u, Some(u) => u.to_string(),
None => String::from("anonymous"), None => String::from("anonymous"),
}; };
let password: String = match password { let password: String = match &params.password {
Some(pwd) => pwd, Some(pwd) => pwd.to_string(),
None => String::new(), None => String::new(),
}; };
info!( info!(
@@ -645,6 +644,7 @@ impl FileTransfer for FtpFileTransfer {
mod tests { mod tests {
use super::*; use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::utils::file::open_file; use crate::utils::file::open_file;
#[cfg(feature = "with-containers")] #[cfg(feature = "with-containers")]
use crate::utils::test_helpers::write_file; use crate::utils::test_helpers::write_file;
@@ -672,17 +672,15 @@ mod tests {
// Sample file // Sample file
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect // 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"); let hostname: String = String::from("127.0.0.1");
assert!(ftp assert!(ftp
.connect( .connect(&ProtocolParams::Generic(
hostname, GenericProtocolParams::default()
10021, .address(hostname)
Some(String::from("test")), .port(10021)
Some(String::from("test")), .username(Some("test"))
) .password(Some("test"))
))
.is_ok()); .is_ok());
assert_eq!(ftp.is_connected(), true); assert_eq!(ftp.is_connected(), true);
// Get pwd // Get pwd
@@ -810,12 +808,13 @@ mod tests {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect // Connect
assert!(ftp assert!(ftp
.connect( .connect(&ProtocolParams::Generic(
String::from("127.0.0.1"), GenericProtocolParams::default()
10021, .address("127.0.0.1")
Some(String::from("omar")), .port(10021)
Some(String::from("ommlar")), .username(Some("omar"))
) .password(Some("ommlar"))
))
.is_err()); .is_err());
} }
@@ -824,7 +823,13 @@ mod tests {
fn test_filetransfer_ftp_no_credentials() { fn test_filetransfer_ftp_no_credentials() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
assert!(ftp 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()); .is_err());
} }
@@ -833,12 +838,13 @@ mod tests {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect // Connect
assert!(ftp assert!(ftp
.connect( .connect(&ProtocolParams::Generic(
String::from("mybadserver.veryverybad.awful"), GenericProtocolParams::default()
21, .address("mybad.veribad.server")
Some(String::from("omar")), .port(21)
Some(String::from("ommlar")), .username::<&str>(None)
) .password::<&str>(None)
))
.is_err()); .is_err());
} }
@@ -890,12 +896,13 @@ mod tests {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false); let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect // Connect
assert!(ftp assert!(ftp
.connect( .connect(&ProtocolParams::Generic(
String::from("test.rebex.net"), GenericProtocolParams::default()
21, .address("test.rebex.net")
Some(String::from("demo")), .port(21)
Some(String::from("password")) .username(Some("demo"))
) .password(Some("password"))
))
.is_ok()); .is_ok());
// Pwd // Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/")); 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 //! `scps_transfer` is the module which provides the implementation for the SCP file transfer
@@ -26,7 +26,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
// Locals // Locals
use super::{FileTransfer, FileTransferError, FileTransferErrorType}; use super::{FileTransfer, FileTransferError, FileTransferErrorType, ProtocolParams};
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use crate::system::sshkey_storage::SshKeyStorage; use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::{fmt_time, shadow_password}; use crate::utils::fmt::{fmt_time, shadow_password};
@@ -333,17 +333,15 @@ impl FileTransfer for ScpFileTransfer {
/// ### connect /// ### connect
/// ///
/// Connect to the remote server /// Connect to the remote server
fn connect( fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError> {
&mut self, let params = match params.generic_params() {
address: String, Some(params) => params,
port: u16, None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
username: Option<String>, };
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
// Setup tcp stream // Setup tcp stream
info!("Connecting to {}:{}", address, port); info!("Connecting to {}:{}", params.address, params.port);
let socket_addresses: Vec<SocketAddr> = 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(), Ok(s) => s.collect(),
Err(err) => { Err(err) => {
return Err(FileTransferError::new_ex( return Err(FileTransferError::new_ex(
@@ -398,14 +396,14 @@ impl FileTransfer for ScpFileTransfer {
err.to_string(), err.to_string(),
)); ));
} }
let username: String = match username { let username: String = match &params.username {
Some(u) => u, Some(u) => u.to_string(),
None => String::from(""), None => String::from(""),
}; };
// Check if it is possible to authenticate using a RSA key // Check if it is possible to authenticate using a RSA key
match self match self
.key_storage .key_storage
.resolve(address.as_str(), username.as_str()) .resolve(params.address.as_str(), username.as_str())
{ {
Some(rsa_key) => { Some(rsa_key) => {
debug!( debug!(
@@ -418,7 +416,7 @@ impl FileTransfer for ScpFileTransfer {
username.as_str(), username.as_str(),
None, None,
rsa_key.as_path(), rsa_key.as_path(),
password.as_deref(), params.password.as_deref(),
) { ) {
error!("Authentication failed: {}", err); error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex( return Err(FileTransferError::new_ex(
@@ -432,11 +430,16 @@ impl FileTransfer for ScpFileTransfer {
debug!( debug!(
"Authenticating with username {} and password {}", "Authenticating with username {} and password {}",
username, username,
shadow_password(password.as_deref().unwrap_or("")) shadow_password(params.password.as_deref().unwrap_or(""))
); );
if let Err(err) = session.userauth_password( if let Err(err) = session.userauth_password(
username.as_str(), 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); error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex( 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::utils::test_helpers::make_fsentry; use crate::utils::test_helpers::make_fsentry;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -993,12 +973,13 @@ mod tests {
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect // Connect
assert!(client assert!(client
.connect( .connect(&ProtocolParams::Generic(
String::from("127.0.0.1"), GenericProtocolParams::default()
10222, .address("127.0.0.1")
Some(String::from("sftp")), .port(10222)
Some(String::from("password")) .username(Some("sftp"))
) .password(Some("password"))
))
.is_ok()); .is_ok());
// Check session and sftp // Check session and sftp
assert!(client.session.is_some()); assert!(client.session.is_some());
@@ -1180,12 +1161,13 @@ mod tests {
let mut client: ScpFileTransfer = ScpFileTransfer::new(storage); let mut client: ScpFileTransfer = ScpFileTransfer::new(storage);
// Connect // Connect
assert!(client assert!(client
.connect( .connect(&ProtocolParams::Generic(
String::from("127.0.0.1"), GenericProtocolParams::default()
10222, .address("127.0.0.1")
Some(String::from("sftp")), .port(10222)
None, .username(Some("sftp"))
) .password::<&str>(None)
))
.is_ok()); .is_ok());
assert_eq!(client.is_connected(), true); assert_eq!(client.is_connected(), true);
assert!(client.disconnect().is_ok()); assert!(client.disconnect().is_ok());
@@ -1195,12 +1177,13 @@ mod tests {
fn test_filetransfer_scp_bad_auth() { fn test_filetransfer_scp_bad_auth() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client assert!(client
.connect( .connect(&ProtocolParams::Generic(
String::from("127.0.0.1"), GenericProtocolParams::default()
10222, .address("127.0.0.1")
Some(String::from("demo")), .port(10222)
Some(String::from("badpassword")) .username(Some("sftp"))
) .password(Some("badpassword"))
))
.is_err()); .is_err());
} }
@@ -1209,7 +1192,13 @@ mod tests {
fn test_filetransfer_scp_no_credentials() { fn test_filetransfer_scp_no_credentials() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client 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()); .is_err());
} }
@@ -1217,12 +1206,13 @@ mod tests {
fn test_filetransfer_scp_bad_server() { fn test_filetransfer_scp_bad_server() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty()); let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client assert!(client
.connect( .connect(&ProtocolParams::Generic(
String::from("mybadserver.veryverybad.awful"), GenericProtocolParams::default()
22, .address("mybad.verybad.server")
None, .port(10222)
None .username(Some("sftp"))
) .password(Some("password"))
))
.is_err()); .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 //! `sftp_transfer` is the module which provides the implementation for the SFTP file transfer
@@ -26,7 +26,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
// Locals // Locals
use super::{FileTransfer, FileTransferError, FileTransferErrorType}; use super::{FileTransfer, FileTransferError, FileTransferErrorType, ProtocolParams};
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use crate::system::sshkey_storage::SshKeyStorage; use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::{fmt_time, shadow_password}; use crate::utils::fmt::{fmt_time, shadow_password};
@@ -257,17 +257,15 @@ impl FileTransfer for SftpFileTransfer {
/// ### connect /// ### connect
/// ///
/// Connect to the remote server /// Connect to the remote server
fn connect( fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError> {
&mut self, let params = match params.generic_params() {
address: String, Some(params) => params,
port: u16, None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
username: Option<String>, };
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
// Setup tcp stream // Setup tcp stream
info!("Connecting to {}:{}", address, port); info!("Connecting to {}:{}", params.address, params.port);
let socket_addresses: Vec<SocketAddr> = 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(), Ok(s) => s.collect(),
Err(err) => { Err(err) => {
return Err(FileTransferError::new_ex( return Err(FileTransferError::new_ex(
@@ -321,14 +319,14 @@ impl FileTransfer for SftpFileTransfer {
err.to_string(), err.to_string(),
)); ));
} }
let username: String = match username { let username: String = match &params.username {
Some(u) => u, Some(u) => u.to_string(),
None => String::from(""), None => String::from(""),
}; };
// Check if it is possible to authenticate using a RSA key // Check if it is possible to authenticate using a RSA key
match self match self
.key_storage .key_storage
.resolve(address.as_str(), username.as_str()) .resolve(params.address.as_str(), username.as_str())
{ {
Some(rsa_key) => { Some(rsa_key) => {
debug!( debug!(
@@ -341,7 +339,7 @@ impl FileTransfer for SftpFileTransfer {
username.as_str(), username.as_str(),
None, None,
rsa_key.as_path(), rsa_key.as_path(),
password.as_deref(), params.password.as_deref(),
) { ) {
error!("Authentication failed: {}", err); error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex( return Err(FileTransferError::new_ex(
@@ -355,11 +353,16 @@ impl FileTransfer for SftpFileTransfer {
debug!( debug!(
"Authenticating with username {} and password {}", "Authenticating with username {} and password {}",
username, username,
shadow_password(password.as_deref().unwrap_or("")) shadow_password(params.password.as_deref().unwrap_or(""))
); );
if let Err(err) = session.userauth_password( if let Err(err) = session.userauth_password(
username.as_str(), 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); error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex( 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::utils::test_helpers::make_fsentry; use crate::utils::test_helpers::make_fsentry;
#[cfg(feature = "with-containers")] #[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; use pretty_assertions::assert_eq;
#[cfg(feature = "with-containers")]
use std::fs::File;
#[test] #[test]
fn test_filetransfer_sftp_new() { fn test_filetransfer_sftp_new() {
@@ -814,12 +802,13 @@ mod tests {
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry(); let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect // Connect
assert!(client assert!(client
.connect( .connect(&ProtocolParams::Generic(
String::from("127.0.0.1"), GenericProtocolParams::default()
10022, .address("127.0.0.1")
Some(String::from("sftp")), .port(10022)
Some(String::from("password")) .username(Some("sftp"))
) .password(Some("password"))
))
.is_ok()); .is_ok());
// Check session and sftp // Check session and sftp
assert!(client.session.is_some()); assert!(client.session.is_some());
@@ -889,6 +878,11 @@ mod tests {
.unwrap(); .unwrap();
write_file(&file, &mut writable); write_file(&file, &mut writable);
assert!(client.on_sent(writable).is_ok()); 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) // Upload file (err)
assert!(client assert!(client
.send_file(&entry, PathBuf::from("/ommlar/omarone").as_path()) .send_file(&entry, PathBuf::from("/ommlar/omarone").as_path())
@@ -898,10 +892,10 @@ mod tests {
.list_dir(PathBuf::from("/tmp/omar").as_path()) .list_dir(PathBuf::from("/tmp/omar").as_path())
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(list.len(), 2); assert_eq!(list.len(), 3);
// Find // Find
assert_eq!(client.find("*.txt").ok().unwrap().len(), 1); 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); assert_eq!(client.find("*.jpeg").ok().unwrap().len(), 0);
// Rename // Rename
assert!(client assert!(client
@@ -955,6 +949,9 @@ mod tests {
let mut data: Vec<u8> = vec![0; 1024]; let mut data: Vec<u8> = vec![0; 1024];
assert!(readable.read(&mut data).is_ok()); assert!(readable.read(&mut data).is_ok());
assert!(client.on_recv(readable).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) // Receive file (err)
assert!(client.recv_file(&entry).is_err()); assert!(client.recv_file(&entry).is_err());
// Cleanup // Cleanup
@@ -979,12 +976,13 @@ mod tests {
let mut client: SftpFileTransfer = SftpFileTransfer::new(storage); let mut client: SftpFileTransfer = SftpFileTransfer::new(storage);
// Connect // Connect
assert!(client assert!(client
.connect( .connect(&ProtocolParams::Generic(
String::from("127.0.0.1"), GenericProtocolParams::default()
10022, .address("127.0.0.1")
Some(String::from("sftp")), .port(10022)
None, .username(Some("sftp"))
) .password::<&str>(None)
))
.is_ok()); .is_ok());
assert_eq!(client.is_connected(), true); assert_eq!(client.is_connected(), true);
assert!(client.disconnect().is_ok()); assert!(client.disconnect().is_ok());
@@ -994,12 +992,13 @@ mod tests {
fn test_filetransfer_sftp_bad_auth() { fn test_filetransfer_sftp_bad_auth() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client assert!(client
.connect( .connect(&ProtocolParams::Generic(
String::from("127.0.0.1"), GenericProtocolParams::default()
10022, .address("127.0.0.1")
Some(String::from("demo")), .port(10022)
Some(String::from("badpassword")) .username(Some("sftp"))
) .password(Some("badpassword"))
))
.is_err()); .is_err());
} }
@@ -1008,7 +1007,13 @@ mod tests {
fn test_filetransfer_sftp_no_credentials() { fn test_filetransfer_sftp_no_credentials() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client 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()); .is_err());
} }
@@ -1018,12 +1023,13 @@ mod tests {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
// Connect // Connect
assert!(client assert!(client
.connect( .connect(&ProtocolParams::Generic(
String::from("127.0.0.1"), GenericProtocolParams::default()
10022, .address("127.0.0.1")
Some(String::from("sftp")), .port(10022)
Some(String::from("password")) .username(Some("sftp"))
) .password(Some("password"))
))
.is_ok()); .is_ok());
// get realpath // get realpath
assert!(client assert!(client
@@ -1054,12 +1060,13 @@ mod tests {
fn test_filetransfer_sftp_bad_server() { fn test_filetransfer_sftp_bad_server() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty()); let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client assert!(client
.connect( .connect(&ProtocolParams::Generic(
String::from("mybadserver.veryverybad.awful"), GenericProtocolParams::default()
22, .address("myverybad.verybad.server")
None, .port(10022)
None .username(Some("sftp"))
) .password(Some("password"))
))
.is_err()); .is_err());
} }

View File

@@ -38,7 +38,9 @@ use std::fs::set_permissions;
use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::fs::{MetadataExt, PermissionsExt};
// Locals // 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; use crate::utils::path;
/// ## HostErrorType /// ## HostErrorType

View File

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

View File

@@ -66,7 +66,12 @@ enum Task {
#[derive(FromArgs)] #[derive(FromArgs)]
#[argh(description = " #[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, report issues to <https://github.com/veeso/termscp>
Please, consider supporting the author <https://www.buymeacoffee.com/veeso>")] 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) => { Ok(mut remote) => {
// If password is provided, set password // If password is provided, set password
if let Some(passwd) = args.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 // Set params
run_opts.remote = Some(remote); 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> { fn read_password(run_opts: &mut RunOpts) -> Result<(), String> {
// Initialize client if necessary // Initialize client if necessary
if let Some(remote) = run_opts.remote.as_mut() { 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 let Some(mut params) = remote.params.mut_generic_params() {
if remote.password.is_none() { if params.password.is_none() {
// Ask password if unspecified // Ask password if unspecified
remote.password = match rpassword::read_password_from_tty(Some("Password: ")) { params.password = match rpassword::read_password_from_tty(Some("Password: ")) {
Ok(p) => { Ok(p) => {
if p.is_empty() { if p.is_empty() {
None None
} else { } else {
debug!( debug!(
"Read password from tty: {}", "Read password from tty: {}",
utils::fmt::shadow_password(p.as_str()) utils::fmt::shadow_password(p.as_str())
); );
Some(p) Some(p)
}
} }
} Err(_) => {
Err(_) => { return Err("Could not read password from prompt".to_string());
return Err("Could not read password from prompt".to_string()); }
} };
}; }
} }
} }
Ok(()) Ok(())

View File

@@ -34,14 +34,13 @@ use crate::config::{
bookmarks::{Bookmark, UserHosts}, bookmarks::{Bookmark, UserHosts},
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind}, serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
}; };
use crate::filetransfer::FileTransferProtocol; use crate::filetransfer::FileTransferParams;
use crate::utils::crypto; use crate::utils::crypto;
use crate::utils::fmt::fmt_time; use crate::utils::fmt::fmt_time;
use crate::utils::random::random_alphanumeric_with_len; use crate::utils::random::random_alphanumeric_with_len;
// Ext // Ext
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString; use std::string::ToString;
use std::time::SystemTime; use std::time::SystemTime;
@@ -166,59 +165,45 @@ impl BookmarksClient {
/// ### get_bookmark /// ### get_bookmark
/// ///
/// Get bookmark associated to key /// Get bookmark associated to key
pub fn get_bookmark( pub fn get_bookmark(&self, key: &str) -> Option<FileTransferParams> {
&self,
key: &str,
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
debug!("Getting bookmark {}", key); debug!("Getting bookmark {}", key);
Some(( let mut entry: Bookmark = self.hosts.bookmarks.get(key).cloned()?;
entry.address.clone(), // Decrypt password first
entry.port, if let Some(pwd) = entry.password.as_mut() {
match FileTransferProtocol::from_str(entry.protocol.as_str()) { match self.decrypt_str(pwd.as_str()) {
Ok(proto) => proto, Ok(decrypted_pwd) => {
Err(err) => { *pwd = decrypted_pwd;
error!(
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
err
);
FileTransferProtocol::Sftp // Default
} }
}, Err(err) => {
entry.username.clone(), error!("Failed to decrypt password for bookmark: {}", err);
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), // Then convert into
Err(err) => { Some(FileTransferParams::from(entry))
error!("Failed to decrypt password for bookmark: {}", err);
None
}
},
None => None,
},
))
} }
/// ### add_recent /// ### add_recent
/// ///
/// Add a new recent to bookmarks /// Add a new recent to bookmarks
pub fn add_bookmark( pub fn add_bookmark<S: AsRef<str>>(
&mut self, &mut self,
name: String, name: S,
addr: String, params: FileTransferParams,
port: u16, save_password: bool,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
) { ) {
let name: String = name.as_ref().to_string();
if name.is_empty() { if name.is_empty() {
error!("Fatal error; bookmark name is empty"); error!("Fatal error; bookmark name is empty");
panic!("Bookmark name can't be empty"); panic!("Bookmark name can't be empty");
} }
// Make bookmark // Make bookmark
info!("Added bookmark {} with address {}", name, addr); info!("Added bookmark {}", name);
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password); 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); self.hosts.bookmarks.insert(name, host);
} }
@@ -239,43 +224,25 @@ impl BookmarksClient {
/// ### get_recent /// ### get_recent
/// ///
/// Get recent associated to key /// 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 // NOTE: password is not decrypted; recents will never have password
info!("Getting bookmark {}", key); info!("Getting bookmark {}", key);
let entry: &Bookmark = self.hosts.recents.get(key)?; let entry: Bookmark = self.hosts.recents.get(key).cloned()?;
Some(( Some(FileTransferParams::from(entry))
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(),
))
} }
/// ### add_recent /// ### add_recent
/// ///
/// Add a new recent to bookmarks /// Add a new recent to bookmarks
pub fn add_recent( pub fn add_recent(&mut self, params: FileTransferParams) {
&mut self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
) {
// Make bookmark // 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 // Check if duplicated
for recent_host in self.hosts.recents.values() { for (key, value) in &self.hosts.recents {
if *recent_host == host { if *value == host {
debug!("Discarding recent since duplicated ({})", host.address); debug!("Discarding recent since duplicated ({})", key);
// Don't save duplicates // Don't save duplicates
return; return;
} }
@@ -300,7 +267,7 @@ impl BookmarksClient {
} }
} }
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S"); 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); self.hosts.recents.insert(name, host);
} }
@@ -376,21 +343,13 @@ impl BookmarksClient {
/// ### make_bookmark /// ### make_bookmark
/// ///
/// Make bookmark from credentials /// Make bookmark from credentials
fn make_bookmark( fn make_bookmark(&self, params: FileTransferParams) -> Bookmark {
&self, let mut bookmark: Bookmark = Bookmark::from(params);
addr: String, // Encrypt password
port: u16, if let Some(pwd) = bookmark.password {
protocol: FileTransferProtocol, bookmark.password = Some(self.encrypt_str(pwd.as_str()));
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())),
} }
bookmark
} }
/// ### encrypt_str /// ### encrypt_str
@@ -419,6 +378,8 @@ impl BookmarksClient {
mod tests { mod tests {
use super::*; use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::filetransfer::{FileTransferProtocol, ProtocolParams};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use std::thread::sleep; use std::thread::sleep;
@@ -473,19 +434,23 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add some bookmarks // Add some bookmarks
client.add_bookmark( client.add_bookmark(
String::from("raspberry"), "raspberry",
String::from("192.168.1.31"), make_generic_ftparams(
22, FileTransferProtocol::Sftp,
FileTransferProtocol::Sftp, "192.168.1.31",
String::from("pi"), 22,
Some(String::from("mypassword")), "pi",
Some("mypassword"),
),
true,
); );
client.add_recent( client.add_recent(make_generic_ftparams(
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp, FileTransferProtocol::Sftp,
String::from("pi"), "192.168.1.31",
); 22,
"pi",
Some("mypassword"),
));
let recent_key: String = String::from(client.iter_recents().next().unwrap()); let recent_key: String = String::from(client.iter_recents().next().unwrap());
assert!(client.write_bookmarks().is_ok()); assert!(client.write_bookmarks().is_ok());
let key: String = client.key.clone(); let key: String = client.key.clone();
@@ -494,19 +459,18 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Verify it loaded parameters correctly // Verify it loaded parameters correctly
assert_eq!(client.key, key); assert_eq!(client.key, key);
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) = let bookmark = ftparams_to_tup(client.get_bookmark("raspberry").unwrap());
client.get_bookmark(&String::from("raspberry")).unwrap();
assert_eq!(bookmark.0, String::from("192.168.1.31")); assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22); assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp); assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi")); assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword")); assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword"));
let bookmark: (String, u16, FileTransferProtocol, String) = let bookmark = ftparams_to_tup(client.get_recent(&recent_key).unwrap());
client.get_recent(&recent_key).unwrap();
assert_eq!(bookmark.0, String::from("192.168.1.31")); assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22); assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp); assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi")); assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(bookmark.4, None);
} }
#[test] #[test]
@@ -519,26 +483,31 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark // Add bookmark
client.add_bookmark( client.add_bookmark(
String::from("raspberry"), "raspberry",
String::from("192.168.1.31"), make_generic_ftparams(
22, FileTransferProtocol::Sftp,
FileTransferProtocol::Sftp, "192.168.1.31",
String::from("pi"), 22,
Some(String::from("mypassword")), "pi",
Some("mypassword"),
),
true,
); );
client.add_bookmark( client.add_bookmark(
String::from("raspberry2"), "raspberry2",
String::from("192.168.1.32"), make_generic_ftparams(
22, FileTransferProtocol::Sftp,
FileTransferProtocol::Sftp, "192.168.1.31",
String::from("pi"), 22,
Some(String::from("mypassword2")), "pi",
Some("mypassword2"),
),
true,
); );
// Iter // Iter
assert_eq!(client.iter_bookmarks().count(), 2); assert_eq!(client.iter_bookmarks().count(), 2);
// Get bookmark // Get bookmark
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) = let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
client.get_bookmark(&String::from("raspberry")).unwrap();
assert_eq!(bookmark.0, String::from("192.168.1.31")); assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22); assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp); assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
@@ -565,15 +534,45 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark // Add bookmark
client.add_bookmark( client.add_bookmark(
String::from(""), "",
String::from("192.168.1.31"), make_generic_ftparams(
22, FileTransferProtocol::Sftp,
FileTransferProtocol::Sftp, "192.168.1.31",
String::from("pi"), 22,
Some(String::from("mypassword")), "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] #[test]
fn test_system_bookmarks_manipulate_recents() { fn test_system_bookmarks_manipulate_recents() {
@@ -583,22 +582,23 @@ mod tests {
let mut client: BookmarksClient = let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark // Add bookmark
client.add_recent( client.add_recent(make_generic_ftparams(
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp, FileTransferProtocol::Sftp,
String::from("pi"), "192.168.1.31",
); 22,
"pi",
Some("mypassword"),
));
// Iter // Iter
assert_eq!(client.iter_recents().count(), 1); assert_eq!(client.iter_recents().count(), 1);
let key: String = String::from(client.iter_recents().next().unwrap()); let key: String = String::from(client.iter_recents().next().unwrap());
// Get bookmark // Get bookmark
let bookmark: (String, u16, FileTransferProtocol, String) = let bookmark = ftparams_to_tup(client.get_recent(&key).unwrap());
client.get_recent(&key).unwrap();
assert_eq!(bookmark.0, String::from("192.168.1.31")); assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22); assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp); assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi")); assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(bookmark.4, None);
// Write bookmarks // Write bookmarks
assert!(client.write_bookmarks().is_ok()); assert!(client.write_bookmarks().is_ok());
// Delete bookmark // Delete bookmark
@@ -618,18 +618,20 @@ mod tests {
let mut client: BookmarksClient = let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap(); BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark // Add bookmark
client.add_recent( client.add_recent(make_generic_ftparams(
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp, FileTransferProtocol::Sftp,
String::from("pi"), "192.168.1.31",
);
client.add_recent(
String::from("192.168.1.31"),
22, 22,
"pi",
Some("mypassword"),
));
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp, FileTransferProtocol::Sftp,
String::from("pi"), "192.168.1.31",
); 22,
"pi",
Some("mypassword"),
));
// There should be only one recent // There should be only one recent
assert_eq!(client.iter_recents().count(), 1); 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(); 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) // Add recent, wait 1 second for each one (cause the name depends on time)
// 1 // 1
client.add_recent( client.add_recent(make_generic_ftparams(
String::from("192.168.1.1"),
22,
FileTransferProtocol::Sftp, FileTransferProtocol::Sftp,
String::from("pi"), "192.168.1.1",
); 22,
"pi",
Some("mypassword"),
));
sleep(Duration::from_secs(1)); sleep(Duration::from_secs(1));
// 2 // 2
client.add_recent( client.add_recent(make_generic_ftparams(
String::from("192.168.1.2"),
22,
FileTransferProtocol::Sftp, FileTransferProtocol::Sftp,
String::from("pi"), "192.168.1.2",
); 22,
"pi",
Some("mypassword"),
));
sleep(Duration::from_secs(1)); sleep(Duration::from_secs(1));
// 3 // 3
client.add_recent( client.add_recent(make_generic_ftparams(
String::from("192.168.1.3"),
22,
FileTransferProtocol::Sftp, FileTransferProtocol::Sftp,
String::from("pi"), "192.168.1.3",
); 22,
"pi",
Some("mypassword"),
));
// Limit is 2 // Limit is 2
assert_eq!(client.iter_recents().count(), 2); assert_eq!(client.iter_recents().count(), 2);
// Check that 192.168.1.1 has been removed // Check that 192.168.1.1 has been removed
let key: String = client.iter_recents().nth(0).unwrap().to_string(); let key: String = client.iter_recents().nth(0).unwrap().to_string();
assert!(matches!( 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" "192.168.1.2" | "192.168.1.3"
)); ));
let key: String = client.iter_recents().nth(1).unwrap().to_string(); let key: String = client.iter_recents().nth(1).unwrap().to_string();
assert!(matches!( 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" "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(); BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark // Add bookmark
client.add_bookmark( client.add_bookmark(
String::from(""), "",
String::from("192.168.1.31"), make_generic_ftparams(
22, FileTransferProtocol::Sftp,
FileTransferProtocol::Sftp, "192.168.1.31",
String::from("pi"), 22,
Some(String::from("mypassword")), "pi",
Some("mypassword"),
),
true,
); );
} }
@@ -724,4 +750,35 @@ mod tests {
c.push("bookmarks.toml"); c.push("bookmarks.toml");
(c, k) (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. * SOFTWARE.
*/ */
// Locals // 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::bookmarks_client::BookmarksClient;
use crate::system::environment; use crate::system::environment;
// Ext // Ext
use std::path::PathBuf; use std::path::PathBuf;
use tui_realm_stdlib::{input::InputPropsBuilder, radio::RadioPropsBuilder}; use tui_realm_stdlib::{input::InputPropsBuilder, radio::RadioPropsBuilder};
use tuirealm::{Payload, PropsBuilder, Value}; use tuirealm::PropsBuilder;
impl AuthActivity { impl AuthActivity {
/// ### del_bookmark /// ### del_bookmark
@@ -62,9 +63,7 @@ impl AuthActivity {
if let Some(key) = self.bookmarks_list.get(idx) { if let Some(key) = self.bookmarks_list.get(idx) {
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) { if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
// Load parameters into components // Load parameters into components
self.load_bookmark_into_gui( self.load_bookmark_into_gui(bookmark);
bookmark.0, bookmark.1, bookmark.2, bookmark.3, bookmark.4,
);
} }
} }
} }
@@ -74,20 +73,15 @@ impl AuthActivity {
/// ///
/// Save current input fields as a bookmark /// Save current input fields as a bookmark
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) { 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() { if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
// Check if password must be saved bookmarks_cli.add_bookmark(name.clone(), params, save_password);
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);
// Save bookmarks // Save bookmarks
self.write_bookmarks(); self.write_bookmarks();
// Remove `name` from bookmarks if exists // Remove `name` from bookmarks if exists
@@ -122,9 +116,7 @@ impl AuthActivity {
if let Some(key) = self.recents_list.get(idx) { if let Some(key) = self.recents_list.get(idx) {
if let Some(bookmark) = client.get_recent(key) { if let Some(bookmark) = client.get_recent(key) {
// Load parameters // Load parameters
self.load_bookmark_into_gui( self.load_bookmark_into_gui(bookmark);
bookmark.0, bookmark.1, bookmark.2, bookmark.3, None,
);
} }
} }
} }
@@ -134,9 +126,15 @@ impl AuthActivity {
/// ///
/// Save current input fields as a "recent" /// Save current input fields as a "recent"
pub(super) fn save_recent(&mut self) { 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() { 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 // Save bookmarks
self.write_bookmarks(); self.write_bookmarks();
} }
@@ -234,40 +232,66 @@ impl AuthActivity {
/// ### load_bookmark_into_gui /// ### load_bookmark_into_gui
/// ///
/// Load bookmark data into the gui components /// Load bookmark data into the gui components
fn load_bookmark_into_gui( fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
&mut self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
) {
// Load parameters into components // 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) { 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); self.view.update(super::COMPONENT_INPUT_ADDR, props);
} }
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) { if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
let props = InputPropsBuilder::from(props) let props = InputPropsBuilder::from(props)
.with_value(port.to_string()) .with_value(params.port.to_string())
.build(); .build();
self.view.update(super::COMPONENT_INPUT_PORT, props); 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) { 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); 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) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) { let props = InputPropsBuilder::from(props)
let props = InputPropsBuilder::from(props).with_value(password).build(); .with_value(params.password.as_deref().unwrap_or_default().to_string())
self.view.update(super::COMPONENT_INPUT_PASSWORD, props); .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. * SOFTWARE.
*/ */
use super::{AuthActivity, FileTransferParams, FileTransferProtocol}; use super::{AuthActivity, FileTransferParams, FileTransferProtocol};
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
impl AuthActivity { impl AuthActivity {
/// ### protocol_opt_to_enum /// ### protocol_opt_to_enum
@@ -36,6 +37,7 @@ impl AuthActivity {
1 => FileTransferProtocol::Scp, 1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false), 2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true), 3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp, _ => FileTransferProtocol::Sftp,
} }
} }
@@ -49,6 +51,7 @@ impl AuthActivity {
FileTransferProtocol::Scp => 1, FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2, FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3, FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
} }
} }
@@ -59,6 +62,7 @@ impl AuthActivity {
match protocol { match protocol {
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22, FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
FileTransferProtocol::Ftp(_) => 21, FileTransferProtocol::Ftp(_) => 21,
FileTransferProtocol::AwsS3 => 22, // Doesn't matter, since not used
} }
} }
@@ -83,15 +87,24 @@ impl AuthActivity {
/// ### collect_host_params /// ### 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> { pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
let (address, port, protocol, username, password): ( let protocol: FileTransferProtocol = self.get_protocol();
String, match protocol {
u16, FileTransferProtocol::AwsS3 => self.collect_s3_host_params(protocol),
FileTransferProtocol, protocol => self.collect_generic_host_params(protocol),
String, }
String, }
) = self.get_input();
/// ### 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() { if address.is_empty() {
return Err("Invalid host"); return Err("Invalid host");
} }
@@ -99,17 +112,42 @@ impl AuthActivity {
return Err("Invalid port"); return Err("Invalid port");
} }
Ok(FileTransferParams { Ok(FileTransferParams {
address,
port,
protocol, protocol,
username: match username.is_empty() { params: ProtocolParams::Generic(
true => None, GenericProtocolParams::default()
false => Some(username), .address(address)
}, .port(port)
password: match password.is_empty() { .username(match username.is_empty() {
true => None, true => None,
false => Some(password), 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, 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_USERNAME: &str = "INPUT_USERNAME";
const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD"; const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD";
const COMPONENT_INPUT_BOOKMARK_NAME: &str = "INPUT_BOOKMARK_NAME"; 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_PROTOCOL: &str = "RADIO_PROTOCOL";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT"; const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK"; const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK";
@@ -163,6 +166,16 @@ impl AuthActivity {
fn theme(&self) -> &Theme { fn theme(&self) -> &Theme {
self.context().theme_provider().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 { 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 // locals
use super::{ 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_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_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD, COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR, COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR,
@@ -53,54 +54,80 @@ impl Update for AuthActivity {
Some(msg) => match msg { Some(msg) => match msg {
// Focus ( DOWN ) // Focus ( DOWN )
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => { (COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => {
// Give focus to port // Give focus based on current mask
self.view.active(COMPONENT_INPUT_ADDR); match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_ADDR),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_BUCKET),
};
None None
} }
// -- generic mask (DOWN)
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => { (COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PORT); self.view.active(COMPONENT_INPUT_PORT);
None None
} }
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => { (COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_USERNAME); self.view.active(COMPONENT_INPUT_USERNAME);
None None
} }
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => { (COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PASSWORD); self.view.active(COMPONENT_INPUT_PASSWORD);
None None
} }
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => { (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); self.view.active(COMPONENT_RADIO_PROTOCOL);
None None
} }
// Focus ( UP ) // Focus ( UP )
// -- generic (UP)
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => { (COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_USERNAME); self.view.active(COMPONENT_INPUT_USERNAME);
None None
} }
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => { (COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PORT); self.view.active(COMPONENT_INPUT_PORT);
None None
} }
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => { (COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_ADDR); self.view.active(COMPONENT_INPUT_ADDR);
None None
} }
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => { (COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_RADIO_PROTOCOL); self.view.active(COMPONENT_RADIO_PROTOCOL);
None 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 => { (COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => {
// Give focus to port // Give focus based on current mask
self.view.active(COMPONENT_INPUT_PASSWORD); match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_PASSWORD),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_PROFILE),
};
None None
} }
// Protocol - On Change // Protocol - On Change
@@ -144,14 +171,20 @@ impl Update for AuthActivity {
// Enter // Enter
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => { (COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_bookmark(*idx); self.load_bookmark(*idx);
// Give focus to input password // Give focus to input password (or to protocol if not generic)
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 None
} }
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => { (COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_recent(*idx); self.load_recent(*idx);
// Give focus to input password // 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 None
} }
// Bookmark radio // Bookmark radio
@@ -320,7 +353,7 @@ impl Update for AuthActivity {
if key == &MSG_KEY_TAB => if key == &MSG_KEY_TAB =>
{ {
// Give focus to address // Give focus to address
self.view.active(COMPONENT_INPUT_ADDR); self.view.active(COMPONENT_RADIO_PROTOCOL);
None None
} }
// Any <TAB>, go to bookmarks // Any <TAB>, go to bookmarks

View File

@@ -26,7 +26,9 @@
* SOFTWARE. * SOFTWARE.
*/ */
// Locals // 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::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
use crate::utils::ui::draw_area_in; use crate::utils::ui::draw_area_in;
// Ext // Ext
@@ -109,7 +111,7 @@ impl AuthActivity {
.with_inverted_color(Color::Black) .with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, protocol_color) .with_borders(Borders::ALL, BorderType::Rounded, protocol_color)
.with_title("Protocol", Alignment::Left) .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)) .with_value(Self::protocol_enum_to_opt(default_protocol))
.rewind(true) .rewind(true)
.build(), .build(),
@@ -163,6 +165,39 @@ impl AuthActivity {
.build(), .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 // Version notice
if let Some(version) = self if let Some(version) = self
.context() .context()
@@ -240,20 +275,43 @@ impl AuthActivity {
let auth_chunks = Layout::default() let auth_chunks = Layout::default()
.constraints( .constraints(
[ [
Constraint::Length(1), // h1 Constraint::Length(1), // h1
Constraint::Length(1), // h2 Constraint::Length(1), // h2
Constraint::Length(1), // Version Constraint::Length(1), // Version
Constraint::Length(3), // protocol Constraint::Length(3), // protocol
Constraint::Length(3), // host Constraint::Length(self.input_mask_size()), // Input mask
Constraint::Length(3), // port Constraint::Length(3), // footer
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // footer
] ]
.as_ref(), .as_ref(),
) )
.direction(Direction::Vertical) .direction(Direction::Vertical)
.split(chunks[0]); .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 // Create bookmark chunks
let bookmark_chunks = Layout::default() let bookmark_chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .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]); .render(super::COMPONENT_TEXT_NEW_VERSION, f, auth_chunks[2]);
self.view self.view
.render(super::COMPONENT_RADIO_PROTOCOL, f, auth_chunks[3]); .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 self.view
.render(super::COMPONENT_INPUT_ADDR, f, auth_chunks[4]); .render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[5]);
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]);
// Bookmark chunks // Bookmark chunks
self.view self.view
.render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]); .render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]);
@@ -388,19 +459,13 @@ impl AuthActivity {
.bookmarks_list .bookmarks_list
.iter() .iter()
.map(|x| { .map(|x| {
let entry: (String, u16, FileTransferProtocol, String, _) = self Self::fmt_bookmark(
.bookmarks_client
.as_ref()
.unwrap()
.get_bookmark(x)
.unwrap();
format!(
"{} ({}://{}@{}:{})",
x, x,
entry.2.to_string().to_lowercase(), self.bookmarks_client
entry.3, .as_ref()
entry.0, .unwrap()
entry.1 .get_bookmark(x)
.unwrap(),
) )
}) })
.collect(); .collect();
@@ -426,19 +491,12 @@ impl AuthActivity {
.recents_list .recents_list
.iter() .iter()
.map(|x| { .map(|x| {
let entry: (String, u16, FileTransferProtocol, String) = self Self::fmt_recent(
.bookmarks_client self.bookmarks_client
.as_ref() .as_ref()
.unwrap() .unwrap()
.get_recent(x) .get_recent(x)
.unwrap(); .unwrap(),
format!(
"{}://{}@{}:{}",
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
) )
}) })
.collect(); .collect();
@@ -743,16 +801,32 @@ impl AuthActivity {
self.view.umount(super::COMPONENT_TEXT_NEW_VERSION_NOTES); 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 /// 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 addr: String = self.get_input_addr();
let port: u16 = self.get_input_port(); let port: u16 = self.get_input_port();
let protocol: FileTransferProtocol = self.get_input_protocol();
let username: String = self.get_input_username(); let username: String = self.get_input_username();
let password: String = self.get_input_password(); 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 { pub(super) fn get_input_addr(&self) -> String {
@@ -792,4 +866,75 @@ impl AuthActivity {
_ => String::new(), _ => 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() { Err(err) => match err.kind() {
FileTransferErrorType::UnsupportedFeature => { FileTransferErrorType::UnsupportedFeature => {
// If copy is not supported, perform the tricky copy // If copy is not supported, perform the tricky copy
self.tricky_copy(entry, dest); let _ = self.tricky_copy(entry, dest);
} }
_ => self.log_and_alert( _ => self.log_and_alert(
LogLevel::Error, LogLevel::Error,
@@ -143,7 +143,7 @@ impl FileTransferActivity {
/// ### tricky_copy /// ### tricky_copy
/// ///
/// Tricky copy will be used whenever copy command is not available on remote host /// 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 // NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen
self.umount_wait(); self.umount_wait();
// match entry // match entry
@@ -157,7 +157,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!("Copy failed: could not create temporary file: {}", err), format!("Copy failed: could not create temporary file: {}", err),
); );
return; return Err(String::from("Could not create temporary file"));
} }
}; };
// Download file // Download file
@@ -170,7 +170,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!("Copy failed: could not download to temporary file: {}", err), format!("Copy failed: could not download to temporary file: {}", err),
); );
return; return Err(err);
} }
// Get local fs entry // Get local fs entry
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) { let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) {
@@ -184,7 +184,7 @@ impl FileTransferActivity {
err err
), ),
); );
return; return Err(err.to_string());
} }
}; };
// Upload file to destination // Upload file to destination
@@ -202,8 +202,9 @@ impl FileTransferActivity {
err err
), ),
); );
return; return Err(err);
} }
Ok(())
} }
FsEntry::Directory(_) => { FsEntry::Directory(_) => {
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() { let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
@@ -213,7 +214,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!("Copy failed: could not create temporary directory: {}", err), format!("Copy failed: could not create temporary directory: {}", err),
); );
return; return Err(err.to_string());
} }
}; };
// Get path of dest // Get path of dest
@@ -227,7 +228,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!("Copy failed: failed to download file: {}", err), format!("Copy failed: failed to download file: {}", err),
); );
return; return Err(err);
} }
// Stat dir // Stat dir
let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) { let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) {
@@ -241,7 +242,7 @@ impl FileTransferActivity {
err err
), ),
); );
return; return Err(err.to_string());
} }
}; };
// Upload to destination // Upload to destination
@@ -255,8 +256,9 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!("Copy failed: failed to send file: {}", err), format!("Copy failed: failed to send file: {}", err),
); );
return; return Err(err);
} }
Ok(())
} }
} }
} }

View File

@@ -27,6 +27,7 @@
*/ */
// locals // locals
use super::{FileTransferActivity, FsEntry, LogLevel}; use super::{FileTransferActivity, FsEntry, LogLevel};
use std::fs::File;
use std::path::PathBuf; use std::path::PathBuf;
impl FileTransferActivity { impl FileTransferActivity {
@@ -99,24 +100,29 @@ impl FileTransferActivity {
}; };
if let FsEntry::File(local_file) = local_file { if let FsEntry::File(local_file) = local_file {
// Create 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( Err(err) => self.log_and_alert(
LogLevel::Error, LogLevel::Error,
format!("Could not create file \"{}\": {}", file_path.display(), err), format!("Could not create file \"{}\": {}", file_path.display(), err),
), ),
Ok(writer) => { Ok(_) => {
// Finalize write self.log(
if let Err(err) = self.client.on_sent(writer) { LogLevel::Info,
self.log_and_alert( format!("Created file \"{}\"", file_path.display()),
LogLevel::Warn, );
format!("Could not finalize file: {}", err),
);
} else {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
}
// Reload files // Reload files
self.reload_remote_dir(); self.reload_remote_dir();
} }

View File

@@ -27,6 +27,7 @@
*/ */
// locals // locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry}; use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use crate::filetransfer::FileTransferErrorType;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
impl FileTransferActivity { 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( Err(err) => self.log_and_alert(
LogLevel::Error, LogLevel::Error,
format!( 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 /// Calculate progress in a range between 0.0 to 1.0
pub fn calc_progress(&self) -> f64 { 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); let prog: f64 = (self.written as f64) / (self.total as f64);
match prog > 1.0 { match prog > 1.0 {
true => 1.0, true => 1.0,
@@ -238,6 +242,11 @@ mod test {
// Check if terminated at started // Check if terminated at started
states.started = Instant::now(); states.started = Instant::now();
assert_eq!(states.calc_bytes_per_second(), 1024); 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] #[test]

View File

@@ -23,6 +23,7 @@
*/ */
// Locals // Locals
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord}; use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
use crate::filetransfer::ProtocolParams;
use crate::system::environment; use crate::system::environment;
use crate::system::sshkey_storage::SshKeyStorage; use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::path; use crate::utils::path;
@@ -134,4 +135,15 @@ impl FileTransferActivity {
pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf { pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf {
path::absolutize(self.remote().wrkdir.as_path(), path) 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 // locals
use super::{Activity, Context, ExitReason}; use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme; use crate::config::themes::Theme;
use crate::filetransfer::ftp_transfer::FtpFileTransfer; use crate::filetransfer::{FileTransfer, FileTransferProtocol, ProtocolParams};
use crate::filetransfer::scp_transfer::ScpFileTransfer; use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
use crate::filetransfer::sftp_transfer::SftpFileTransfer;
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
use crate::fs::explorer::FileExplorer; use crate::fs::explorer::FileExplorer;
use crate::fs::FsEntry; use crate::fs::FsEntry;
use crate::host::Localhost; use crate::host::Localhost;
@@ -155,6 +153,7 @@ impl FileTransferActivity {
FileTransferProtocol::Scp => { FileTransferProtocol::Scp => {
Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client))) Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client)))
} }
FileTransferProtocol::AwsS3 => Box::new(S3FileTransfer::default()),
}, },
browser: Browser::new(&config_client), browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
@@ -237,6 +236,28 @@ impl FileTransferActivity {
fn theme(&self) -> &Theme { fn theme(&self) -> &Theme {
self.context().theme_provider().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) // 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() { if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
let params = self.context().ft_params().unwrap(); let ftparams = self.context().ft_params().unwrap();
info!( // print params
"Client is not connected to remote; connecting to {}:{}", let msg: String = Self::get_connection_msg(&ftparams.params);
params.address, params.port
);
let msg: String = format!("Connecting to {}:{}", params.address, params.port);
// Set init state to connecting popup // Set init state to connecting popup
self.mount_wait(msg.as_str()); self.mount_wait(msg.as_str());
// Force ui draw // Force ui draw

View File

@@ -34,6 +34,7 @@ use crate::utils::fmt::fmt_millis;
// Ext // Ext
use bytesize::ByteSize; use bytesize::ByteSize;
use std::fs::File;
use std::io::{Read, Seek, Write}; use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Instant; use std::time::Instant;
@@ -76,22 +77,20 @@ impl FileTransferActivity {
/// ///
/// Connect to remote /// Connect to remote
pub(super) fn connect(&mut self) { pub(super) fn connect(&mut self) {
let params = self.context().ft_params().unwrap().clone(); let ft_params = self.context().ft_params().unwrap().clone();
let addr: String = params.address.clone(); let entry_dir: Option<PathBuf> = ft_params.entry_directory.clone();
let entry_dir: Option<PathBuf> = params.entry_directory.clone();
// Connect to remote // Connect to remote
match self.client.connect( match self.client.connect(&ft_params.params) {
params.address,
params.port,
params.username,
params.password,
) {
Ok(welcome) => { Ok(welcome) => {
if let Some(banner) = welcome { if let Some(banner) = welcome {
// Log welcome // Log welcome
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!("Established connection with '{}': \"{}\"", addr, banner), format!(
"Established connection with '{}': \"{}\"",
self.get_remote_hostname(),
banner
),
); );
} }
// Try to change directory to entry directory // Try to change directory to entry directory
@@ -121,8 +120,7 @@ impl FileTransferActivity {
/// ///
/// disconnect from remote /// disconnect from remote
pub(super) fn disconnect(&mut self) { pub(super) fn disconnect(&mut self) {
let params = self.context().ft_params().unwrap(); let msg: String = format!("Disconnecting from {}", self.get_remote_hostname());
let msg: String = format!("Disconnecting from {}", params.address);
// Show popup disconnecting // Show popup disconnecting
self.mount_wait(msg.as_str()); self.mount_wait(msg.as_str());
// Disconnect // Disconnect
@@ -442,103 +440,165 @@ impl FileTransferActivity {
// Upload file // Upload file
// Try to open local file // Try to open local file
match self.host.open_file_read(local.abs_path.as_path()) { match self.host.open_file_read(local.abs_path.as_path()) {
Ok(mut fhnd) => match self.client.send_file(local, remote) { Ok(fhnd) => match self.client.send_file(local, remote) {
Ok(mut rhnd) => { Ok(rhnd) => {
// Write file self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd)
let file_size: usize = }
fhnd.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
// Init transfer self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd)
self.transfer.partial.init(file_size); }
// rewind Err(err) => Err(TransferErrorReason::FileTransferError(err)),
if let Err(err) = fhnd.seek(std::io::SeekFrom::Start(0)) { },
return Err(TransferErrorReason::CouldNotRewind(err)); Err(err) => Err(TransferErrorReason::HostError(err)),
} }
// Write remote file }
let mut total_bytes_written: usize = 0;
let mut last_progress_val: f64 = 0.0; /// ### filetransfer_send_one_with_stream
let mut last_input_event_fetch: Option<Instant> = None; ///
// While the entire file hasn't been completely written, /// Send file to remote using stream
// Or filetransfer has been aborted fn filetransfer_send_one_with_stream(
while total_bytes_written < file_size && !self.transfer.aborted() { &mut self,
// Handle input events (each 500ms) or if never fetched before local: &FsFile,
if last_input_event_fetch.is_none() remote: &Path,
|| last_input_event_fetch file_name: String,
.unwrap_or_else(Instant::now) mut reader: File,
.elapsed() mut writer: Box<dyn Write>,
.as_millis() ) -> Result<(), TransferErrorReason> {
>= 500 // Write file
{ let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Read events // Init transfer
self.read_input_event(); self.transfer.partial.init(file_size);
// Reset instant // rewind
last_input_event_fetch = Some(Instant::now()); if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
} return Err(TransferErrorReason::CouldNotRewind(err));
// Read till you can }
let mut buffer: [u8; 65536] = [0; 65536]; // Write remote file
let delta: usize = match fhnd.read(&mut buffer) { let mut total_bytes_written: usize = 0;
Ok(bytes_read) => { let mut last_progress_val: f64 = 0.0;
total_bytes_written += bytes_read; let mut last_input_event_fetch: Option<Instant> = None;
if bytes_read == 0 { // While the entire file hasn't been completely written,
continue; // Or filetransfer has been aborted
} else { while total_bytes_written < file_size && !self.transfer.aborted() {
let mut delta: usize = 0; // Handle input events (each 500ms) or if never fetched before
while delta < bytes_read { if last_input_event_fetch.is_none()
// Write bytes || last_input_event_fetch
match rhnd.write(&buffer[delta..bytes_read]) { .unwrap_or_else(Instant::now)
Ok(bytes) => { .elapsed()
delta += bytes; .as_millis()
} >= 500
Err(err) => { {
return Err(TransferErrorReason::RemoteIoError( // Read events
err, self.read_input_event();
)); // Reset instant
} last_input_event_fetch = Some(Instant::now());
} }
} // Read till you can
delta 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::LocalIoError(err));
Err(err) => return Err(TransferErrorReason::HostError(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(()) Ok(())
} }
@@ -796,120 +856,187 @@ impl FileTransferActivity {
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// Try to open local file // Try to open local file
match self.host.open_file_write(local) { match self.host.open_file_write(local) {
Ok(mut local_file) => { Ok(local_file) => {
// Download file from remote // Download file from remote
match self.client.recv_file(remote) { match self.client.recv_file(remote) {
Ok(mut rhnd) => { Ok(rhnd) => self.filetransfer_recv_one_with_stream(
let mut total_bytes_written: usize = 0; local, remote, file_name, rhnd, local_file,
// Init transfer ),
self.transfer.partial.init(remote.size); Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
// Write local file self.filetransfer_recv_one_wno_stream(local, remote, file_name)
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()),
),
);
} }
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(()) Ok(())
} }

View File

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

View File

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

View File

@@ -26,7 +26,10 @@
* SOFTWARE. * SOFTWARE.
*/ */
// Locals // 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 #[cfg(not(test))] // NOTE: don't use configuration during tests
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
#[cfg(not(test))] // NOTE: don't use configuration during tests #[cfg(not(test))] // NOTE: don't use configuration during tests
@@ -43,15 +46,34 @@ use tuirealm::tui::style::Color;
// Regex // Regex
lazy_static! { 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: * Regex matches:
* - group 1: Some(protocol) | None * - group 1: Some(user) | None
* - group 2: Some(user) | None * - group 2: Address
* - group 3: Address * - group 3: Some(port) | None
* - group 4: Some(port) | None * - group 4: Some(path) | None
* - group 5: 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: * Regex matches:
* - group 1: Version * - 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(); 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_opt
/// ///
/// Parse remote option string. Returns in case of success a RemoteOptions struct /// 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:4022
/// - sftp://172.26.104.1 /// - 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 // Set protocol to default protocol
#[cfg(not(test))] // NOTE: don't use configuration during tests #[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 { Ok(p) => match p {
Some(p) => { Some(p) => {
// Create config client // Create config client
@@ -111,28 +135,60 @@ pub fn parse_remote_opt(remote: &str) -> Result<FileTransferParams, String> {
Err(_) => FileTransferProtocol::Sftp, Err(_) => FileTransferProtocol::Sftp,
}; };
#[cfg(test)] // NOTE: during test set protocol just to Sftp #[cfg(test)] // NOTE: during test set protocol just to Sftp
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; let default_protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
// Match against regex // Get protocol
match REMOTE_OPT_REGEX.captures(remote) { 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) => { 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 // 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()), Some(group) => Some(group.as_str().to_string()),
None => match protocol { None => match protocol {
// If group is empty, set to current user // If group is empty, set to current user
@@ -143,25 +199,62 @@ pub fn parse_remote_opt(remote: &str) -> Result<FileTransferParams, String> {
}, },
}; };
// Get address // Get address
let address: String = match groups.get(3) { let address: String = match groups.get(2) {
Some(group) => group.as_str().to_string(), Some(group) => group.as_str().to_string(),
None => return Err(String::from("Missing address")), None => return Err(String::from("Missing address")),
}; };
// Get port // Get port
if let Some(group) = groups.get(4) { let port: u16 = match groups.get(3) {
port = match group.as_str().parse::<u16>() { Some(port) => match port.as_str().parse::<u16>() {
// Try to parse port
Ok(p) => p, 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 // Get workdir
let entry_directory: Option<PathBuf> = let entry_directory: Option<PathBuf> =
groups.get(5).map(|group| PathBuf::from(group.as_str())); groups.get(4).map(|group| PathBuf::from(group.as_str()));
Ok(FileTransferParams::new(address) let params: ProtocolParams = ProtocolParams::Generic(
.port(port) GenericProtocolParams::default()
.protocol(protocol) .address(address)
.username(username) .port(port)
.entry_directory(entry_directory)) .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!")), 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")) let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 22);
assert_eq!(result.protocol, FileTransferProtocol::Sftp); 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 // User case
let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1")) let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 22);
assert_eq!(result.protocol, FileTransferProtocol::Sftp); 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()); assert!(result.entry_directory.is_none());
// User + port // User + port
let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1:8022")) let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 8022); 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.protocol, FileTransferProtocol::Sftp);
assert_eq!(result.username.unwrap(), String::from("root"));
assert!(result.entry_directory.is_none()); assert!(result.entry_directory.is_none());
// Port only // Port only
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:4022")) let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:4022"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 4022);
assert_eq!(result.protocol, FileTransferProtocol::Sftp); 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()); assert!(result.entry_directory.is_none());
// Protocol // Protocol
let result: FileTransferParams = parse_remote_opt(&String::from("ftp://172.26.104.1")) let result: FileTransferParams = parse_remote_opt(&String::from("ftp://172.26.104.1"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 21); // Fallback to ftp default
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false)); 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()); assert!(result.entry_directory.is_none());
// Protocol // Protocol
let result: FileTransferParams = parse_remote_opt(&String::from("sftp://172.26.104.1")) let result: FileTransferParams = parse_remote_opt(&String::from("sftp://172.26.104.1"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 22); // Fallback to sftp default
assert_eq!(result.protocol, FileTransferProtocol::Sftp); 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()); assert!(result.entry_directory.is_none());
let result: FileTransferParams = parse_remote_opt(&String::from("scp://172.26.104.1")) let result: FileTransferParams = parse_remote_opt(&String::from("scp://172.26.104.1"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 22); // Fallback to scp default
assert_eq!(result.protocol, FileTransferProtocol::Scp); 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()); assert!(result.entry_directory.is_none());
// Protocol + user // Protocol + user
let result: FileTransferParams = let result: FileTransferParams =
parse_remote_opt(&String::from("ftps://anon@172.26.104.1")) parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 21); // Fallback to ftp default
assert_eq!(result.protocol, FileTransferProtocol::Ftp(true)); 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()); assert!(result.entry_directory.is_none());
// Path // Path
let result: FileTransferParams = let result: FileTransferParams =
parse_remote_opt(&String::from("root@172.26.104.1:8022:/var")) parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 8022);
assert_eq!(result.protocol, FileTransferProtocol::Sftp); 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")); assert_eq!(result.entry_directory.unwrap(), PathBuf::from("/var"));
// Port only // Port only
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:home")) let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:home"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 22);
assert_eq!(result.protocol, FileTransferProtocol::Sftp); 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")); assert_eq!(result.entry_directory.unwrap(), PathBuf::from("home"));
// All together now // All together now
let result: FileTransferParams = let result: FileTransferParams =
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp")) parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp"))
.ok() .ok()
.unwrap(); .unwrap();
assert_eq!(result.address, String::from("172.26.104.1")); let params = result.params.generic_params().unwrap();
assert_eq!(result.port, 8021); // Fallback to ftp default
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false)); 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")); assert_eq!(result.entry_directory.unwrap(), PathBuf::from("/tmp"));
// bad syntax // bad syntax
// Bad protocol // Bad protocol
@@ -573,6 +692,56 @@ mod tests {
assert!(parse_remote_opt(&String::from("scp://172.26.104.1:650000")).is_err()); 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] #[test]
fn test_utils_parse_lstime() { fn test_utils_parse_lstime() {
// Good cases // Good cases

View File

@@ -2,7 +2,7 @@
//! //!
//! Path related utilities //! Path related utilities
use std::path::{Path, PathBuf}; use std::path::{Component, Path, PathBuf};
/// ### absolutize /// ### 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)] #[cfg(test)]
mod test { mod test {
@@ -40,4 +98,26 @@ mod test {
Path::new("/tmp/readme.txt") 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}; use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
// ext // ext
use std::fs::File; use std::fs::File;
#[cfg(feature = "with-containers")] #[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
use std::fs::OpenOptions; use std::fs::OpenOptions;
#[cfg(feature = "with-containers")] #[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
use std::io::Read; use std::io::Read;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; 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()) 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>) { pub fn write_file(file: &NamedTempFile, writable: &mut Box<dyn Write>) {
let mut fhnd = OpenOptions::new() let mut fhnd = OpenOptions::new()
.create(false) .create(false)
@@ -153,7 +153,8 @@ RorU9FCmS/654wAAABFyb290QDhjNTBmZDRjMzQ1YQECAw==
/// ### make_fsentry /// ### make_fsentry
/// ///
/// Create a FsEntry at specified path /// 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 { match is_dir {
true => FsEntry::Directory(FsDirectory { true => FsEntry::Directory(FsDirectory {
name: path.file_name().unwrap().to_string_lossy().to_string(), name: path.file_name().unwrap().to_string_lossy().to_string(),