Aws s3 support
This commit is contained in:
parent
f31f58aa79
commit
1d09095ab9
|
@ -9,6 +9,7 @@ ignore:
|
|||
- src/main.rs
|
||||
- src/lib.rs
|
||||
- src/activity_manager.rs
|
||||
- src/filetransfer/transfer/s3/mod.rs
|
||||
- src/support.rs
|
||||
- "src/ui/activities/*"
|
||||
- src/ui/context.rs
|
||||
|
|
|
@ -27,6 +27,12 @@ Released on ??
|
|||
|
||||
> 🍁 Autumn update 🍇
|
||||
|
||||
- **Aws S3**
|
||||
- Added support for the aws-s3 protocol
|
||||
- Operate on your bucket directly from the file explorer
|
||||
- You can also save your buckets as bookmarks
|
||||
- Aws s3 reads credentials directly from your credentials file at `$HOME/.aws/credentials` or from environment. Read more in the user manual.
|
||||
|
||||
## 0.6.1
|
||||
|
||||
Released on 31/08/2021
|
||||
|
|
|
@ -33,6 +33,12 @@ dependencies = [
|
|||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
|
@ -51,6 +57,12 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf"
|
||||
|
||||
[[package]]
|
||||
name = "argh"
|
||||
version = "0.1.5"
|
||||
|
@ -80,12 +92,78 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a61eb019cb8f415d162cb9f12130ee6bbe9168b7d953c17f4ad049e4051ca00"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attohttpc"
|
||||
version = "0.16.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247"
|
||||
dependencies = [
|
||||
"http",
|
||||
"log",
|
||||
"native-tls",
|
||||
"openssl",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"url",
|
||||
"wildmatch 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attohttpc"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8bda305457262b339322106c776e3fd21df860018e566eb6a5b1aa4b6ae02d"
|
||||
dependencies = [
|
||||
"http",
|
||||
"log",
|
||||
"native-tls",
|
||||
"openssl",
|
||||
"url",
|
||||
"wildmatch 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
|
||||
[[package]]
|
||||
name = "aws-creds"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1331d069460a674d42bd27c12b47ce578f789954c7bd7f239fd030771eca6616"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"attohttpc 0.16.3",
|
||||
"dirs",
|
||||
"rust-ini",
|
||||
"serde",
|
||||
"serde-xml-rs",
|
||||
"serde_derive",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aws-region"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2884b8f2aaeb4a4bf80b219b4fe1d340139ca9331679c57e0fd4a24f571a78bd"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.13.0"
|
||||
|
@ -136,6 +214,12 @@ version = "1.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
|
||||
|
||||
[[package]]
|
||||
name = "bytesize"
|
||||
version = "1.1.0"
|
||||
|
@ -368,6 +452,15 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dlv-list"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b"
|
||||
dependencies = [
|
||||
"rand 0.8.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "edit"
|
||||
version = "0.1.3"
|
||||
|
@ -384,6 +477,12 @@ version = "1.6.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
|
@ -409,6 +508,12 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.4"
|
||||
|
@ -441,6 +546,15 @@ dependencies = [
|
|||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.3.3"
|
||||
|
@ -450,6 +564,12 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "hkdf"
|
||||
version = "0.10.0"
|
||||
|
@ -481,6 +601,17 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"fnv",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.2.3"
|
||||
|
@ -622,6 +753,17 @@ version = "0.1.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
|
||||
|
||||
[[package]]
|
||||
name = "maybe-async"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6007f9dad048e0a224f27ca599d669fca8cfa0dac804725aab542b2eb032bce6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.9.1"
|
||||
|
@ -633,12 +775,27 @@ dependencies = [
|
|||
"opaque-debug",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
|
||||
|
||||
[[package]]
|
||||
name = "minidom"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "332592c2149fc7dd40a64fc9ef6f0d65607284b474cef9817d1fc8c7e7b3608e"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.7.13"
|
||||
|
@ -819,6 +976,16 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-multimap"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485"
|
||||
dependencies = [
|
||||
"dlv-list",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "output_vt100"
|
||||
version = "0.1.2"
|
||||
|
@ -895,6 +1062,12 @@ version = "2.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.19"
|
||||
|
@ -928,6 +1101,15 @@ dependencies = [
|
|||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26aab6b48e2590e4a64d1ed808749ba06257882b461d01ca71baeb747074a6dd"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.9"
|
||||
|
@ -1094,6 +1276,46 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-ini"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55b134767a87e0b086f73a4ce569ac9ce7d202f39c8eab6caa266e2617e73ac6"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"ordered-multimap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-s3"
|
||||
version = "0.27.0-rc4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c93272c1d654d492f8ab30b94cd43d98f2700b1db55b2576aff7712ce40e3ef"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"attohttpc 0.17.0",
|
||||
"aws-creds",
|
||||
"aws-region",
|
||||
"base64",
|
||||
"cfg-if 1.0.0",
|
||||
"chrono",
|
||||
"hex",
|
||||
"hmac",
|
||||
"http",
|
||||
"log",
|
||||
"maybe-async",
|
||||
"md5",
|
||||
"minidom",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde-xml-rs",
|
||||
"serde_derive",
|
||||
"sha2",
|
||||
"tokio-stream",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.19.1"
|
||||
|
@ -1210,6 +1432,18 @@ dependencies = [
|
|||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde-xml-rs"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.127"
|
||||
|
@ -1392,6 +1626,7 @@ dependencies = [
|
|||
"rand 0.8.4",
|
||||
"regex",
|
||||
"rpassword",
|
||||
"rust-s3",
|
||||
"serde",
|
||||
"simplelog",
|
||||
"ssh2",
|
||||
|
@ -1405,7 +1640,7 @@ dependencies = [
|
|||
"ureq",
|
||||
"users",
|
||||
"whoami",
|
||||
"wildmatch",
|
||||
"wildmatch 2.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1476,6 +1711,27 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92036be488bb6594459f2e03b60e42df6f937fe6ca5c5ffdcb539c6b84dc40f5"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.8"
|
||||
|
@ -1741,6 +1997,12 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wildmatch"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a"
|
||||
|
||||
[[package]]
|
||||
name = "wildmatch"
|
||||
version = "2.1.0"
|
||||
|
@ -1777,3 +2039,9 @@ name = "winapi-x86_64-pc-windows-gnu"
|
|||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
authors = ["Christian Visintin"]
|
||||
categories = ["command-line-utilities"]
|
||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP"
|
||||
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/S3"
|
||||
documentation = "https://docs.rs/termscp"
|
||||
edition = "2018"
|
||||
homepage = "https://veeso.github.io/termscp/"
|
||||
|
@ -44,6 +44,7 @@ open = "2.0.1"
|
|||
rand = "0.8.4"
|
||||
regex = "1.5.4"
|
||||
rpassword = "5.0.1"
|
||||
rust-s3 = { version = "0.27.0-rc4", default-features = false, features = [ "sync-native-tls", "sync" ] }
|
||||
serde = { version = "^1.0.0", features = [ "derive" ] }
|
||||
simplelog = "0.10.0"
|
||||
ssh2 = "0.9.0"
|
||||
|
@ -63,7 +64,8 @@ pretty_assertions = "0.7.2"
|
|||
|
||||
[features]
|
||||
default = [ "with-keyring" ]
|
||||
github-actions = []
|
||||
github-actions = [ ]
|
||||
with-s3-ci = []
|
||||
with-containers = []
|
||||
with-keyring = [ "keyring" ]
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
|
||||
## About termscp 🖥
|
||||
|
||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **BSD** and **Windows** compatible and supports SFTP, SCP, FTP and FTPS.
|
||||
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/S3. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **BSD** and **Windows** compatible and supports SFTP, SCP, FTP, FTPS and S3.
|
||||
|
||||
![Explorer](assets/images/explorer.gif)
|
||||
|
||||
|
@ -36,6 +36,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
|
|||
- SFTP
|
||||
- SCP
|
||||
- FTP and FTPS
|
||||
- Aws S3
|
||||
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
|
||||
- Create, remove, rename, search, view and edit files
|
||||
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
|
||||
|
@ -160,6 +161,7 @@ termscp is powered by these aweseome projects:
|
|||
- [keyring-rs](https://github.com/hwchen/keyring-rs)
|
||||
- [open-rs](https://github.com/Byron/open-rs)
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-s3](https://github.com/durch/rust-s3)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
||||
- [textwrap](https://github.com/mgeisler/textwrap)
|
||||
|
|
40
docs/man.md
40
docs/man.md
|
@ -3,6 +3,7 @@
|
|||
- [User manual 🎓](#user-manual-)
|
||||
- [Usage ❓](#usage-)
|
||||
- [Address argument 🌎](#address-argument-)
|
||||
- [AWS S3 address argument](#aws-s3-address-argument)
|
||||
- [How Password can be provided 🔐](#how-password-can-be-provided-)
|
||||
- [File explorer 📂](#file-explorer-)
|
||||
- [Keybindings ⌨](#keybindings-)
|
||||
|
@ -13,6 +14,7 @@
|
|||
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
|
||||
- [Linux Keyring](#linux-keyring)
|
||||
- [KeepassXC setup for termscp](#keepassxc-setup-for-termscp)
|
||||
- [Aws S3 credentials 🦊](#aws-s3-credentials-)
|
||||
- [Configuration ⚙️](#configuration-️)
|
||||
- [SSH Key Storage 🔐](#ssh-key-storage-)
|
||||
- [File Explorer Format](#file-explorer-format)
|
||||
|
@ -78,6 +80,20 @@ Let's see some example of this particular syntax, since it's very comfortable an
|
|||
termscp scp://omar@192.168.1.31:4022:/tmp
|
||||
```
|
||||
|
||||
#### AWS S3 address argument
|
||||
|
||||
Aws S3 has a different syntax for CLI address argument, for obvious reasons, but I managed to keep it the more similiar as possible to the generic address argument:
|
||||
|
||||
```txt
|
||||
s3://<bucket-name>@<region>[:profile][:/wrkdir]
|
||||
```
|
||||
|
||||
e.g.
|
||||
|
||||
```txt
|
||||
s3://buckethead@eu-central-1:default:/assets
|
||||
```
|
||||
|
||||
#### How Password can be provided 🔐
|
||||
|
||||
You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
|
||||
|
@ -246,6 +262,30 @@ Follow these steps in order to setup keepassXC for termscp:
|
|||
|
||||
---
|
||||
|
||||
## Aws S3 credentials 🦊
|
||||
|
||||
In order to connect to an Aws S3 bucket you must obviously provide some credentials.
|
||||
There are basically two ways to achieve this, and as you've probably already noticed you **can't** do that via the authentication form.
|
||||
So these are the ways you can provide the credentials for s3:
|
||||
|
||||
1. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form.
|
||||
2. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below:
|
||||
|
||||
These should always be mandatory:
|
||||
|
||||
- `AWS_ACCESS_KEY_ID`: aws access key ID (usually starts with `AKIA...`)
|
||||
- `AWS_SECRET_ACCESS_KEY`: the secret access key
|
||||
|
||||
In case you've configured a stronger security, you *may* require these too:
|
||||
|
||||
- `AWS_SECURITY_TOKEN`: security token
|
||||
- `AWS_SESSION_TOKEN`: session token
|
||||
|
||||
⚠️ Your credentials are safe: termscp won't manipulate these values directly! Your credentials are directly consumed by the **s3** crate.
|
||||
In case you've got some concern regarding security, please contact the library author on [Github](https://github.com/durch/rust-s3) ⚠️
|
||||
|
||||
---
|
||||
|
||||
## Configuration ⚙️
|
||||
|
||||
termscp supports some user defined parameters, which can be defined in the configuration.
|
||||
|
|
|
@ -25,31 +25,57 @@
|
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
|
||||
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## UserHosts
|
||||
///
|
||||
/// UserHosts contains all the hosts saved by the user in the data storage
|
||||
/// It contains both `Bookmark`
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct UserHosts {
|
||||
pub bookmarks: HashMap<String, Bookmark>,
|
||||
pub recents: HashMap<String, Bookmark>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug, PartialEq)]
|
||||
/// ## Bookmark
|
||||
///
|
||||
/// Bookmark describes a single bookmark entry in the user hosts storage
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
|
||||
pub struct Bookmark {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub protocol: String,
|
||||
pub username: String,
|
||||
pub password: Option<String>, // Password is optional; base64, aes-128 encrypted password
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_protocol",
|
||||
serialize_with = "serialize_protocol"
|
||||
)]
|
||||
pub protocol: FileTransferProtocol,
|
||||
/// Address for generic parameters
|
||||
pub address: Option<String>,
|
||||
/// Port number for generic parameters
|
||||
pub port: Option<u16>,
|
||||
/// Username for generic parameters
|
||||
pub username: Option<String>,
|
||||
/// Password is optional; base64, aes-128 encrypted password
|
||||
pub password: Option<String>,
|
||||
/// S3 params; optional. When used other fields are empty for sure
|
||||
pub s3: Option<S3Params>,
|
||||
}
|
||||
|
||||
/// ## S3Params
|
||||
///
|
||||
/// Connection parameters for Aws s3 protocol
|
||||
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Default)]
|
||||
pub struct S3Params {
|
||||
pub bucket: String,
|
||||
pub region: String,
|
||||
pub profile: Option<String>,
|
||||
}
|
||||
|
||||
// -- impls
|
||||
|
||||
impl Default for UserHosts {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
|
@ -59,6 +85,87 @@ impl Default for UserHosts {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<FileTransferParams> for Bookmark {
|
||||
fn from(params: FileTransferParams) -> Self {
|
||||
let protocol: FileTransferProtocol = params.protocol;
|
||||
// Create generic or others
|
||||
match params.params {
|
||||
ProtocolParams::Generic(params) => Self {
|
||||
protocol,
|
||||
address: Some(params.address),
|
||||
port: Some(params.port),
|
||||
username: params.username,
|
||||
password: params.password,
|
||||
s3: None,
|
||||
},
|
||||
ProtocolParams::AwsS3(params) => Self {
|
||||
protocol,
|
||||
address: None,
|
||||
port: None,
|
||||
username: None,
|
||||
password: None,
|
||||
s3: Some(S3Params::from(params)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bookmark> for FileTransferParams {
|
||||
fn from(bookmark: Bookmark) -> Self {
|
||||
// Create generic or others based on protocol
|
||||
match bookmark.protocol {
|
||||
FileTransferProtocol::AwsS3 => {
|
||||
let params = bookmark.s3.unwrap_or_default();
|
||||
let params = AwsS3Params::from(params);
|
||||
Self::new(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params))
|
||||
}
|
||||
protocol => {
|
||||
let params = GenericProtocolParams::default()
|
||||
.address(bookmark.address.unwrap_or_default())
|
||||
.port(bookmark.port.unwrap_or(22))
|
||||
.username(bookmark.username)
|
||||
.password(bookmark.password);
|
||||
Self::new(protocol, ProtocolParams::Generic(params))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AwsS3Params> for S3Params {
|
||||
fn from(params: AwsS3Params) -> Self {
|
||||
S3Params {
|
||||
bucket: params.bucket_name,
|
||||
region: params.region,
|
||||
profile: params.profile,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<S3Params> for AwsS3Params {
|
||||
fn from(params: S3Params) -> Self {
|
||||
AwsS3Params::new(params.bucket, params.region, params.profile)
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_protocol<'de, D>(deserializer: D) -> Result<FileTransferProtocol, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: &str = Deserialize::deserialize(deserializer)?;
|
||||
// Parse color
|
||||
match FileTransferProtocol::from_str(s) {
|
||||
Err(err) => Err(DeError::custom(err)),
|
||||
Ok(protocol) => Ok(protocol),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_protocol<S>(protocol: &FileTransferProtocol, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(protocol.to_string().as_str())
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -77,48 +184,117 @@ mod tests {
|
|||
#[test]
|
||||
fn test_bookmarks_bookmark_new() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
address: String::from("192.168.1.1"),
|
||||
port: 22,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("root"),
|
||||
address: Some(String::from("192.168.1.1")),
|
||||
port: Some(22),
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: Some(String::from("root")),
|
||||
password: Some(String::from("password")),
|
||||
s3: None,
|
||||
};
|
||||
let recent: Bookmark = Bookmark {
|
||||
address: String::from("192.168.1.2"),
|
||||
port: 22,
|
||||
protocol: String::from("SCP"),
|
||||
username: String::from("admin"),
|
||||
address: Some(String::from("192.168.1.2")),
|
||||
port: Some(22),
|
||||
protocol: FileTransferProtocol::Scp,
|
||||
username: Some(String::from("admin")),
|
||||
password: Some(String::from("password")),
|
||||
s3: None,
|
||||
};
|
||||
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1);
|
||||
bookmarks.insert(String::from("test"), bookmark);
|
||||
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
|
||||
recents.insert(String::from("ISO20201218T181432"), recent);
|
||||
let hosts: UserHosts = UserHosts {
|
||||
bookmarks: bookmarks,
|
||||
recents: recents,
|
||||
};
|
||||
let hosts: UserHosts = UserHosts { bookmarks, recents };
|
||||
// Verify
|
||||
let bookmark: &Bookmark = hosts.bookmarks.get(&String::from("test")).unwrap();
|
||||
assert_eq!(bookmark.address, String::from("192.168.1.1"));
|
||||
assert_eq!(bookmark.port, 22);
|
||||
assert_eq!(bookmark.protocol, String::from("SFTP"));
|
||||
assert_eq!(bookmark.username, String::from("root"));
|
||||
assert_eq!(
|
||||
*bookmark.password.as_ref().unwrap(),
|
||||
String::from("password")
|
||||
);
|
||||
assert_eq!(bookmark.address.as_deref().unwrap(), "192.168.1.1");
|
||||
assert_eq!(bookmark.port.unwrap(), 22);
|
||||
assert_eq!(bookmark.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(bookmark.username.as_deref().unwrap(), "root");
|
||||
assert_eq!(bookmark.password.as_deref().unwrap(), "password");
|
||||
let bookmark: &Bookmark = hosts
|
||||
.recents
|
||||
.get(&String::from("ISO20201218T181432"))
|
||||
.unwrap();
|
||||
assert_eq!(bookmark.address, String::from("192.168.1.2"));
|
||||
assert_eq!(bookmark.port, 22);
|
||||
assert_eq!(bookmark.protocol, String::from("SCP"));
|
||||
assert_eq!(bookmark.username, String::from("admin"));
|
||||
assert_eq!(
|
||||
*bookmark.password.as_ref().unwrap(),
|
||||
String::from("password")
|
||||
);
|
||||
assert_eq!(bookmark.address.as_deref().unwrap(), "192.168.1.2");
|
||||
assert_eq!(bookmark.port.unwrap(), 22);
|
||||
assert_eq!(bookmark.protocol, FileTransferProtocol::Scp);
|
||||
assert_eq!(bookmark.username.as_deref().unwrap(), "admin");
|
||||
assert_eq!(bookmark.password.as_deref().unwrap(), "password");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bookmark_from_generic_ftparams() {
|
||||
let params = ProtocolParams::Generic(GenericProtocolParams {
|
||||
address: "127.0.0.1".to_string(),
|
||||
port: 10222,
|
||||
username: Some(String::from("root")),
|
||||
password: Some(String::from("omar")),
|
||||
});
|
||||
let params: FileTransferParams = FileTransferParams::new(FileTransferProtocol::Scp, params);
|
||||
let bookmark = Bookmark::from(params);
|
||||
assert_eq!(bookmark.protocol, FileTransferProtocol::Scp);
|
||||
assert_eq!(bookmark.address.as_deref().unwrap(), "127.0.0.1");
|
||||
assert_eq!(bookmark.port.unwrap(), 10222);
|
||||
assert_eq!(bookmark.username.as_deref().unwrap(), "root");
|
||||
assert_eq!(bookmark.password.as_deref().unwrap(), "omar");
|
||||
assert!(bookmark.s3.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bookmark_from_s3_ftparams() {
|
||||
let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test")));
|
||||
let params: FileTransferParams =
|
||||
FileTransferParams::new(FileTransferProtocol::AwsS3, params);
|
||||
let bookmark = Bookmark::from(params);
|
||||
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
|
||||
assert!(bookmark.address.is_none());
|
||||
assert!(bookmark.port.is_none());
|
||||
assert!(bookmark.username.is_none());
|
||||
assert!(bookmark.password.is_none());
|
||||
let s3: &S3Params = bookmark.s3.as_ref().unwrap();
|
||||
assert_eq!(s3.bucket.as_str(), "omar");
|
||||
assert_eq!(s3.region.as_str(), "eu-west-1");
|
||||
assert_eq!(s3.profile.as_deref().unwrap(), "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ftparams_from_generic_bookmark() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
address: Some(String::from("192.168.1.1")),
|
||||
port: Some(22),
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: Some(String::from("root")),
|
||||
password: Some(String::from("password")),
|
||||
s3: None,
|
||||
};
|
||||
let params = FileTransferParams::from(bookmark);
|
||||
assert_eq!(params.protocol, FileTransferProtocol::Sftp);
|
||||
let gparams = params.params.generic_params().unwrap();
|
||||
assert_eq!(gparams.address.as_str(), "192.168.1.1");
|
||||
assert_eq!(gparams.port, 22);
|
||||
assert_eq!(gparams.username.as_deref().unwrap(), "root");
|
||||
assert_eq!(gparams.password.as_deref().unwrap(), "password");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ftparams_from_s3_bookmark() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
protocol: FileTransferProtocol::AwsS3,
|
||||
address: None,
|
||||
port: None,
|
||||
username: None,
|
||||
password: None,
|
||||
s3: Some(S3Params {
|
||||
bucket: String::from("veeso"),
|
||||
region: String::from("eu-west-1"),
|
||||
profile: None,
|
||||
}),
|
||||
};
|
||||
let params = FileTransferParams::from(bookmark);
|
||||
assert_eq!(params.protocol, FileTransferProtocol::AwsS3);
|
||||
let gparams = params.params.s3_params().unwrap();
|
||||
assert_eq!(gparams.bucket_name.as_str(), "veeso");
|
||||
assert_eq!(gparams.region.as_str(), "eu-west-1");
|
||||
assert_eq!(gparams.profile, None);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -141,17 +141,19 @@ where
|
|||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::config::bookmarks::{Bookmark, S3Params, UserHosts};
|
||||
use crate::config::params::UserConfig;
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::utils::test_helpers::create_file_ioers;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use std::path::PathBuf;
|
||||
use tuirealm::tui::style::Color;
|
||||
|
||||
use crate::config::bookmarks::{Bookmark, UserHosts};
|
||||
use crate::config::params::UserConfig;
|
||||
use crate::config::themes::Theme;
|
||||
use crate::utils::test_helpers::create_file_ioers;
|
||||
|
||||
#[test]
|
||||
fn test_config_serialization_errors() {
|
||||
let error: SerializerError = SerializerError::new(SerializerErrorKind::Syntax);
|
||||
|
@ -373,31 +375,42 @@ mod tests {
|
|||
// Verify recents
|
||||
assert_eq!(hosts.recents.len(), 1);
|
||||
let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap();
|
||||
assert_eq!(host.address, String::from("172.16.104.10"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SCP"));
|
||||
assert_eq!(host.username, String::from("root"));
|
||||
assert_eq!(host.address.as_deref().unwrap(), "172.16.104.10");
|
||||
assert_eq!(host.port.unwrap(), 22);
|
||||
assert_eq!(host.protocol, FileTransferProtocol::Scp);
|
||||
assert_eq!(host.username.as_deref().unwrap(), "root");
|
||||
assert_eq!(host.password, None);
|
||||
// Verify bookmarks
|
||||
assert_eq!(hosts.bookmarks.len(), 3);
|
||||
assert_eq!(hosts.bookmarks.len(), 4);
|
||||
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
|
||||
assert_eq!(host.address, String::from("192.168.1.31"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SFTP"));
|
||||
assert_eq!(host.username, String::from("root"));
|
||||
assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword"));
|
||||
assert_eq!(host.address.as_deref().unwrap(), "192.168.1.31");
|
||||
assert_eq!(host.port.unwrap(), 22);
|
||||
assert_eq!(host.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(host.username.as_deref().unwrap(), "root");
|
||||
assert_eq!(host.password.as_deref().unwrap(), "mypassword");
|
||||
let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap();
|
||||
assert_eq!(host.address, String::from("192.168.1.30"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SFTP"));
|
||||
assert_eq!(host.username, String::from("cvisintin"));
|
||||
assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret"));
|
||||
assert_eq!(host.address.as_deref().unwrap(), "192.168.1.30");
|
||||
assert_eq!(host.port.unwrap(), 22);
|
||||
assert_eq!(host.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(host.username.as_deref().unwrap(), "cvisintin");
|
||||
assert_eq!(host.password.as_deref().unwrap(), "mysecret");
|
||||
let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap();
|
||||
assert_eq!(host.address, String::from("51.23.67.12"));
|
||||
assert_eq!(host.port, 21);
|
||||
assert_eq!(host.protocol, String::from("FTPS"));
|
||||
assert_eq!(host.username, String::from("aws001"));
|
||||
assert_eq!(host.address.as_deref().unwrap(), "51.23.67.12");
|
||||
assert_eq!(host.port.unwrap(), 21);
|
||||
assert_eq!(host.protocol, FileTransferProtocol::Ftp(true));
|
||||
assert_eq!(host.username.as_deref().unwrap(), "aws001");
|
||||
assert_eq!(host.password, None);
|
||||
// Aws s3 bucket
|
||||
let host: &Bookmark = hosts.bookmarks.get("my-bucket").unwrap();
|
||||
assert_eq!(host.address, None);
|
||||
assert_eq!(host.port, None);
|
||||
assert_eq!(host.username, None);
|
||||
assert_eq!(host.password, None);
|
||||
assert_eq!(host.protocol, FileTransferProtocol::AwsS3);
|
||||
let s3 = host.s3.as_ref().unwrap();
|
||||
assert_eq!(s3.bucket.as_str(), "veeso");
|
||||
assert_eq!(s3.region.as_str(), "eu-west-1");
|
||||
assert_eq!(s3.profile.as_deref().unwrap(), "default");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -416,32 +429,50 @@ mod tests {
|
|||
bookmarks.insert(
|
||||
String::from("raspberrypi2"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.31"),
|
||||
port: 22,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("root"),
|
||||
address: Some(String::from("192.168.1.31")),
|
||||
port: Some(22),
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: Some(String::from("root")),
|
||||
password: None,
|
||||
s3: None,
|
||||
},
|
||||
);
|
||||
bookmarks.insert(
|
||||
String::from("msi-estrem"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.30"),
|
||||
port: 4022,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("cvisintin"),
|
||||
address: Some(String::from("192.168.1.30")),
|
||||
port: Some(4022),
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: Some(String::from("cvisintin")),
|
||||
password: Some(String::from("password")),
|
||||
s3: None,
|
||||
},
|
||||
);
|
||||
bookmarks.insert(
|
||||
String::from("my-bucket"),
|
||||
Bookmark {
|
||||
address: None,
|
||||
port: None,
|
||||
protocol: FileTransferProtocol::AwsS3,
|
||||
username: None,
|
||||
password: None,
|
||||
s3: Some(S3Params {
|
||||
bucket: "veeso".to_string(),
|
||||
region: "eu-west-1".to_string(),
|
||||
profile: None,
|
||||
}),
|
||||
},
|
||||
);
|
||||
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
|
||||
recents.insert(
|
||||
String::from("ISO20201215T094000Z"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.254"),
|
||||
port: 3022,
|
||||
protocol: String::from("SCP"),
|
||||
username: String::from("omar"),
|
||||
address: Some(String::from("192.168.1.254")),
|
||||
port: Some(3022),
|
||||
protocol: FileTransferProtocol::Scp,
|
||||
username: Some(String::from("omar")),
|
||||
password: Some(String::from("aaa")),
|
||||
s3: None,
|
||||
},
|
||||
);
|
||||
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
|
@ -482,6 +513,14 @@ mod tests {
|
|||
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" }
|
||||
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" }
|
||||
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
|
||||
|
||||
[bookmarks.my-bucket]
|
||||
protocol = "S3"
|
||||
|
||||
[bookmarks.my-bucket.s3]
|
||||
bucket = "veeso"
|
||||
region = "eu-west-1"
|
||||
profile = "default"
|
||||
|
||||
[recents]
|
||||
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
|
||||
|
@ -497,7 +536,7 @@ mod tests {
|
|||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root"}
|
||||
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP" }
|
||||
msi-estrem = { address = "192.168.1.30", port = 22 }
|
||||
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
|
||||
|
||||
[recents]
|
||||
|
|
|
@ -28,27 +28,29 @@
|
|||
// locals
|
||||
use crate::fs::{FsEntry, FsFile};
|
||||
// ext
|
||||
use std::io::{Read, Write};
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
use wildmatch::WildMatch;
|
||||
// exports
|
||||
pub mod ftp_transfer;
|
||||
pub mod params;
|
||||
pub mod scp_transfer;
|
||||
pub mod sftp_transfer;
|
||||
mod transfer;
|
||||
|
||||
pub use params::FileTransferParams;
|
||||
// -- export types
|
||||
pub use params::{FileTransferParams, ProtocolParams};
|
||||
pub use transfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
|
||||
|
||||
/// ## FileTransferProtocol
|
||||
///
|
||||
/// This enum defines the different transfer protocol available in termscp
|
||||
|
||||
#[derive(PartialEq, Debug, std::clone::Clone, Copy)]
|
||||
#[derive(PartialEq, Debug, Clone, Copy)]
|
||||
pub enum FileTransferProtocol {
|
||||
Sftp,
|
||||
Scp,
|
||||
Ftp(bool), // Bool is for secure (true => ftps)
|
||||
AwsS3,
|
||||
}
|
||||
|
||||
/// ## FileTransferError
|
||||
|
@ -130,25 +132,16 @@ impl std::fmt::Display for FileTransferError {
|
|||
/// ## FileTransfer
|
||||
///
|
||||
/// File transfer trait must be implemented by all the file transfers and defines the method used by a generic file transfer
|
||||
|
||||
pub trait FileTransfer {
|
||||
/// ### connect
|
||||
///
|
||||
/// Connect to the remote server
|
||||
/// Can return banner / welcome message on success
|
||||
|
||||
fn connect(
|
||||
&mut self,
|
||||
address: String,
|
||||
port: u16,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError>;
|
||||
fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError>;
|
||||
|
||||
/// ### disconnect
|
||||
///
|
||||
/// Disconnect from the remote server
|
||||
|
||||
fn disconnect(&mut self) -> Result<(), FileTransferError>;
|
||||
|
||||
/// ### is_connected
|
||||
|
@ -210,18 +203,28 @@ pub trait FileTransfer {
|
|||
/// Send file to remote
|
||||
/// File name is referred to the name of the file as it will be saved
|
||||
/// Data contains the file data
|
||||
/// Returns file and its size
|
||||
/// Returns file and its size.
|
||||
/// By default returns unsupported feature
|
||||
fn send_file(
|
||||
&mut self,
|
||||
local: &FsFile,
|
||||
file_name: &Path,
|
||||
) -> Result<Box<dyn Write>, FileTransferError>;
|
||||
_local: &FsFile,
|
||||
_file_name: &Path,
|
||||
) -> Result<Box<dyn Write>, FileTransferError> {
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### recv_file
|
||||
///
|
||||
/// Receive file from remote with provided name
|
||||
/// Returns file and its size
|
||||
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError>;
|
||||
/// By default returns unsupported feature
|
||||
fn recv_file(&mut self, _file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### on_sent
|
||||
///
|
||||
|
@ -230,7 +233,10 @@ pub trait FileTransfer {
|
|||
/// The purpose of this method is to finalize the connection with the peer when writing data.
|
||||
/// This is necessary for some protocols such as FTP.
|
||||
/// You must call this method each time you want to finalize the write of the remote file.
|
||||
fn on_sent(&mut self, writable: Box<dyn Write>) -> Result<(), FileTransferError>;
|
||||
/// By default this function returns already `Ok(())`
|
||||
fn on_sent(&mut self, _writable: Box<dyn Write>) -> Result<(), FileTransferError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### on_recv
|
||||
///
|
||||
|
@ -239,7 +245,71 @@ pub trait FileTransfer {
|
|||
/// The purpose of this method is to finalize the connection with the peer when reading data.
|
||||
/// This mighe be necessary for some protocols.
|
||||
/// You must call this method each time you want to finalize the read of the remote file.
|
||||
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError>;
|
||||
/// By default this function returns already `Ok(())`
|
||||
fn on_recv(&mut self, _readable: Box<dyn Read>) -> Result<(), FileTransferError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### send_file_wno_stream
|
||||
///
|
||||
/// Send a file to remote WITHOUT using streams.
|
||||
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
|
||||
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
|
||||
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
|
||||
/// By default this function uses the streams function to copy content from reader to writer
|
||||
fn send_file_wno_stream(
|
||||
&mut self,
|
||||
src: &FsFile,
|
||||
dest: &Path,
|
||||
mut reader: Box<dyn Read>,
|
||||
) -> Result<(), FileTransferError> {
|
||||
match self.is_connected() {
|
||||
true => {
|
||||
let mut stream = self.send_file(src, dest)?;
|
||||
io::copy(&mut reader, &mut stream).map_err(|e| {
|
||||
FileTransferError::new_ex(FileTransferErrorType::ProtocolError, e.to_string())
|
||||
})?;
|
||||
self.on_sent(stream)
|
||||
}
|
||||
false => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### recv_file_wno_stream
|
||||
///
|
||||
/// Receive a file from remote WITHOUT using streams.
|
||||
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
|
||||
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
|
||||
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
|
||||
/// For safety reasons this function doesn't accept the `Write` trait, but the destination path.
|
||||
/// By default this function uses the streams function to copy content from reader to writer
|
||||
fn recv_file_wno_stream(&mut self, src: &FsFile, dest: &Path) -> Result<(), FileTransferError> {
|
||||
match self.is_connected() {
|
||||
true => {
|
||||
let mut writer = File::create(dest).map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::FileCreateDenied,
|
||||
format!("Could not open local file: {}", e),
|
||||
)
|
||||
})?;
|
||||
let mut stream = self.recv_file(src)?;
|
||||
io::copy(&mut stream, &mut writer)
|
||||
.map(|_| ())
|
||||
.map_err(|e| {
|
||||
FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
e.to_string(),
|
||||
)
|
||||
})?;
|
||||
self.on_recv(stream)
|
||||
}
|
||||
false => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### find
|
||||
///
|
||||
|
@ -314,6 +384,7 @@ impl std::string::ToString for FileTransferProtocol {
|
|||
},
|
||||
FileTransferProtocol::Scp => "SCP",
|
||||
FileTransferProtocol::Sftp => "SFTP",
|
||||
FileTransferProtocol::AwsS3 => "S3",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -326,6 +397,7 @@ impl std::str::FromStr for FileTransferProtocol {
|
|||
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
|
||||
"SCP" => Ok(FileTransferProtocol::Scp),
|
||||
"SFTP" => Ok(FileTransferProtocol::Sftp),
|
||||
"S3" => Ok(FileTransferProtocol::AwsS3),
|
||||
_ => Err(s.to_string()),
|
||||
}
|
||||
}
|
||||
|
@ -385,6 +457,14 @@ mod tests {
|
|||
FileTransferProtocol::from_str("scp").ok().unwrap(),
|
||||
FileTransferProtocol::Scp
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("S3").ok().unwrap(),
|
||||
FileTransferProtocol::AwsS3
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("s3").ok().unwrap(),
|
||||
FileTransferProtocol::AwsS3
|
||||
);
|
||||
// Error
|
||||
assert!(FileTransferProtocol::from_str("dummy").is_err());
|
||||
// To String
|
||||
|
@ -398,6 +478,7 @@ mod tests {
|
|||
);
|
||||
assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP"));
|
||||
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
|
||||
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -32,44 +32,132 @@ use std::path::{Path, PathBuf};
|
|||
/// ### FileTransferParams
|
||||
///
|
||||
/// Holds connection parameters for file transfers
|
||||
#[derive(Clone)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileTransferParams {
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub params: ProtocolParams,
|
||||
pub entry_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// ## ProtocolParams
|
||||
///
|
||||
/// Container for protocol params
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProtocolParams {
|
||||
Generic(GenericProtocolParams),
|
||||
AwsS3(AwsS3Params),
|
||||
}
|
||||
|
||||
/// ## GenericProtocolParams
|
||||
///
|
||||
/// Protocol params used by most common protocols
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GenericProtocolParams {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub entry_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// ## AwsS3Params
|
||||
///
|
||||
/// Connection parameters for AWS S3 protocol
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AwsS3Params {
|
||||
pub bucket_name: String,
|
||||
pub region: String,
|
||||
pub profile: Option<String>,
|
||||
}
|
||||
|
||||
impl FileTransferParams {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new `FileTransferParams`
|
||||
pub fn new<S: AsRef<str>>(address: S) -> Self {
|
||||
pub fn new(protocol: FileTransferProtocol, params: ProtocolParams) -> Self {
|
||||
Self {
|
||||
address: address.as_ref().to_string(),
|
||||
port: 22,
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: None,
|
||||
password: None,
|
||||
protocol,
|
||||
params,
|
||||
entry_directory: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### port
|
||||
/// ### entry_directory
|
||||
///
|
||||
/// Set port for params
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
/// Set entry directory
|
||||
pub fn entry_directory<P: AsRef<Path>>(mut self, dir: Option<P>) -> Self {
|
||||
self.entry_directory = dir.map(|x| x.as_ref().to_path_buf());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileTransferParams {
|
||||
fn default() -> Self {
|
||||
Self::new(FileTransferProtocol::Sftp, ProtocolParams::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProtocolParams {
|
||||
fn default() -> Self {
|
||||
Self::Generic(GenericProtocolParams::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolParams {
|
||||
/// ### generic_params
|
||||
///
|
||||
/// Retrieve generic parameters from protocol params if any
|
||||
pub fn generic_params(&self) -> Option<&GenericProtocolParams> {
|
||||
match self {
|
||||
ProtocolParams::Generic(params) => Some(params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mut_generic_params(&mut self) -> Option<&mut GenericProtocolParams> {
|
||||
match self {
|
||||
ProtocolParams::Generic(params) => Some(params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### s3_params
|
||||
///
|
||||
/// Retrieve AWS S3 parameters if any
|
||||
pub fn s3_params(&self) -> Option<&AwsS3Params> {
|
||||
match self {
|
||||
ProtocolParams::AwsS3(params) => Some(params),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Generic protocol params
|
||||
|
||||
impl Default for GenericProtocolParams {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
address: "localhost".to_string(),
|
||||
port: 22,
|
||||
username: None,
|
||||
password: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GenericProtocolParams {
|
||||
/// ### address
|
||||
///
|
||||
/// Set address to params
|
||||
pub fn address<S: AsRef<str>>(mut self, address: S) -> Self {
|
||||
self.address = address.as_ref().to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// ### protocol
|
||||
/// ### port
|
||||
///
|
||||
/// Set protocol for params
|
||||
pub fn protocol(mut self, protocol: FileTransferProtocol) -> Self {
|
||||
self.protocol = protocol;
|
||||
/// Set port to params
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -88,19 +176,20 @@ impl FileTransferParams {
|
|||
self.password = password.map(|x| x.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// ### entry_directory
|
||||
///
|
||||
/// Set entry directory
|
||||
pub fn entry_directory<P: AsRef<Path>>(mut self, dir: Option<P>) -> Self {
|
||||
self.entry_directory = dir.map(|x| x.as_ref().to_path_buf());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileTransferParams {
|
||||
fn default() -> Self {
|
||||
Self::new("localhost")
|
||||
// -- S3 params
|
||||
|
||||
impl AwsS3Params {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new `AwsS3Params` struct
|
||||
pub fn new<S: AsRef<str>>(bucket: S, region: S, profile: Option<S>) -> Self {
|
||||
Self {
|
||||
bucket_name: bucket.as_ref().to_string(),
|
||||
region: region.as_ref().to_string(),
|
||||
profile: profile.map(|x| x.as_ref().to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,26 +201,49 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_params() {
|
||||
let params: FileTransferParams = FileTransferParams::new("test.rebex.net")
|
||||
.port(2222)
|
||||
.protocol(FileTransferProtocol::Scp)
|
||||
.username(Some("omar"))
|
||||
.password(Some("foobar"))
|
||||
.entry_directory(Some(&Path::new("/tmp")));
|
||||
assert_eq!(params.address.as_str(), "test.rebex.net");
|
||||
assert_eq!(params.port, 2222);
|
||||
let params: FileTransferParams =
|
||||
FileTransferParams::new(FileTransferProtocol::Scp, ProtocolParams::default())
|
||||
.entry_directory(Some(&Path::new("/tmp")));
|
||||
assert_eq!(
|
||||
params.params.generic_params().unwrap().address.as_str(),
|
||||
"localhost"
|
||||
);
|
||||
assert_eq!(params.protocol, FileTransferProtocol::Scp);
|
||||
assert_eq!(params.username.as_ref().unwrap(), "omar");
|
||||
assert_eq!(params.password.as_ref().unwrap(), "foobar");
|
||||
assert_eq!(
|
||||
params.entry_directory.as_deref().unwrap(),
|
||||
Path::new("/tmp")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_params_default() {
|
||||
let params: FileTransferParams = FileTransferParams::default();
|
||||
fn params_default() {
|
||||
let params: GenericProtocolParams = ProtocolParams::default()
|
||||
.generic_params()
|
||||
.unwrap()
|
||||
.to_owned();
|
||||
assert_eq!(params.address.as_str(), "localhost");
|
||||
assert_eq!(params.port, 22);
|
||||
assert_eq!(params.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(params.username.is_none());
|
||||
assert!(params.password.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn params_aws_s3() {
|
||||
let params: AwsS3Params = AwsS3Params::new("omar", "eu-west-1", Some("test"));
|
||||
assert_eq!(params.bucket_name.as_str(), "omar");
|
||||
assert_eq!(params.region.as_str(), "eu-west-1");
|
||||
assert_eq!(params.profile.as_deref().unwrap(), "test");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn references() {
|
||||
let mut params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test")));
|
||||
assert!(params.s3_params().is_some());
|
||||
assert!(params.generic_params().is_none());
|
||||
assert!(params.mut_generic_params().is_none());
|
||||
let mut params = ProtocolParams::default();
|
||||
assert!(params.s3_params().is_none());
|
||||
assert!(params.generic_params().is_some());
|
||||
assert!(params.mut_generic_params().is_some());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! ## Ftp_transfer
|
||||
//! ## FTP transfer
|
||||
//!
|
||||
//! `ftp_transfer` is the module which provides the implementation for the FTP/FTPS file transfer
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
|||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType, ProtocolParams};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
|
||||
use crate::utils::fmt::shadow_password;
|
||||
use crate::utils::path;
|
||||
|
@ -178,25 +178,24 @@ impl FileTransfer for FtpFileTransfer {
|
|||
///
|
||||
/// Connect to the remote server
|
||||
|
||||
fn connect(
|
||||
&mut self,
|
||||
address: String,
|
||||
port: u16,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError> {
|
||||
// Get stream
|
||||
info!("Connecting to {}:{}", address, port);
|
||||
let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
error!("Failed to connect: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError> {
|
||||
let params = match params.generic_params() {
|
||||
Some(params) => params,
|
||||
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
|
||||
};
|
||||
// Get stream
|
||||
info!("Connecting to {}:{}", params.address, params.port);
|
||||
let mut stream: FtpStream =
|
||||
match FtpStream::connect(format!("{}:{}", params.address, params.port)) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
error!("Failed to connect: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ConnectionError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
// If SSL, open secure session
|
||||
if self.ftps {
|
||||
info!("Setting up TLS stream...");
|
||||
|
@ -214,7 +213,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
));
|
||||
}
|
||||
};
|
||||
stream = match stream.into_secure(ctx, address.as_str()) {
|
||||
stream = match stream.into_secure(ctx, params.address.as_str()) {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
error!("Failed to setup TLS stream: {}", err);
|
||||
|
@ -226,12 +225,12 @@ impl FileTransfer for FtpFileTransfer {
|
|||
};
|
||||
}
|
||||
// Login (use anonymous if credentials are unspecified)
|
||||
let username: String = match username {
|
||||
Some(u) => u,
|
||||
let username: String = match ¶ms.username {
|
||||
Some(u) => u.to_string(),
|
||||
None => String::from("anonymous"),
|
||||
};
|
||||
let password: String = match password {
|
||||
Some(pwd) => pwd,
|
||||
let password: String = match ¶ms.password {
|
||||
Some(pwd) => pwd.to_string(),
|
||||
None => String::new(),
|
||||
};
|
||||
info!(
|
||||
|
@ -645,6 +644,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::filetransfer::params::GenericProtocolParams;
|
||||
use crate::utils::file::open_file;
|
||||
#[cfg(feature = "with-containers")]
|
||||
use crate::utils::test_helpers::write_file;
|
||||
|
@ -672,17 +672,15 @@ mod tests {
|
|||
// Sample file
|
||||
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
|
||||
// Connect
|
||||
#[cfg(not(feature = "github-actions"))]
|
||||
let hostname: String = String::from("127.0.0.1");
|
||||
#[cfg(feature = "github-actions")]
|
||||
let hostname: String = String::from("127.0.0.1");
|
||||
assert!(ftp
|
||||
.connect(
|
||||
hostname,
|
||||
10021,
|
||||
Some(String::from("test")),
|
||||
Some(String::from("test")),
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address(hostname)
|
||||
.port(10021)
|
||||
.username(Some("test"))
|
||||
.password(Some("test"))
|
||||
))
|
||||
.is_ok());
|
||||
assert_eq!(ftp.is_connected(), true);
|
||||
// Get pwd
|
||||
|
@ -810,12 +808,13 @@ mod tests {
|
|||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Connect
|
||||
assert!(ftp
|
||||
.connect(
|
||||
String::from("127.0.0.1"),
|
||||
10021,
|
||||
Some(String::from("omar")),
|
||||
Some(String::from("ommlar")),
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10021)
|
||||
.username(Some("omar"))
|
||||
.password(Some("ommlar"))
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
|
@ -824,7 +823,13 @@ mod tests {
|
|||
fn test_filetransfer_ftp_no_credentials() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
assert!(ftp
|
||||
.connect(String::from("127.0.0.1"), 10021, None, None)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10021)
|
||||
.username::<&str>(None)
|
||||
.password::<&str>(None)
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
|
@ -833,12 +838,13 @@ mod tests {
|
|||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Connect
|
||||
assert!(ftp
|
||||
.connect(
|
||||
String::from("mybadserver.veryverybad.awful"),
|
||||
21,
|
||||
Some(String::from("omar")),
|
||||
Some(String::from("ommlar")),
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("mybad.veribad.server")
|
||||
.port(21)
|
||||
.username::<&str>(None)
|
||||
.password::<&str>(None)
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
|
@ -890,12 +896,13 @@ mod tests {
|
|||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Connect
|
||||
assert!(ftp
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
21,
|
||||
Some(String::from("demo")),
|
||||
Some(String::from("password"))
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("test.rebex.net")
|
||||
.port(21)
|
||||
.username(Some("demo"))
|
||||
.password(Some("password"))
|
||||
))
|
||||
.is_ok());
|
||||
// Pwd
|
||||
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
|
|
@ -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;
|
|
@ -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(¶ms).is_ok());
|
||||
// Check is connected
|
||||
assert_eq!(s3.is_connected(), true);
|
||||
// Pwd
|
||||
assert_eq!(s3.pwd().ok().unwrap(), PathBuf::from("/"));
|
||||
// Go to github-ci directory
|
||||
assert!(s3.change_dir(&Path::new("/github-ci")).is_ok());
|
||||
assert_eq!(s3.pwd().ok().unwrap(), PathBuf::from("/github-ci"));
|
||||
// Find
|
||||
assert_eq!(s3.find("*.jpg").ok().unwrap().len(), 1);
|
||||
// List directory (3 entries)
|
||||
assert_eq!(s3.list_dir(&Path::new("/github-ci")).ok().unwrap().len(), 3);
|
||||
// Go to playground
|
||||
assert!(s3.change_dir(&Path::new("/github-ci/playground")).is_ok());
|
||||
assert_eq!(
|
||||
s3.pwd().ok().unwrap(),
|
||||
PathBuf::from("/github-ci/playground")
|
||||
);
|
||||
// Create directory
|
||||
let dir_name: String = format!("{}/", random::random_alphanumeric_with_len(8));
|
||||
let mut dir_path: PathBuf = PathBuf::from("/github-ci/playground");
|
||||
dir_path.push(dir_name.as_str());
|
||||
let dir_entry = test_helpers::make_fsentry(dir_path.as_path(), true);
|
||||
assert!(s3.mkdir(dir_path.as_path()).is_ok());
|
||||
assert!(s3.change_dir(dir_path.as_path()).is_ok());
|
||||
// Copy/rename file is unsupported
|
||||
assert!(s3.copy(&dir_entry, &Path::new("/copia")).is_err());
|
||||
assert!(s3.rename(&dir_entry, &Path::new("/copia")).is_err());
|
||||
// Exec is unsupported
|
||||
assert!(s3.exec("omar!").is_err());
|
||||
// Stat file
|
||||
let entry = s3
|
||||
.stat(&Path::new("/github-ci/avril_lavigne.jpg"))
|
||||
.ok()
|
||||
.unwrap()
|
||||
.unwrap_file();
|
||||
assert_eq!(entry.name.as_str(), "avril_lavigne.jpg");
|
||||
assert_eq!(
|
||||
entry.abs_path.as_path(),
|
||||
Path::new("/github-ci/avril_lavigne.jpg")
|
||||
);
|
||||
assert_eq!(entry.ftype.as_deref().unwrap(), "jpg");
|
||||
assert_eq!(entry.size, 101738);
|
||||
assert_eq!(entry.user, None);
|
||||
assert_eq!(entry.group, None);
|
||||
assert_eq!(entry.unix_pex, None);
|
||||
// Download file
|
||||
let (local_file_entry, local_file): (FsFile, NamedTempFile) =
|
||||
test_helpers::create_sample_file_entry();
|
||||
let remote_entry =
|
||||
test_helpers::make_fsentry(&Path::new("/github-ci/avril_lavigne.jpg"), false)
|
||||
.unwrap_file();
|
||||
assert!(s3
|
||||
.recv_file_wno_stream(&remote_entry, local_file.path())
|
||||
.is_ok());
|
||||
// Upload file
|
||||
let mut dest_path = dir_path.clone();
|
||||
dest_path.push("aurellia_lavagna.jpg");
|
||||
let reader = Box::new(File::open(local_file.path()).ok().unwrap());
|
||||
assert!(s3
|
||||
.send_file_wno_stream(&local_file_entry, dest_path.as_path(), reader)
|
||||
.is_ok());
|
||||
// Remove temp dir
|
||||
assert!(s3.remove(&dir_entry).is_ok());
|
||||
// Disconnect
|
||||
assert!(s3.disconnect().is_ok());
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-s3-ci")]
|
||||
fn get_ftparams(bucket: String, region: String) -> ProtocolParams {
|
||||
ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, None))
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
//! ## SCP_Transfer
|
||||
//! ## SCP transfer
|
||||
//!
|
||||
//! `scps_transfer` is the module which provides the implementation for the SCP file transfer
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
|||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType, ProtocolParams};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||
|
@ -333,17 +333,15 @@ impl FileTransfer for ScpFileTransfer {
|
|||
/// ### connect
|
||||
///
|
||||
/// Connect to the remote server
|
||||
fn connect(
|
||||
&mut self,
|
||||
address: String,
|
||||
port: u16,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError> {
|
||||
fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError> {
|
||||
let params = match params.generic_params() {
|
||||
Some(params) => params,
|
||||
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
|
||||
};
|
||||
// Setup tcp stream
|
||||
info!("Connecting to {}:{}", address, port);
|
||||
info!("Connecting to {}:{}", params.address, params.port);
|
||||
let socket_addresses: Vec<SocketAddr> =
|
||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||
match format!("{}:{}", params.address, params.port).to_socket_addrs() {
|
||||
Ok(s) => s.collect(),
|
||||
Err(err) => {
|
||||
return Err(FileTransferError::new_ex(
|
||||
|
@ -398,14 +396,14 @@ impl FileTransfer for ScpFileTransfer {
|
|||
err.to_string(),
|
||||
));
|
||||
}
|
||||
let username: String = match username {
|
||||
Some(u) => u,
|
||||
let username: String = match ¶ms.username {
|
||||
Some(u) => u.to_string(),
|
||||
None => String::from(""),
|
||||
};
|
||||
// Check if it is possible to authenticate using a RSA key
|
||||
match self
|
||||
.key_storage
|
||||
.resolve(address.as_str(), username.as_str())
|
||||
.resolve(params.address.as_str(), username.as_str())
|
||||
{
|
||||
Some(rsa_key) => {
|
||||
debug!(
|
||||
|
@ -418,7 +416,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
username.as_str(),
|
||||
None,
|
||||
rsa_key.as_path(),
|
||||
password.as_deref(),
|
||||
params.password.as_deref(),
|
||||
) {
|
||||
error!("Authentication failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
|
@ -432,11 +430,16 @@ impl FileTransfer for ScpFileTransfer {
|
|||
debug!(
|
||||
"Authenticating with username {} and password {}",
|
||||
username,
|
||||
shadow_password(password.as_deref().unwrap_or(""))
|
||||
shadow_password(params.password.as_deref().unwrap_or(""))
|
||||
);
|
||||
if let Err(err) = session.userauth_password(
|
||||
username.as_str(),
|
||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||
params
|
||||
.password
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| String::from(""))
|
||||
.as_str(),
|
||||
) {
|
||||
error!("Authentication failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
|
@ -942,36 +945,13 @@ impl FileTransfer for ScpFileTransfer {
|
|||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_sent
|
||||
///
|
||||
/// Finalize send method.
|
||||
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
|
||||
/// The purpose of this method is to finalize the connection with the peer when writing data.
|
||||
/// This is necessary for some protocols such as FTP.
|
||||
/// You must call this method each time you want to finalize the write of the remote file.
|
||||
fn on_sent(&mut self, _writable: Box<dyn Write>) -> Result<(), FileTransferError> {
|
||||
// Nothing to do
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### on_recv
|
||||
///
|
||||
/// Finalize recv method.
|
||||
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
|
||||
/// The purpose of this method is to finalize the connection with the peer when reading data.
|
||||
/// This mighe be necessary for some protocols.
|
||||
/// You must call this method each time you want to finalize the read of the remote file.
|
||||
fn on_recv(&mut self, _readable: Box<dyn Read>) -> Result<(), FileTransferError> {
|
||||
// Nothing to do
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::filetransfer::params::GenericProtocolParams;
|
||||
use crate::utils::test_helpers::make_fsentry;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
|
@ -993,12 +973,13 @@ mod tests {
|
|||
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
|
||||
// Connect
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("127.0.0.1"),
|
||||
10222,
|
||||
Some(String::from("sftp")),
|
||||
Some(String::from("password"))
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10222)
|
||||
.username(Some("sftp"))
|
||||
.password(Some("password"))
|
||||
))
|
||||
.is_ok());
|
||||
// Check session and sftp
|
||||
assert!(client.session.is_some());
|
||||
|
@ -1180,12 +1161,13 @@ mod tests {
|
|||
let mut client: ScpFileTransfer = ScpFileTransfer::new(storage);
|
||||
// Connect
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("127.0.0.1"),
|
||||
10222,
|
||||
Some(String::from("sftp")),
|
||||
None,
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10222)
|
||||
.username(Some("sftp"))
|
||||
.password::<&str>(None)
|
||||
))
|
||||
.is_ok());
|
||||
assert_eq!(client.is_connected(), true);
|
||||
assert!(client.disconnect().is_ok());
|
||||
|
@ -1195,12 +1177,13 @@ mod tests {
|
|||
fn test_filetransfer_scp_bad_auth() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("127.0.0.1"),
|
||||
10222,
|
||||
Some(String::from("demo")),
|
||||
Some(String::from("badpassword"))
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10222)
|
||||
.username(Some("sftp"))
|
||||
.password(Some("badpassword"))
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
|
@ -1209,7 +1192,13 @@ mod tests {
|
|||
fn test_filetransfer_scp_no_credentials() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(String::from("127.0.0.1"), 10222, None, None)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10222)
|
||||
.username::<&str>(None)
|
||||
.password::<&str>(None)
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
|
@ -1217,12 +1206,13 @@ mod tests {
|
|||
fn test_filetransfer_scp_bad_server() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("mybadserver.veryverybad.awful"),
|
||||
22,
|
||||
None,
|
||||
None
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("mybad.verybad.server")
|
||||
.port(10222)
|
||||
.username(Some("sftp"))
|
||||
.password(Some("password"))
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
//! ## SFTP_Transfer
|
||||
//! ## SFTP transfer
|
||||
//!
|
||||
//! `sftp_transfer` is the module which provides the implementation for the SFTP file transfer
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
|||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType, ProtocolParams};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::fmt::{fmt_time, shadow_password};
|
||||
|
@ -257,17 +257,15 @@ impl FileTransfer for SftpFileTransfer {
|
|||
/// ### connect
|
||||
///
|
||||
/// Connect to the remote server
|
||||
fn connect(
|
||||
&mut self,
|
||||
address: String,
|
||||
port: u16,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError> {
|
||||
fn connect(&mut self, params: &ProtocolParams) -> Result<Option<String>, FileTransferError> {
|
||||
let params = match params.generic_params() {
|
||||
Some(params) => params,
|
||||
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
|
||||
};
|
||||
// Setup tcp stream
|
||||
info!("Connecting to {}:{}", address, port);
|
||||
info!("Connecting to {}:{}", params.address, params.port);
|
||||
let socket_addresses: Vec<SocketAddr> =
|
||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||
match format!("{}:{}", params.address, params.port).to_socket_addrs() {
|
||||
Ok(s) => s.collect(),
|
||||
Err(err) => {
|
||||
return Err(FileTransferError::new_ex(
|
||||
|
@ -321,14 +319,14 @@ impl FileTransfer for SftpFileTransfer {
|
|||
err.to_string(),
|
||||
));
|
||||
}
|
||||
let username: String = match username {
|
||||
Some(u) => u,
|
||||
let username: String = match ¶ms.username {
|
||||
Some(u) => u.to_string(),
|
||||
None => String::from(""),
|
||||
};
|
||||
// Check if it is possible to authenticate using a RSA key
|
||||
match self
|
||||
.key_storage
|
||||
.resolve(address.as_str(), username.as_str())
|
||||
.resolve(params.address.as_str(), username.as_str())
|
||||
{
|
||||
Some(rsa_key) => {
|
||||
debug!(
|
||||
|
@ -341,7 +339,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
username.as_str(),
|
||||
None,
|
||||
rsa_key.as_path(),
|
||||
password.as_deref(),
|
||||
params.password.as_deref(),
|
||||
) {
|
||||
error!("Authentication failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
|
@ -355,11 +353,16 @@ impl FileTransfer for SftpFileTransfer {
|
|||
debug!(
|
||||
"Authenticating with username {} and password {}",
|
||||
username,
|
||||
shadow_password(password.as_deref().unwrap_or(""))
|
||||
shadow_password(params.password.as_deref().unwrap_or(""))
|
||||
);
|
||||
if let Err(err) = session.userauth_password(
|
||||
username.as_str(),
|
||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||
params
|
||||
.password
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| String::from(""))
|
||||
.as_str(),
|
||||
) {
|
||||
error!("Authentication failed: {}", err);
|
||||
return Err(FileTransferError::new_ex(
|
||||
|
@ -766,36 +769,21 @@ impl FileTransfer for SftpFileTransfer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_sent
|
||||
///
|
||||
/// Finalize send method. This method must be implemented only if necessary.
|
||||
/// The purpose of this method is to finalize the connection with the peer when writing data.
|
||||
/// This is necessary for some protocols such as FTP.
|
||||
/// You must call this method each time you want to finalize the write of the remote file.
|
||||
fn on_sent(&mut self, _writable: Box<dyn Write>) -> Result<(), FileTransferError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### on_recv
|
||||
///
|
||||
/// Finalize recv method. This method must be implemented only if necessary.
|
||||
/// The purpose of this method is to finalize the connection with the peer when reading data.
|
||||
/// This mighe be necessary for some protocols.
|
||||
/// You must call this method each time you want to finalize the read of the remote file.
|
||||
fn on_recv(&mut self, _readable: Box<dyn Read>) -> Result<(), FileTransferError> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::filetransfer::params::GenericProtocolParams;
|
||||
use crate::utils::test_helpers::make_fsentry;
|
||||
#[cfg(feature = "with-containers")]
|
||||
use crate::utils::test_helpers::{create_sample_file_entry, write_file, write_ssh_key};
|
||||
use crate::utils::test_helpers::{
|
||||
create_sample_file, create_sample_file_entry, write_file, write_ssh_key,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(feature = "with-containers")]
|
||||
use std::fs::File;
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_new() {
|
||||
|
@ -814,12 +802,13 @@ mod tests {
|
|||
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
|
||||
// Connect
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("127.0.0.1"),
|
||||
10022,
|
||||
Some(String::from("sftp")),
|
||||
Some(String::from("password"))
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10022)
|
||||
.username(Some("sftp"))
|
||||
.password(Some("password"))
|
||||
))
|
||||
.is_ok());
|
||||
// Check session and sftp
|
||||
assert!(client.session.is_some());
|
||||
|
@ -889,6 +878,11 @@ mod tests {
|
|||
.unwrap();
|
||||
write_file(&file, &mut writable);
|
||||
assert!(client.on_sent(writable).is_ok());
|
||||
// Upload file without stream
|
||||
let reader = Box::new(File::open(entry.abs_path.as_path()).ok().unwrap());
|
||||
assert!(client
|
||||
.send_file_wno_stream(&entry, PathBuf::from("README2.md").as_path(), reader)
|
||||
.is_ok());
|
||||
// Upload file (err)
|
||||
assert!(client
|
||||
.send_file(&entry, PathBuf::from("/ommlar/omarone").as_path())
|
||||
|
@ -898,10 +892,10 @@ mod tests {
|
|||
.list_dir(PathBuf::from("/tmp/omar").as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(list.len(), 2);
|
||||
assert_eq!(list.len(), 3);
|
||||
// Find
|
||||
assert_eq!(client.find("*.txt").ok().unwrap().len(), 1);
|
||||
assert_eq!(client.find("*.md").ok().unwrap().len(), 1);
|
||||
assert_eq!(client.find("*.md").ok().unwrap().len(), 2);
|
||||
assert_eq!(client.find("*.jpeg").ok().unwrap().len(), 0);
|
||||
// Rename
|
||||
assert!(client
|
||||
|
@ -955,6 +949,9 @@ mod tests {
|
|||
let mut data: Vec<u8> = vec![0; 1024];
|
||||
assert!(readable.read(&mut data).is_ok());
|
||||
assert!(client.on_recv(readable).is_ok());
|
||||
let dest_file = create_sample_file();
|
||||
// Receive file wno stream
|
||||
assert!(client.recv_file_wno_stream(&file, dest_file.path()).is_ok());
|
||||
// Receive file (err)
|
||||
assert!(client.recv_file(&entry).is_err());
|
||||
// Cleanup
|
||||
|
@ -979,12 +976,13 @@ mod tests {
|
|||
let mut client: SftpFileTransfer = SftpFileTransfer::new(storage);
|
||||
// Connect
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("127.0.0.1"),
|
||||
10022,
|
||||
Some(String::from("sftp")),
|
||||
None,
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10022)
|
||||
.username(Some("sftp"))
|
||||
.password::<&str>(None)
|
||||
))
|
||||
.is_ok());
|
||||
assert_eq!(client.is_connected(), true);
|
||||
assert!(client.disconnect().is_ok());
|
||||
|
@ -994,12 +992,13 @@ mod tests {
|
|||
fn test_filetransfer_sftp_bad_auth() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("127.0.0.1"),
|
||||
10022,
|
||||
Some(String::from("demo")),
|
||||
Some(String::from("badpassword"))
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10022)
|
||||
.username(Some("sftp"))
|
||||
.password(Some("badpassword"))
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
|
@ -1008,7 +1007,13 @@ mod tests {
|
|||
fn test_filetransfer_sftp_no_credentials() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(String::from("127.0.0.1"), 10022, None, None)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10022)
|
||||
.username::<&str>(None)
|
||||
.password::<&str>(None)
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
|
@ -1018,12 +1023,13 @@ mod tests {
|
|||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
// Connect
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("127.0.0.1"),
|
||||
10022,
|
||||
Some(String::from("sftp")),
|
||||
Some(String::from("password"))
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("127.0.0.1")
|
||||
.port(10022)
|
||||
.username(Some("sftp"))
|
||||
.password(Some("password"))
|
||||
))
|
||||
.is_ok());
|
||||
// get realpath
|
||||
assert!(client
|
||||
|
@ -1054,12 +1060,13 @@ mod tests {
|
|||
fn test_filetransfer_sftp_bad_server() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("mybadserver.veryverybad.awful"),
|
||||
22,
|
||||
None,
|
||||
None
|
||||
)
|
||||
.connect(&ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address("myverybad.verybad.server")
|
||||
.port(10022)
|
||||
.username(Some("sftp"))
|
||||
.password(Some("password"))
|
||||
))
|
||||
.is_err());
|
||||
}
|
||||
|
|
@ -38,7 +38,9 @@ use std::fs::set_permissions;
|
|||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
|
||||
// Locals
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
|
||||
#[cfg(target_family = "unix")]
|
||||
use crate::fs::UnixPex;
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::utils::path;
|
||||
|
||||
/// ## HostErrorType
|
||||
|
|
|
@ -52,6 +52,7 @@ extern crate open;
|
|||
extern crate path_slash;
|
||||
extern crate rand;
|
||||
extern crate regex;
|
||||
extern crate s3;
|
||||
extern crate ssh2;
|
||||
extern crate suppaftp;
|
||||
extern crate tempfile;
|
||||
|
|
48
src/main.rs
48
src/main.rs
|
@ -66,7 +66,12 @@ enum Task {
|
|||
|
||||
#[derive(FromArgs)]
|
||||
#[argh(description = "
|
||||
where positional can be: [protocol://user@address:port:wrkdir] [local-wrkdir]
|
||||
where positional can be: [address] [local-wrkdir]
|
||||
|
||||
Address syntax can be:
|
||||
|
||||
- `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp
|
||||
- `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol
|
||||
|
||||
Please, report issues to <https://github.com/veeso/termscp>
|
||||
Please, consider supporting the author <https://www.buymeacoffee.com/veeso>")]
|
||||
|
@ -180,7 +185,9 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
|||
Ok(mut remote) => {
|
||||
// If password is provided, set password
|
||||
if let Some(passwd) = args.password {
|
||||
remote = remote.password(Some(passwd));
|
||||
if let Some(mut params) = remote.params.mut_generic_params() {
|
||||
params.password = Some(passwd);
|
||||
}
|
||||
}
|
||||
// Set params
|
||||
run_opts.remote = Some(remote);
|
||||
|
@ -209,25 +216,26 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
|
|||
fn read_password(run_opts: &mut RunOpts) -> Result<(), String> {
|
||||
// Initialize client if necessary
|
||||
if let Some(remote) = run_opts.remote.as_mut() {
|
||||
debug!("User has specified remote options: address: {:?}, port: {:?}, protocol: {:?}, user: {:?}, password: {}", remote.address, remote.port, remote.protocol, remote.username, utils::fmt::shadow_password(remote.password.as_deref().unwrap_or("")));
|
||||
if remote.password.is_none() {
|
||||
// Ask password if unspecified
|
||||
remote.password = match rpassword::read_password_from_tty(Some("Password: ")) {
|
||||
Ok(p) => {
|
||||
if p.is_empty() {
|
||||
None
|
||||
} else {
|
||||
debug!(
|
||||
"Read password from tty: {}",
|
||||
utils::fmt::shadow_password(p.as_str())
|
||||
);
|
||||
Some(p)
|
||||
if let Some(mut params) = remote.params.mut_generic_params() {
|
||||
if params.password.is_none() {
|
||||
// Ask password if unspecified
|
||||
params.password = match rpassword::read_password_from_tty(Some("Password: ")) {
|
||||
Ok(p) => {
|
||||
if p.is_empty() {
|
||||
None
|
||||
} else {
|
||||
debug!(
|
||||
"Read password from tty: {}",
|
||||
utils::fmt::shadow_password(p.as_str())
|
||||
);
|
||||
Some(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
return Err("Could not read password from prompt".to_string());
|
||||
}
|
||||
};
|
||||
Err(_) => {
|
||||
return Err("Could not read password from prompt".to_string());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
|
|
@ -34,14 +34,13 @@ use crate::config::{
|
|||
bookmarks::{Bookmark, UserHosts},
|
||||
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
||||
};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::utils::crypto;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
use crate::utils::random::random_alphanumeric_with_len;
|
||||
// Ext
|
||||
use std::fs::OpenOptions;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
use std::time::SystemTime;
|
||||
|
||||
|
@ -166,59 +165,45 @@ impl BookmarksClient {
|
|||
/// ### get_bookmark
|
||||
///
|
||||
/// Get bookmark associated to key
|
||||
pub fn get_bookmark(
|
||||
&self,
|
||||
key: &str,
|
||||
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
|
||||
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
|
||||
pub fn get_bookmark(&self, key: &str) -> Option<FileTransferParams> {
|
||||
debug!("Getting bookmark {}", key);
|
||||
Some((
|
||||
entry.address.clone(),
|
||||
entry.port,
|
||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||
Ok(proto) => proto,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
|
||||
err
|
||||
);
|
||||
FileTransferProtocol::Sftp // Default
|
||||
let mut entry: Bookmark = self.hosts.bookmarks.get(key).cloned()?;
|
||||
// Decrypt password first
|
||||
if let Some(pwd) = entry.password.as_mut() {
|
||||
match self.decrypt_str(pwd.as_str()) {
|
||||
Ok(decrypted_pwd) => {
|
||||
*pwd = decrypted_pwd;
|
||||
}
|
||||
},
|
||||
entry.username.clone(),
|
||||
match &entry.password {
|
||||
// Decrypted password if Some; if decryption fails return None
|
||||
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
|
||||
Ok(decrypted_pwd) => Some(decrypted_pwd),
|
||||
Err(err) => {
|
||||
error!("Failed to decrypt password for bookmark: {}", err);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
},
|
||||
))
|
||||
Err(err) => {
|
||||
error!("Failed to decrypt password for bookmark: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Then convert into
|
||||
Some(FileTransferParams::from(entry))
|
||||
}
|
||||
|
||||
/// ### add_recent
|
||||
///
|
||||
/// Add a new recent to bookmarks
|
||||
pub fn add_bookmark(
|
||||
pub fn add_bookmark<S: AsRef<str>>(
|
||||
&mut self,
|
||||
name: String,
|
||||
addr: String,
|
||||
port: u16,
|
||||
protocol: FileTransferProtocol,
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
name: S,
|
||||
params: FileTransferParams,
|
||||
save_password: bool,
|
||||
) {
|
||||
let name: String = name.as_ref().to_string();
|
||||
if name.is_empty() {
|
||||
error!("Fatal error; bookmark name is empty");
|
||||
panic!("Bookmark name can't be empty");
|
||||
}
|
||||
// Make bookmark
|
||||
info!("Added bookmark {} with address {}", name, addr);
|
||||
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
|
||||
info!("Added bookmark {}", name);
|
||||
let mut host: Bookmark = self.make_bookmark(params);
|
||||
// If not save_password, set password to `None`
|
||||
if !save_password {
|
||||
host.password = None;
|
||||
}
|
||||
self.hosts.bookmarks.insert(name, host);
|
||||
}
|
||||
|
||||
|
@ -239,43 +224,25 @@ impl BookmarksClient {
|
|||
/// ### get_recent
|
||||
///
|
||||
/// Get recent associated to key
|
||||
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
|
||||
pub fn get_recent(&self, key: &str) -> Option<FileTransferParams> {
|
||||
// NOTE: password is not decrypted; recents will never have password
|
||||
info!("Getting bookmark {}", key);
|
||||
let entry: &Bookmark = self.hosts.recents.get(key)?;
|
||||
Some((
|
||||
entry.address.clone(),
|
||||
entry.port,
|
||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||
Ok(proto) => proto,
|
||||
Err(err) => {
|
||||
error!(
|
||||
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
|
||||
err
|
||||
);
|
||||
FileTransferProtocol::Sftp // Default
|
||||
}
|
||||
},
|
||||
entry.username.clone(),
|
||||
))
|
||||
let entry: Bookmark = self.hosts.recents.get(key).cloned()?;
|
||||
Some(FileTransferParams::from(entry))
|
||||
}
|
||||
|
||||
/// ### add_recent
|
||||
///
|
||||
/// Add a new recent to bookmarks
|
||||
pub fn add_recent(
|
||||
&mut self,
|
||||
addr: String,
|
||||
port: u16,
|
||||
protocol: FileTransferProtocol,
|
||||
username: String,
|
||||
) {
|
||||
pub fn add_recent(&mut self, params: FileTransferParams) {
|
||||
// Make bookmark
|
||||
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, None);
|
||||
let mut host: Bookmark = self.make_bookmark(params);
|
||||
// Null password for recents
|
||||
host.password = None;
|
||||
// Check if duplicated
|
||||
for recent_host in self.hosts.recents.values() {
|
||||
if *recent_host == host {
|
||||
debug!("Discarding recent since duplicated ({})", host.address);
|
||||
for (key, value) in &self.hosts.recents {
|
||||
if *value == host {
|
||||
debug!("Discarding recent since duplicated ({})", key);
|
||||
// Don't save duplicates
|
||||
return;
|
||||
}
|
||||
|
@ -300,7 +267,7 @@ impl BookmarksClient {
|
|||
}
|
||||
}
|
||||
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
||||
info!("Saved recent host {} ({})", name, host.address);
|
||||
info!("Saved recent host {}", name);
|
||||
self.hosts.recents.insert(name, host);
|
||||
}
|
||||
|
||||
|
@ -376,21 +343,13 @@ impl BookmarksClient {
|
|||
/// ### make_bookmark
|
||||
///
|
||||
/// Make bookmark from credentials
|
||||
fn make_bookmark(
|
||||
&self,
|
||||
addr: String,
|
||||
port: u16,
|
||||
protocol: FileTransferProtocol,
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
) -> Bookmark {
|
||||
Bookmark {
|
||||
address: addr,
|
||||
port,
|
||||
username,
|
||||
protocol: protocol.to_string(),
|
||||
password: password.map(|p| self.encrypt_str(p.as_str())),
|
||||
fn make_bookmark(&self, params: FileTransferParams) -> Bookmark {
|
||||
let mut bookmark: Bookmark = Bookmark::from(params);
|
||||
// Encrypt password
|
||||
if let Some(pwd) = bookmark.password {
|
||||
bookmark.password = Some(self.encrypt_str(pwd.as_str()));
|
||||
}
|
||||
bookmark
|
||||
}
|
||||
|
||||
/// ### encrypt_str
|
||||
|
@ -419,6 +378,8 @@ impl BookmarksClient {
|
|||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::filetransfer::params::GenericProtocolParams;
|
||||
use crate::filetransfer::{FileTransferProtocol, ProtocolParams};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::thread::sleep;
|
||||
|
@ -473,19 +434,23 @@ mod tests {
|
|||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add some bookmarks
|
||||
client.add_bookmark(
|
||||
String::from("raspberry"),
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
Some(String::from("mypassword")),
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
);
|
||||
client.add_recent(
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
let recent_key: String = String::from(client.iter_recents().next().unwrap());
|
||||
assert!(client.write_bookmarks().is_ok());
|
||||
let key: String = client.key.clone();
|
||||
|
@ -494,19 +459,18 @@ mod tests {
|
|||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Verify it loaded parameters correctly
|
||||
assert_eq!(client.key, key);
|
||||
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) =
|
||||
client.get_bookmark(&String::from("raspberry")).unwrap();
|
||||
let bookmark = ftparams_to_tup(client.get_bookmark("raspberry").unwrap());
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
assert_eq!(bookmark.1, 22);
|
||||
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(bookmark.3, String::from("pi"));
|
||||
assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword"));
|
||||
let bookmark: (String, u16, FileTransferProtocol, String) =
|
||||
client.get_recent(&recent_key).unwrap();
|
||||
let bookmark = ftparams_to_tup(client.get_recent(&recent_key).unwrap());
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
assert_eq!(bookmark.1, 22);
|
||||
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(bookmark.3, String::from("pi"));
|
||||
assert_eq!(bookmark.4, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -519,26 +483,31 @@ mod tests {
|
|||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
String::from("raspberry"),
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
Some(String::from("mypassword")),
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
);
|
||||
client.add_bookmark(
|
||||
String::from("raspberry2"),
|
||||
String::from("192.168.1.32"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
Some(String::from("mypassword2")),
|
||||
"raspberry2",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword2"),
|
||||
),
|
||||
true,
|
||||
);
|
||||
// Iter
|
||||
assert_eq!(client.iter_bookmarks().count(), 2);
|
||||
// Get bookmark
|
||||
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) =
|
||||
client.get_bookmark(&String::from("raspberry")).unwrap();
|
||||
let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
assert_eq!(bookmark.1, 22);
|
||||
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||
|
@ -565,15 +534,45 @@ mod tests {
|
|||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
String::from(""),
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
Some(String::from("mypassword")),
|
||||
"",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_bookmark_wno_password() {
|
||||
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
"raspberry",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
false,
|
||||
);
|
||||
let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
assert_eq!(bookmark.1, 22);
|
||||
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(bookmark.3, String::from("pi"));
|
||||
assert_eq!(bookmark.4, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_manipulate_recents() {
|
||||
|
@ -583,22 +582,23 @@ mod tests {
|
|||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_recent(
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
// Iter
|
||||
assert_eq!(client.iter_recents().count(), 1);
|
||||
let key: String = String::from(client.iter_recents().next().unwrap());
|
||||
// Get bookmark
|
||||
let bookmark: (String, u16, FileTransferProtocol, String) =
|
||||
client.get_recent(&key).unwrap();
|
||||
let bookmark = ftparams_to_tup(client.get_recent(&key).unwrap());
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
assert_eq!(bookmark.1, 22);
|
||||
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(bookmark.3, String::from("pi"));
|
||||
assert_eq!(bookmark.4, None);
|
||||
// Write bookmarks
|
||||
assert!(client.write_bookmarks().is_ok());
|
||||
// Delete bookmark
|
||||
|
@ -618,18 +618,20 @@ mod tests {
|
|||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_recent(
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
client.add_recent(
|
||||
String::from("192.168.1.31"),
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
// There should be only one recent
|
||||
assert_eq!(client.iter_recents().count(), 1);
|
||||
}
|
||||
|
@ -644,39 +646,60 @@ mod tests {
|
|||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2).unwrap();
|
||||
// Add recent, wait 1 second for each one (cause the name depends on time)
|
||||
// 1
|
||||
client.add_recent(
|
||||
String::from("192.168.1.1"),
|
||||
22,
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
"192.168.1.1",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
sleep(Duration::from_secs(1));
|
||||
// 2
|
||||
client.add_recent(
|
||||
String::from("192.168.1.2"),
|
||||
22,
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
"192.168.1.2",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
sleep(Duration::from_secs(1));
|
||||
// 3
|
||||
client.add_recent(
|
||||
String::from("192.168.1.3"),
|
||||
22,
|
||||
client.add_recent(make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
"192.168.1.3",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
));
|
||||
// Limit is 2
|
||||
assert_eq!(client.iter_recents().count(), 2);
|
||||
// Check that 192.168.1.1 has been removed
|
||||
let key: String = client.iter_recents().nth(0).unwrap().to_string();
|
||||
assert!(matches!(
|
||||
client.hosts.recents.get(&key).unwrap().address.as_str(),
|
||||
client
|
||||
.hosts
|
||||
.recents
|
||||
.get(&key)
|
||||
.unwrap()
|
||||
.address
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.as_str(),
|
||||
"192.168.1.2" | "192.168.1.3"
|
||||
));
|
||||
let key: String = client.iter_recents().nth(1).unwrap().to_string();
|
||||
assert!(matches!(
|
||||
client.hosts.recents.get(&key).unwrap().address.as_str(),
|
||||
client
|
||||
.hosts
|
||||
.recents
|
||||
.get(&key)
|
||||
.unwrap()
|
||||
.address
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.as_str(),
|
||||
"192.168.1.2" | "192.168.1.3"
|
||||
));
|
||||
}
|
||||
|
@ -691,12 +714,15 @@ mod tests {
|
|||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
String::from(""),
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
Some(String::from("mypassword")),
|
||||
"",
|
||||
make_generic_ftparams(
|
||||
FileTransferProtocol::Sftp,
|
||||
"192.168.1.31",
|
||||
22,
|
||||
"pi",
|
||||
Some("mypassword"),
|
||||
),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -724,4 +750,35 @@ mod tests {
|
|||
c.push("bookmarks.toml");
|
||||
(c, k)
|
||||
}
|
||||
|
||||
fn make_generic_ftparams(
|
||||
protocol: FileTransferProtocol,
|
||||
address: &str,
|
||||
port: u16,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
) -> FileTransferParams {
|
||||
let params = ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address(address)
|
||||
.port(port)
|
||||
.username(Some(username))
|
||||
.password(password),
|
||||
);
|
||||
FileTransferParams::new(protocol, params)
|
||||
}
|
||||
|
||||
fn ftparams_to_tup(
|
||||
params: FileTransferParams,
|
||||
) -> (String, u16, FileTransferProtocol, String, Option<String>) {
|
||||
let protocol = params.protocol;
|
||||
let p = params.params.generic_params().unwrap();
|
||||
(
|
||||
p.address.to_string(),
|
||||
p.port,
|
||||
protocol,
|
||||
p.username.as_ref().cloned().unwrap_or_default(),
|
||||
p.password.as_ref().cloned(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,14 +26,15 @@
|
|||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{AuthActivity, FileTransferProtocol};
|
||||
use super::{AuthActivity, FileTransferParams};
|
||||
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::environment;
|
||||
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
use tui_realm_stdlib::{input::InputPropsBuilder, radio::RadioPropsBuilder};
|
||||
use tuirealm::{Payload, PropsBuilder, Value};
|
||||
use tuirealm::PropsBuilder;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### del_bookmark
|
||||
|
@ -62,9 +63,7 @@ impl AuthActivity {
|
|||
if let Some(key) = self.bookmarks_list.get(idx) {
|
||||
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
|
||||
// Load parameters into components
|
||||
self.load_bookmark_into_gui(
|
||||
bookmark.0, bookmark.1, bookmark.2, bookmark.3, bookmark.4,
|
||||
);
|
||||
self.load_bookmark_into_gui(bookmark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,20 +73,15 @@ impl AuthActivity {
|
|||
///
|
||||
/// Save current input fields as a bookmark
|
||||
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) {
|
||||
let (address, port, protocol, username, password) = self.get_input();
|
||||
let params = match self.collect_host_params() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
|
||||
// Check if password must be saved
|
||||
let password: Option<String> = match save_password {
|
||||
true => match self
|
||||
.view
|
||||
.get_state(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
|
||||
{
|
||||
Some(Payload::One(Value::Usize(0))) => Some(password), // Yes
|
||||
_ => None, // No such component / No
|
||||
},
|
||||
false => None,
|
||||
};
|
||||
bookmarks_cli.add_bookmark(name.clone(), address, port, protocol, username, password);
|
||||
bookmarks_cli.add_bookmark(name.clone(), params, save_password);
|
||||
// Save bookmarks
|
||||
self.write_bookmarks();
|
||||
// Remove `name` from bookmarks if exists
|
||||
|
@ -122,9 +116,7 @@ impl AuthActivity {
|
|||
if let Some(key) = self.recents_list.get(idx) {
|
||||
if let Some(bookmark) = client.get_recent(key) {
|
||||
// Load parameters
|
||||
self.load_bookmark_into_gui(
|
||||
bookmark.0, bookmark.1, bookmark.2, bookmark.3, None,
|
||||
);
|
||||
self.load_bookmark_into_gui(bookmark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -134,9 +126,15 @@ impl AuthActivity {
|
|||
///
|
||||
/// Save current input fields as a "recent"
|
||||
pub(super) fn save_recent(&mut self) {
|
||||
let (address, port, protocol, username, _password) = self.get_input();
|
||||
let params = match self.collect_host_params() {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
self.mount_error(e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
|
||||
bookmarks_cli.add_recent(address, port, protocol, username);
|
||||
bookmarks_cli.add_recent(params);
|
||||
// Save bookmarks
|
||||
self.write_bookmarks();
|
||||
}
|
||||
|
@ -234,40 +232,66 @@ impl AuthActivity {
|
|||
/// ### load_bookmark_into_gui
|
||||
///
|
||||
/// Load bookmark data into the gui components
|
||||
fn load_bookmark_into_gui(
|
||||
&mut self,
|
||||
addr: String,
|
||||
port: u16,
|
||||
protocol: FileTransferProtocol,
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
) {
|
||||
fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
|
||||
// Load parameters into components
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
|
||||
let props = RadioPropsBuilder::from(props)
|
||||
.with_value(Self::protocol_enum_to_opt(bookmark.protocol))
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
|
||||
}
|
||||
match bookmark.params {
|
||||
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params),
|
||||
ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
|
||||
let props = InputPropsBuilder::from(props).with_value(addr).build();
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(params.address.clone())
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_INPUT_ADDR, props);
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(port.to_string())
|
||||
.with_value(params.port.to_string())
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_INPUT_PORT, props);
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
|
||||
let props = RadioPropsBuilder::from(props)
|
||||
.with_value(Self::protocol_enum_to_opt(protocol))
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
|
||||
}
|
||||
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
|
||||
let props = InputPropsBuilder::from(props).with_value(username).build();
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(params.username.as_deref().unwrap_or_default().to_string())
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
|
||||
}
|
||||
if let Some(password) = password {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
|
||||
let props = InputPropsBuilder::from(props).with_value(password).build();
|
||||
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(params.password.as_deref().unwrap_or_default().to_string())
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
|
||||
}
|
||||
}
|
||||
|
||||
fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) {
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_BUCKET) {
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(params.bucket_name.clone())
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_INPUT_S3_BUCKET, props);
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_REGION) {
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(params.region.clone())
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_INPUT_S3_REGION, props);
|
||||
}
|
||||
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_PROFILE) {
|
||||
let props = InputPropsBuilder::from(props)
|
||||
.with_value(params.profile.as_deref().unwrap_or_default().to_string())
|
||||
.build();
|
||||
self.view.update(super::COMPONENT_INPUT_S3_PROFILE, props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
* SOFTWARE.
|
||||
*/
|
||||
use super::{AuthActivity, FileTransferParams, FileTransferProtocol};
|
||||
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### protocol_opt_to_enum
|
||||
|
@ -36,6 +37,7 @@ impl AuthActivity {
|
|||
1 => FileTransferProtocol::Scp,
|
||||
2 => FileTransferProtocol::Ftp(false),
|
||||
3 => FileTransferProtocol::Ftp(true),
|
||||
4 => FileTransferProtocol::AwsS3,
|
||||
_ => FileTransferProtocol::Sftp,
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +51,7 @@ impl AuthActivity {
|
|||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(false) => 2,
|
||||
FileTransferProtocol::Ftp(true) => 3,
|
||||
FileTransferProtocol::AwsS3 => 4,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,6 +62,7 @@ impl AuthActivity {
|
|||
match protocol {
|
||||
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
|
||||
FileTransferProtocol::Ftp(_) => 21,
|
||||
FileTransferProtocol::AwsS3 => 22, // Doesn't matter, since not used
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -83,15 +87,24 @@ impl AuthActivity {
|
|||
|
||||
/// ### collect_host_params
|
||||
///
|
||||
/// Get input values from fields or return an error if fields are invalid
|
||||
/// Collect host params as `FileTransferParams`
|
||||
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
|
||||
let (address, port, protocol, username, password): (
|
||||
String,
|
||||
u16,
|
||||
FileTransferProtocol,
|
||||
String,
|
||||
String,
|
||||
) = self.get_input();
|
||||
let protocol: FileTransferProtocol = self.get_protocol();
|
||||
match protocol {
|
||||
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(protocol),
|
||||
protocol => self.collect_generic_host_params(protocol),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### collect_generic_host_params
|
||||
///
|
||||
/// Get input values from fields or return an error if fields are invalid to work as generic
|
||||
pub(super) fn collect_generic_host_params(
|
||||
&self,
|
||||
protocol: FileTransferProtocol,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let (address, port, username, password): (String, u16, String, String) =
|
||||
self.get_generic_params_input();
|
||||
if address.is_empty() {
|
||||
return Err("Invalid host");
|
||||
}
|
||||
|
@ -99,17 +112,42 @@ impl AuthActivity {
|
|||
return Err("Invalid port");
|
||||
}
|
||||
Ok(FileTransferParams {
|
||||
address,
|
||||
port,
|
||||
protocol,
|
||||
username: match username.is_empty() {
|
||||
true => None,
|
||||
false => Some(username),
|
||||
},
|
||||
password: match password.is_empty() {
|
||||
true => None,
|
||||
false => Some(password),
|
||||
},
|
||||
params: ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address(address)
|
||||
.port(port)
|
||||
.username(match username.is_empty() {
|
||||
true => None,
|
||||
false => Some(username),
|
||||
})
|
||||
.password(match password.is_empty() {
|
||||
true => None,
|
||||
false => Some(password),
|
||||
}),
|
||||
),
|
||||
entry_directory: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// ### collect_s3_host_params
|
||||
///
|
||||
/// Get input values from fields or return an error if fields are invalid to work as aws s3
|
||||
pub(super) fn collect_s3_host_params(
|
||||
&self,
|
||||
protocol: FileTransferProtocol,
|
||||
) -> Result<FileTransferParams, &'static str> {
|
||||
let (bucket, region, profile): (String, String, Option<String>) =
|
||||
self.get_s3_params_input();
|
||||
if bucket.is_empty() {
|
||||
return Err("Invalid bucket");
|
||||
}
|
||||
if region.is_empty() {
|
||||
return Err("Invalid region");
|
||||
}
|
||||
Ok(FileTransferParams {
|
||||
protocol,
|
||||
params: ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)),
|
||||
entry_directory: None,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -57,6 +57,9 @@ const COMPONENT_INPUT_PORT: &str = "INPUT_PORT";
|
|||
const COMPONENT_INPUT_USERNAME: &str = "INPUT_USERNAME";
|
||||
const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD";
|
||||
const COMPONENT_INPUT_BOOKMARK_NAME: &str = "INPUT_BOOKMARK_NAME";
|
||||
const COMPONENT_INPUT_S3_BUCKET: &str = "INPUT_S3_BUCKET";
|
||||
const COMPONENT_INPUT_S3_REGION: &str = "INPUT_S3_REGION";
|
||||
const COMPONENT_INPUT_S3_PROFILE: &str = "INPUT_S3_PROFILE";
|
||||
const COMPONENT_RADIO_PROTOCOL: &str = "RADIO_PROTOCOL";
|
||||
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
|
||||
const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK";
|
||||
|
@ -163,6 +166,16 @@ impl AuthActivity {
|
|||
fn theme(&self) -> &Theme {
|
||||
self.context().theme_provider().theme()
|
||||
}
|
||||
|
||||
/// ### input_mask
|
||||
///
|
||||
/// Get current input mask to show
|
||||
fn input_mask(&self) -> InputMask {
|
||||
match self.get_protocol() {
|
||||
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
|
||||
_ => InputMask::Generic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity for AuthActivity {
|
||||
|
@ -261,3 +274,12 @@ impl Activity for AuthActivity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## InputMask
|
||||
///
|
||||
/// Auth form input mask
|
||||
#[derive(Eq, PartialEq)]
|
||||
enum InputMask {
|
||||
Generic,
|
||||
AwsS3,
|
||||
}
|
||||
|
|
|
@ -27,8 +27,9 @@
|
|||
*/
|
||||
// locals
|
||||
use super::{
|
||||
AuthActivity, FileTransferProtocol, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
|
||||
AuthActivity, FileTransferProtocol, InputMask, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
|
||||
COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT,
|
||||
COMPONENT_INPUT_S3_BUCKET, COMPONENT_INPUT_S3_PROFILE, COMPONENT_INPUT_S3_REGION,
|
||||
COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
|
||||
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
|
||||
COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR,
|
||||
|
@ -53,54 +54,80 @@ impl Update for AuthActivity {
|
|||
Some(msg) => match msg {
|
||||
// Focus ( DOWN )
|
||||
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => {
|
||||
// Give focus to port
|
||||
self.view.active(COMPONENT_INPUT_ADDR);
|
||||
// Give focus based on current mask
|
||||
match self.input_mask() {
|
||||
InputMask::Generic => self.view.active(COMPONENT_INPUT_ADDR),
|
||||
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_BUCKET),
|
||||
};
|
||||
None
|
||||
}
|
||||
// -- generic mask (DOWN)
|
||||
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => {
|
||||
// Give focus to port
|
||||
self.view.active(COMPONENT_INPUT_PORT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => {
|
||||
// Give focus to port
|
||||
self.view.active(COMPONENT_INPUT_USERNAME);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => {
|
||||
// Give focus to port
|
||||
self.view.active(COMPONENT_INPUT_PASSWORD);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => {
|
||||
// Give focus to port
|
||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||
None
|
||||
}
|
||||
// -- s3 mask (DOWN)
|
||||
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_INPUT_S3_REGION);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_INPUT_S3_PROFILE);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_DOWN => {
|
||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||
None
|
||||
}
|
||||
// Focus ( UP )
|
||||
// -- generic (UP)
|
||||
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => {
|
||||
// Give focus to port
|
||||
self.view.active(COMPONENT_INPUT_USERNAME);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => {
|
||||
// Give focus to port
|
||||
self.view.active(COMPONENT_INPUT_PORT);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => {
|
||||
// Give focus to port
|
||||
self.view.active(COMPONENT_INPUT_ADDR);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => {
|
||||
// Give focus to port
|
||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||
None
|
||||
}
|
||||
// -- s3 (UP)
|
||||
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_INPUT_S3_BUCKET);
|
||||
None
|
||||
}
|
||||
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_UP => {
|
||||
self.view.active(COMPONENT_INPUT_S3_REGION);
|
||||
None
|
||||
}
|
||||
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => {
|
||||
// Give focus to port
|
||||
self.view.active(COMPONENT_INPUT_PASSWORD);
|
||||
// Give focus based on current mask
|
||||
match self.input_mask() {
|
||||
InputMask::Generic => self.view.active(COMPONENT_INPUT_PASSWORD),
|
||||
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_PROFILE),
|
||||
};
|
||||
None
|
||||
}
|
||||
// Protocol - On Change
|
||||
|
@ -144,14 +171,20 @@ impl Update for AuthActivity {
|
|||
// Enter
|
||||
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
|
||||
self.load_bookmark(*idx);
|
||||
// Give focus to input password
|
||||
self.view.active(COMPONENT_INPUT_PASSWORD);
|
||||
// Give focus to input password (or to protocol if not generic)
|
||||
self.view.active(match self.input_mask() {
|
||||
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
|
||||
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
|
||||
});
|
||||
None
|
||||
}
|
||||
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
|
||||
self.load_recent(*idx);
|
||||
// Give focus to input password
|
||||
self.view.active(COMPONENT_INPUT_PASSWORD);
|
||||
self.view.active(match self.input_mask() {
|
||||
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
|
||||
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
|
||||
});
|
||||
None
|
||||
}
|
||||
// Bookmark radio
|
||||
|
@ -320,7 +353,7 @@ impl Update for AuthActivity {
|
|||
if key == &MSG_KEY_TAB =>
|
||||
{
|
||||
// Give focus to address
|
||||
self.view.active(COMPONENT_INPUT_ADDR);
|
||||
self.view.active(COMPONENT_RADIO_PROTOCOL);
|
||||
None
|
||||
}
|
||||
// Any <TAB>, go to bookmarks
|
||||
|
|
|
@ -26,7 +26,9 @@
|
|||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use super::{AuthActivity, Context, FileTransferProtocol};
|
||||
use super::{AuthActivity, Context, FileTransferProtocol, InputMask};
|
||||
use crate::filetransfer::params::ProtocolParams;
|
||||
use crate::filetransfer::FileTransferParams;
|
||||
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
|
||||
use crate::utils::ui::draw_area_in;
|
||||
// Ext
|
||||
|
@ -109,7 +111,7 @@ impl AuthActivity {
|
|||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, protocol_color)
|
||||
.with_title("Protocol", Alignment::Left)
|
||||
.with_options(&["SFTP", "SCP", "FTP", "FTPS"])
|
||||
.with_options(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
|
||||
.with_value(Self::protocol_enum_to_opt(default_protocol))
|
||||
.rewind(true)
|
||||
.build(),
|
||||
|
@ -163,6 +165,39 @@ impl AuthActivity {
|
|||
.build(),
|
||||
)),
|
||||
);
|
||||
// Bucket
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_S3_BUCKET,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(addr_color)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, addr_color)
|
||||
.with_label("Bucket name", Alignment::Left)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Region
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_S3_REGION,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(port_color)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, port_color)
|
||||
.with_label("Region", Alignment::Left)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Profile
|
||||
self.view.mount(
|
||||
super::COMPONENT_INPUT_S3_PROFILE,
|
||||
Box::new(Input::new(
|
||||
InputPropsBuilder::default()
|
||||
.with_foreground(username_color)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, username_color)
|
||||
.with_label("Profile", Alignment::Left)
|
||||
.build(),
|
||||
)),
|
||||
);
|
||||
// Version notice
|
||||
if let Some(version) = self
|
||||
.context()
|
||||
|
@ -240,20 +275,43 @@ impl AuthActivity {
|
|||
let auth_chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(1), // h1
|
||||
Constraint::Length(1), // h2
|
||||
Constraint::Length(1), // Version
|
||||
Constraint::Length(3), // protocol
|
||||
Constraint::Length(3), // host
|
||||
Constraint::Length(3), // port
|
||||
Constraint::Length(3), // username
|
||||
Constraint::Length(3), // password
|
||||
Constraint::Length(3), // footer
|
||||
Constraint::Length(1), // h1
|
||||
Constraint::Length(1), // h2
|
||||
Constraint::Length(1), // Version
|
||||
Constraint::Length(3), // protocol
|
||||
Constraint::Length(self.input_mask_size()), // Input mask
|
||||
Constraint::Length(3), // footer
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(chunks[0]);
|
||||
// Input mask chunks
|
||||
let input_mask = match self.input_mask() {
|
||||
InputMask::AwsS3 => Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // bucket
|
||||
Constraint::Length(3), // region
|
||||
Constraint::Length(3), // profile
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(auth_chunks[4]),
|
||||
InputMask::Generic => Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // host
|
||||
Constraint::Length(3), // port
|
||||
Constraint::Length(3), // username
|
||||
Constraint::Length(3), // password
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(auth_chunks[4]),
|
||||
};
|
||||
// Create bookmark chunks
|
||||
let bookmark_chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
|
@ -269,16 +327,29 @@ impl AuthActivity {
|
|||
.render(super::COMPONENT_TEXT_NEW_VERSION, f, auth_chunks[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_RADIO_PROTOCOL, f, auth_chunks[3]);
|
||||
// Render input mask
|
||||
match self.input_mask() {
|
||||
InputMask::AwsS3 => {
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_S3_BUCKET, f, input_mask[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_S3_REGION, f, input_mask[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_S3_PROFILE, f, input_mask[2]);
|
||||
}
|
||||
InputMask::Generic => {
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_ADDR, f, input_mask[0]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_PORT, f, input_mask[1]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_USERNAME, f, input_mask[2]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_PASSWORD, f, input_mask[3]);
|
||||
}
|
||||
}
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_ADDR, f, auth_chunks[4]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_PORT, f, auth_chunks[5]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_USERNAME, f, auth_chunks[6]);
|
||||
self.view
|
||||
.render(super::COMPONENT_INPUT_PASSWORD, f, auth_chunks[7]);
|
||||
self.view
|
||||
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[8]);
|
||||
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[5]);
|
||||
// Bookmark chunks
|
||||
self.view
|
||||
.render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]);
|
||||
|
@ -388,19 +459,13 @@ impl AuthActivity {
|
|||
.bookmarks_list
|
||||
.iter()
|
||||
.map(|x| {
|
||||
let entry: (String, u16, FileTransferProtocol, String, _) = self
|
||||
.bookmarks_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_bookmark(x)
|
||||
.unwrap();
|
||||
format!(
|
||||
"{} ({}://{}@{}:{})",
|
||||
Self::fmt_bookmark(
|
||||
x,
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
self.bookmarks_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_bookmark(x)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
@ -426,19 +491,12 @@ impl AuthActivity {
|
|||
.recents_list
|
||||
.iter()
|
||||
.map(|x| {
|
||||
let entry: (String, u16, FileTransferProtocol, String) = self
|
||||
.bookmarks_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_recent(x)
|
||||
.unwrap();
|
||||
|
||||
format!(
|
||||
"{}://{}@{}:{}",
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
Self::fmt_recent(
|
||||
self.bookmarks_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_recent(x)
|
||||
.unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
@ -743,16 +801,32 @@ impl AuthActivity {
|
|||
self.view.umount(super::COMPONENT_TEXT_NEW_VERSION_NOTES);
|
||||
}
|
||||
|
||||
/// ### get_input
|
||||
/// ### get_protocol
|
||||
///
|
||||
/// Get protocol from view
|
||||
pub(super) fn get_protocol(&self) -> FileTransferProtocol {
|
||||
self.get_input_protocol()
|
||||
}
|
||||
|
||||
/// ### get_generic_params
|
||||
///
|
||||
/// Collect input values from view
|
||||
pub(super) fn get_input(&self) -> (String, u16, FileTransferProtocol, String, String) {
|
||||
pub(super) fn get_generic_params_input(&self) -> (String, u16, String, String) {
|
||||
let addr: String = self.get_input_addr();
|
||||
let port: u16 = self.get_input_port();
|
||||
let protocol: FileTransferProtocol = self.get_input_protocol();
|
||||
let username: String = self.get_input_username();
|
||||
let password: String = self.get_input_password();
|
||||
(addr, port, protocol, username, password)
|
||||
(addr, port, username, password)
|
||||
}
|
||||
|
||||
/// ### get_s3_params_input
|
||||
///
|
||||
/// Collect s3 input values from view
|
||||
pub(super) fn get_s3_params_input(&self) -> (String, String, Option<String>) {
|
||||
let bucket: String = self.get_input_s3_bucket();
|
||||
let region: String = self.get_input_s3_region();
|
||||
let profile: Option<String> = self.get_input_s3_profile();
|
||||
(bucket, region, profile)
|
||||
}
|
||||
|
||||
pub(super) fn get_input_addr(&self) -> String {
|
||||
|
@ -792,4 +866,75 @@ impl AuthActivity {
|
|||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_input_s3_bucket(&self) -> String {
|
||||
match self.view.get_state(super::COMPONENT_INPUT_S3_BUCKET) {
|
||||
Some(Payload::One(Value::Str(x))) => x,
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_input_s3_region(&self) -> String {
|
||||
match self.view.get_state(super::COMPONENT_INPUT_S3_REGION) {
|
||||
Some(Payload::One(Value::Str(x))) => x,
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_input_s3_profile(&self) -> Option<String> {
|
||||
match self.view.get_state(super::COMPONENT_INPUT_S3_PROFILE) {
|
||||
Some(Payload::One(Value::Str(x))) => match x.is_empty() {
|
||||
true => None,
|
||||
false => Some(x),
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### input_mask_size
|
||||
///
|
||||
/// Returns the input mask size based on current input mask
|
||||
pub(super) fn input_mask_size(&self) -> u16 {
|
||||
match self.input_mask() {
|
||||
InputMask::AwsS3 => 9,
|
||||
InputMask::Generic => 12,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt_bookmark
|
||||
///
|
||||
/// Format bookmark to display on ui
|
||||
fn fmt_bookmark(name: &str, b: FileTransferParams) -> String {
|
||||
let addr: String = Self::fmt_recent(b);
|
||||
format!("{} ({})", name, addr)
|
||||
}
|
||||
|
||||
/// ### fmt_recent
|
||||
///
|
||||
/// Format recent connection to display on ui
|
||||
fn fmt_recent(b: FileTransferParams) -> String {
|
||||
let protocol: String = b.protocol.to_string().to_lowercase();
|
||||
match b.params {
|
||||
ProtocolParams::AwsS3(s3) => {
|
||||
let profile: String = match s3.profile {
|
||||
Some(p) => format!("[{}]", p),
|
||||
None => String::default(),
|
||||
};
|
||||
format!(
|
||||
"{}://{} ({}) {}",
|
||||
protocol, s3.bucket_name, s3.region, profile
|
||||
)
|
||||
}
|
||||
ProtocolParams::Generic(params) => {
|
||||
let username: String = match params.username {
|
||||
None => String::default(),
|
||||
Some(u) => format!("{}@", u),
|
||||
};
|
||||
format!(
|
||||
"{}://{}{}:{}",
|
||||
protocol, username, params.address, params.port
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,7 +125,7 @@ impl FileTransferActivity {
|
|||
Err(err) => match err.kind() {
|
||||
FileTransferErrorType::UnsupportedFeature => {
|
||||
// If copy is not supported, perform the tricky copy
|
||||
self.tricky_copy(entry, dest);
|
||||
let _ = self.tricky_copy(entry, dest);
|
||||
}
|
||||
_ => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
|
@ -143,7 +143,7 @@ impl FileTransferActivity {
|
|||
/// ### tricky_copy
|
||||
///
|
||||
/// Tricky copy will be used whenever copy command is not available on remote host
|
||||
fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) {
|
||||
pub(super) fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) -> Result<(), String> {
|
||||
// NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen
|
||||
self.umount_wait();
|
||||
// match entry
|
||||
|
@ -157,7 +157,7 @@ impl FileTransferActivity {
|
|||
LogLevel::Error,
|
||||
format!("Copy failed: could not create temporary file: {}", err),
|
||||
);
|
||||
return;
|
||||
return Err(String::from("Could not create temporary file"));
|
||||
}
|
||||
};
|
||||
// Download file
|
||||
|
@ -170,7 +170,7 @@ impl FileTransferActivity {
|
|||
LogLevel::Error,
|
||||
format!("Copy failed: could not download to temporary file: {}", err),
|
||||
);
|
||||
return;
|
||||
return Err(err);
|
||||
}
|
||||
// Get local fs entry
|
||||
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) {
|
||||
|
@ -184,7 +184,7 @@ impl FileTransferActivity {
|
|||
err
|
||||
),
|
||||
);
|
||||
return;
|
||||
return Err(err.to_string());
|
||||
}
|
||||
};
|
||||
// Upload file to destination
|
||||
|
@ -202,8 +202,9 @@ impl FileTransferActivity {
|
|||
err
|
||||
),
|
||||
);
|
||||
return;
|
||||
return Err(err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
FsEntry::Directory(_) => {
|
||||
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
|
||||
|
@ -213,7 +214,7 @@ impl FileTransferActivity {
|
|||
LogLevel::Error,
|
||||
format!("Copy failed: could not create temporary directory: {}", err),
|
||||
);
|
||||
return;
|
||||
return Err(err.to_string());
|
||||
}
|
||||
};
|
||||
// Get path of dest
|
||||
|
@ -227,7 +228,7 @@ impl FileTransferActivity {
|
|||
LogLevel::Error,
|
||||
format!("Copy failed: failed to download file: {}", err),
|
||||
);
|
||||
return;
|
||||
return Err(err);
|
||||
}
|
||||
// Stat dir
|
||||
let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) {
|
||||
|
@ -241,7 +242,7 @@ impl FileTransferActivity {
|
|||
err
|
||||
),
|
||||
);
|
||||
return;
|
||||
return Err(err.to_string());
|
||||
}
|
||||
};
|
||||
// Upload to destination
|
||||
|
@ -255,8 +256,9 @@ impl FileTransferActivity {
|
|||
LogLevel::Error,
|
||||
format!("Copy failed: failed to send file: {}", err),
|
||||
);
|
||||
return;
|
||||
return Err(err);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
*/
|
||||
// locals
|
||||
use super::{FileTransferActivity, FsEntry, LogLevel};
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
impl FileTransferActivity {
|
||||
|
@ -99,24 +100,29 @@ impl FileTransferActivity {
|
|||
};
|
||||
if let FsEntry::File(local_file) = local_file {
|
||||
// Create file
|
||||
match self.client.send_file(&local_file, file_path.as_path()) {
|
||||
let reader = Box::new(match File::open(tfile.path()) {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not open tempfile: {}", err),
|
||||
);
|
||||
return;
|
||||
}
|
||||
});
|
||||
match self
|
||||
.client
|
||||
.send_file_wno_stream(&local_file, file_path.as_path(), reader)
|
||||
{
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create file \"{}\": {}", file_path.display(), err),
|
||||
),
|
||||
Ok(writer) => {
|
||||
// Finalize write
|
||||
if let Err(err) = self.client.on_sent(writer) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Warn,
|
||||
format!("Could not finalize file: {}", err),
|
||||
);
|
||||
} else {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Created file \"{}\"", file_path.display()),
|
||||
);
|
||||
}
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Created file \"{}\"", file_path.display()),
|
||||
);
|
||||
// Reload files
|
||||
self.reload_remote_dir();
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@
|
|||
*/
|
||||
// locals
|
||||
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
||||
use crate::filetransfer::FileTransferErrorType;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
impl FileTransferActivity {
|
||||
|
@ -114,6 +115,9 @@ impl FileTransferActivity {
|
|||
),
|
||||
);
|
||||
}
|
||||
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
|
||||
self.tricky_move(entry, dest);
|
||||
}
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
|
@ -125,4 +129,41 @@ impl FileTransferActivity {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### tricky_move
|
||||
///
|
||||
/// Tricky move will be used whenever copy command is not available on remote host.
|
||||
/// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`)
|
||||
fn tricky_move(&mut self, entry: &FsEntry, dest: &Path) {
|
||||
debug!(
|
||||
"Using tricky-move to move entry {} to {}",
|
||||
entry.get_abs_path().display(),
|
||||
dest.display()
|
||||
);
|
||||
if self.tricky_copy(entry.clone(), dest).is_ok() {
|
||||
// Delete remote existing entry
|
||||
debug!("Tricky-copy worked; removing existing remote entry");
|
||||
match self.client.remove(entry) {
|
||||
Ok(_) => self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Moved \"{}\" to \"{}\"",
|
||||
entry.get_abs_path().display(),
|
||||
dest.display()
|
||||
),
|
||||
),
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Copied \"{}\" to \"{}\"; but failed to remove src: {}",
|
||||
entry.get_abs_path().display(),
|
||||
dest.display(),
|
||||
err
|
||||
),
|
||||
),
|
||||
}
|
||||
} else {
|
||||
error!("Tricky move aborted due to tricky-copy failure");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -140,6 +140,10 @@ impl ProgressStates {
|
|||
///
|
||||
/// Calculate progress in a range between 0.0 to 1.0
|
||||
pub fn calc_progress(&self) -> f64 {
|
||||
// Prevent dividing by 0
|
||||
if self.total == 0 {
|
||||
return 0.0;
|
||||
}
|
||||
let prog: f64 = (self.written as f64) / (self.total as f64);
|
||||
match prog > 1.0 {
|
||||
true => 1.0,
|
||||
|
@ -238,6 +242,11 @@ mod test {
|
|||
// Check if terminated at started
|
||||
states.started = Instant::now();
|
||||
assert_eq!(states.calc_bytes_per_second(), 1024);
|
||||
// Divide by zero
|
||||
let states: ProgressStates = ProgressStates::default();
|
||||
assert_eq!(states.total, 0);
|
||||
assert_eq!(states.written, 0);
|
||||
assert_eq!(states.calc_progress(), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
*/
|
||||
// Locals
|
||||
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
|
||||
use crate::filetransfer::ProtocolParams;
|
||||
use crate::system::environment;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::path;
|
||||
|
@ -134,4 +135,15 @@ impl FileTransferActivity {
|
|||
pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf {
|
||||
path::absolutize(self.remote().wrkdir.as_path(), path)
|
||||
}
|
||||
|
||||
/// ### get_remote_hostname
|
||||
///
|
||||
/// Get remote hostname
|
||||
pub(super) fn get_remote_hostname(&self) -> String {
|
||||
let ft_params = self.context().ft_params().unwrap();
|
||||
match &ft_params.params {
|
||||
ProtocolParams::Generic(params) => params.address.clone(),
|
||||
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,10 +36,8 @@ pub(self) mod view;
|
|||
// locals
|
||||
use super::{Activity, Context, ExitReason};
|
||||
use crate::config::themes::Theme;
|
||||
use crate::filetransfer::ftp_transfer::FtpFileTransfer;
|
||||
use crate::filetransfer::scp_transfer::ScpFileTransfer;
|
||||
use crate::filetransfer::sftp_transfer::SftpFileTransfer;
|
||||
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
||||
use crate::filetransfer::{FileTransfer, FileTransferProtocol, ProtocolParams};
|
||||
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
|
||||
use crate::fs::explorer::FileExplorer;
|
||||
use crate::fs::FsEntry;
|
||||
use crate::host::Localhost;
|
||||
|
@ -155,6 +153,7 @@ impl FileTransferActivity {
|
|||
FileTransferProtocol::Scp => {
|
||||
Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client)))
|
||||
}
|
||||
FileTransferProtocol::AwsS3 => Box::new(S3FileTransfer::default()),
|
||||
},
|
||||
browser: Browser::new(&config_client),
|
||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||
|
@ -237,6 +236,28 @@ impl FileTransferActivity {
|
|||
fn theme(&self) -> &Theme {
|
||||
self.context().theme_provider().theme()
|
||||
}
|
||||
|
||||
/// ### get_connection_msg
|
||||
///
|
||||
/// Get connection message to show to client
|
||||
fn get_connection_msg(params: &ProtocolParams) -> String {
|
||||
match params {
|
||||
ProtocolParams::Generic(params) => {
|
||||
info!(
|
||||
"Client is not connected to remote; connecting to {}:{}",
|
||||
params.address, params.port
|
||||
);
|
||||
format!("Connecting to {}:{}…", params.address, params.port)
|
||||
}
|
||||
ProtocolParams::AwsS3(params) => {
|
||||
info!(
|
||||
"Client is not connected to remote; connecting to {} ({})",
|
||||
params.bucket_name, params.region
|
||||
);
|
||||
format!("Connecting to {}…", params.bucket_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -290,12 +311,9 @@ impl Activity for FileTransferActivity {
|
|||
}
|
||||
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
|
||||
let params = self.context().ft_params().unwrap();
|
||||
info!(
|
||||
"Client is not connected to remote; connecting to {}:{}",
|
||||
params.address, params.port
|
||||
);
|
||||
let msg: String = format!("Connecting to {}:{}…", params.address, params.port);
|
||||
let ftparams = self.context().ft_params().unwrap();
|
||||
// print params
|
||||
let msg: String = Self::get_connection_msg(&ftparams.params);
|
||||
// Set init state to connecting popup
|
||||
self.mount_wait(msg.as_str());
|
||||
// Force ui draw
|
||||
|
|
|
@ -34,6 +34,7 @@ use crate::utils::fmt::fmt_millis;
|
|||
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
@ -76,22 +77,20 @@ impl FileTransferActivity {
|
|||
///
|
||||
/// Connect to remote
|
||||
pub(super) fn connect(&mut self) {
|
||||
let params = self.context().ft_params().unwrap().clone();
|
||||
let addr: String = params.address.clone();
|
||||
let entry_dir: Option<PathBuf> = params.entry_directory.clone();
|
||||
let ft_params = self.context().ft_params().unwrap().clone();
|
||||
let entry_dir: Option<PathBuf> = ft_params.entry_directory.clone();
|
||||
// Connect to remote
|
||||
match self.client.connect(
|
||||
params.address,
|
||||
params.port,
|
||||
params.username,
|
||||
params.password,
|
||||
) {
|
||||
match self.client.connect(&ft_params.params) {
|
||||
Ok(welcome) => {
|
||||
if let Some(banner) = welcome {
|
||||
// Log welcome
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Established connection with '{}': \"{}\"", addr, banner),
|
||||
format!(
|
||||
"Established connection with '{}': \"{}\"",
|
||||
self.get_remote_hostname(),
|
||||
banner
|
||||
),
|
||||
);
|
||||
}
|
||||
// Try to change directory to entry directory
|
||||
|
@ -121,8 +120,7 @@ impl FileTransferActivity {
|
|||
///
|
||||
/// disconnect from remote
|
||||
pub(super) fn disconnect(&mut self) {
|
||||
let params = self.context().ft_params().unwrap();
|
||||
let msg: String = format!("Disconnecting from {}…", params.address);
|
||||
let msg: String = format!("Disconnecting from {}…", self.get_remote_hostname());
|
||||
// Show popup disconnecting
|
||||
self.mount_wait(msg.as_str());
|
||||
// Disconnect
|
||||
|
@ -442,103 +440,165 @@ impl FileTransferActivity {
|
|||
// Upload file
|
||||
// Try to open local file
|
||||
match self.host.open_file_read(local.abs_path.as_path()) {
|
||||
Ok(mut fhnd) => match self.client.send_file(local, remote) {
|
||||
Ok(mut rhnd) => {
|
||||
// Write file
|
||||
let file_size: usize =
|
||||
fhnd.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
|
||||
// Init transfer
|
||||
self.transfer.partial.init(file_size);
|
||||
// rewind
|
||||
if let Err(err) = fhnd.seek(std::io::SeekFrom::Start(0)) {
|
||||
return Err(TransferErrorReason::CouldNotRewind(err));
|
||||
}
|
||||
// Write remote file
|
||||
let mut total_bytes_written: usize = 0;
|
||||
let mut last_progress_val: f64 = 0.0;
|
||||
let mut last_input_event_fetch: Option<Instant> = None;
|
||||
// While the entire file hasn't been completely written,
|
||||
// Or filetransfer has been aborted
|
||||
while total_bytes_written < file_size && !self.transfer.aborted() {
|
||||
// Handle input events (each 500ms) or if never fetched before
|
||||
if last_input_event_fetch.is_none()
|
||||
|| last_input_event_fetch
|
||||
.unwrap_or_else(Instant::now)
|
||||
.elapsed()
|
||||
.as_millis()
|
||||
>= 500
|
||||
{
|
||||
// Read events
|
||||
self.read_input_event();
|
||||
// Reset instant
|
||||
last_input_event_fetch = Some(Instant::now());
|
||||
}
|
||||
// Read till you can
|
||||
let mut buffer: [u8; 65536] = [0; 65536];
|
||||
let delta: usize = match fhnd.read(&mut buffer) {
|
||||
Ok(bytes_read) => {
|
||||
total_bytes_written += bytes_read;
|
||||
if bytes_read == 0 {
|
||||
continue;
|
||||
} else {
|
||||
let mut delta: usize = 0;
|
||||
while delta < bytes_read {
|
||||
// Write bytes
|
||||
match rhnd.write(&buffer[delta..bytes_read]) {
|
||||
Ok(bytes) => {
|
||||
delta += bytes;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::RemoteIoError(
|
||||
err,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
delta
|
||||
Ok(fhnd) => match self.client.send_file(local, remote) {
|
||||
Ok(rhnd) => {
|
||||
self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd)
|
||||
}
|
||||
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
|
||||
self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd)
|
||||
}
|
||||
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
|
||||
},
|
||||
Err(err) => Err(TransferErrorReason::HostError(err)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### filetransfer_send_one_with_stream
|
||||
///
|
||||
/// Send file to remote using stream
|
||||
fn filetransfer_send_one_with_stream(
|
||||
&mut self,
|
||||
local: &FsFile,
|
||||
remote: &Path,
|
||||
file_name: String,
|
||||
mut reader: File,
|
||||
mut writer: Box<dyn Write>,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
// Write file
|
||||
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
|
||||
// Init transfer
|
||||
self.transfer.partial.init(file_size);
|
||||
// rewind
|
||||
if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
|
||||
return Err(TransferErrorReason::CouldNotRewind(err));
|
||||
}
|
||||
// Write remote file
|
||||
let mut total_bytes_written: usize = 0;
|
||||
let mut last_progress_val: f64 = 0.0;
|
||||
let mut last_input_event_fetch: Option<Instant> = None;
|
||||
// While the entire file hasn't been completely written,
|
||||
// Or filetransfer has been aborted
|
||||
while total_bytes_written < file_size && !self.transfer.aborted() {
|
||||
// Handle input events (each 500ms) or if never fetched before
|
||||
if last_input_event_fetch.is_none()
|
||||
|| last_input_event_fetch
|
||||
.unwrap_or_else(Instant::now)
|
||||
.elapsed()
|
||||
.as_millis()
|
||||
>= 500
|
||||
{
|
||||
// Read events
|
||||
self.read_input_event();
|
||||
// Reset instant
|
||||
last_input_event_fetch = Some(Instant::now());
|
||||
}
|
||||
// Read till you can
|
||||
let mut buffer: [u8; 65536] = [0; 65536];
|
||||
let delta: usize = match reader.read(&mut buffer) {
|
||||
Ok(bytes_read) => {
|
||||
total_bytes_written += bytes_read;
|
||||
if bytes_read == 0 {
|
||||
continue;
|
||||
} else {
|
||||
let mut delta: usize = 0;
|
||||
while delta < bytes_read {
|
||||
// Write bytes
|
||||
match writer.write(&buffer[delta..bytes_read]) {
|
||||
Ok(bytes) => {
|
||||
delta += bytes;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::RemoteIoError(err));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::LocalIoError(err));
|
||||
}
|
||||
};
|
||||
// Increase progress
|
||||
self.transfer.partial.update_progress(delta);
|
||||
self.transfer.full.update_progress(delta);
|
||||
// Draw only if a significant progress has been made (performance improvement)
|
||||
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
|
||||
// Draw
|
||||
self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
|
||||
self.view();
|
||||
last_progress_val = self.transfer.partial.calc_progress();
|
||||
}
|
||||
delta
|
||||
}
|
||||
// Finalize stream
|
||||
if let Err(err) = self.client.on_sent(rhnd) {
|
||||
self.log(
|
||||
LogLevel::Warn,
|
||||
format!("Could not finalize remote stream: \"{}\"", err),
|
||||
);
|
||||
}
|
||||
// if upload was abrupted, return error
|
||||
if self.transfer.aborted() {
|
||||
return Err(TransferErrorReason::Abrupted);
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||
local.abs_path.display(),
|
||||
remote.display(),
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(err) => return Err(TransferErrorReason::FileTransferError(err)),
|
||||
},
|
||||
Err(err) => return Err(TransferErrorReason::HostError(err)),
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::LocalIoError(err));
|
||||
}
|
||||
};
|
||||
// Increase progress
|
||||
self.transfer.partial.update_progress(delta);
|
||||
self.transfer.full.update_progress(delta);
|
||||
// Draw only if a significant progress has been made (performance improvement)
|
||||
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
|
||||
// Draw
|
||||
self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
|
||||
self.view();
|
||||
last_progress_val = self.transfer.partial.calc_progress();
|
||||
}
|
||||
}
|
||||
// Finalize stream
|
||||
if let Err(err) = self.client.on_sent(writer) {
|
||||
self.log(
|
||||
LogLevel::Warn,
|
||||
format!("Could not finalize remote stream: \"{}\"", err),
|
||||
);
|
||||
}
|
||||
// if upload was abrupted, return error
|
||||
if self.transfer.aborted() {
|
||||
return Err(TransferErrorReason::Abrupted);
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||
local.abs_path.display(),
|
||||
remote.display(),
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### filetransfer_send_one_wno_stream
|
||||
///
|
||||
/// Send an `FsFile` to remote without using streams.
|
||||
fn filetransfer_send_one_wno_stream(
|
||||
&mut self,
|
||||
local: &FsFile,
|
||||
remote: &Path,
|
||||
file_name: String,
|
||||
mut reader: File,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
// Write file
|
||||
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
|
||||
// Init transfer
|
||||
self.transfer.partial.init(file_size);
|
||||
// rewind
|
||||
if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
|
||||
return Err(TransferErrorReason::CouldNotRewind(err));
|
||||
}
|
||||
// Draw before
|
||||
self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
|
||||
self.view();
|
||||
// Send file
|
||||
if let Err(err) = self
|
||||
.client
|
||||
.send_file_wno_stream(local, remote, Box::new(reader))
|
||||
{
|
||||
return Err(TransferErrorReason::FileTransferError(err));
|
||||
}
|
||||
// Set transfer size ok
|
||||
self.transfer.partial.update_progress(file_size);
|
||||
self.transfer.full.update_progress(file_size);
|
||||
// Draw again after
|
||||
self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
|
||||
self.view();
|
||||
// log and return Ok
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||
local.abs_path.display(),
|
||||
remote.display(),
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -796,120 +856,187 @@ impl FileTransferActivity {
|
|||
) -> Result<(), TransferErrorReason> {
|
||||
// Try to open local file
|
||||
match self.host.open_file_write(local) {
|
||||
Ok(mut local_file) => {
|
||||
Ok(local_file) => {
|
||||
// Download file from remote
|
||||
match self.client.recv_file(remote) {
|
||||
Ok(mut rhnd) => {
|
||||
let mut total_bytes_written: usize = 0;
|
||||
// Init transfer
|
||||
self.transfer.partial.init(remote.size);
|
||||
// Write local file
|
||||
let mut last_progress_val: f64 = 0.0;
|
||||
let mut last_input_event_fetch: Option<Instant> = None;
|
||||
// While the entire file hasn't been completely read,
|
||||
// Or filetransfer has been aborted
|
||||
while total_bytes_written < remote.size && !self.transfer.aborted() {
|
||||
// Handle input events (each 500 ms) or is None
|
||||
if last_input_event_fetch.is_none()
|
||||
|| last_input_event_fetch
|
||||
.unwrap_or_else(Instant::now)
|
||||
.elapsed()
|
||||
.as_millis()
|
||||
>= 500
|
||||
{
|
||||
// Read events
|
||||
self.read_input_event();
|
||||
// Reset instant
|
||||
last_input_event_fetch = Some(Instant::now());
|
||||
}
|
||||
// Read till you can
|
||||
let mut buffer: [u8; 65536] = [0; 65536];
|
||||
let delta: usize = match rhnd.read(&mut buffer) {
|
||||
Ok(bytes_read) => {
|
||||
total_bytes_written += bytes_read;
|
||||
if bytes_read == 0 {
|
||||
continue;
|
||||
} else {
|
||||
let mut delta: usize = 0;
|
||||
while delta < bytes_read {
|
||||
// Write bytes
|
||||
match local_file.write(&buffer[delta..bytes_read]) {
|
||||
Ok(bytes) => delta += bytes,
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::LocalIoError(
|
||||
err,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
delta
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::RemoteIoError(err));
|
||||
}
|
||||
};
|
||||
// Set progress
|
||||
self.transfer.partial.update_progress(delta);
|
||||
self.transfer.full.update_progress(delta);
|
||||
// Draw only if a significant progress has been made (performance improvement)
|
||||
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
|
||||
// Draw
|
||||
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
|
||||
self.view();
|
||||
last_progress_val = self.transfer.partial.calc_progress();
|
||||
}
|
||||
}
|
||||
// Finalize stream
|
||||
if let Err(err) = self.client.on_recv(rhnd) {
|
||||
self.log(
|
||||
LogLevel::Warn,
|
||||
format!("Could not finalize remote stream: \"{}\"", err),
|
||||
);
|
||||
}
|
||||
// If download was abrupted, return Error
|
||||
if self.transfer.aborted() {
|
||||
return Err(TransferErrorReason::Abrupted);
|
||||
}
|
||||
// Apply file mode to file
|
||||
#[cfg(any(
|
||||
target_family = "unix",
|
||||
target_os = "macos",
|
||||
target_os = "linux"
|
||||
))]
|
||||
if let Some((owner, group, others)) = remote.unix_pex {
|
||||
if let Err(err) = self
|
||||
.host
|
||||
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
|
||||
{
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not apply file mode {:?} to \"{}\": {}",
|
||||
(owner.as_byte(), group.as_byte(), others.as_byte()),
|
||||
local.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Log
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||
remote.abs_path.display(),
|
||||
local.display(),
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
),
|
||||
);
|
||||
Ok(rhnd) => self.filetransfer_recv_one_with_stream(
|
||||
local, remote, file_name, rhnd, local_file,
|
||||
),
|
||||
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
|
||||
self.filetransfer_recv_one_wno_stream(local, remote, file_name)
|
||||
}
|
||||
Err(err) => return Err(TransferErrorReason::FileTransferError(err)),
|
||||
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(TransferErrorReason::HostError(err)),
|
||||
Err(err) => Err(TransferErrorReason::HostError(err)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### filetransfer_recv_one_with_stream
|
||||
///
|
||||
/// Receive an `FsEntry` from remote using stream
|
||||
fn filetransfer_recv_one_with_stream(
|
||||
&mut self,
|
||||
local: &Path,
|
||||
remote: &FsFile,
|
||||
file_name: String,
|
||||
mut reader: Box<dyn Read>,
|
||||
mut writer: File,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
let mut total_bytes_written: usize = 0;
|
||||
// Init transfer
|
||||
self.transfer.partial.init(remote.size);
|
||||
// Write local file
|
||||
let mut last_progress_val: f64 = 0.0;
|
||||
let mut last_input_event_fetch: Option<Instant> = None;
|
||||
// While the entire file hasn't been completely read,
|
||||
// Or filetransfer has been aborted
|
||||
while total_bytes_written < remote.size && !self.transfer.aborted() {
|
||||
// Handle input events (each 500 ms) or is None
|
||||
if last_input_event_fetch.is_none()
|
||||
|| last_input_event_fetch
|
||||
.unwrap_or_else(Instant::now)
|
||||
.elapsed()
|
||||
.as_millis()
|
||||
>= 500
|
||||
{
|
||||
// Read events
|
||||
self.read_input_event();
|
||||
// Reset instant
|
||||
last_input_event_fetch = Some(Instant::now());
|
||||
}
|
||||
// Read till you can
|
||||
let mut buffer: [u8; 65536] = [0; 65536];
|
||||
let delta: usize = match reader.read(&mut buffer) {
|
||||
Ok(bytes_read) => {
|
||||
total_bytes_written += bytes_read;
|
||||
if bytes_read == 0 {
|
||||
continue;
|
||||
} else {
|
||||
let mut delta: usize = 0;
|
||||
while delta < bytes_read {
|
||||
// Write bytes
|
||||
match writer.write(&buffer[delta..bytes_read]) {
|
||||
Ok(bytes) => delta += bytes,
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::LocalIoError(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
delta
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(TransferErrorReason::RemoteIoError(err));
|
||||
}
|
||||
};
|
||||
// Set progress
|
||||
self.transfer.partial.update_progress(delta);
|
||||
self.transfer.full.update_progress(delta);
|
||||
// Draw only if a significant progress has been made (performance improvement)
|
||||
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
|
||||
// Draw
|
||||
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
|
||||
self.view();
|
||||
last_progress_val = self.transfer.partial.calc_progress();
|
||||
}
|
||||
}
|
||||
// Finalize stream
|
||||
if let Err(err) = self.client.on_recv(reader) {
|
||||
self.log(
|
||||
LogLevel::Warn,
|
||||
format!("Could not finalize remote stream: \"{}\"", err),
|
||||
);
|
||||
}
|
||||
// If download was abrupted, return Error
|
||||
if self.transfer.aborted() {
|
||||
return Err(TransferErrorReason::Abrupted);
|
||||
}
|
||||
// Apply file mode to file
|
||||
#[cfg(target_family = "unix")]
|
||||
if let Some((owner, group, others)) = remote.unix_pex {
|
||||
if let Err(err) = self
|
||||
.host
|
||||
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
|
||||
{
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not apply file mode {:?} to \"{}\": {}",
|
||||
(owner.as_byte(), group.as_byte(), others.as_byte()),
|
||||
local.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Log
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||
remote.abs_path.display(),
|
||||
local.display(),
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### filetransfer_recv_one_with_stream
|
||||
///
|
||||
/// Receive an `FsEntry` from remote without using stream
|
||||
fn filetransfer_recv_one_wno_stream(
|
||||
&mut self,
|
||||
local: &Path,
|
||||
remote: &FsFile,
|
||||
file_name: String,
|
||||
) -> Result<(), TransferErrorReason> {
|
||||
// Init transfer
|
||||
self.transfer.partial.init(remote.size);
|
||||
// Draw before transfer
|
||||
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
|
||||
self.view();
|
||||
// recv wno stream
|
||||
if let Err(err) = self.client.recv_file_wno_stream(remote, local) {
|
||||
return Err(TransferErrorReason::FileTransferError(err));
|
||||
}
|
||||
// Update progress at the end
|
||||
self.transfer.partial.update_progress(remote.size);
|
||||
self.transfer.full.update_progress(remote.size);
|
||||
// Draw after transfer
|
||||
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
|
||||
self.view();
|
||||
// Apply file mode to file
|
||||
#[cfg(target_family = "unix")]
|
||||
if let Some((owner, group, others)) = remote.unix_pex {
|
||||
if let Err(err) = self
|
||||
.host
|
||||
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
|
||||
{
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not apply file mode {:?} to \"{}\": {}",
|
||||
(owner.as_byte(), group.as_byte(), others.as_byte()),
|
||||
local.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Log
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||
remote.abs_path.display(),
|
||||
local.display(),
|
||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||
),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -810,14 +810,14 @@ impl FileTransferActivity {
|
|||
.store()
|
||||
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
|
||||
.unwrap_or(256);
|
||||
let params = self.context().ft_params().unwrap();
|
||||
let hostname = self.get_remote_hostname();
|
||||
let hostname: String = format!(
|
||||
"{}:{} ",
|
||||
params.address,
|
||||
hostname,
|
||||
fmt_path_elide_ex(
|
||||
self.remote().wrkdir.as_path(),
|
||||
width,
|
||||
params.address.len() + 3 // 3 because of '/…/'
|
||||
hostname.len() + 3 // 3 because of '/…/'
|
||||
)
|
||||
);
|
||||
let files: Vec<String> = self
|
||||
|
|
|
@ -81,12 +81,7 @@ impl SetupActivity {
|
|||
.with_inverted_color(Color::Black)
|
||||
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
|
||||
.with_title("Default file transfer protocol", Alignment::Left)
|
||||
.with_options(&[
|
||||
String::from("SFTP"),
|
||||
String::from("SCP"),
|
||||
String::from("FTP"),
|
||||
String::from("FTPS"),
|
||||
])
|
||||
.with_options(&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"])
|
||||
.rewind(true)
|
||||
.build(),
|
||||
)),
|
||||
|
@ -265,6 +260,7 @@ impl SetupActivity {
|
|||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(false) => 2,
|
||||
FileTransferProtocol::Ftp(true) => 3,
|
||||
FileTransferProtocol::AwsS3 => 4,
|
||||
};
|
||||
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
|
||||
let _ = self
|
||||
|
@ -334,6 +330,7 @@ impl SetupActivity {
|
|||
1 => FileTransferProtocol::Scp,
|
||||
2 => FileTransferProtocol::Ftp(false),
|
||||
3 => FileTransferProtocol::Ftp(true),
|
||||
4 => FileTransferProtocol::AwsS3,
|
||||
_ => FileTransferProtocol::Sftp,
|
||||
};
|
||||
self.config_mut().set_default_protocol(protocol);
|
||||
|
|
|
@ -26,7 +26,10 @@
|
|||
* SOFTWARE.
|
||||
*/
|
||||
// Locals
|
||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
||||
use crate::filetransfer::{
|
||||
params::{AwsS3Params, GenericProtocolParams, ProtocolParams},
|
||||
FileTransferParams, FileTransferProtocol,
|
||||
};
|
||||
#[cfg(not(test))] // NOTE: don't use configuration during tests
|
||||
use crate::system::config_client::ConfigClient;
|
||||
#[cfg(not(test))] // NOTE: don't use configuration during tests
|
||||
|
@ -43,15 +46,34 @@ use tuirealm::tui::style::Color;
|
|||
|
||||
// Regex
|
||||
lazy_static! {
|
||||
|
||||
/**
|
||||
* This regex matches the protocol used as option
|
||||
* Regex matches:
|
||||
* - group 1: Some(protocol) | None
|
||||
* - group 2: Some(other args)
|
||||
*/
|
||||
static ref REMOTE_OPT_PROTOCOL_REGEX: Regex = Regex::new(r"(?:([a-z0-9]+)://)?(?:(.+))").unwrap();
|
||||
|
||||
/**
|
||||
* Regex matches:
|
||||
* - group 1: Some(protocol) | None
|
||||
* - group 2: Some(user) | None
|
||||
* - group 3: Address
|
||||
* - group 4: Some(port) | None
|
||||
* - group 5: Some(path) | None
|
||||
* - group 1: Some(user) | None
|
||||
* - group 2: Address
|
||||
* - group 3: Some(port) | None
|
||||
* - group 4: Some(path) | None
|
||||
*/
|
||||
static ref REMOTE_OPT_REGEX: Regex = Regex::new(r"(?:([a-z]+)://)?(?:([^@]+)@)?(?:([^:]+))(?::((?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])))?(?::([^:]+))?").ok().unwrap();
|
||||
static ref REMOTE_GENERIC_OPT_REGEX: Regex = Regex::new(r"(?:([^@]+)@)?(?:([^:]+))(?::((?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])))?(?::([^:]+))?").ok().unwrap();
|
||||
|
||||
/**
|
||||
* Regex matches:
|
||||
* - group 1: Bucket
|
||||
* - group 2: Region
|
||||
* - group 3: Some(profile) | None
|
||||
* - group 4: Some(path) | None
|
||||
*/
|
||||
static ref REMOTE_S3_OPT_REGEX: Regex = Regex::new(r"(?:([^@]+)@)(?:([^:]+))(?::([a-zA-Z0-9][^:]+))?(?::([^:]+))?").unwrap();
|
||||
|
||||
|
||||
/**
|
||||
* Regex matches:
|
||||
* - group 1: Version
|
||||
|
@ -75,6 +97,8 @@ lazy_static! {
|
|||
static ref COLOR_RGB_REGEX: Regex = Regex::new(r"^(rgb)?\(?([01]?\d\d?|2[0-4]\d|25[0-5])(\W+)([01]?\d\d?|2[0-4]\d|25[0-5])\W+(([01]?\d\d?|2[0-4]\d|25[0-5])\)?)").unwrap();
|
||||
}
|
||||
|
||||
// -- remote opts
|
||||
|
||||
/// ### parse_remote_opt
|
||||
///
|
||||
/// Parse remote option string. Returns in case of success a RemoteOptions struct
|
||||
|
@ -93,10 +117,10 @@ lazy_static! {
|
|||
/// - sftp://172.26.104.1:4022
|
||||
/// - sftp://172.26.104.1
|
||||
/// - ...
|
||||
pub fn parse_remote_opt(remote: &str) -> Result<FileTransferParams, String> {
|
||||
pub fn parse_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
||||
// Set protocol to default protocol
|
||||
#[cfg(not(test))] // NOTE: don't use configuration during tests
|
||||
let mut protocol: FileTransferProtocol = match environment::init_config_dir() {
|
||||
let default_protocol: FileTransferProtocol = match environment::init_config_dir() {
|
||||
Ok(p) => match p {
|
||||
Some(p) => {
|
||||
// Create config client
|
||||
|
@ -111,28 +135,60 @@ pub fn parse_remote_opt(remote: &str) -> Result<FileTransferParams, String> {
|
|||
Err(_) => FileTransferProtocol::Sftp,
|
||||
};
|
||||
#[cfg(test)] // NOTE: during test set protocol just to Sftp
|
||||
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
|
||||
// Match against regex
|
||||
match REMOTE_OPT_REGEX.captures(remote) {
|
||||
let default_protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
|
||||
// Get protocol
|
||||
let (protocol, s): (FileTransferProtocol, String) =
|
||||
parse_remote_opt_protocol(s, default_protocol)?;
|
||||
// Match against regex for protocol type
|
||||
match protocol {
|
||||
FileTransferProtocol::AwsS3 => parse_s3_remote_opt(s.as_str()),
|
||||
protocol => parse_generic_remote_opt(s.as_str(), protocol),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### parse_remote_opt_protocol
|
||||
///
|
||||
/// Parse protocol from CLI option. In case of success, return the protocol to be used and the remaining arguments
|
||||
fn parse_remote_opt_protocol(
|
||||
s: &str,
|
||||
default: FileTransferProtocol,
|
||||
) -> Result<(FileTransferProtocol, String), String> {
|
||||
match REMOTE_OPT_PROTOCOL_REGEX.captures(s) {
|
||||
Some(groups) => {
|
||||
// Parse protocol or use default
|
||||
let protocol = groups.get(1).map(|x| {
|
||||
FileTransferProtocol::from_str(x.as_str())
|
||||
.map_err(|_| format!("Unknown protocol \"{}\"", x.as_str()))
|
||||
});
|
||||
let protocol = match protocol {
|
||||
Some(Ok(protocol)) => protocol,
|
||||
Some(Err(err)) => return Err(err),
|
||||
None => default,
|
||||
};
|
||||
// Return protocol and remaining arguments
|
||||
Ok((
|
||||
protocol,
|
||||
groups
|
||||
.get(2)
|
||||
.map(|x| x.as_str().to_string())
|
||||
.unwrap_or_default(),
|
||||
))
|
||||
}
|
||||
None => Err("Invalid args".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### parse_generic_remote_opt
|
||||
///
|
||||
/// Parse generic remote options
|
||||
fn parse_generic_remote_opt(
|
||||
s: &str,
|
||||
protocol: FileTransferProtocol,
|
||||
) -> Result<FileTransferParams, String> {
|
||||
match REMOTE_GENERIC_OPT_REGEX.captures(s) {
|
||||
Some(groups) => {
|
||||
// Match protocol
|
||||
let mut port: u16 = 22;
|
||||
if let Some(group) = groups.get(1) {
|
||||
// Set protocol from group
|
||||
let (m_protocol, m_port) = match FileTransferProtocol::from_str(group.as_str()) {
|
||||
Ok(proto) => match proto {
|
||||
FileTransferProtocol::Ftp(_) => (proto, 21),
|
||||
FileTransferProtocol::Scp => (proto, 22),
|
||||
FileTransferProtocol::Sftp => (proto, 22),
|
||||
},
|
||||
Err(_) => return Err(format!("Unknown protocol \"{}\"", group.as_str())),
|
||||
};
|
||||
// NOTE: tuple destructuring assignment is not supported yet :(
|
||||
protocol = m_protocol;
|
||||
port = m_port;
|
||||
}
|
||||
// Match user
|
||||
let username: Option<String> = match groups.get(2) {
|
||||
let username: Option<String> = match groups.get(1) {
|
||||
Some(group) => Some(group.as_str().to_string()),
|
||||
None => match protocol {
|
||||
// If group is empty, set to current user
|
||||
|
@ -143,25 +199,62 @@ pub fn parse_remote_opt(remote: &str) -> Result<FileTransferParams, String> {
|
|||
},
|
||||
};
|
||||
// Get address
|
||||
let address: String = match groups.get(3) {
|
||||
let address: String = match groups.get(2) {
|
||||
Some(group) => group.as_str().to_string(),
|
||||
None => return Err(String::from("Missing address")),
|
||||
};
|
||||
// Get port
|
||||
if let Some(group) = groups.get(4) {
|
||||
port = match group.as_str().parse::<u16>() {
|
||||
let port: u16 = match groups.get(3) {
|
||||
Some(port) => match port.as_str().parse::<u16>() {
|
||||
// Try to parse port
|
||||
Ok(p) => p,
|
||||
Err(err) => return Err(format!("Bad port \"{}\": {}", group.as_str(), err)),
|
||||
};
|
||||
}
|
||||
Err(err) => return Err(format!("Bad port \"{}\": {}", port.as_str(), err)),
|
||||
},
|
||||
None => match protocol {
|
||||
// Set port based on protocol
|
||||
FileTransferProtocol::Ftp(_) => 21,
|
||||
FileTransferProtocol::Scp => 22,
|
||||
FileTransferProtocol::Sftp => 22,
|
||||
_ => 22, // Doesn't matter
|
||||
},
|
||||
};
|
||||
// Get workdir
|
||||
let entry_directory: Option<PathBuf> =
|
||||
groups.get(5).map(|group| PathBuf::from(group.as_str()));
|
||||
Ok(FileTransferParams::new(address)
|
||||
.port(port)
|
||||
.protocol(protocol)
|
||||
.username(username)
|
||||
.entry_directory(entry_directory))
|
||||
groups.get(4).map(|group| PathBuf::from(group.as_str()));
|
||||
let params: ProtocolParams = ProtocolParams::Generic(
|
||||
GenericProtocolParams::default()
|
||||
.address(address)
|
||||
.port(port)
|
||||
.username(username),
|
||||
);
|
||||
Ok(FileTransferParams::new(protocol, params).entry_directory(entry_directory))
|
||||
}
|
||||
None => Err(String::from("Bad remote host syntax!")),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### parse_s3_remote_opt
|
||||
///
|
||||
/// Parse remote options for s3 protocol
|
||||
fn parse_s3_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
||||
match REMOTE_S3_OPT_REGEX.captures(s) {
|
||||
Some(groups) => {
|
||||
let bucket: String = groups
|
||||
.get(1)
|
||||
.map(|x| x.as_str().to_string())
|
||||
.unwrap_or_default();
|
||||
let region: String = groups
|
||||
.get(2)
|
||||
.map(|x| x.as_str().to_string())
|
||||
.unwrap_or_default();
|
||||
let profile: Option<String> = groups.get(3).map(|x| x.as_str().to_string());
|
||||
let entry_directory: Option<PathBuf> =
|
||||
groups.get(4).map(|group| PathBuf::from(group.as_str()));
|
||||
Ok(FileTransferParams::new(
|
||||
FileTransferProtocol::AwsS3,
|
||||
ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)),
|
||||
)
|
||||
.entry_directory(entry_directory))
|
||||
}
|
||||
None => Err(String::from("Bad remote host syntax!")),
|
||||
}
|
||||
|
@ -470,101 +563,127 @@ mod tests {
|
|||
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22);
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some());
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 22);
|
||||
assert!(params.username.is_some());
|
||||
// User case
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22);
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.username.unwrap(), String::from("root"));
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 22);
|
||||
assert_eq!(
|
||||
params.username.as_deref().unwrap().to_string(),
|
||||
String::from("root")
|
||||
);
|
||||
assert!(result.entry_directory.is_none());
|
||||
// User + port
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 8022);
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 8022);
|
||||
assert_eq!(
|
||||
params.username.as_deref().unwrap().to_string(),
|
||||
String::from("root")
|
||||
);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.username.unwrap(), String::from("root"));
|
||||
assert!(result.entry_directory.is_none());
|
||||
// Port only
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:4022"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 4022);
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some());
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 4022);
|
||||
assert!(params.username.is_some());
|
||||
assert!(result.entry_directory.is_none());
|
||||
// Protocol
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("ftp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 21); // Fallback to ftp default
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
|
||||
assert!(result.username.is_none()); // Doesn't fall back
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 21); // Fallback to ftp default
|
||||
assert!(params.username.is_none()); // Doesn't fall back
|
||||
assert!(result.entry_directory.is_none());
|
||||
// Protocol
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("sftp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22); // Fallback to sftp default
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some()); // Doesn't fall back
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 22); // Fallback to sftp default
|
||||
assert!(params.username.is_some()); // Doesn't fall back
|
||||
assert!(result.entry_directory.is_none());
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("scp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22); // Fallback to scp default
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Scp);
|
||||
assert!(result.username.is_some()); // Doesn't fall back
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 22); // Fallback to scp default
|
||||
assert!(params.username.is_some()); // Doesn't fall back
|
||||
assert!(result.entry_directory.is_none());
|
||||
// Protocol + user
|
||||
let result: FileTransferParams =
|
||||
parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 21); // Fallback to ftp default
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Ftp(true));
|
||||
assert_eq!(result.username.unwrap(), String::from("anon"));
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 21); // Fallback to ftp default
|
||||
assert_eq!(
|
||||
params.username.as_deref().unwrap().to_string(),
|
||||
String::from("anon")
|
||||
);
|
||||
assert!(result.entry_directory.is_none());
|
||||
// Path
|
||||
let result: FileTransferParams =
|
||||
parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 8022);
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.username.unwrap(), String::from("root"));
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 8022);
|
||||
assert_eq!(
|
||||
params.username.as_deref().unwrap().to_string(),
|
||||
String::from("root")
|
||||
);
|
||||
assert_eq!(result.entry_directory.unwrap(), PathBuf::from("/var"));
|
||||
// Port only
|
||||
let result: FileTransferParams = parse_remote_opt(&String::from("172.26.104.1:home"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22);
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some());
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 22);
|
||||
assert!(params.username.is_some());
|
||||
assert_eq!(result.entry_directory.unwrap(), PathBuf::from("home"));
|
||||
// All together now
|
||||
let result: FileTransferParams =
|
||||
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.address, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 8021); // Fallback to ftp default
|
||||
let params = result.params.generic_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
|
||||
assert_eq!(result.username.unwrap(), String::from("anon"));
|
||||
assert_eq!(params.address, String::from("172.26.104.1"));
|
||||
assert_eq!(params.port, 8021); // Fallback to ftp default
|
||||
assert_eq!(
|
||||
params.username.as_deref().unwrap().to_string(),
|
||||
String::from("anon")
|
||||
);
|
||||
assert_eq!(result.entry_directory.unwrap(), PathBuf::from("/tmp"));
|
||||
// bad syntax
|
||||
// Bad protocol
|
||||
|
@ -573,6 +692,56 @@ mod tests {
|
|||
assert!(parse_remote_opt(&String::from("scp://172.26.104.1:650000")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_aws_s3_opt() {
|
||||
// Simple
|
||||
let result: FileTransferParams =
|
||||
parse_remote_opt(&String::from("s3://mybucket@eu-central-1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
let params = result.params.s3_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
|
||||
assert_eq!(result.entry_directory, None);
|
||||
assert_eq!(params.bucket_name.as_str(), "mybucket");
|
||||
assert_eq!(params.region.as_str(), "eu-central-1");
|
||||
assert_eq!(params.profile, None);
|
||||
// With profile
|
||||
let result: FileTransferParams =
|
||||
parse_remote_opt(&String::from("s3://mybucket@eu-central-1:default"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
let params = result.params.s3_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
|
||||
assert_eq!(result.entry_directory, None);
|
||||
assert_eq!(params.bucket_name.as_str(), "mybucket");
|
||||
assert_eq!(params.region.as_str(), "eu-central-1");
|
||||
assert_eq!(params.profile.as_deref(), Some("default"));
|
||||
// With wrkdir only
|
||||
let result: FileTransferParams =
|
||||
parse_remote_opt(&String::from("s3://mybucket@eu-central-1:/foobar"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
let params = result.params.s3_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
|
||||
assert_eq!(result.entry_directory, Some(PathBuf::from("/foobar")));
|
||||
assert_eq!(params.bucket_name.as_str(), "mybucket");
|
||||
assert_eq!(params.region.as_str(), "eu-central-1");
|
||||
assert_eq!(params.profile, None);
|
||||
// With all arguments
|
||||
let result: FileTransferParams =
|
||||
parse_remote_opt(&String::from("s3://mybucket@eu-central-1:default:/foobar"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
let params = result.params.s3_params().unwrap();
|
||||
assert_eq!(result.protocol, FileTransferProtocol::AwsS3);
|
||||
assert_eq!(result.entry_directory, Some(PathBuf::from("/foobar")));
|
||||
assert_eq!(params.bucket_name.as_str(), "mybucket");
|
||||
assert_eq!(params.region.as_str(), "eu-central-1");
|
||||
assert_eq!(params.profile.as_deref(), Some("default"));
|
||||
// -- bad args
|
||||
assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_parse_lstime() {
|
||||
// Good cases
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//!
|
||||
//! Path related utilities
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::{Component, Path, PathBuf};
|
||||
|
||||
/// ### absolutize
|
||||
///
|
||||
|
@ -24,6 +24,64 @@ pub fn absolutize(wrkdir: &Path, target: &Path) -> PathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
/// ### diff_paths
|
||||
///
|
||||
/// This function will get the difference from path `path` to `base`. Basically will remove `base` from `path`
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```rust
|
||||
/// assert_eq!(diff_paths(&Path::new("/foo/bar"), &Path::new("/")).as_path(), Path::new("foo/bar"));
|
||||
/// assert_eq!(diff_paths(&Path::new("/foo/bar"), &Path::new("/foo")).as_path(), Path::new("bar"));
|
||||
/// ```
|
||||
///
|
||||
/// This function has been written by <https://github.com/Manishearth>
|
||||
/// and is licensed under the APACHE-2/MIT license <https://github.com/Manishearth/pathdiff>
|
||||
pub fn diff_paths<P, B>(path: P, base: B) -> Option<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let base = base.as_ref();
|
||||
|
||||
if path.is_absolute() != base.is_absolute() {
|
||||
if path.is_absolute() {
|
||||
Some(PathBuf::from(path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let mut ita = path.components();
|
||||
let mut itb = base.components();
|
||||
let mut comps: Vec<Component> = vec![];
|
||||
loop {
|
||||
match (ita.next(), itb.next()) {
|
||||
(None, None) => break,
|
||||
(Some(a), None) => {
|
||||
comps.push(a);
|
||||
comps.extend(ita.by_ref());
|
||||
break;
|
||||
}
|
||||
(None, _) => comps.push(Component::ParentDir),
|
||||
(Some(a), Some(b)) if comps.is_empty() && a == b => (),
|
||||
(Some(a), Some(b)) if b == Component::CurDir => comps.push(a),
|
||||
(Some(_), Some(b)) if b == Component::ParentDir => return None,
|
||||
(Some(a), Some(_)) => {
|
||||
comps.push(Component::ParentDir);
|
||||
for _ in itb {
|
||||
comps.push(Component::ParentDir);
|
||||
}
|
||||
comps.push(a);
|
||||
comps.extend(ita.by_ref());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(comps.iter().map(|c| c.as_os_str()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
|
@ -40,4 +98,26 @@ mod test {
|
|||
Path::new("/tmp/readme.txt")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calc_diff_paths() {
|
||||
assert_eq!(
|
||||
diff_paths(&Path::new("/foo/bar"), &Path::new("/"))
|
||||
.unwrap()
|
||||
.as_path(),
|
||||
Path::new("foo/bar")
|
||||
);
|
||||
assert_eq!(
|
||||
diff_paths(&Path::new("/foo/bar"), &Path::new("/foo"))
|
||||
.unwrap()
|
||||
.as_path(),
|
||||
Path::new("bar")
|
||||
);
|
||||
assert_eq!(
|
||||
diff_paths(&Path::new("/foo/bar/chiedo.gif"), &Path::new("/"))
|
||||
.unwrap()
|
||||
.as_path(),
|
||||
Path::new("foo/bar/chiedo.gif")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,9 +28,9 @@
|
|||
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
|
||||
// ext
|
||||
use std::fs::File;
|
||||
#[cfg(feature = "with-containers")]
|
||||
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
|
||||
use std::fs::OpenOptions;
|
||||
#[cfg(feature = "with-containers")]
|
||||
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -97,7 +97,7 @@ pub fn make_dir_at(dir: &Path, dirname: &str) -> std::io::Result<()> {
|
|||
std::fs::create_dir(p.as_path())
|
||||
}
|
||||
|
||||
#[cfg(feature = "with-containers")]
|
||||
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
|
||||
pub fn write_file(file: &NamedTempFile, writable: &mut Box<dyn Write>) {
|
||||
let mut fhnd = OpenOptions::new()
|
||||
.create(false)
|
||||
|
@ -153,7 +153,8 @@ RorU9FCmS/654wAAABFyb290QDhjNTBmZDRjMzQ1YQECAw==
|
|||
/// ### make_fsentry
|
||||
///
|
||||
/// Create a FsEntry at specified path
|
||||
pub fn make_fsentry(path: PathBuf, is_dir: bool) -> FsEntry {
|
||||
pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> FsEntry {
|
||||
let path: PathBuf = path.as_ref().to_path_buf();
|
||||
match is_dir {
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
name: path.file_name().unwrap().to_string_lossy().to_string(),
|
||||
|
|
Loading…
Reference in New Issue