Merge branch '0.3.0' into main
This commit is contained in:
commit
eeed99b013
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -1,6 +1,7 @@
|
|||
# Changelog
|
||||
|
||||
- [Changelog](#changelog)
|
||||
- [0.3.0](#030)
|
||||
- [0.2.0](#020)
|
||||
- [0.1.3](#013)
|
||||
- [0.1.2](#012)
|
||||
|
@ -9,6 +10,57 @@
|
|||
|
||||
---
|
||||
|
||||
## 0.3.0
|
||||
|
||||
Released on 10/01/2021
|
||||
|
||||
> The SSH Key Storage Update
|
||||
|
||||
- **SSH Key Storage**
|
||||
- Added the possibility to store SSH private keys to access to remote hosts; this feature is supported in both SFTP and SCP.
|
||||
- SSH Keys can be manipulated through the new **Setup Interface**
|
||||
- **Setup Interface**
|
||||
- Added a new area in the interface, where is possible to customize termscp. Access to this interface is achieved pressing `<CTRL+C>` from the home page (`AuthActivity`).
|
||||
- **Configuration**:
|
||||
- Added configuration; configuration is stored at
|
||||
- Linux: `/home/alice/.config/termscp/config.toml`
|
||||
- MacOS: `/Users/Alice/Library/Application Support/termscp/config.toml`
|
||||
- Windows: `C:\Users\Alice\AppData\Roaming\termscp\config.toml`
|
||||
- Added Text editor to configuration
|
||||
- Added Default File transfer protocol to configuration
|
||||
- Added "Show hidden files" to configuration
|
||||
- Added "Group directories" to configuration
|
||||
- Added SSH keys to configuration; SSH keys will be stored at
|
||||
- Linux: `/home/alice/.config/termscp/.ssh/`
|
||||
- MacOS: `/Users/Alice/Library/Application Support/termscp/.ssh/`
|
||||
- Windows: `C:\Users\Alice\AppData\Roaming\termscp\.ssh\`
|
||||
- Enhancements:
|
||||
- Replaced `sha256` sum with last modification time check, to verify if a file has been changed in the text editor
|
||||
- **FTP**
|
||||
- Added `LIST` command parser for Windows server (DOS-like syntax)
|
||||
- Default protocol changed to default protocol in configuration when providing address as CLI argument
|
||||
- Explorers:
|
||||
- Hidden files are now not shown by default; use `A` to show hidden files.
|
||||
- Append `/` to directories name.
|
||||
- Keybindings:
|
||||
- `A`: Toggle hidden files
|
||||
- `B`: Sort files by (name, size, creation time, modify time)
|
||||
- `N`: New file
|
||||
- Bugfix:
|
||||
- SCP client didn't show file types for files
|
||||
- FTP client didn't show file types for files
|
||||
- FTP file transfer not working properly with `STOR` and `RETR`.
|
||||
- Fixed `0 B/S` transfer rate displayed after completing download in less than 1 second
|
||||
- Dependencies:
|
||||
- added `bitflags 1.2.1`
|
||||
- removed `data-encoding`
|
||||
- updated `ftp` to `4.0.2`
|
||||
- updated `rand` to `0.8.0`
|
||||
- removed `ring`
|
||||
- updated `textwrap` to `0.13.1`
|
||||
- updated `toml` to `0.5.8`
|
||||
- updated `whoami` to `1.0.1`
|
||||
|
||||
## 0.2.0
|
||||
|
||||
Released on 21/12/2020
|
||||
|
@ -19,8 +71,8 @@ Released on 21/12/2020
|
|||
- Bookmarks and recent connections are now displayed in the home page
|
||||
- Bookmarks are saved at
|
||||
- Linux: `/home/alice/.config/termscp/bookmarks.toml`
|
||||
- Windows: `C:\Users\Alice\AppData\Roaming\termscp\bookmarks.toml`
|
||||
- MacOS: `/Users/Alice/Library/Application Support/termscp/bookmarks.toml`
|
||||
- Windows: `C:\Users\Alice\AppData\Roaming\termscp\bookmarks.toml`
|
||||
- **Text Editor**
|
||||
- Added text editor feature to explorer view
|
||||
- Added `o` to keybindings to open a text file
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Contributing
|
||||
|
||||
Before contributing to this repository, please first discuss the change you wish to make via issue of this repository before making a change.
|
||||
Please note we have a [code of conduct](./CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
|
||||
Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.
|
||||
|
||||
- [Contributing](#contributing)
|
||||
- [Preferred contributions](#preferred-contributions)
|
||||
|
@ -19,7 +19,6 @@ At the moment, these kind of contributions are more appreciated and should be pr
|
|||
|
||||
- Fix for issues described in [Known Issues](./README.md#known-issues) or [issues reported by the community](https://github.com/veeso/termscp/issues)
|
||||
- New file transfers: for further details see [Implementing File Transfer](#implementing-file-transfers)
|
||||
- Improvements to translators: any improvement to transliteration is accepted if makes sense, consider that my implementations could be not 100% correct (and probably they're not), indeed consider that I don't speak all these languages (tbh I only can speak Russian as a language with a different alphabet from latin - and I can't even speak it very well).
|
||||
- Code optimizations: any optimization to the code is welcome
|
||||
|
||||
For any other kind of contribution, especially for new features, please submit an issue first.
|
||||
|
@ -30,9 +29,9 @@ Let's make it simple and clear:
|
|||
|
||||
1. Open an issue with an **appropriate label** (e.g. bug, enhancement, ...).
|
||||
2. Write a **properly documentation** compliant with **rustdoc** standard.
|
||||
3. Write tests for your code. This doesn't apply necessarily for implementation regarding the user-interface module (`ui`).
|
||||
3. Write tests for your code. This doesn't apply necessarily for implementation regarding the user-interface module (`ui`) and (if a test server is not available) for file transfers.
|
||||
4. Report changes to the issue you opened, writing a report of what you changed and what you have introduced.
|
||||
5. Update the `CHANGELOG.md` file with details of changes to the application.
|
||||
5. Update the `CHANGELOG.md` file with details of changes to the application. In changelog report changes under a chapter called `PR{PULL_REQUEST_NUMBER}` (e.g. PR12).
|
||||
6. Request maintainers to merge your changes.
|
||||
|
||||
## Developer contributions guide
|
||||
|
|
210
Cargo.lock
generated
210
Cargo.lock
generated
|
@ -125,9 +125,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
|
|||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.3.4"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
|
||||
checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b"
|
||||
|
||||
[[package]]
|
||||
name = "bytesize"
|
||||
|
@ -272,12 +272,6 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "993a608597367c6377b258c25d7120740f00ed23a2252b729b1932dd7866f908"
|
||||
|
||||
[[package]]
|
||||
name = "debug-helper"
|
||||
version = "0.3.10"
|
||||
|
@ -360,9 +354,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
|||
|
||||
[[package]]
|
||||
name = "ftp4"
|
||||
version = "4.0.1"
|
||||
version = "4.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6318bd155755b6e07ccb7bf8e5b1b7cb221c74fff7c6440692ef38eb2ec1d42c"
|
||||
checksum = "7e03634a7a0e74618f9adf1e088495caa54ea07e72d449813e6439ce8ac9906f"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
|
@ -409,15 +403,26 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.1.15"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
|
||||
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4060f4657be78b8e766215b02b18a2e862d83745545de804638e2b545e81aee6"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hostname"
|
||||
version = "0.3.1"
|
||||
|
@ -455,9 +460,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.81"
|
||||
version = "0.2.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
|
||||
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
|
||||
|
||||
[[package]]
|
||||
name = "libssh2-sys"
|
||||
|
@ -555,9 +560,9 @@ checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
|
|||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.7.6"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f33bc887064ef1fd66020c9adfc45bb9f33d75a42096c81e7c56c65b75dd1a8b"
|
||||
checksum = "e50ae3f04d169fcc9bde0b547d1c205219b7157e07ded9c5aff03e0637cb3ed7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
|
@ -578,9 +583,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.6"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6fcc7939b5edc4e4f86b1b4a04bb1498afaaf871b1a6691838ed06fcb48d3a3f"
|
||||
checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
|
@ -622,12 +627,6 @@ dependencies = [
|
|||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.0"
|
||||
|
@ -636,12 +635,12 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
|||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.30"
|
||||
version = "0.10.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d575eff3665419f9b83678ff2815858ad9d11567e082f5ac1814baba4e2bcb4"
|
||||
checksum = "038d43985d1ddca7a9900630d8cd031b56e4794eecc2e9ea39dd17aa04399a70"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if 0.1.10",
|
||||
"cfg-if 1.0.0",
|
||||
"foreign-types",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
|
@ -656,9 +655,9 @@ checksum = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
|
|||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.58"
|
||||
version = "0.9.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a842db4709b604f0fe5d1170ae3565899be2ad3d9cbc72dedc789ac0511f78de"
|
||||
checksum = "921fc71883267538946025deffb622905ecad223c28efbfdef9bb59a0175f3e6"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
|
@ -685,7 +684,7 @@ checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
|
|||
dependencies = [
|
||||
"instant",
|
||||
"lock_api 0.4.2",
|
||||
"parking_lot_core 0.8.1",
|
||||
"parking_lot_core 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -704,9 +703,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "parking_lot_core"
|
||||
version = "0.8.1"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7c6d9b8427445284a09c55be860a15855ab580a417ccad9da88f5a06787ced0"
|
||||
checksum = "9ccb628cad4f84851442432c60ad8e1f607e29752d0bf072cbd0baf28aa34272"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"instant",
|
||||
|
@ -739,9 +738,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.7"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
|
||||
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
@ -752,11 +751,23 @@ version = "0.7.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.1.16",
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
"rand_hc",
|
||||
"rand_chacha 0.2.2",
|
||||
"rand_core 0.5.1",
|
||||
"rand_hc 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c24fcd450d3fa2b592732565aa4f17a27a61c65ece4726353e000939b0edee34"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha 0.3.0",
|
||||
"rand_core 0.6.1",
|
||||
"rand_hc 0.3.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -766,7 +777,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core 0.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -775,7 +796,16 @@ version = "0.5.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.1.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c026d7df8b298d90ccbbc5190bd04d85e159eaf5576caeacf8741da93ccbd2e5"
|
||||
dependencies = [
|
||||
"getrandom 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -784,7 +814,16 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
|
||||
dependencies = [
|
||||
"rand_core",
|
||||
"rand_core 0.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_hc"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
|
||||
dependencies = [
|
||||
"rand_core 0.6.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -799,16 +838,16 @@ version = "0.3.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de0737333e7a9502c789a36d7c7fa6092a49895d4faa31ca5df163857ded2e9d"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.1.16",
|
||||
"redox_syscall",
|
||||
"rust-argon2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.4.2"
|
||||
version = "1.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c"
|
||||
checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
|
@ -818,9 +857,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.21"
|
||||
version = "0.6.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189"
|
||||
checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
|
@ -831,21 +870,6 @@ dependencies = [
|
|||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "024a1e66fea74c66c66624ee5622a7ff0e4b73a13b4f5c326ddb50c708944226"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "5.0.0"
|
||||
|
@ -942,9 +966,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.1.16"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "604508c1418b99dfe1925ca9224829bb2a8a9a04dda655cc01fcad46f4ab05ed"
|
||||
checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
|
@ -953,18 +977,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.2.2"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce32ea0c6c56d5eacaeb814fbed9960547021d3edd010ded1425f180536b20ab"
|
||||
checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.5.1"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75"
|
||||
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
|
@ -974,22 +998,15 @@ checksum = "e1bc737c97d093feb72e67f4926d9b22d717ce8580cd25f0ce86d74e859c466d"
|
|||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.3.17"
|
||||
version = "0.3.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c29947abdee2a218277abeca306f25789c938e500ea5a9d4b12a5a504466902"
|
||||
checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "ssh2"
|
||||
version = "0.9.0"
|
||||
|
@ -1004,9 +1021,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.53"
|
||||
version = "1.0.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8833e20724c24de12bbaba5ad230ea61c3eafb05b881c7c9d3cfe8638b187e68"
|
||||
checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1021,7 +1038,7 @@ checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9"
|
|||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"libc",
|
||||
"rand",
|
||||
"rand 0.7.3",
|
||||
"redox_syscall",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
|
@ -1029,13 +1046,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "termscp"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bytesize",
|
||||
"chrono",
|
||||
"content_inspector",
|
||||
"crossterm",
|
||||
"data-encoding",
|
||||
"dirs",
|
||||
"edit",
|
||||
"ftp4",
|
||||
|
@ -1043,9 +1060,8 @@ dependencies = [
|
|||
"hostname",
|
||||
"lazy_static",
|
||||
"magic-crypt",
|
||||
"rand",
|
||||
"rand 0.8.1",
|
||||
"regex",
|
||||
"ring",
|
||||
"rpassword",
|
||||
"serde",
|
||||
"ssh2",
|
||||
|
@ -1060,9 +1076,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.13.0"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1bca196a5c5a7bc57a5c92809cf5670e16bcbca3bf0d09ef47150bf97221f6f"
|
||||
checksum = "3cdcf6b66102d38821c33eea2bf1e8b7bd738072171cbf8a0683fbb46fcb8b0b"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
"unicode-width",
|
||||
|
@ -1070,9 +1086,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
|
||||
checksum = "bb9bc092d0d51e76b2b19d9d85534ffc9ec2db959a2523cdae0697e2972cd447"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
@ -1101,9 +1117,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.7"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645"
|
||||
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
@ -1145,12 +1161,6 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
|
||||
|
||||
[[package]]
|
||||
name = "users"
|
||||
version = "0.11.0"
|
||||
|
@ -1163,9 +1173,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.10"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
|
||||
checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
|
@ -1260,9 +1270,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "whoami"
|
||||
version = "1.0.0"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0b81b8c1996f45197d8aad89b388f0a419afb4c5c876a23006d2d1435cb82d8"
|
||||
checksum = "d595b2e146f36183d6a590b8d41568e2bc84c922267f43baf61c956330eeb436"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
|
|
18
Cargo.toml
18
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "termscp"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
authors = ["Christian Visintin"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0"
|
||||
|
@ -15,37 +15,33 @@ readme = "README.md"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bitflags = "1.2.1"
|
||||
bytesize = "1.0.1"
|
||||
chrono = "0.4.19"
|
||||
content_inspector = "0.2.4"
|
||||
crossterm = "0.18.2"
|
||||
dirs = "3.0.1"
|
||||
edit = "0.1.2"
|
||||
ftp4 = { version = "^4.0.1", features = ["secure"] }
|
||||
ftp4 = { version = "^4.0.2", features = ["secure"] }
|
||||
getopts = "0.2.21"
|
||||
hostname = "0.3.1"
|
||||
lazy_static = "1.4.0"
|
||||
magic-crypt = "3.1.6"
|
||||
rand = "0.7.3"
|
||||
rand = "0.8.0"
|
||||
regex = "1.4.2"
|
||||
rpassword = "5.0.0"
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
ssh2 = "0.9.0"
|
||||
tempfile = "3.1.0"
|
||||
textwrap = "0.13.0"
|
||||
toml = "0.5.7"
|
||||
textwrap = "0.13.1"
|
||||
toml = "0.5.8"
|
||||
tui = { version = "0.13.0", features = ["crossterm"], default-features = false }
|
||||
unicode-width = "0.1.7"
|
||||
whoami = "1.0.0"
|
||||
ring = "0.16.19"
|
||||
data-encoding = "2.3.1"
|
||||
whoami = "1.0.1"
|
||||
|
||||
[target.'cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))'.dependencies]
|
||||
users = "0.11.0"
|
||||
|
||||
#[patch.crates-io]
|
||||
#ftp = { git = "https://github.com/ChristianVisintin/rust-ftp" }
|
||||
|
||||
[[bin]]
|
||||
name = "termscp"
|
||||
path = "src/main.rs"
|
||||
|
|
4
LICENSE
4
LICENSE
|
@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
|
|||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
TermSCP
|
||||
Copyright (C) 2020 Christian Visintin
|
||||
Copyright (C) 2020-2021 Christian Visintin
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
|||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
TermSCP Copyright (C) 2020 Christian Visintin
|
||||
TermSCP Copyright (C) 2020-2021 Christian Visintin
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
|
136
README.md
136
README.md
|
@ -1,12 +1,12 @@
|
|||
# TermSCP
|
||||
|
||||
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.2.0-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
|
||||
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.3.0-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
|
||||
|
||||
[![Build](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions) [![codecov](https://codecov.io/gh/veeso/termscp/branch/main/graph/badge.svg?token=au67l7nQah)](https://codecov.io/gh/veeso/termscp)
|
||||
|
||||
~ Basically, WinSCP on a terminal ~
|
||||
Developed by Christian Visintin
|
||||
Current version: 0.2.0 (21/12/2020)
|
||||
Current version: 0.3.0 (10/01/2021)
|
||||
|
||||
---
|
||||
|
||||
|
@ -28,6 +28,8 @@ Current version: 0.2.0 (21/12/2020)
|
|||
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
|
||||
- [Text Editor ✏](#text-editor-)
|
||||
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
|
||||
- [Configuration ⚙️](#configuration-️)
|
||||
- [SSH Key Storage 🔐](#ssh-key-storage-)
|
||||
- [Keybindings ⌨](#keybindings-)
|
||||
- [Documentation 📚](#documentation-)
|
||||
- [Known issues 🧻](#known-issues-)
|
||||
|
@ -50,7 +52,7 @@ TermSCP is basically a porting of WinSCP to terminal. So basically is a terminal
|
|||
|
||||
### Why TermSCP 🤔
|
||||
|
||||
It happens quite often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me then to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there midnight commander too, but actually I don't like it very much tbh (and hasn't a decent support for scp).
|
||||
It happens quite often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me then to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there is midnight commander too, but actually I don't like it very much tbh (and hasn't a decent support for scp).
|
||||
|
||||
## Features 🎁
|
||||
|
||||
|
@ -61,9 +63,12 @@ It happens quite often to me, when using SCP at work to forget the path of a fil
|
|||
- Practical user interface to explore and operate on the remote and on the local machine file system
|
||||
- Bookmarks and recent connections can be saved to access quickly to your favourite hosts
|
||||
- Supports text editors to view and edit text files
|
||||
- Supports both SFTP/SCP authentication through SSH keys and username/password
|
||||
- User customization directly from the user interface
|
||||
- Compatible with Windows, Linux, BSD and MacOS
|
||||
- Written in Rust
|
||||
- Easy to extend with new file transfers protocols
|
||||
- Developed keeping an eye on performance
|
||||
|
||||
---
|
||||
|
||||
|
@ -81,8 +86,8 @@ cargo install termscp
|
|||
|
||||
### Deb package 📦
|
||||
|
||||
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.2.0_amd64.deb)
|
||||
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.2.0_amd64.deb`
|
||||
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.0_amd64.deb)
|
||||
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.0_amd64.deb`
|
||||
|
||||
then install through dpkg:
|
||||
|
||||
|
@ -94,8 +99,8 @@ gdebi termscp_*.deb
|
|||
|
||||
### RPM package 📦
|
||||
|
||||
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.2.0-1.x86_64.rpm)
|
||||
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.2.0-1.x86_64.rpm`
|
||||
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.0-1.x86_64.rpm)
|
||||
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.0-1.x86_64.rpm`
|
||||
|
||||
then install through rpm:
|
||||
|
||||
|
@ -121,7 +126,7 @@ Start PowerShell as administrator and run
|
|||
choco install termscp
|
||||
```
|
||||
|
||||
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.2.0.nupkg)
|
||||
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.3.0.nupkg)
|
||||
|
||||
and then with PowerShell started with administrator previleges, run:
|
||||
|
||||
|
@ -164,13 +169,13 @@ The address argument has the following syntax:
|
|||
|
||||
Let's see some example of this particular syntax, since it's very comfortable and you'll probably going to use this instead of the other one...
|
||||
|
||||
- Connect using default protocol (sftp) to 192.168.1.31, port is default for this protocol (22); username is current user's name
|
||||
- Connect using default protocol (*defined in configuration*) to 192.168.1.31, port is default for this protocol (22); username is current user's name
|
||||
|
||||
```sh
|
||||
termscp 192.168.1.31
|
||||
```
|
||||
|
||||
- Connect using default protocol (sftp) to 192.168.1.31, port is default for this protocol (22); username is `root`
|
||||
- Connect using default protocol (*defined in configuration*) to 192.168.1.31, port is default for this protocol (22); username is `root`
|
||||
|
||||
```sh
|
||||
termscp root@192.168.1.31
|
||||
|
@ -201,9 +206,9 @@ This feature allows you to load all the parameters required to connect to a cert
|
|||
|
||||
Bookmarks will be saved, if possible at:
|
||||
|
||||
- `$HOME/.config/termscp/` on Linux
|
||||
- `FOLDERID_RoamingAppData\termscp\` on Windows
|
||||
- `$HOME/.config/termscp/` on Linux/BSD
|
||||
- `$HOME/Library/Application Support/termscp` on MacOs
|
||||
- `FOLDERID_RoamingAppData\termscp\` on Windows
|
||||
|
||||
For bookmarks only (this won't apply to recent hosts) it is also possible to save the password used to authenticate. The password is not saved by default and must be specified through the prompt when saving a new Bookmark.
|
||||
|
||||
|
@ -228,46 +233,82 @@ As said before, bookmarks are saved in your configuration directory along with p
|
|||
## Text Editor ✏
|
||||
|
||||
TermSCP has, as you might have noticed, many features, one of these is the possibility to view and edit text file. It doesn't matter if the file is located on the local host or on the remote host, termscp provides the possibility to open a file in your favourite text editor.
|
||||
In case the file is located on remote host, the file will be first downloaded into your temporary file directory and then, **only** if changes were made to the file, re-uploaded to the remote host. TermSCP checks if you made changes to the file calculating the digest of the file using `sha256`.
|
||||
In case the file is located on remote host, the file will be first downloaded into your temporary file directory and then, **only** if changes were made to the file, re-uploaded to the remote host. TermSCP checks if you made changes to the file verifying the last modification time of the file.
|
||||
|
||||
Just a reminder: **you can edit only textual file**; binary files are not supported.
|
||||
|
||||
### How do I configure the text editor 🦥
|
||||
|
||||
Text editor is automatically found using this [awesome crate](https://github.com/milkey-mouse/edit), if you want to change the text editor it has chosen for you, just set the `EDITOR` variable in your environment.
|
||||
Text editor is automatically found using this [awesome crate](https://github.com/milkey-mouse/edit), if you want to change the text editor to use, change it in termscp configuration. [View more](#configuration-️)
|
||||
|
||||
> This mechanism will probably change in 0.3.0, since I'm going to introduce the possibility to configure directly in termscp's settings.
|
||||
---
|
||||
|
||||
## Configuration ⚙️
|
||||
|
||||
TermSCP supports some user defined parameters, which can be defined in the configuration.
|
||||
Underhood termscp has a TOML file and some other directories where all the parameters will be saved, but don't worry, you won't touch any of these files, since I made possible to configure termscp from its user interface entirely.
|
||||
|
||||
termscp, like for bookmarks, just requires to have these paths accessible:
|
||||
|
||||
- `$HOME/.config/termscp/` on Linux/BSD
|
||||
- `$HOME/Library/Application Support/termscp` on MacOs
|
||||
- `FOLDERID_RoamingAppData\termscp\` on Windows
|
||||
|
||||
To access configuration, you just have to press `<CTRL+C>` from the home of termscp.
|
||||
|
||||
These parameters can be changed:
|
||||
|
||||
- **Default Protocol**: the default protocol is the default value for the file transfer protocol to be used in termscp. This applies for the login page and for the address CLI argument.
|
||||
- **Text Editor**: the text editor to use. By default termscp will find the default editor for you; with this option you can force an editor to be used (e.g. `vim`). **Also GUI editors are supported**, unless they `nohup` from the parent process so if you ask: yes, you can use `notepad.exe`, and no: **Visual Studio Code doesn't work**.
|
||||
- **Show Hidden Files**: select whether hidden files shall be displayed by default. You will be able to decide whether to show or not hidden files at runtime pressing `A` anyway.
|
||||
- **Group Dirs**: select whether directories should be groupped or not in file explorers. If `Display first` is selected, directories will be sorted using the configured method but displayed before files, viceversa if `Display last` is selected.
|
||||
|
||||
### SSH Key Storage 🔐
|
||||
|
||||
Along with configuration, termscp provides also an **essential** feature for **SFTP/SCP clients**: the SSH key storage.
|
||||
|
||||
You can access the SSH key storage, from configuration moving to the `SSH Keys` tab, once there you can:
|
||||
|
||||
- **Add a new key**: just press `<CTRL+N>` and you will be prompted to create a new key. Provide the hostname/ip address and the username associated to the key and finally a text editor will open up: paste the **PRIVATE** ssh key into the text editor, save and quit.
|
||||
- **Remove an existing key**: just press `<DEL>` or `<CTRL+E>` on the key you want to remove, to delete persistently the key from termscp.
|
||||
- **Edit an existing key**: just press `<ENTER>` on the key you want to edit, to change the private key.
|
||||
|
||||
> Q: Wait, my private key is protected with password, can I use it?
|
||||
> A: Of course you can. The password provided for authentication in termscp, is valid both for username/password authentication and for RSA key authentication.
|
||||
|
||||
---
|
||||
|
||||
## Keybindings ⌨
|
||||
|
||||
| Key | Command | Reminder |
|
||||
|---------------|-------------------------------------------------------|-----------|
|
||||
| `<ESC>` | Disconnect from remote; return to authentication page | |
|
||||
| `<TAB>` | Switch between log tab and explorer | |
|
||||
| `<BACKSPACE>` | Go to previous directory in stack | |
|
||||
| `<RIGHT>` | Move to remote explorer tab | |
|
||||
| `<LEFT>` | Move to local explorer tab | |
|
||||
| `<UP>` | Move up in selected list | |
|
||||
| `<DOWN>` | Move down in selected list | |
|
||||
| `<PGUP>` | Move up in selected list by 8 rows | |
|
||||
| `<PGDOWN>` | Move down in selected list by 8 rows | |
|
||||
| `<ENTER>` | Enter directory | |
|
||||
| `<SPACE>` | Upload / download selected file | |
|
||||
| `<C>` | Copy file/directory | Copy |
|
||||
| `<D>` | Make directory | Directory |
|
||||
| `<E>` | Delete file (Same as `CANC`) | Erase |
|
||||
| `<G>` | Go to supplied path | Go to |
|
||||
| `<H>` | Show help | Help |
|
||||
| `<I>` | Show info about selected file or directory | Info |
|
||||
| `<L>` | Reload current directory's content | List |
|
||||
| `<O>` | Edit file; see [Text editor](#text-editor-) | Open |
|
||||
| `<Q>` | Quit TermSCP | Quit |
|
||||
| `<R>` | Rename file | Rename |
|
||||
| `<U>` | Go to parent directory | Upper |
|
||||
| `<DEL>` | Delete file | |
|
||||
| `<CTRL+C>` | Abort file transfer process | |
|
||||
| Key | Command | Reminder |
|
||||
|---------------|-------------------------------------------------------|-------------|
|
||||
| `<ESC>` | Disconnect from remote; return to authentication page | |
|
||||
| `<TAB>` | Switch between log tab and explorer | |
|
||||
| `<BACKSPACE>` | Go to previous directory in stack | |
|
||||
| `<RIGHT>` | Move to remote explorer tab | |
|
||||
| `<LEFT>` | Move to local explorer tab | |
|
||||
| `<UP>` | Move up in selected list | |
|
||||
| `<DOWN>` | Move down in selected list | |
|
||||
| `<PGUP>` | Move up in selected list by 8 rows | |
|
||||
| `<PGDOWN>` | Move down in selected list by 8 rows | |
|
||||
| `<ENTER>` | Enter directory | |
|
||||
| `<SPACE>` | Upload / download selected file | |
|
||||
| `<A>` | Toggle hidden files | All |
|
||||
| `<B>` | Sort files by | Bubblesort? |
|
||||
| `<C>` | Copy file/directory | Copy |
|
||||
| `<D>` | Make directory | Directory |
|
||||
| `<E>` | Delete file (Same as `DEL`) | Erase |
|
||||
| `<G>` | Go to supplied path | Go to |
|
||||
| `<H>` | Show help | Help |
|
||||
| `<I>` | Show info about selected file or directory | Info |
|
||||
| `<L>` | Reload current directory's content | List |
|
||||
| `<N>` | Create new file with provided name | New |
|
||||
| `<O>` | Edit file; see [Text editor](#text-editor-) | Open |
|
||||
| `<Q>` | Quit TermSCP | Quit |
|
||||
| `<R>` | Rename file | Rename |
|
||||
| `<U>` | Go to parent directory | Upper |
|
||||
| `<DEL>` | Delete file | |
|
||||
| `<CTRL+C>` | Abort file transfer process | |
|
||||
|
||||
---
|
||||
|
||||
|
@ -279,17 +320,13 @@ The developer documentation can be found on Rust Docs at <https://docs.rs/termsc
|
|||
|
||||
## Known issues 🧻
|
||||
|
||||
- Ftp:
|
||||
- Time in explorer is `1 Jan 1970`, but shouldn't be: that's because chrono can't parse date in a different locale. So if your server has a locale different from the one on your machine, it won't be able to parse the date.
|
||||
- Some servers don't work: yes, some kind of ftp server don't work correctly, sometimes it won't display any files in the directories, some other times uploading files will fail. Up to date, `vsftpd` is the only one server which I saw working correctly with TermSCP. Am I going to solve this? I'd like to, but it's not my fault at all. Unfortunately [rust-ftp](https://github.com/mattnenterprise/rust-ftp) is an abandoned project (up to 2020), indeed I had to patch many stuff by myself. I'll try to solve these issues, but it will take a long time.
|
||||
- `NoSuchFileOrDirectory` on connect: let me guess, you're running on WSL and you've installed termscp through cargo. I know about this issue and it's a glitch of WSL I guess. Don't worry about it, just move the termscp executable into another PATH location, such as `/usr/bin`.
|
||||
- `NoSuchFileOrDirectory` on connect (WSL): I know about this issue and it's a glitch of WSL I guess. Don't worry about it, just move the termscp executable into another PATH location, such as `/usr/bin`, or install it through the appropriate package format (e.g. deb).
|
||||
|
||||
---
|
||||
|
||||
## Upcoming Features 🧪
|
||||
|
||||
- **SSH Key storage**: termscp 0.3.0 will (finally) support the SSH key storage. From the configuration interface, you will be able to add SSH keys to the termscp's storage as you do indeed with other similiar clients.
|
||||
- **User customizations**: termscp 0.3.0 will support some user customizations, such as the possibility to setup the text editor directly from termscp and the default communication protocol. Everything will be configurable directly from the termscp user interface.
|
||||
- **Custom explorer format**: possibility to customize the file line in the explorer directly from configuration, with the possibility to choose with information to display.
|
||||
- **Find command in explorer**: possibility to search for files in explorers.
|
||||
|
||||
---
|
||||
|
@ -314,6 +351,7 @@ TermSCP is powered by these aweseome projects:
|
|||
- [crossterm](https://github.com/crossterm-rs/crossterm)
|
||||
- [edit](https://github.com/milkey-mouse/edit)
|
||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||
- [rust-ftp](https://github.com/mattnenterprise/rust-ftp)
|
||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
||||
- [textwrap](https://github.com/mgeisler/textwrap)
|
||||
- [tui-rs](https://github.com/fdehau/tui-rs)
|
||||
|
@ -331,6 +369,10 @@ TermSCP is powered by these aweseome projects:
|
|||
|
||||
![Bookmarks](assets/images/bookmarks.gif)
|
||||
|
||||
> Setup
|
||||
|
||||
![Setup](assets/images/config.gif)
|
||||
|
||||
> Text editor
|
||||
|
||||
![TextEditor](assets/images/text-editor.gif)
|
||||
|
|
BIN
assets/images/config.gif
Normal file
BIN
assets/images/config.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 689 KiB |
|
@ -3,3 +3,5 @@ ignore:
|
|||
- src/lib.rs
|
||||
- src/activity_manager.rs
|
||||
- src/ui/
|
||||
fixes:
|
||||
- "/::"
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -29,9 +29,8 @@ use std::path::PathBuf;
|
|||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::host::Localhost;
|
||||
use crate::ui::activities::{
|
||||
auth_activity::AuthActivity,
|
||||
filetransfer_activity::FileTransferActivity, filetransfer_activity::FileTransferParams,
|
||||
Activity,
|
||||
auth_activity::AuthActivity, filetransfer_activity::FileTransferActivity,
|
||||
filetransfer_activity::FileTransferParams, setup_activity::SetupActivity, Activity,
|
||||
};
|
||||
use crate::ui::context::Context;
|
||||
|
||||
|
@ -45,6 +44,7 @@ use std::time::Duration;
|
|||
pub enum NextActivity {
|
||||
Authentication,
|
||||
FileTransfer,
|
||||
SetupActivity,
|
||||
}
|
||||
|
||||
/// ### ActivityManager
|
||||
|
@ -60,10 +60,7 @@ impl ActivityManager {
|
|||
/// ### new
|
||||
///
|
||||
/// Initializes a new Activity Manager
|
||||
pub fn new(
|
||||
local_dir: &PathBuf,
|
||||
interval: Duration,
|
||||
) -> Result<ActivityManager, ()> {
|
||||
pub fn new(local_dir: &PathBuf, interval: Duration) -> Result<ActivityManager, ()> {
|
||||
// Prepare Context
|
||||
let host: Localhost = match Localhost::new(local_dir.clone()) {
|
||||
Ok(h) => h,
|
||||
|
@ -109,6 +106,7 @@ impl ActivityManager {
|
|||
Some(activity) => match activity {
|
||||
NextActivity::Authentication => self.run_authentication(),
|
||||
NextActivity::FileTransfer => self.run_filetransfer(),
|
||||
NextActivity::SetupActivity => self.run_setup(),
|
||||
},
|
||||
None => break, // Exit
|
||||
}
|
||||
|
@ -126,13 +124,13 @@ impl ActivityManager {
|
|||
/// Returns the next activity to run
|
||||
fn run_authentication(&mut self) -> Option<NextActivity> {
|
||||
// Prepare activity
|
||||
let mut activity: AuthActivity = AuthActivity::new();
|
||||
let mut activity: AuthActivity = AuthActivity::default();
|
||||
// Prepare result
|
||||
let result: Option<NextActivity>;
|
||||
// Get context
|
||||
let ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
None => return None
|
||||
None => return None,
|
||||
};
|
||||
// Create activity
|
||||
activity.on_create(ctx);
|
||||
|
@ -145,6 +143,11 @@ impl ActivityManager {
|
|||
result = None;
|
||||
break;
|
||||
}
|
||||
if activity.setup {
|
||||
// User requested activity
|
||||
result = Some(NextActivity::SetupActivity);
|
||||
break;
|
||||
}
|
||||
if activity.submit {
|
||||
// User submitted, set next activity
|
||||
result = Some(NextActivity::FileTransfer);
|
||||
|
@ -189,7 +192,7 @@ impl ActivityManager {
|
|||
// Get context
|
||||
let ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
None => return None
|
||||
None => return None,
|
||||
};
|
||||
// Create activity
|
||||
activity.on_create(ctx);
|
||||
|
@ -214,4 +217,35 @@ impl ActivityManager {
|
|||
self.context = activity.on_destroy();
|
||||
result
|
||||
}
|
||||
|
||||
/// ### run_setup
|
||||
///
|
||||
/// `SetupActivity` run loop.
|
||||
/// Returns when activity terminates.
|
||||
/// Returns the next activity to run
|
||||
fn run_setup(&mut self) -> Option<NextActivity> {
|
||||
// Prepare activity
|
||||
let mut activity: SetupActivity = SetupActivity::default();
|
||||
// Get context
|
||||
let ctx: Context = match self.context.take() {
|
||||
Some(ctx) => ctx,
|
||||
None => return None,
|
||||
};
|
||||
// Create activity
|
||||
activity.on_create(ctx);
|
||||
loop {
|
||||
// Draw activity
|
||||
activity.on_draw();
|
||||
// Check if activity has terminated
|
||||
if activity.quit {
|
||||
break;
|
||||
}
|
||||
// Sleep for ticks
|
||||
sleep(self.interval);
|
||||
}
|
||||
// Destroy activity
|
||||
self.context = activity.on_destroy();
|
||||
// This activity always returns to AuthActivity
|
||||
Some(NextActivity::Authentication)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
|
230
src/config/mod.rs
Normal file
230
src/config/mod.rs
Normal file
|
@ -0,0 +1,230 @@
|
|||
//! ## Config
|
||||
//!
|
||||
//! `config` is the module which provides access to termscp configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Modules
|
||||
pub mod serializer;
|
||||
|
||||
// Deps
|
||||
extern crate edit;
|
||||
|
||||
// Locals
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
|
||||
// Ext
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## UserConfig
|
||||
///
|
||||
/// UserConfig contains all the configurations for the user,
|
||||
/// supported by termscp
|
||||
pub struct UserConfig {
|
||||
pub user_interface: UserInterfaceConfig,
|
||||
pub remote: RemoteConfig,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## UserInterfaceConfig
|
||||
///
|
||||
/// UserInterfaceConfig provides all the keys to configure the user interface
|
||||
pub struct UserInterfaceConfig {
|
||||
pub text_editor: PathBuf,
|
||||
pub default_protocol: String,
|
||||
pub show_hidden_files: bool,
|
||||
pub group_dirs: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## RemoteConfig
|
||||
///
|
||||
/// Contains configuratio related to remote hosts
|
||||
pub struct RemoteConfig {
|
||||
pub ssh_keys: HashMap<String, PathBuf>, // Association between host name and path to private key
|
||||
}
|
||||
|
||||
impl Default for UserConfig {
|
||||
fn default() -> Self {
|
||||
UserConfig {
|
||||
user_interface: UserInterfaceConfig::default(),
|
||||
remote: RemoteConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UserInterfaceConfig {
|
||||
fn default() -> Self {
|
||||
UserInterfaceConfig {
|
||||
text_editor: match edit::get_editor() {
|
||||
Ok(p) => p,
|
||||
Err(_) => PathBuf::from("nano"), // Default to nano
|
||||
},
|
||||
default_protocol: FileTransferProtocol::Sftp.to_string(),
|
||||
show_hidden_files: false,
|
||||
group_dirs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RemoteConfig {
|
||||
fn default() -> Self {
|
||||
RemoteConfig {
|
||||
ssh_keys: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Errors
|
||||
|
||||
/// ## SerializerError
|
||||
///
|
||||
/// Contains the error for serializer/deserializer
|
||||
#[derive(std::fmt::Debug)]
|
||||
pub struct SerializerError {
|
||||
kind: SerializerErrorKind,
|
||||
msg: Option<String>,
|
||||
}
|
||||
|
||||
/// ## SerializerErrorKind
|
||||
///
|
||||
/// Describes the kind of error for the serializer/deserializer
|
||||
#[derive(std::fmt::Debug, PartialEq)]
|
||||
pub enum SerializerErrorKind {
|
||||
IoError,
|
||||
SerializationError,
|
||||
SyntaxError,
|
||||
}
|
||||
|
||||
impl SerializerError {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new `SerializerError`
|
||||
pub fn new(kind: SerializerErrorKind) -> SerializerError {
|
||||
SerializerError { kind, msg: None }
|
||||
}
|
||||
|
||||
/// ### new_ex
|
||||
///
|
||||
/// Instantiates a new `SerializerError` with description message
|
||||
pub fn new_ex(kind: SerializerErrorKind, msg: String) -> SerializerError {
|
||||
let mut err: SerializerError = SerializerError::new(kind);
|
||||
err.msg = Some(msg);
|
||||
err
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SerializerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let err: String = match &self.kind {
|
||||
SerializerErrorKind::IoError => String::from("IO error"),
|
||||
SerializerErrorKind::SerializationError => String::from("Serialization error"),
|
||||
SerializerErrorKind::SyntaxError => String::from("Syntax error"),
|
||||
};
|
||||
match &self.msg {
|
||||
Some(msg) => write!(f, "{} ({})", err, msg),
|
||||
None => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::env;
|
||||
|
||||
#[test]
|
||||
fn test_config_mod_new() {
|
||||
let mut keys: HashMap<String, PathBuf> = HashMap::with_capacity(1);
|
||||
keys.insert(
|
||||
String::from("192.168.1.31"),
|
||||
PathBuf::from("/tmp/private.key"),
|
||||
);
|
||||
let remote: RemoteConfig = RemoteConfig { ssh_keys: keys };
|
||||
let ui: UserInterfaceConfig = UserInterfaceConfig {
|
||||
default_protocol: String::from("SFTP"),
|
||||
text_editor: PathBuf::from("nano"),
|
||||
show_hidden_files: true,
|
||||
group_dirs: Some(String::from("first")),
|
||||
};
|
||||
let cfg: UserConfig = UserConfig {
|
||||
user_interface: ui,
|
||||
remote: remote,
|
||||
};
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.31"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/tmp/private.key")
|
||||
);
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_mod_new_default() {
|
||||
// Force vim editor
|
||||
env::set_var(String::from("EDITOR"), String::from("vim"));
|
||||
// Get default
|
||||
let cfg: UserConfig = UserConfig::default();
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
|
||||
assert_eq!(cfg.remote.ssh_keys.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_mod_errors() {
|
||||
let error: SerializerError = SerializerError::new(SerializerErrorKind::SyntaxError);
|
||||
assert_eq!(error.kind, SerializerErrorKind::SyntaxError);
|
||||
assert!(error.msg.is_none());
|
||||
assert_eq!(format!("{}", error), String::from("Syntax error"));
|
||||
let error: SerializerError =
|
||||
SerializerError::new_ex(SerializerErrorKind::SyntaxError, String::from("bad syntax"));
|
||||
assert_eq!(error.kind, SerializerErrorKind::SyntaxError);
|
||||
assert!(error.msg.is_some());
|
||||
assert_eq!(
|
||||
format!("{}", error),
|
||||
String::from("Syntax error (bad syntax)")
|
||||
);
|
||||
// Fmt
|
||||
assert_eq!(
|
||||
format!("{}", SerializerError::new(SerializerErrorKind::IoError)),
|
||||
String::from("IO error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
SerializerError::new(SerializerErrorKind::SerializationError)
|
||||
),
|
||||
String::from("Serialization error")
|
||||
);
|
||||
}
|
||||
}
|
241
src/config/serializer.rs
Normal file
241
src/config/serializer.rs
Normal file
|
@ -0,0 +1,241 @@
|
|||
//! ## Serializer
|
||||
//!
|
||||
//! `serializer` is the module which provides the serializer/deserializer for configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{SerializerError, SerializerErrorKind, UserConfig};
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub struct ConfigSerializer {}
|
||||
|
||||
impl ConfigSerializer {
|
||||
/// ### serialize
|
||||
///
|
||||
/// Serialize `UserConfig` into TOML and write content to writable
|
||||
pub fn serialize(
|
||||
&self,
|
||||
mut writable: Box<dyn Write>,
|
||||
cfg: &UserConfig,
|
||||
) -> Result<(), SerializerError> {
|
||||
// Serialize content
|
||||
let data: String = match toml::ser::to_string(cfg) {
|
||||
Ok(dt) => dt,
|
||||
Err(err) => {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SerializationError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
};
|
||||
// Write file
|
||||
match writable.write_all(data.as_bytes()) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### deserialize
|
||||
///
|
||||
/// Read data from readable and deserialize its content as TOML
|
||||
pub fn deserialize(&self, mut readable: Box<dyn Read>) -> Result<UserConfig, SerializerError> {
|
||||
// Read file content
|
||||
let mut data: String = String::new();
|
||||
if let Err(err) = readable.read_to_string(&mut data) {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
));
|
||||
}
|
||||
// Deserialize
|
||||
match toml::de::from_str(data.as_str()) {
|
||||
Ok(hosts) => Ok(hosts),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::io::{Seek, SeekFrom};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_deserialize_ok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
let cfg = deserializer.deserialize(Box::new(toml_file));
|
||||
println!("{:?}", cfg);
|
||||
assert!(cfg.is_ok());
|
||||
let cfg: UserConfig = cfg.ok().unwrap();
|
||||
// Verify configuration
|
||||
// Verify ui
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
|
||||
// Verify keys
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.31"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/raspberry.key")
|
||||
);
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.32"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/beaglebone.key")
|
||||
);
|
||||
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_deserialize_ok_no_opts() {
|
||||
let toml_file: tempfile::NamedTempFile = create_good_toml_no_opts();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
let cfg = deserializer.deserialize(Box::new(toml_file));
|
||||
println!("{:?}", cfg);
|
||||
assert!(cfg.is_ok());
|
||||
let cfg: UserConfig = cfg.ok().unwrap();
|
||||
// Verify configuration
|
||||
// Verify ui
|
||||
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
|
||||
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
|
||||
assert_eq!(cfg.user_interface.show_hidden_files, true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, None);
|
||||
// Verify keys
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.31"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/raspberry.key")
|
||||
);
|
||||
assert_eq!(
|
||||
*cfg.remote
|
||||
.ssh_keys
|
||||
.get(&String::from("192.168.1.32"))
|
||||
.unwrap(),
|
||||
PathBuf::from("/home/omar/.ssh/beaglebone.key")
|
||||
);
|
||||
assert!(cfg.remote.ssh_keys.get(&String::from("1.1.1.1")).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_deserialize_nok() {
|
||||
let toml_file: tempfile::NamedTempFile = create_bad_toml();
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
// Parse
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
assert!(deserializer.deserialize(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_serializer_serialize() {
|
||||
let mut cfg: UserConfig = UserConfig::default();
|
||||
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
|
||||
// Insert key
|
||||
cfg.remote.ssh_keys.insert(
|
||||
String::from("192.168.1.31"),
|
||||
PathBuf::from("/home/omar/.ssh/id_rsa"),
|
||||
);
|
||||
// Serialize
|
||||
let serializer: ConfigSerializer = ConfigSerializer {};
|
||||
let writer: Box<dyn Write> = Box::new(std::fs::File::create(toml_file.path()).unwrap());
|
||||
assert!(serializer.serialize(writer, &cfg).is_ok());
|
||||
// Reload configuration and check if it's ok
|
||||
toml_file.as_file().sync_all().unwrap();
|
||||
toml_file.as_file().seek(SeekFrom::Start(0)).unwrap();
|
||||
assert!(serializer.deserialize(Box::new(toml_file)).is_ok());
|
||||
}
|
||||
|
||||
fn create_good_toml() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SCP"
|
||||
text_editor = "vim"
|
||||
show_hidden_files = true
|
||||
group_dirs = "last"
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_good_toml_no_opts() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SCP"
|
||||
text_editor = "vim"
|
||||
show_hidden_files = true
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_bad_toml() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SFTP"
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -30,7 +30,7 @@ extern crate regex;
|
|||
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::utils::parser::parse_lstime;
|
||||
use crate::utils::parser::{parse_datetime, parse_lstime};
|
||||
|
||||
// Includes
|
||||
use ftp4::native_tls::TlsConnector;
|
||||
|
@ -60,6 +60,25 @@ impl FtpFileTransfer {
|
|||
///
|
||||
/// Parse a line of LIST command output and instantiates an FsEntry from it
|
||||
fn parse_list_line(&self, path: &Path, line: &str) -> Result<FsEntry, ()> {
|
||||
// Try to parse using UNIX syntax
|
||||
match self.parse_unix_list_line(path, line) {
|
||||
Ok(entry) => Ok(entry),
|
||||
Err(_) => match self.parse_dos_list_line(path, line) {
|
||||
// If UNIX parsing fails, try DOS
|
||||
Ok(entry) => Ok(entry),
|
||||
Err(_) => Err(()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// ### parse_unix_list_line
|
||||
///
|
||||
/// Try to parse a "LIST" output command line in UNIX format.
|
||||
/// Returns error if syntax is not UNIX compliant.
|
||||
/// UNIX syntax has the following syntax:
|
||||
/// {FILE_TYPE}{UNIX_PEX} {HARD_LINKS} {USER} {GROUP} {SIZE} {DATE} {FILENAME}
|
||||
/// -rw-r--r-- 1 cvisintin staff 4968 27 Dic 10:46 CHANGELOG.md
|
||||
fn parse_unix_list_line(&self, path: &Path, line: &str) -> Result<FsEntry, ()> {
|
||||
// Prepare list regex
|
||||
// NOTE: about this damn regex <https://stackoverflow.com/questions/32480890/is-there-a-regex-to-parse-the-values-from-an-ftp-directory-listing>
|
||||
lazy_static! {
|
||||
|
@ -171,11 +190,12 @@ impl FtpFileTransfer {
|
|||
return Err(());
|
||||
}
|
||||
let mut abs_path: PathBuf = PathBuf::from(path);
|
||||
abs_path.push(file_name.as_str());
|
||||
// get extension
|
||||
let extension: Option<String> = match abs_path.as_path().extension() {
|
||||
None => None,
|
||||
Some(s) => Some(String::from(s.to_string_lossy())),
|
||||
};
|
||||
abs_path.push(file_name.as_str());
|
||||
// Return
|
||||
// Push to entries
|
||||
Ok(match is_dir {
|
||||
|
@ -210,6 +230,96 @@ impl FtpFileTransfer {
|
|||
None => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### parse_dos_list_line
|
||||
///
|
||||
/// Try to parse a "LIST" output command line in DOS format.
|
||||
/// Returns error if syntax is not DOS compliant.
|
||||
/// DOS syntax has the following syntax:
|
||||
/// {DATE} {TIME} {<DIR> | SIZE} {FILENAME}
|
||||
/// 10-19-20 03:19PM <DIR> pub
|
||||
/// 04-08-14 03:09PM 403 readme.txt
|
||||
fn parse_dos_list_line(&self, path: &Path, line: &str) -> Result<FsEntry, ()> {
|
||||
// Prepare list regex
|
||||
// NOTE: you won't find this regex on the internet. It seems I'm the only person in the world who needs this
|
||||
lazy_static! {
|
||||
static ref DOS_RE: Regex = Regex::new(
|
||||
r#"^(\d{2}\-\d{2}\-\d{2}\s+\d{2}:\d{2}\s*[AP]M)\s+(<DIR>)?([\d,]*)\s+(.+)$"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
// Apply regex to result
|
||||
match DOS_RE.captures(line) {
|
||||
// String matches regex
|
||||
Some(metadata) => {
|
||||
// NOTE: metadata fmt: (regex, date_time, is_dir?, file_size?, file_name)
|
||||
// Expected 4 + 1 (5) values: + 1 cause regex is repeated at 0
|
||||
if metadata.len() < 5 {
|
||||
return Err(());
|
||||
}
|
||||
// Parse date time
|
||||
let time: SystemTime =
|
||||
match parse_datetime(metadata.get(1).unwrap().as_str(), "%d-%m-%y %I:%M%p") {
|
||||
Ok(t) => t,
|
||||
Err(_) => SystemTime::UNIX_EPOCH, // Don't return error
|
||||
};
|
||||
// Get if is a directory
|
||||
let is_dir: bool = metadata.get(2).is_some();
|
||||
// Get file size
|
||||
let file_size: usize = match is_dir {
|
||||
true => 0, // If is directory, filesize is 0
|
||||
false => match metadata.get(3) {
|
||||
// If is file, parse arg 3
|
||||
Some(val) => match val.as_str().parse::<usize>() {
|
||||
Ok(sz) => sz,
|
||||
Err(_) => 0,
|
||||
},
|
||||
None => 0, // Should not happen
|
||||
},
|
||||
};
|
||||
// Get file name
|
||||
let file_name: String = String::from(metadata.get(4).unwrap().as_str());
|
||||
// Get absolute path
|
||||
let mut abs_path: PathBuf = PathBuf::from(path);
|
||||
abs_path.push(file_name.as_str());
|
||||
// Get extension
|
||||
let extension: Option<String> = match abs_path.as_path().extension() {
|
||||
None => None,
|
||||
Some(s) => Some(String::from(s.to_string_lossy())),
|
||||
};
|
||||
// Return entry
|
||||
Ok(match is_dir {
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
name: file_name,
|
||||
abs_path,
|
||||
last_change_time: time,
|
||||
last_access_time: time,
|
||||
creation_time: time,
|
||||
readonly: false,
|
||||
symlink: None,
|
||||
user: None,
|
||||
group: None,
|
||||
unix_pex: None,
|
||||
}),
|
||||
false => FsEntry::File(FsFile {
|
||||
name: file_name,
|
||||
abs_path,
|
||||
last_change_time: time,
|
||||
last_access_time: time,
|
||||
creation_time: time,
|
||||
size: file_size,
|
||||
ftype: extension,
|
||||
readonly: false,
|
||||
symlink: None,
|
||||
user: None,
|
||||
group: None,
|
||||
unix_pex: None,
|
||||
}),
|
||||
})
|
||||
}
|
||||
None => Err(()), // Invalid syntax
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileTransfer for FtpFileTransfer {
|
||||
|
@ -595,6 +705,7 @@ impl FileTransfer for FtpFileTransfer {
|
|||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
|
@ -609,7 +720,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_parse_list_line() {
|
||||
fn test_filetransfer_ftp_parse_list_line_unix() {
|
||||
let ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Simple file
|
||||
let fs_entry: FsEntry = ftp
|
||||
|
@ -668,25 +779,16 @@ mod tests {
|
|||
assert_eq!(file.group, Some(9));
|
||||
assert_eq!(file.unix_pex.unwrap(), (7, 5, 5));
|
||||
assert_eq!(
|
||||
file.last_access_time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1604593920)
|
||||
fmt_time(file.last_access_time, "%m %d %M").as_str(),
|
||||
"11 05 32"
|
||||
);
|
||||
assert_eq!(
|
||||
file.last_change_time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1604593920)
|
||||
fmt_time(file.last_change_time, "%m %d %M").as_str(),
|
||||
"11 05 32"
|
||||
);
|
||||
assert_eq!(
|
||||
file.creation_time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1604593920)
|
||||
fmt_time(file.creation_time, "%m %d %M").as_str(),
|
||||
"11 05 32"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected file, got directory");
|
||||
|
@ -740,6 +842,95 @@ mod tests {
|
|||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_parse_list_line_dos() {
|
||||
let ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Simple file
|
||||
let fs_entry: FsEntry = ftp
|
||||
.parse_list_line(
|
||||
PathBuf::from("/tmp").as_path(),
|
||||
"04-08-14 03:09PM 8192 omar.txt",
|
||||
)
|
||||
.ok()
|
||||
.unwrap();
|
||||
if let FsEntry::File(file) = fs_entry {
|
||||
assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt"));
|
||||
assert_eq!(file.name, String::from("omar.txt"));
|
||||
assert_eq!(file.size, 8192);
|
||||
assert!(file.symlink.is_none());
|
||||
assert_eq!(file.user, None);
|
||||
assert_eq!(file.group, None);
|
||||
assert_eq!(file.unix_pex, None);
|
||||
assert_eq!(
|
||||
file.last_access_time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1407164940)
|
||||
);
|
||||
assert_eq!(
|
||||
file.last_change_time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1407164940)
|
||||
);
|
||||
assert_eq!(
|
||||
file.creation_time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1407164940)
|
||||
);
|
||||
} else {
|
||||
panic!("Expected file, got directory");
|
||||
}
|
||||
// Directory
|
||||
let fs_entry: FsEntry = ftp
|
||||
.parse_list_line(
|
||||
PathBuf::from("/tmp").as_path(),
|
||||
"04-08-14 03:09PM <DIR> docs",
|
||||
)
|
||||
.ok()
|
||||
.unwrap();
|
||||
if let FsEntry::Directory(dir) = fs_entry {
|
||||
assert_eq!(dir.abs_path, PathBuf::from("/tmp/docs"));
|
||||
assert_eq!(dir.name, String::from("docs"));
|
||||
assert!(dir.symlink.is_none());
|
||||
assert_eq!(dir.user, None);
|
||||
assert_eq!(dir.group, None);
|
||||
assert_eq!(dir.unix_pex, None);
|
||||
assert_eq!(
|
||||
dir.last_access_time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1407164940)
|
||||
);
|
||||
assert_eq!(
|
||||
dir.last_change_time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1407164940)
|
||||
);
|
||||
assert_eq!(
|
||||
dir.creation_time
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1407164940)
|
||||
);
|
||||
assert_eq!(dir.readonly, false);
|
||||
} else {
|
||||
panic!("Expected directory, got directory");
|
||||
}
|
||||
// Error
|
||||
assert!(ftp
|
||||
.parse_list_line(PathBuf::from("/").as_path(), "04-08-14 omar.txt")
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_connect_unsecure_anonymous() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
|
@ -835,47 +1026,49 @@ mod tests {
|
|||
.is_err());
|
||||
}
|
||||
|
||||
/* NOTE: they don't work
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_list_dir() {
|
||||
fn test_filetransfer_ftp_list_dir_dos_syntax() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Connect
|
||||
assert!(ftp.connect(String::from("speedtest.tele2.net"), 21, None, None).is_ok());
|
||||
assert!(ftp
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
21,
|
||||
Some(String::from("demo")),
|
||||
Some(String::from("password"))
|
||||
)
|
||||
.is_ok());
|
||||
// Pwd
|
||||
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
|
||||
// List dir
|
||||
println!("{:?}", ftp.list_dir(PathBuf::from("/").as_path()));
|
||||
let files: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap();
|
||||
// There should be 19 files
|
||||
assert_eq!(files.len(), 19);
|
||||
// Verify first entry (1000GB.zip)
|
||||
let first: &FsEntry = files.get(0).unwrap();
|
||||
if let FsEntry::File(f) = first {
|
||||
assert_eq!(f.name, String::from("1000GB.zip"));
|
||||
assert_eq!(f.abs_path, PathBuf::from("/1000GB.zip"));
|
||||
assert_eq!(f.size, 1073741824000);
|
||||
assert_eq!(*f.ftype.as_ref().unwrap(), String::from("zip"));
|
||||
assert_eq!(f.unix_pex.unwrap(), (6, 4, 4));
|
||||
assert_eq!(f.creation_time.duration_since(SystemTime::UNIX_EPOCH).unwrap(), Duration::from_secs(1455840000));
|
||||
assert_eq!(f.last_access_time.duration_since(SystemTime::UNIX_EPOCH).unwrap(), Duration::from_secs(1455840000));
|
||||
assert_eq!(f.last_change_time.duration_since(SystemTime::UNIX_EPOCH).unwrap(), Duration::from_secs(1455840000));
|
||||
} else {
|
||||
panic!("First should be a file, but it a directory");
|
||||
}
|
||||
// Verify last entry (directory upload)
|
||||
let last: &FsEntry = files.get(18).unwrap();
|
||||
if let FsEntry::Directory(d) = last {
|
||||
assert_eq!(d.name, String::from("upload"));
|
||||
assert_eq!(d.abs_path, PathBuf::from("/upload"));
|
||||
assert_eq!(d.readonly, false);
|
||||
assert_eq!(d.unix_pex.unwrap(), (7, 5, 5));
|
||||
} else {
|
||||
panic!("Last should be a directory, but is a file");
|
||||
}
|
||||
// There should be at least 1 file
|
||||
assert!(files.len() > 0);
|
||||
// Disconnect
|
||||
assert!(ftp.disconnect().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn test_filetransfer_ftp_list_dir_unix_syntax() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Connect
|
||||
assert!(ftp
|
||||
.connect(String::from("speedtest.tele2.net"), 21, None, None)
|
||||
.is_ok());
|
||||
// Pwd
|
||||
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
|
||||
// List dir
|
||||
println!("{:?}", ftp.list_dir(PathBuf::from("/").as_path()));
|
||||
let files: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap();
|
||||
// There should be at least 1 file
|
||||
assert!(files.len() > 0);
|
||||
// Disconnect
|
||||
assert!(ftp.disconnect().is_ok());
|
||||
}
|
||||
|
||||
/* NOTE: they don't work
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_recv() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -226,6 +226,34 @@ pub trait FileTransfer {
|
|||
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError>;
|
||||
}
|
||||
|
||||
// Traits
|
||||
|
||||
impl std::string::ToString for FileTransferProtocol {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(match self {
|
||||
FileTransferProtocol::Ftp(secure) => match secure {
|
||||
true => "FTPS",
|
||||
false => "FTP",
|
||||
},
|
||||
FileTransferProtocol::Scp => "SCP",
|
||||
FileTransferProtocol::Sftp => "SFTP",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for FileTransferProtocol {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_ascii_uppercase().as_str() {
|
||||
"FTP" => Ok(FileTransferProtocol::Ftp(false)),
|
||||
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
|
||||
"SCP" => Ok(FileTransferProtocol::Scp),
|
||||
"SFTP" => Ok(FileTransferProtocol::Sftp),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -233,6 +261,9 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_mod_protocol() {
|
||||
assert_eq!(
|
||||
|
@ -243,6 +274,52 @@ mod tests {
|
|||
FileTransferProtocol::Ftp(false),
|
||||
FileTransferProtocol::Ftp(false)
|
||||
);
|
||||
// From str
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("FTPS").ok().unwrap(),
|
||||
FileTransferProtocol::Ftp(true)
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("ftps").ok().unwrap(),
|
||||
FileTransferProtocol::Ftp(true)
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("FTP").ok().unwrap(),
|
||||
FileTransferProtocol::Ftp(false)
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("ftp").ok().unwrap(),
|
||||
FileTransferProtocol::Ftp(false)
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("SFTP").ok().unwrap(),
|
||||
FileTransferProtocol::Sftp
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("sftp").ok().unwrap(),
|
||||
FileTransferProtocol::Sftp
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("SCP").ok().unwrap(),
|
||||
FileTransferProtocol::Scp
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::from_str("scp").ok().unwrap(),
|
||||
FileTransferProtocol::Scp
|
||||
);
|
||||
// Error
|
||||
assert!(FileTransferProtocol::from_str("dummy").is_err());
|
||||
// To String
|
||||
assert_eq!(
|
||||
FileTransferProtocol::Ftp(true).to_string(),
|
||||
String::from("FTPS")
|
||||
);
|
||||
assert_eq!(
|
||||
FileTransferProtocol::Ftp(false).to_string(),
|
||||
String::from("FTP")
|
||||
);
|
||||
assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP"));
|
||||
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -30,6 +30,7 @@ extern crate ssh2;
|
|||
// Locals
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::parser::parse_lstime;
|
||||
|
||||
// Includes
|
||||
|
@ -46,22 +47,18 @@ use std::time::SystemTime;
|
|||
pub struct ScpFileTransfer {
|
||||
session: Option<Session>,
|
||||
wrkdir: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for ScpFileTransfer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
key_storage: SshKeyStorage,
|
||||
}
|
||||
|
||||
impl ScpFileTransfer {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new ScpFileTransfer
|
||||
pub fn new() -> ScpFileTransfer {
|
||||
pub fn new(key_storage: SshKeyStorage) -> ScpFileTransfer {
|
||||
ScpFileTransfer {
|
||||
session: None,
|
||||
wrkdir: PathBuf::from("~"),
|
||||
key_storage,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,11 +189,12 @@ impl ScpFileTransfer {
|
|||
return Err(());
|
||||
}
|
||||
let mut abs_path: PathBuf = PathBuf::from(path);
|
||||
abs_path.push(file_name.as_str());
|
||||
// Get extension
|
||||
let extension: Option<String> = match abs_path.as_path().extension() {
|
||||
None => None,
|
||||
Some(s) => Some(String::from(s.to_string_lossy())),
|
||||
};
|
||||
abs_path.push(file_name.as_str());
|
||||
// Return
|
||||
// Push to entries
|
||||
Ok(match is_dir {
|
||||
|
@ -345,17 +343,36 @@ impl FileTransfer for ScpFileTransfer {
|
|||
Some(u) => u,
|
||||
None => String::from(""),
|
||||
};
|
||||
// Try authenticating with user agent
|
||||
if session.userauth_agent(username.as_str()).is_err() {
|
||||
// Try authentication with password then
|
||||
if let Err(err) = session.userauth_password(
|
||||
username.as_str(),
|
||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||
) {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
format!("{}", err),
|
||||
));
|
||||
// Check if it is possible to authenticate using a RSA key
|
||||
match self
|
||||
.key_storage
|
||||
.resolve(address.as_str(), username.as_str())
|
||||
{
|
||||
Some(rsa_key) => {
|
||||
// Authenticate with RSA key
|
||||
if let Err(err) = session.userauth_pubkey_file(
|
||||
username.as_str(),
|
||||
None,
|
||||
rsa_key.as_path(),
|
||||
password.as_deref(),
|
||||
) {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
format!("{}", err),
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Proceeed with username/password authentication
|
||||
if let Err(err) = session.userauth_password(
|
||||
username.as_str(),
|
||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||
) {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
format!("{}", err),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Get banner
|
||||
|
@ -755,7 +772,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
(mtime, atime)
|
||||
};
|
||||
match session.scp_send(file_name, mode, local.size as u64, Some(times)) {
|
||||
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(8192, channel))),
|
||||
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
format!("{}", err),
|
||||
|
@ -778,7 +795,7 @@ impl FileTransfer for ScpFileTransfer {
|
|||
// Set blocking to true
|
||||
session.set_blocking(true);
|
||||
match session.scp_recv(file.abs_path.as_path()) {
|
||||
Ok(reader) => Ok(Box::new(BufReader::with_capacity(8192, reader.0))),
|
||||
Ok(reader) => Ok(Box::new(BufReader::with_capacity(65536, reader.0))),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
format!("{}", err),
|
||||
|
@ -823,14 +840,14 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_scp_new() {
|
||||
let client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client.session.is_none());
|
||||
assert_eq!(client.is_connected(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_scp_connect() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert_eq!(client.is_connected(), false);
|
||||
assert!(client
|
||||
.connect(
|
||||
|
@ -849,7 +866,7 @@ mod tests {
|
|||
}
|
||||
#[test]
|
||||
fn test_filetransfer_scp_bad_auth() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -862,7 +879,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_scp_no_credentials() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(String::from("test.rebex.net"), 22, None, None)
|
||||
.is_err());
|
||||
|
@ -870,7 +887,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_scp_bad_server() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("mybadserver.veryverybad.awful"),
|
||||
|
@ -882,7 +899,7 @@ mod tests {
|
|||
}
|
||||
#[test]
|
||||
fn test_filetransfer_scp_pwd() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -902,7 +919,7 @@ mod tests {
|
|||
#[test]
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
fn test_filetransfer_scp_cwd() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -923,7 +940,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_scp_cwd_error() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -946,7 +963,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_scp_ls() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -967,7 +984,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_scp_stat() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -991,7 +1008,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_scp_recv() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -1023,7 +1040,7 @@ mod tests {
|
|||
}
|
||||
#[test]
|
||||
fn test_filetransfer_scp_recv_failed_nosuchfile() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -1058,7 +1075,7 @@ mod tests {
|
|||
/* NOTE: the server doesn't allow you to create directories
|
||||
#[test]
|
||||
fn test_filetransfer_scp_mkdir() {
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client.connect(String::from("test.rebex.net"), 22, Some(String::from("demo")), Some(String::from("password"))).is_ok());
|
||||
let dir: String = String::from("foo");
|
||||
// Mkdir
|
||||
|
@ -1087,7 +1104,7 @@ mod tests {
|
|||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
};
|
||||
let mut scp: ScpFileTransfer = ScpFileTransfer::new();
|
||||
let mut scp: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(scp.change_dir(Path::new("/tmp")).is_err());
|
||||
assert!(scp.disconnect().is_err());
|
||||
assert!(scp.list_dir(Path::new("/tmp")).is_err());
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -29,6 +29,7 @@ extern crate ssh2;
|
|||
// Locals
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
|
||||
// Includes
|
||||
use ssh2::{FileStat, OpenFlags, OpenType, Session, Sftp};
|
||||
|
@ -44,23 +45,19 @@ pub struct SftpFileTransfer {
|
|||
session: Option<Session>,
|
||||
sftp: Option<Sftp>,
|
||||
wrkdir: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for SftpFileTransfer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
key_storage: SshKeyStorage,
|
||||
}
|
||||
|
||||
impl SftpFileTransfer {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new SftpFileTransfer
|
||||
pub fn new() -> SftpFileTransfer {
|
||||
pub fn new(key_storage: SshKeyStorage) -> SftpFileTransfer {
|
||||
SftpFileTransfer {
|
||||
session: None,
|
||||
sftp: None,
|
||||
wrkdir: PathBuf::from("~"),
|
||||
key_storage,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,17 +235,36 @@ impl FileTransfer for SftpFileTransfer {
|
|||
Some(u) => u,
|
||||
None => String::from(""),
|
||||
};
|
||||
// Try authenticating with user agent
|
||||
if session.userauth_agent(username.as_str()).is_err() {
|
||||
// Try authentication with password then
|
||||
if let Err(err) = session.userauth_password(
|
||||
username.as_str(),
|
||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||
) {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
format!("{}", err),
|
||||
));
|
||||
// Check if it is possible to authenticate using a RSA key
|
||||
match self
|
||||
.key_storage
|
||||
.resolve(address.as_str(), username.as_str())
|
||||
{
|
||||
Some(rsa_key) => {
|
||||
// Authenticate with RSA key
|
||||
if let Err(err) = session.userauth_pubkey_file(
|
||||
username.as_str(),
|
||||
None,
|
||||
rsa_key.as_path(),
|
||||
password.as_deref(),
|
||||
) {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
format!("{}", err),
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// Proceeed with username/password authentication
|
||||
if let Err(err) = session.userauth_password(
|
||||
username.as_str(),
|
||||
password.unwrap_or_else(|| String::from("")).as_str(),
|
||||
) {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::AuthenticationFailed,
|
||||
format!("{}", err),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set blocking to true
|
||||
|
@ -562,7 +578,7 @@ impl FileTransfer for SftpFileTransfer {
|
|||
};
|
||||
// Open remote file
|
||||
match sftp.open(remote_path.as_path()) {
|
||||
Ok(file) => Ok(Box::new(BufReader::with_capacity(8192, file))),
|
||||
Ok(file) => Ok(Box::new(BufReader::with_capacity(65536, file))),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::NoSuchFileOrDirectory,
|
||||
format!("{}", err),
|
||||
|
@ -600,7 +616,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_new() {
|
||||
let client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client.session.is_none());
|
||||
assert!(client.sftp.is_none());
|
||||
assert_eq!(client.wrkdir, PathBuf::from("~"));
|
||||
|
@ -609,7 +625,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_connect() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert_eq!(client.is_connected(), false);
|
||||
assert!(client
|
||||
.connect(
|
||||
|
@ -631,7 +647,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_bad_auth() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -644,7 +660,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_no_credentials() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(String::from("test.rebex.net"), 22, None, None)
|
||||
.is_err());
|
||||
|
@ -652,7 +668,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_bad_server() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("mybadserver.veryverybad.awful"),
|
||||
|
@ -665,7 +681,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_pwd() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -686,7 +702,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_cwd() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -713,7 +729,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_copy() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -748,7 +764,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_cwd_error() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -771,7 +787,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_ls() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -794,7 +810,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_stat() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -820,7 +836,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_recv() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -854,7 +870,7 @@ mod tests {
|
|||
}
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_recv_failed_nosuchfile() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
|
@ -892,7 +908,7 @@ mod tests {
|
|||
/* NOTE: the server doesn't allow you to create directories
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_mkdir() {
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(client.connect(String::from("test.rebex.net"), 22, Some(String::from("demo")), Some(String::from("password"))).is_ok());
|
||||
let dir: String = String::from("foo");
|
||||
// Mkdir
|
||||
|
@ -921,7 +937,7 @@ mod tests {
|
|||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
};
|
||||
let mut sftp: SftpFileTransfer = SftpFileTransfer::new();
|
||||
let mut sftp: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
|
||||
assert!(sftp.change_dir(Path::new("/tmp")).is_err());
|
||||
assert!(sftp.disconnect().is_err());
|
||||
assert!(sftp.list_dir(Path::new("/tmp")).is_err());
|
||||
|
|
129
src/fs/explorer/builder.rs
Normal file
129
src/fs/explorer/builder.rs
Normal file
|
@ -0,0 +1,129 @@
|
|||
//! ## Builder
|
||||
//!
|
||||
//! `builder` is the module which provides a builder for FileExplorer
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::{ExplorerOpts, FileExplorer, FileSorting, GroupDirs};
|
||||
// Ext
|
||||
use std::collections::VecDeque;
|
||||
|
||||
/// ## FileExplorerBuilder
|
||||
///
|
||||
/// Struct used to create a `FileExplorer`
|
||||
pub struct FileExplorerBuilder {
|
||||
explorer: Option<FileExplorer>,
|
||||
}
|
||||
|
||||
impl FileExplorerBuilder {
|
||||
/// ### new
|
||||
///
|
||||
/// Build a new `FileExplorerBuilder`
|
||||
pub fn new() -> Self {
|
||||
FileExplorerBuilder {
|
||||
explorer: Some(FileExplorer::default()),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### build
|
||||
///
|
||||
/// Take FileExplorer out of builder
|
||||
pub fn build(&mut self) -> FileExplorer {
|
||||
self.explorer.take().unwrap()
|
||||
}
|
||||
|
||||
/// ### with_hidden_files
|
||||
///
|
||||
/// Enable HIDDEN_FILES option
|
||||
pub fn with_hidden_files(&mut self, val: bool) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
match val {
|
||||
true => e.opts.insert(ExplorerOpts::SHOW_HIDDEN_FILES),
|
||||
false => e.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES),
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_file_sorting
|
||||
///
|
||||
/// Set sorting method
|
||||
pub fn with_file_sorting(&mut self, sorting: FileSorting) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
e.sort_by(sorting);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_dirs_first
|
||||
///
|
||||
/// Enable DIRS_FIRST option
|
||||
pub fn with_group_dirs(&mut self, group_dirs: Option<GroupDirs>) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
e.group_dirs_by(group_dirs);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// ### with_stack_size
|
||||
///
|
||||
/// Set stack size for FileExplorer
|
||||
pub fn with_stack_size(&mut self, sz: usize) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
e.stack_size = sz;
|
||||
e.dirstack = VecDeque::with_capacity(sz);
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_builder_new_default() {
|
||||
let explorer: FileExplorer = FileExplorerBuilder::new().build();
|
||||
// Verify
|
||||
assert!(!explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES));
|
||||
assert_eq!(explorer.file_sorting, FileSorting::ByName); // Default
|
||||
assert_eq!(explorer.group_dirs, None);
|
||||
assert_eq!(explorer.stack_size, 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_builder_new_all() {
|
||||
let explorer: FileExplorer = FileExplorerBuilder::new()
|
||||
.with_file_sorting(FileSorting::ByModifyTime)
|
||||
.with_group_dirs(Some(GroupDirs::First))
|
||||
.with_hidden_files(true)
|
||||
.with_stack_size(24)
|
||||
.build();
|
||||
// Verify
|
||||
assert!(explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES));
|
||||
assert_eq!(explorer.file_sorting, FileSorting::ByModifyTime); // Default
|
||||
assert_eq!(explorer.group_dirs, Some(GroupDirs::First));
|
||||
assert_eq!(explorer.stack_size, 24);
|
||||
}
|
||||
}
|
913
src/fs/explorer/mod.rs
Normal file
913
src/fs/explorer/mod.rs
Normal file
|
@ -0,0 +1,913 @@
|
|||
//! ## Explorer
|
||||
//!
|
||||
//! `explorer` is the module which provides an Helper in handling Directory status through
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Mods
|
||||
pub(crate) mod builder;
|
||||
// Deps
|
||||
extern crate bitflags;
|
||||
// Locals
|
||||
use super::FsEntry;
|
||||
// Ext
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
bitflags! {
|
||||
/// ## ExplorerOpts
|
||||
///
|
||||
/// ExplorerOpts are bit options which provides different behaviours to `FileExplorer`
|
||||
pub(crate) struct ExplorerOpts: u32 {
|
||||
const SHOW_HIDDEN_FILES = 0b00000001;
|
||||
}
|
||||
}
|
||||
|
||||
/// ## FileSorting
|
||||
///
|
||||
/// FileSorting defines the criteria for sorting files
|
||||
#[derive(Copy, Clone, PartialEq, std::fmt::Debug)]
|
||||
pub enum FileSorting {
|
||||
ByName,
|
||||
ByModifyTime,
|
||||
ByCreationTime,
|
||||
BySize,
|
||||
}
|
||||
|
||||
/// ## GroupDirs
|
||||
///
|
||||
/// GroupDirs defines how directories should be grouped in sorting files
|
||||
#[derive(PartialEq, std::fmt::Debug)]
|
||||
pub enum GroupDirs {
|
||||
First,
|
||||
Last,
|
||||
}
|
||||
|
||||
/// ## FileExplorer
|
||||
///
|
||||
/// File explorer states
|
||||
pub struct FileExplorer {
|
||||
pub wrkdir: PathBuf, // Current directory
|
||||
pub(crate) dirstack: VecDeque<PathBuf>, // Stack of visited directory (max 16)
|
||||
pub(crate) stack_size: usize, // Directory stack size
|
||||
pub(crate) file_sorting: FileSorting, // File sorting criteria
|
||||
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
|
||||
pub(crate) opts: ExplorerOpts, // Explorer options
|
||||
index: usize, // Selected file
|
||||
files: Vec<FsEntry>, // Files in directory
|
||||
}
|
||||
|
||||
impl Default for FileExplorer {
|
||||
fn default() -> Self {
|
||||
FileExplorer {
|
||||
wrkdir: PathBuf::from("/"),
|
||||
dirstack: VecDeque::with_capacity(16),
|
||||
stack_size: 16,
|
||||
file_sorting: FileSorting::ByName,
|
||||
group_dirs: None,
|
||||
opts: ExplorerOpts::empty(),
|
||||
index: 0,
|
||||
files: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileExplorer {
|
||||
/// ### pushd
|
||||
///
|
||||
/// push directory to stack
|
||||
pub fn pushd(&mut self, dir: &Path) {
|
||||
// Check if stack would overflow the size
|
||||
while self.dirstack.len() >= self.stack_size {
|
||||
self.dirstack.pop_front(); // Start cleaning events from back
|
||||
}
|
||||
// Eventually push front the new record
|
||||
self.dirstack.push_back(PathBuf::from(dir));
|
||||
}
|
||||
|
||||
/// ### popd
|
||||
///
|
||||
/// Pop directory from the stack and return the directory
|
||||
pub fn popd(&mut self) -> Option<PathBuf> {
|
||||
self.dirstack.pop_back()
|
||||
}
|
||||
|
||||
/// ### set_files
|
||||
///
|
||||
/// Set Explorer files
|
||||
/// This method will also sort entries based on current options
|
||||
/// Once all sorting have been performed, index is moved to first valid entry.
|
||||
pub fn set_files(&mut self, files: Vec<FsEntry>) {
|
||||
self.files = files;
|
||||
// Sort
|
||||
self.sort();
|
||||
// Reset index
|
||||
self.index_at_first();
|
||||
}
|
||||
|
||||
/// ### count
|
||||
///
|
||||
/// Return amount of files
|
||||
pub fn count(&self) -> usize {
|
||||
self.files.len()
|
||||
}
|
||||
|
||||
/// ### iter_files
|
||||
///
|
||||
/// Iterate over files
|
||||
/// Filters are applied based on current options (e.g. hidden files not returned)
|
||||
pub fn iter_files(&self) -> Box<dyn Iterator<Item = &FsEntry> + '_> {
|
||||
// Filter
|
||||
let opts: ExplorerOpts = self.opts;
|
||||
Box::new(self.files.iter().filter(move |x| {
|
||||
// If true, element IS NOT filtered
|
||||
let mut pass: bool = true;
|
||||
// If hidden files SHOULDN'T be shown, AND pass with not hidden
|
||||
if !opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
|
||||
pass &= !x.is_hidden();
|
||||
}
|
||||
pass
|
||||
}))
|
||||
}
|
||||
|
||||
/// ### iter_files_all
|
||||
///
|
||||
/// Iterate all files; doesn't care about options
|
||||
pub fn iter_files_all(&self) -> Box<dyn Iterator<Item = &FsEntry> + '_> {
|
||||
Box::new(self.files.iter())
|
||||
}
|
||||
|
||||
/// ### get_current_file
|
||||
///
|
||||
/// Get file at index
|
||||
pub fn get_current_file(&self) -> Option<&FsEntry> {
|
||||
self.files.get(self.index)
|
||||
}
|
||||
|
||||
// Sorting
|
||||
|
||||
/// ### sort_by
|
||||
///
|
||||
/// Choose sorting method; then sort files
|
||||
pub fn sort_by(&mut self, sorting: FileSorting) {
|
||||
// If method HAS ACTUALLY CHANGED, sort (performance!)
|
||||
if self.file_sorting != sorting {
|
||||
self.file_sorting = sorting;
|
||||
self.sort();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_file_sorting
|
||||
///
|
||||
/// Get current file sorting method
|
||||
pub fn get_file_sorting(&self) -> FileSorting {
|
||||
self.file_sorting
|
||||
}
|
||||
|
||||
/// ### group_dirs_by
|
||||
///
|
||||
/// Choose group dirs method; then sort files
|
||||
pub fn group_dirs_by(&mut self, group_dirs: Option<GroupDirs>) {
|
||||
// If method HAS ACTUALLY CHANGED, sort (performance!)
|
||||
if self.group_dirs != group_dirs {
|
||||
self.group_dirs = group_dirs;
|
||||
self.sort();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### sort
|
||||
///
|
||||
/// Sort files based on Explorer options.
|
||||
fn sort(&mut self) {
|
||||
// Choose sorting method
|
||||
match &self.file_sorting {
|
||||
FileSorting::ByName => self.sort_files_by_name(),
|
||||
FileSorting::ByCreationTime => self.sort_files_by_creation_time(),
|
||||
FileSorting::ByModifyTime => self.sort_files_by_mtime(),
|
||||
FileSorting::BySize => self.sort_files_by_size(),
|
||||
}
|
||||
// Directories first (NOTE: MUST COME AFTER OTHER SORTING)
|
||||
// Group directories if necessary
|
||||
if let Some(group_dirs) = &self.group_dirs {
|
||||
match group_dirs {
|
||||
GroupDirs::First => self.sort_files_directories_first(),
|
||||
GroupDirs::Last => self.sort_files_directories_last(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### sort_files_by_name
|
||||
///
|
||||
/// Sort explorer files by their name. All names are converted to lowercase
|
||||
fn sort_files_by_name(&mut self) {
|
||||
self.files
|
||||
.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase());
|
||||
}
|
||||
|
||||
/// ### sort_files_by_mtime
|
||||
///
|
||||
/// Sort files by mtime; the newest comes first
|
||||
fn sort_files_by_mtime(&mut self) {
|
||||
self.files.sort_by(|a: &FsEntry, b: &FsEntry| {
|
||||
b.get_last_change_time().cmp(&a.get_last_change_time())
|
||||
});
|
||||
}
|
||||
|
||||
/// ### sort_files_by_creation_time
|
||||
///
|
||||
/// Sort files by creation time; the newest comes first
|
||||
fn sort_files_by_creation_time(&mut self) {
|
||||
self.files
|
||||
.sort_by(|a: &FsEntry, b: &FsEntry| b.get_creation_time().cmp(&a.get_creation_time()));
|
||||
}
|
||||
|
||||
/// ### sort_files_by_size
|
||||
///
|
||||
/// Sort files by size
|
||||
fn sort_files_by_size(&mut self) {
|
||||
self.files
|
||||
.sort_by(|a: &FsEntry, b: &FsEntry| b.get_size().cmp(&a.get_size()));
|
||||
}
|
||||
|
||||
/// ### sort_files_directories_first
|
||||
///
|
||||
/// Sort files; directories come first
|
||||
fn sort_files_directories_first(&mut self) {
|
||||
self.files.sort_by_key(|x: &FsEntry| x.is_file());
|
||||
}
|
||||
|
||||
/// ### sort_files_directories_last
|
||||
///
|
||||
/// Sort files; directories come last
|
||||
fn sort_files_directories_last(&mut self) {
|
||||
self.files.sort_by_key(|x: &FsEntry| x.is_dir());
|
||||
}
|
||||
|
||||
/// ### incr_index
|
||||
///
|
||||
/// Increment index to the first visible FsEntry.
|
||||
/// If index goes to `files.len() - 1`, the value will be seto to the minimum acceptable value
|
||||
pub fn incr_index(&mut self) {
|
||||
let sz: usize = self.files.len();
|
||||
// Increment or wrap
|
||||
if self.index + 1 >= sz {
|
||||
self.index = 0; // Wrap
|
||||
} else {
|
||||
self.index += 1; // Increment
|
||||
}
|
||||
// Validate
|
||||
match self.files.get(self.index) {
|
||||
Some(assoc_entry) => {
|
||||
if !self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
|
||||
// Check if file is hidden, otherwise increment
|
||||
if assoc_entry.is_hidden() {
|
||||
// Check if all files are hidden (NOTE: PREVENT STACK OVERFLOWS)
|
||||
let hidden_files: usize =
|
||||
self.files.iter().filter(|x| x.is_hidden()).count();
|
||||
// Only if there are more files, than hidden files keep incrementing
|
||||
if sz > hidden_files {
|
||||
self.incr_index();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => self.index = 0, // Reset to 0, for safety reasons
|
||||
}
|
||||
}
|
||||
|
||||
/// ### incr_index_by
|
||||
///
|
||||
/// Increment index by up to n
|
||||
/// If index goes to `files.len() - 1`, the value will be seto to the minimum acceptable value
|
||||
pub fn incr_index_by(&mut self, n: usize) {
|
||||
for _ in 0..n {
|
||||
let prev_idx: usize = self.index;
|
||||
// Increment
|
||||
self.incr_index();
|
||||
// If prev index is > index and break
|
||||
if prev_idx > self.index {
|
||||
self.index = prev_idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### decr_index
|
||||
///
|
||||
/// Decrement index to the first visible FsEntry.
|
||||
/// If index is 0, its value will be set to the maximum acceptable value
|
||||
pub fn decr_index(&mut self) {
|
||||
let sz: usize = self.files.len();
|
||||
// Increment or wrap
|
||||
if self.index > 0 {
|
||||
self.index -= 1; // Decrement
|
||||
} else {
|
||||
self.index = sz - 1; // Wrap
|
||||
}
|
||||
// Validate index
|
||||
match self.files.get(self.index) {
|
||||
Some(assoc_entry) => {
|
||||
if !self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
|
||||
// Check if file is hidden, otherwise increment
|
||||
if assoc_entry.is_hidden() {
|
||||
// Check if all files are hidden (NOTE: PREVENT STACK OVERFLOWS)
|
||||
let hidden_files: usize =
|
||||
self.files.iter().filter(|x| x.is_hidden()).count();
|
||||
// Only if there are more files, than hidden files keep decrementing
|
||||
if sz > hidden_files {
|
||||
self.decr_index();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => self.index = 0, // Reset to 0, for safety reasons
|
||||
}
|
||||
}
|
||||
|
||||
/// ### decr_index_by
|
||||
///
|
||||
/// Decrement index by up to n
|
||||
pub fn decr_index_by(&mut self, n: usize) {
|
||||
for _ in 0..n {
|
||||
let prev_idx: usize = self.index;
|
||||
// Increment
|
||||
self.decr_index();
|
||||
// If prev index is < index and break
|
||||
if prev_idx < self.index {
|
||||
self.index = prev_idx;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### index_at_first
|
||||
///
|
||||
/// Move index to first "visible" fs entry
|
||||
pub fn index_at_first(&mut self) {
|
||||
self.index = self.get_first_valid_index();
|
||||
}
|
||||
|
||||
/// ### get_first_valid_index
|
||||
///
|
||||
/// Return first valid index
|
||||
fn get_first_valid_index(&self) -> usize {
|
||||
match self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES) {
|
||||
true => 0,
|
||||
false => {
|
||||
// Look for first "non-hidden" entry
|
||||
for (i, f) in self.files.iter().enumerate() {
|
||||
if !f.is_hidden() {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// If all files are hidden, return 0
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_index
|
||||
///
|
||||
/// Return index
|
||||
pub fn get_index(&self) -> usize {
|
||||
self.index
|
||||
}
|
||||
|
||||
/// ### get_relative_index
|
||||
///
|
||||
/// Get relative index based on current options
|
||||
pub fn get_relative_index(&self) -> usize {
|
||||
match self.files.get(self.index) {
|
||||
Some(abs_entry) => {
|
||||
// Search abs entry in relative iterator
|
||||
for (i, f) in self.iter_files().enumerate() {
|
||||
if abs_entry.get_name() == f.get_name() {
|
||||
// If abs entry is f, return index
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// Return 0 if not found
|
||||
0
|
||||
}
|
||||
None => 0, // Absolute entry doesn't exist
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_index
|
||||
///
|
||||
/// Set index to idx.
|
||||
/// If index exceeds size, is set to count() - 1; or 0
|
||||
pub fn set_index(&mut self, idx: usize) {
|
||||
let visible_sz: usize = self.iter_files().count();
|
||||
match idx >= visible_sz {
|
||||
true => match visible_sz {
|
||||
0 => self.index_at_first(),
|
||||
_ => self.index = visible_sz - 1,
|
||||
},
|
||||
false => match self.get_first_valid_index() > idx {
|
||||
true => self.index_at_first(),
|
||||
false => self.index = idx,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// ### toggle_hidden_files
|
||||
///
|
||||
/// Enable/disable hidden files
|
||||
pub fn toggle_hidden_files(&mut self) {
|
||||
self.opts.toggle(ExplorerOpts::SHOW_HIDDEN_FILES);
|
||||
// Adjust index
|
||||
if self.index < self.get_first_valid_index() {
|
||||
self.index_at_first();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traits
|
||||
|
||||
impl ToString for FileSorting {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(match self {
|
||||
FileSorting::ByCreationTime => "by_creation_time",
|
||||
FileSorting::ByModifyTime => "by_mtime",
|
||||
FileSorting::ByName => "by_name",
|
||||
FileSorting::BySize => "by_size",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for FileSorting {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"by_creation_time" => Ok(FileSorting::ByCreationTime),
|
||||
"by_mtime" => Ok(FileSorting::ByModifyTime),
|
||||
"by_name" => Ok(FileSorting::ByName),
|
||||
"by_size" => Ok(FileSorting::BySize),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for GroupDirs {
|
||||
fn to_string(&self) -> String {
|
||||
String::from(match self {
|
||||
GroupDirs::First => "first",
|
||||
GroupDirs::Last => "last",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for GroupDirs {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"first" => Ok(GroupDirs::First),
|
||||
"last" => Ok(GroupDirs::Last),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::fs::{FsDirectory, FsFile};
|
||||
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_new() {
|
||||
let explorer: FileExplorer = FileExplorer::default();
|
||||
// Verify
|
||||
assert_eq!(explorer.dirstack.len(), 0);
|
||||
assert_eq!(explorer.files.len(), 0);
|
||||
assert_eq!(explorer.opts, ExplorerOpts::empty());
|
||||
assert_eq!(explorer.wrkdir, PathBuf::from("/"));
|
||||
assert_eq!(explorer.stack_size, 16);
|
||||
assert_eq!(explorer.index, 0);
|
||||
assert_eq!(explorer.group_dirs, None);
|
||||
assert_eq!(explorer.file_sorting, FileSorting::ByName);
|
||||
assert_eq!(explorer.get_file_sorting(), FileSorting::ByName);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_stack() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
explorer.stack_size = 2;
|
||||
explorer.dirstack = VecDeque::with_capacity(2);
|
||||
// Push dir
|
||||
explorer.pushd(&Path::new("/tmp"));
|
||||
explorer.pushd(&Path::new("/home/omar"));
|
||||
// Pop
|
||||
assert_eq!(explorer.popd().unwrap(), PathBuf::from("/home/omar"));
|
||||
assert_eq!(explorer.dirstack.len(), 1);
|
||||
assert_eq!(explorer.popd().unwrap(), PathBuf::from("/tmp"));
|
||||
assert_eq!(explorer.dirstack.len(), 0);
|
||||
// Dirstack is empty now
|
||||
assert!(explorer.popd().is_none());
|
||||
// Exceed limit
|
||||
explorer.pushd(&Path::new("/tmp"));
|
||||
explorer.pushd(&Path::new("/home/omar"));
|
||||
explorer.pushd(&Path::new("/dev"));
|
||||
assert_eq!(explorer.dirstack.len(), 2);
|
||||
assert_eq!(*explorer.dirstack.get(1).unwrap(), PathBuf::from("/dev"));
|
||||
assert_eq!(
|
||||
*explorer.dirstack.get(0).unwrap(),
|
||||
PathBuf::from("/home/omar")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_files() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
// Don't show hidden files
|
||||
explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES);
|
||||
// Create files
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry("README.md", false),
|
||||
make_fs_entry("src/", true),
|
||||
make_fs_entry(".git/", true),
|
||||
make_fs_entry("CONTRIBUTING.md", false),
|
||||
make_fs_entry("codecov.yml", false),
|
||||
make_fs_entry(".gitignore", false),
|
||||
]);
|
||||
assert_eq!(explorer.count(), 6);
|
||||
// Verify (files are sorted by name)
|
||||
assert_eq!(
|
||||
explorer.files.get(0).unwrap().get_name(),
|
||||
String::from(".git/")
|
||||
);
|
||||
// Iter files (all)
|
||||
assert_eq!(explorer.iter_files_all().count(), 6);
|
||||
// Iter files (hidden excluded) (.git, .gitignore are hidden)
|
||||
assert_eq!(explorer.iter_files().count(), 4);
|
||||
// Toggle hidden
|
||||
explorer.toggle_hidden_files();
|
||||
assert_eq!(explorer.iter_files().count(), 6); // All files are returned now
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_index() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES);
|
||||
// Create files (files are then sorted by name DEFAULT)
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry("README.md", false),
|
||||
make_fs_entry("src/", true),
|
||||
make_fs_entry(".git/", true),
|
||||
make_fs_entry("CONTRIBUTING.md", false),
|
||||
make_fs_entry("CODE_OF_CONDUCT.md", false),
|
||||
make_fs_entry("CHANGELOG.md", false),
|
||||
make_fs_entry("LICENSE", false),
|
||||
make_fs_entry("Cargo.toml", false),
|
||||
make_fs_entry("Cargo.lock", false),
|
||||
make_fs_entry("codecov.yml", false),
|
||||
make_fs_entry(".gitignore", false),
|
||||
]);
|
||||
let sz: usize = explorer.count();
|
||||
// Get first index
|
||||
assert_eq!(explorer.get_first_valid_index(), 2);
|
||||
// Index should be 2 now; files hidden; this happens because `index_at_first` is called after loading files
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
assert_eq!(explorer.get_relative_index(), 0); // Relative index should be 0
|
||||
assert_eq!(
|
||||
explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES),
|
||||
false
|
||||
);
|
||||
// Increment index
|
||||
explorer.incr_index();
|
||||
// Index should now be 3 (was 0, + 2 + 1); first 2 files are hidden (.git, .gitignore)
|
||||
assert_eq!(explorer.get_index(), 3);
|
||||
// Relative index should be 1 instead
|
||||
assert_eq!(explorer.get_relative_index(), 1);
|
||||
// Increment by 2
|
||||
explorer.incr_index_by(2);
|
||||
// Index should now be 5, 3
|
||||
assert_eq!(explorer.get_index(), 5);
|
||||
assert_eq!(explorer.get_relative_index(), 3);
|
||||
// Increment by (exceed size)
|
||||
explorer.incr_index_by(20);
|
||||
// Index should be at last element
|
||||
assert_eq!(explorer.get_index(), sz - 1);
|
||||
assert_eq!(explorer.get_relative_index(), sz - 3);
|
||||
// Increment; should go to 2
|
||||
explorer.incr_index();
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
assert_eq!(explorer.get_relative_index(), 0);
|
||||
// Increment and then decrement
|
||||
explorer.incr_index();
|
||||
explorer.decr_index();
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
assert_eq!(explorer.get_relative_index(), 0);
|
||||
// Decrement (and wrap)
|
||||
explorer.decr_index();
|
||||
// Index should be at last element
|
||||
assert_eq!(explorer.get_index(), sz - 1);
|
||||
assert_eq!(explorer.get_relative_index(), sz - 3);
|
||||
// Set index to 5
|
||||
explorer.set_index(5);
|
||||
assert_eq!(explorer.get_index(), 5);
|
||||
assert_eq!(explorer.get_relative_index(), 3);
|
||||
// Decr by 2
|
||||
explorer.decr_index_by(2);
|
||||
assert_eq!(explorer.get_index(), 3);
|
||||
assert_eq!(explorer.get_relative_index(), 1);
|
||||
// Decr by 2
|
||||
explorer.decr_index_by(2);
|
||||
// Should decrement actually by 1 (since first two files are hidden)
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
assert_eq!(explorer.get_relative_index(), 0);
|
||||
// Toggle hidden files
|
||||
explorer.toggle_hidden_files();
|
||||
assert_eq!(
|
||||
explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES),
|
||||
true
|
||||
);
|
||||
// Move index to 0
|
||||
explorer.set_index(0);
|
||||
assert_eq!(explorer.get_index(), 0);
|
||||
// Toggle hidden files
|
||||
explorer.toggle_hidden_files();
|
||||
// Index should now have been moved to 2
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
// Show hidden files
|
||||
explorer.toggle_hidden_files();
|
||||
// Set index to 5
|
||||
explorer.set_index(5);
|
||||
// Verify index
|
||||
assert_eq!(explorer.get_index(), 5);
|
||||
assert_eq!(explorer.get_relative_index(), 5); // Now relative matches
|
||||
// Decrement by 6, goes to 0
|
||||
explorer.decr_index_by(6);
|
||||
assert_eq!(explorer.get_index(), 0);
|
||||
assert_eq!(explorer.get_relative_index(), 0); // Now relative matches
|
||||
// Toggle; move at first
|
||||
explorer.toggle_hidden_files();
|
||||
assert_eq!(
|
||||
explorer.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES),
|
||||
false
|
||||
);
|
||||
explorer.index_at_first();
|
||||
assert_eq!(explorer.get_index(), 2);
|
||||
assert_eq!(explorer.get_relative_index(), 0);
|
||||
// Verify set index if exceeds
|
||||
let sz: usize = explorer.iter_files().count();
|
||||
explorer.set_index(sz);
|
||||
assert_eq!(explorer.get_index(), sz - 1); // Should be at last element
|
||||
// Empty files
|
||||
explorer.files.clear();
|
||||
explorer.index_at_first();
|
||||
assert_eq!(explorer.get_index(), 0);
|
||||
assert_eq!(explorer.get_relative_index(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_name() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
// Create files (files are then sorted by name)
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry("README.md", false),
|
||||
make_fs_entry("src/", true),
|
||||
make_fs_entry("CONTRIBUTING.md", false),
|
||||
make_fs_entry("CODE_OF_CONDUCT.md", false),
|
||||
make_fs_entry("CHANGELOG.md", false),
|
||||
make_fs_entry("LICENSE", false),
|
||||
make_fs_entry("Cargo.toml", false),
|
||||
make_fs_entry("Cargo.lock", false),
|
||||
make_fs_entry("codecov.yml", false),
|
||||
]);
|
||||
explorer.sort_by(FileSorting::ByName);
|
||||
// First entry should be "Cargo.lock"
|
||||
assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock");
|
||||
// Last should be "src/"
|
||||
assert_eq!(explorer.files.get(8).unwrap().get_name(), "src/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_mtime() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
let entry1: FsEntry = make_fs_entry("README.md", false);
|
||||
// Wait 1 sec
|
||||
sleep(Duration::from_secs(1));
|
||||
let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false);
|
||||
// Create files (files are then sorted by name)
|
||||
explorer.set_files(vec![entry1, entry2]);
|
||||
explorer.sort_by(FileSorting::ByModifyTime);
|
||||
// First entry should be "CODE_OF_CONDUCT.md"
|
||||
assert_eq!(
|
||||
explorer.files.get(0).unwrap().get_name(),
|
||||
"CODE_OF_CONDUCT.md"
|
||||
);
|
||||
// Last should be "src/"
|
||||
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_creation_time() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
let entry1: FsEntry = make_fs_entry("README.md", false);
|
||||
// Wait 1 sec
|
||||
sleep(Duration::from_secs(1));
|
||||
let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false);
|
||||
// Create files (files are then sorted by name)
|
||||
explorer.set_files(vec![entry1, entry2]);
|
||||
explorer.sort_by(FileSorting::ByCreationTime);
|
||||
// First entry should be "CODE_OF_CONDUCT.md"
|
||||
assert_eq!(
|
||||
explorer.files.get(0).unwrap().get_name(),
|
||||
"CODE_OF_CONDUCT.md"
|
||||
);
|
||||
// Last should be "src/"
|
||||
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_size() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
// Create files (files are then sorted by name)
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry_with_size("README.md", false, 1024),
|
||||
make_fs_entry("src/", true),
|
||||
make_fs_entry_with_size("CONTRIBUTING.md", false, 256),
|
||||
]);
|
||||
explorer.sort_by(FileSorting::BySize);
|
||||
// Directory has size 4096
|
||||
assert_eq!(explorer.files.get(0).unwrap().get_name(), "src/");
|
||||
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
|
||||
assert_eq!(explorer.files.get(2).unwrap().get_name(), "CONTRIBUTING.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_name_and_dirs_first() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
// Create files (files are then sorted by name)
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry("README.md", false),
|
||||
make_fs_entry("src/", true),
|
||||
make_fs_entry("docs/", true),
|
||||
make_fs_entry("CONTRIBUTING.md", false),
|
||||
make_fs_entry("CODE_OF_CONDUCT.md", false),
|
||||
make_fs_entry("CHANGELOG.md", false),
|
||||
make_fs_entry("LICENSE", false),
|
||||
make_fs_entry("Cargo.toml", false),
|
||||
make_fs_entry("Cargo.lock", false),
|
||||
make_fs_entry("codecov.yml", false),
|
||||
]);
|
||||
explorer.sort_by(FileSorting::ByName);
|
||||
explorer.group_dirs_by(Some(GroupDirs::First));
|
||||
// First entry should be "docs"
|
||||
assert_eq!(explorer.files.get(0).unwrap().get_name(), "docs/");
|
||||
assert_eq!(explorer.files.get(1).unwrap().get_name(), "src/");
|
||||
// 3rd is file first for alphabetical order
|
||||
assert_eq!(explorer.files.get(2).unwrap().get_name(), "Cargo.lock");
|
||||
// Last should be "README.md" (last file for alphabetical ordening)
|
||||
assert_eq!(explorer.files.get(9).unwrap().get_name(), "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_sort_by_name_and_dirs_last() {
|
||||
let mut explorer: FileExplorer = FileExplorer::default();
|
||||
// Create files (files are then sorted by name)
|
||||
explorer.set_files(vec![
|
||||
make_fs_entry("README.md", false),
|
||||
make_fs_entry("src/", true),
|
||||
make_fs_entry("docs/", true),
|
||||
make_fs_entry("CONTRIBUTING.md", false),
|
||||
make_fs_entry("CODE_OF_CONDUCT.md", false),
|
||||
make_fs_entry("CHANGELOG.md", false),
|
||||
make_fs_entry("LICENSE", false),
|
||||
make_fs_entry("Cargo.toml", false),
|
||||
make_fs_entry("Cargo.lock", false),
|
||||
make_fs_entry("codecov.yml", false),
|
||||
]);
|
||||
explorer.sort_by(FileSorting::ByName);
|
||||
explorer.group_dirs_by(Some(GroupDirs::Last));
|
||||
// Last entry should be "src"
|
||||
assert_eq!(explorer.files.get(8).unwrap().get_name(), "docs/");
|
||||
assert_eq!(explorer.files.get(9).unwrap().get_name(), "src/");
|
||||
// first is file for alphabetical order
|
||||
assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock");
|
||||
// Last in files should be "README.md" (last file for alphabetical ordening)
|
||||
assert_eq!(explorer.files.get(7).unwrap().get_name(), "README.md");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_to_string_from_str_traits() {
|
||||
// File Sorting
|
||||
assert_eq!(FileSorting::ByCreationTime.to_string(), "by_creation_time");
|
||||
assert_eq!(FileSorting::ByModifyTime.to_string(), "by_mtime");
|
||||
assert_eq!(FileSorting::ByName.to_string(), "by_name");
|
||||
assert_eq!(FileSorting::BySize.to_string(), "by_size");
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_creation_time").ok().unwrap(),
|
||||
FileSorting::ByCreationTime
|
||||
);
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_mtime").ok().unwrap(),
|
||||
FileSorting::ByModifyTime
|
||||
);
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_name").ok().unwrap(),
|
||||
FileSorting::ByName
|
||||
);
|
||||
assert_eq!(
|
||||
FileSorting::from_str("by_size").ok().unwrap(),
|
||||
FileSorting::BySize
|
||||
);
|
||||
assert!(FileSorting::from_str("omar").is_err());
|
||||
// Group dirs
|
||||
assert_eq!(GroupDirs::First.to_string(), "first");
|
||||
assert_eq!(GroupDirs::Last.to_string(), "last");
|
||||
assert_eq!(GroupDirs::from_str("first").ok().unwrap(), GroupDirs::First);
|
||||
assert_eq!(GroupDirs::from_str("last").ok().unwrap(), GroupDirs::Last);
|
||||
assert!(GroupDirs::from_str("omar").is_err());
|
||||
}
|
||||
|
||||
fn make_fs_entry(name: &str, is_dir: bool) -> FsEntry {
|
||||
let t_now: SystemTime = SystemTime::now();
|
||||
match is_dir {
|
||||
false => FsEntry::File(FsFile {
|
||||
name: name.to_string(),
|
||||
abs_path: PathBuf::from(name),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: 64,
|
||||
ftype: None, // File type
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
}),
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
name: name.to_string(),
|
||||
abs_path: PathBuf::from(name),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> FsEntry {
|
||||
let t_now: SystemTime = SystemTime::now();
|
||||
match is_dir {
|
||||
false => FsEntry::File(FsFile {
|
||||
name: name.to_string(),
|
||||
abs_path: PathBuf::from(name),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: size,
|
||||
ftype: None, // File type
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
}),
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
name: name.to_string(),
|
||||
abs_path: PathBuf::from(name),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -23,12 +23,16 @@
|
|||
*
|
||||
*/
|
||||
|
||||
// Mod
|
||||
pub mod explorer;
|
||||
|
||||
// Deps
|
||||
extern crate bytesize;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
extern crate users;
|
||||
|
||||
// Locals
|
||||
use crate::utils::fmt::{fmt_pex, fmt_time};
|
||||
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
@ -97,10 +101,10 @@ impl FsEntry {
|
|||
/// ### get_name
|
||||
///
|
||||
/// Get file name from `FsEntry`
|
||||
pub fn get_name(&self) -> String {
|
||||
pub fn get_name(&self) -> &'_ str {
|
||||
match self {
|
||||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
FsEntry::Directory(dir) => dir.name.as_ref(),
|
||||
FsEntry::File(file) => file.name.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,6 +212,13 @@ impl FsEntry {
|
|||
matches!(self, FsEntry::File(_))
|
||||
}
|
||||
|
||||
/// ### is_hidden
|
||||
///
|
||||
/// Returns whether FsEntry is hidden
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
self.get_name().starts_with('.')
|
||||
}
|
||||
|
||||
/// ### get_realfile
|
||||
///
|
||||
/// Return the real file pointed by a `FsEntry`
|
||||
|
@ -273,11 +284,20 @@ impl std::fmt::Display for FsEntry {
|
|||
// Get date
|
||||
let datetime: String = fmt_time(self.get_last_change_time(), "%b %d %Y %H:%M");
|
||||
// Set file name (or elide if too long)
|
||||
let name: String = self.get_name();
|
||||
let name: String = match name.len() >= 24 {
|
||||
false => name,
|
||||
true => format!("{}...", &name.as_str()[0..20]),
|
||||
let name: &str = self.get_name();
|
||||
let last_idx: usize = match self.is_dir() {
|
||||
// NOTE: For directories is 19, since we push '/' to name
|
||||
true => 19,
|
||||
false => 20,
|
||||
};
|
||||
let mut name: String = match name.len() >= 24 {
|
||||
false => name.to_string(),
|
||||
true => format!("{}...", &name[0..last_idx]),
|
||||
};
|
||||
// If is directory, append '/'
|
||||
if self.is_dir() {
|
||||
name.push('/');
|
||||
}
|
||||
write!(
|
||||
f,
|
||||
"{:24}\t{:12}\t{:12}\t{:10}\t{:17}",
|
||||
|
@ -353,6 +373,54 @@ mod tests {
|
|||
assert_eq!(entry.is_file(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_fsentry_hidden_files() {
|
||||
let t_now: SystemTime = SystemTime::now();
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
assert_eq!(entry.is_hidden(), false);
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from(".gitignore"),
|
||||
abs_path: PathBuf::from("/.gitignore"),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
assert_eq!(entry.is_hidden(), true);
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from(".git"),
|
||||
abs_path: PathBuf::from("/.git"),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
});
|
||||
assert_eq!(entry.is_hidden(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_fsentry_realfile_none() {
|
||||
let t_now: SystemTime = SystemTime::now();
|
||||
|
@ -592,7 +660,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects \tdrwxr-xr-x \troot \t4.1 KB \t{}",
|
||||
"projects/ \tdrwxr-xr-x \troot \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
|
@ -600,7 +668,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects \tdrwxr-xr-x \t0 \t4.1 KB \t{}",
|
||||
"projects/ \tdrwxr-xr-x \t0 \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
|
@ -621,7 +689,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects \td????????? \t0 \t4.1 KB \t{}",
|
||||
"projects/ \td????????? \t0 \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
|
@ -629,7 +697,7 @@ mod tests {
|
|||
assert_eq!(
|
||||
format!("{}", entry),
|
||||
format!(
|
||||
"projects \td????????? \t0 \t4.1 KB \t{}",
|
||||
"projects/ \td????????? \t0 \t4.1 KB \t{}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -19,6 +19,8 @@
|
|||
*
|
||||
*/
|
||||
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
|
@ -26,6 +28,7 @@ extern crate magic_crypt;
|
|||
|
||||
pub mod activity_manager;
|
||||
pub mod bookmarks;
|
||||
pub mod config;
|
||||
pub mod filetransfer;
|
||||
pub mod fs;
|
||||
pub mod host;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -25,6 +25,8 @@ const TERMSCP_AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
|||
// Crates
|
||||
extern crate getopts;
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate magic_crypt;
|
||||
|
@ -39,6 +41,7 @@ use std::time::Duration;
|
|||
// Include
|
||||
mod activity_manager;
|
||||
mod bookmarks;
|
||||
mod config;
|
||||
mod filetransfer;
|
||||
mod fs;
|
||||
mod host;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -23,21 +23,19 @@
|
|||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate magic_crypt;
|
||||
extern crate rand;
|
||||
|
||||
// Local
|
||||
use crate::bookmarks::serializer::BookmarkSerializer;
|
||||
use crate::bookmarks::{Bookmark, SerializerError, SerializerErrorKind, UserHosts};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::utils::crypto;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
use crate::utils::random::random_alphanumeric_with_len;
|
||||
// Ext
|
||||
use magic_crypt::MagicCryptTrait;
|
||||
use rand::{distributions::Alphanumeric, Rng};
|
||||
use std::fs::{OpenOptions, Permissions};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// ## BookmarksClient
|
||||
|
@ -113,11 +111,9 @@ impl BookmarksClient {
|
|||
Some((
|
||||
entry.address.clone(),
|
||||
entry.port,
|
||||
match entry.protocol.to_ascii_uppercase().as_str() {
|
||||
"FTP" => FileTransferProtocol::Ftp(false),
|
||||
"FTPS" => FileTransferProtocol::Ftp(true),
|
||||
"SCP" => FileTransferProtocol::Scp,
|
||||
_ => FileTransferProtocol::Sftp,
|
||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||
Ok(proto) => proto,
|
||||
Err(_) => FileTransferProtocol::Sftp, // Default
|
||||
},
|
||||
entry.username.clone(),
|
||||
match &entry.password {
|
||||
|
@ -173,11 +169,9 @@ impl BookmarksClient {
|
|||
Some((
|
||||
entry.address.clone(),
|
||||
entry.port,
|
||||
match entry.protocol.to_ascii_uppercase().as_str() {
|
||||
"FTP" => FileTransferProtocol::Ftp(false),
|
||||
"FTPS" => FileTransferProtocol::Ftp(true),
|
||||
"SCP" => FileTransferProtocol::Scp,
|
||||
_ => FileTransferProtocol::Sftp,
|
||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||
Ok(proto) => proto,
|
||||
Err(_) => FileTransferProtocol::Sftp, // Default
|
||||
},
|
||||
entry.username.clone(),
|
||||
))
|
||||
|
@ -285,10 +279,7 @@ impl BookmarksClient {
|
|||
/// Generate a new AES key and write it to key file
|
||||
fn generate_key(key_file: &Path) -> Result<String, SerializerError> {
|
||||
// Generate 256 bytes (2048 bits) key
|
||||
let key: String = rand::thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(256)
|
||||
.collect::<String>();
|
||||
let key: String = random_alphanumeric_with_len(256);
|
||||
// Write file
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
|
@ -332,14 +323,7 @@ impl BookmarksClient {
|
|||
address: addr,
|
||||
port,
|
||||
username,
|
||||
protocol: match protocol {
|
||||
FileTransferProtocol::Ftp(secure) => match secure {
|
||||
true => String::from("FTPS"),
|
||||
false => String::from("FTP"),
|
||||
},
|
||||
FileTransferProtocol::Scp => String::from("SCP"),
|
||||
FileTransferProtocol::Sftp => String::from("SFTP"),
|
||||
},
|
||||
protocol: protocol.to_string(),
|
||||
password: match password {
|
||||
Some(p) => Some(self.encrypt_str(p.as_str())), // Encrypt password if provided
|
||||
None => None,
|
||||
|
@ -373,16 +357,14 @@ impl BookmarksClient {
|
|||
///
|
||||
/// Encrypt provided string using AES-128. Encrypted buffer is then converted to BASE64
|
||||
fn encrypt_str(&self, txt: &str) -> String {
|
||||
let crypter = new_magic_crypt!(self.key.clone(), 128);
|
||||
crypter.encrypt_str_to_base64(txt.to_string())
|
||||
crypto::aes128_b64_crypt(self.key.as_str(), txt)
|
||||
}
|
||||
|
||||
/// ### decrypt_str
|
||||
///
|
||||
/// Decrypt provided string using AES-128
|
||||
fn decrypt_str(&self, secret: &str) -> Result<String, SerializerError> {
|
||||
let crypter = new_magic_crypt!(self.key.clone(), 128);
|
||||
match crypter.decrypt_base64_to_string(secret.to_string()) {
|
||||
match crypto::aes128_b64_decrypt(self.key.as_str(), secret) {
|
||||
Ok(txt) => Ok(txt),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
|
@ -409,7 +391,7 @@ mod tests {
|
|||
// Verify client
|
||||
assert_eq!(client.hosts.bookmarks.len(), 0);
|
||||
assert_eq!(client.hosts.recents.len(), 0);
|
||||
assert!(client.key.len() > 0);
|
||||
assert_eq!(client.key.len(), 256);
|
||||
assert_eq!(client.bookmarks_file, cfg_path);
|
||||
assert_eq!(client.recents_size, 16);
|
||||
}
|
||||
|
|
534
src/system/config_client.rs
Normal file
534
src/system/config_client.rs
Normal file
|
@ -0,0 +1,534 @@
|
|||
//! ## ConfigClient
|
||||
//!
|
||||
//! `config_client` is the module which provides an API between the Config module and the system
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate rand;
|
||||
// Locals
|
||||
use crate::config::serializer::ConfigSerializer;
|
||||
use crate::config::{SerializerError, SerializerErrorKind, UserConfig};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs;
|
||||
// Ext
|
||||
use std::fs::{create_dir, remove_file, File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
// Types
|
||||
pub type SshHost = (String, String, PathBuf); // 0: host, 1: username, 2: RSA key path
|
||||
|
||||
/// ## ConfigClient
|
||||
///
|
||||
/// ConfigClient provides a high level API to communicate with the termscp configuration
|
||||
pub struct ConfigClient {
|
||||
config: UserConfig, // Configuration loaded
|
||||
config_path: PathBuf, // Configuration TOML Path
|
||||
ssh_key_dir: PathBuf, // SSH Key storage directory
|
||||
}
|
||||
|
||||
impl ConfigClient {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiate a new `ConfigClient` with provided path
|
||||
pub fn new(config_path: &Path, ssh_key_dir: &Path) -> Result<ConfigClient, SerializerError> {
|
||||
// Initialize a default configuration
|
||||
let default_config: UserConfig = UserConfig::default();
|
||||
// Create client
|
||||
let mut client: ConfigClient = ConfigClient {
|
||||
config: default_config,
|
||||
config_path: PathBuf::from(config_path),
|
||||
ssh_key_dir: PathBuf::from(ssh_key_dir),
|
||||
};
|
||||
// If ssh key directory doesn't exist, create it
|
||||
if !ssh_key_dir.exists() {
|
||||
if let Err(err) = create_dir(ssh_key_dir) {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
format!(
|
||||
"Could not create SSH key directory \"{}\": {}",
|
||||
ssh_key_dir.display(),
|
||||
err
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
// If Config file doesn't exist, create it
|
||||
if !config_path.exists() {
|
||||
if let Err(err) = client.write_config() {
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
// otherwise Load configuration from file
|
||||
if let Err(err) = client.read_config() {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
// Text editor
|
||||
|
||||
/// ### get_text_editor
|
||||
///
|
||||
/// Get text editor from configuration
|
||||
pub fn get_text_editor(&self) -> PathBuf {
|
||||
self.config.user_interface.text_editor.clone()
|
||||
}
|
||||
|
||||
/// ### set_text_editor
|
||||
///
|
||||
/// Set text editor path
|
||||
pub fn set_text_editor(&mut self, path: PathBuf) {
|
||||
self.config.user_interface.text_editor = path;
|
||||
}
|
||||
|
||||
// Default protocol
|
||||
|
||||
/// ### get_default_protocol
|
||||
///
|
||||
/// Get default protocol from configuration
|
||||
pub fn get_default_protocol(&self) -> FileTransferProtocol {
|
||||
match FileTransferProtocol::from_str(self.config.user_interface.default_protocol.as_str()) {
|
||||
Ok(p) => p,
|
||||
Err(_) => FileTransferProtocol::Sftp,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_default_protocol
|
||||
///
|
||||
/// Set default protocol to configuration
|
||||
pub fn set_default_protocol(&mut self, proto: FileTransferProtocol) {
|
||||
self.config.user_interface.default_protocol = proto.to_string();
|
||||
}
|
||||
|
||||
/// ### get_show_hidden_files
|
||||
///
|
||||
/// Get value of `show_hidden_files`
|
||||
pub fn get_show_hidden_files(&self) -> bool {
|
||||
self.config.user_interface.show_hidden_files
|
||||
}
|
||||
|
||||
/// ### set_show_hidden_files
|
||||
///
|
||||
/// Set new value for `show_hidden_files`
|
||||
pub fn set_show_hidden_files(&mut self, value: bool) {
|
||||
self.config.user_interface.show_hidden_files = value;
|
||||
}
|
||||
|
||||
/// ### get_group_dirs
|
||||
///
|
||||
/// Get GroupDirs value from configuration (will be converted from string)
|
||||
pub fn get_group_dirs(&self) -> Option<GroupDirs> {
|
||||
// Convert string to `GroupDirs`
|
||||
match &self.config.user_interface.group_dirs {
|
||||
None => None,
|
||||
Some(val) => match GroupDirs::from_str(val.as_str()) {
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_group_dirs
|
||||
///
|
||||
/// Set value for group_dir in configuration.
|
||||
/// Provided value, if `Some` will be converted to `GroupDirs`
|
||||
pub fn set_group_dirs(&mut self, val: Option<GroupDirs>) {
|
||||
self.config.user_interface.group_dirs = match val {
|
||||
None => None,
|
||||
Some(val) => Some(val.to_string()),
|
||||
};
|
||||
}
|
||||
|
||||
// SSH Keys
|
||||
|
||||
/// ### save_ssh_key
|
||||
///
|
||||
/// Save a SSH key into configuration.
|
||||
/// This operation also creates the key file in `ssh_key_dir`
|
||||
/// and also commits changes to configuration, to prevent incoerent data
|
||||
pub fn add_ssh_key(
|
||||
&mut self,
|
||||
host: &str,
|
||||
username: &str,
|
||||
ssh_key: &str,
|
||||
) -> Result<(), SerializerError> {
|
||||
let host_name: String = Self::make_ssh_host_key(host, username);
|
||||
// Get key path
|
||||
let ssh_key_path: PathBuf = {
|
||||
let mut p: PathBuf = self.ssh_key_dir.clone();
|
||||
p.push(format!("{}.key", host_name));
|
||||
p
|
||||
};
|
||||
// Write key to file
|
||||
let mut f: File = match File::create(ssh_key_path.as_path()) {
|
||||
Ok(f) => f,
|
||||
Err(err) => return Self::make_io_err(err),
|
||||
};
|
||||
if let Err(err) = f.write_all(ssh_key.as_bytes()) {
|
||||
return Self::make_io_err(err);
|
||||
}
|
||||
// Add host to keys
|
||||
self.config.remote.ssh_keys.insert(host_name, ssh_key_path);
|
||||
// Write config
|
||||
self.write_config()
|
||||
}
|
||||
|
||||
/// ### del_ssh_key
|
||||
///
|
||||
/// Delete a ssh key from configuration, using host as key.
|
||||
/// This operation also unlinks the key file in `ssh_key_dir`
|
||||
/// and also commits changes to configuration, to prevent incoerent data
|
||||
pub fn del_ssh_key(&mut self, host: &str, username: &str) -> Result<(), SerializerError> {
|
||||
// Remove key from configuration and get key path
|
||||
let key_path: PathBuf = match self
|
||||
.config
|
||||
.remote
|
||||
.ssh_keys
|
||||
.remove(&Self::make_ssh_host_key(host, username))
|
||||
{
|
||||
Some(p) => p,
|
||||
None => return Ok(()), // Return ok if host doesn't exist
|
||||
};
|
||||
// Remove file
|
||||
if let Err(err) = remove_file(key_path.as_path()) {
|
||||
return Self::make_io_err(err);
|
||||
}
|
||||
// Commit changes to configuration
|
||||
self.write_config()
|
||||
}
|
||||
|
||||
/// ### get_ssh_key
|
||||
///
|
||||
/// Get ssh key from host.
|
||||
/// None is returned if key doesn't exist
|
||||
/// `std::io::Error` is returned in case it was not possible to read the key file
|
||||
pub fn get_ssh_key(&self, mkey: &str) -> std::io::Result<Option<SshHost>> {
|
||||
// Check if Key exists
|
||||
match self.config.remote.ssh_keys.get(mkey) {
|
||||
None => Ok(None),
|
||||
Some(key_path) => {
|
||||
// Get host and username
|
||||
let (host, username): (String, String) = Self::get_ssh_tokens(mkey);
|
||||
// Return key
|
||||
Ok(Some((host, username, PathBuf::from(key_path))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### iter_ssh_keys
|
||||
///
|
||||
/// Get an iterator through hosts in the ssh key storage
|
||||
pub fn iter_ssh_keys(&self) -> Box<dyn Iterator<Item = &String> + '_> {
|
||||
Box::new(self.config.remote.ssh_keys.keys())
|
||||
}
|
||||
|
||||
// I/O
|
||||
|
||||
/// ### write_config
|
||||
///
|
||||
/// Write configuration to file
|
||||
pub fn write_config(&self) -> Result<(), SerializerError> {
|
||||
// Open file
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(self.config_path.as_path())
|
||||
{
|
||||
Ok(writer) => {
|
||||
let serializer: ConfigSerializer = ConfigSerializer {};
|
||||
serializer.serialize(Box::new(writer), &self.config)
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### read_config
|
||||
///
|
||||
/// Read configuration from file (or reload it if already read)
|
||||
pub fn read_config(&mut self) -> Result<(), SerializerError> {
|
||||
// Open bookmarks file for read
|
||||
match OpenOptions::new()
|
||||
.read(true)
|
||||
.open(self.config_path.as_path())
|
||||
{
|
||||
Ok(reader) => {
|
||||
// Deserialize
|
||||
let deserializer: ConfigSerializer = ConfigSerializer {};
|
||||
match deserializer.deserialize(Box::new(reader)) {
|
||||
Ok(config) => {
|
||||
self.config = config;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### make_ssh_host_key
|
||||
///
|
||||
/// Hosts are saved as `username@host` into configuration.
|
||||
/// This method creates the key name, starting from host and username
|
||||
fn make_ssh_host_key(host: &str, username: &str) -> String {
|
||||
format!("{}@{}", username, host)
|
||||
}
|
||||
|
||||
/// ### get_ssh_tokens
|
||||
///
|
||||
/// Get ssh tokens starting from ssh host key
|
||||
/// Panics if key has invalid syntax
|
||||
/// Returns: (host, username)
|
||||
fn get_ssh_tokens(host_key: &str) -> (String, String) {
|
||||
let tokens: Vec<&str> = host_key.split('@').collect();
|
||||
assert_eq!(tokens.len(), 2);
|
||||
(String::from(tokens[1]), String::from(tokens[0]))
|
||||
}
|
||||
|
||||
/// ### make_io_err
|
||||
///
|
||||
/// Make serializer error from `std::io::Error`
|
||||
fn make_io_err(err: std::io::Error) -> Result<(), SerializerError> {
|
||||
Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::config::UserConfig;
|
||||
use crate::utils::random::random_alphanumeric_with_len;
|
||||
|
||||
use std::io::Read;
|
||||
|
||||
#[test]
|
||||
fn test_system_config_new() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, ssh_keys_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), ssh_keys_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
// Verify parameters
|
||||
let default_config: UserConfig = UserConfig::default();
|
||||
assert_eq!(client.config.remote.ssh_keys.len(), 0);
|
||||
assert_eq!(
|
||||
client.config.user_interface.default_protocol,
|
||||
default_config.user_interface.default_protocol
|
||||
);
|
||||
assert_eq!(
|
||||
client.config.user_interface.text_editor,
|
||||
default_config.user_interface.text_editor
|
||||
);
|
||||
assert_eq!(client.config_path, cfg_path);
|
||||
assert_eq!(client.ssh_key_dir, ssh_keys_path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_new_err() {
|
||||
assert!(
|
||||
ConfigClient::new(Path::new("/tmp/oifoif/omar"), Path::new("/tmp/efnnu/omar"),)
|
||||
.is_err()
|
||||
);
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, _): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
assert!(ConfigClient::new(cfg_path.as_path(), Path::new("/tmp/efnnu/omar")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_from_existing() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
// Change some stuff
|
||||
client.set_text_editor(PathBuf::from("/usr/bin/vim"));
|
||||
client.set_default_protocol(FileTransferProtocol::Scp);
|
||||
assert!(client
|
||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||
.is_ok());
|
||||
assert!(client.write_config().is_ok());
|
||||
// Istantiate a new client
|
||||
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
// Verify client has updated parameters
|
||||
assert_eq!(client.get_default_protocol(), FileTransferProtocol::Scp);
|
||||
assert_eq!(client.get_text_editor(), PathBuf::from("/usr/bin/vim"));
|
||||
let mut expected_key_path: PathBuf = key_path.clone();
|
||||
expected_key_path.push("pi@192.168.1.31.key");
|
||||
assert_eq!(
|
||||
client.get_ssh_key("pi@192.168.1.31").unwrap().unwrap(),
|
||||
(
|
||||
String::from("192.168.1.31"),
|
||||
String::from("pi"),
|
||||
expected_key_path,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_text_editor() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
client.set_text_editor(PathBuf::from("mcedit"));
|
||||
assert_eq!(client.get_text_editor(), PathBuf::from("mcedit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_default_protocol() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
client.set_default_protocol(FileTransferProtocol::Ftp(true));
|
||||
assert_eq!(
|
||||
client.get_default_protocol(),
|
||||
FileTransferProtocol::Ftp(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_show_hidden_files() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
client.set_show_hidden_files(true);
|
||||
assert_eq!(client.get_show_hidden_files(), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_group_dirs() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
client.set_group_dirs(Some(GroupDirs::First));
|
||||
assert_eq!(client.get_group_dirs(), Some(GroupDirs::First),);
|
||||
client.set_group_dirs(None);
|
||||
assert_eq!(client.get_group_dirs(), None,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_ssh_keys() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
// Add a new key
|
||||
let rsa_key: String = get_sample_rsa_key();
|
||||
assert!(client
|
||||
.add_ssh_key("192.168.1.31", "pi", rsa_key.as_str())
|
||||
.is_ok());
|
||||
// Iterate keys
|
||||
for key in client.iter_ssh_keys() {
|
||||
let host: SshHost = client.get_ssh_key(key).ok().unwrap().unwrap();
|
||||
println!("{:?}", host);
|
||||
assert_eq!(host.0, String::from("192.168.1.31"));
|
||||
assert_eq!(host.1, String::from("pi"));
|
||||
let mut expected_key_path: PathBuf = key_path.clone();
|
||||
expected_key_path.push("pi@192.168.1.31.key");
|
||||
assert_eq!(host.2, expected_key_path);
|
||||
// Read rsa key
|
||||
let mut key_file: File = File::open(expected_key_path.as_path()).ok().unwrap();
|
||||
// Read
|
||||
let mut key: String = String::new();
|
||||
assert!(key_file.read_to_string(&mut key).is_ok());
|
||||
// Verify rsa key
|
||||
assert_eq!(key, rsa_key);
|
||||
}
|
||||
// Unexisting key
|
||||
assert!(client.get_ssh_key("test").ok().unwrap().is_none());
|
||||
// Delete key
|
||||
assert!(client.del_ssh_key("192.168.1.31", "pi").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_make_key() {
|
||||
assert_eq!(
|
||||
ConfigClient::make_ssh_host_key("192.168.1.31", "pi"),
|
||||
String::from("pi@192.168.1.31")
|
||||
);
|
||||
assert_eq!(
|
||||
ConfigClient::get_ssh_tokens("pi@192.168.1.31"),
|
||||
(String::from("192.168.1.31"), String::from("pi"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_config_make_io_err() {
|
||||
let err: SerializerError =
|
||||
ConfigClient::make_io_err(std::io::Error::from(std::io::ErrorKind::PermissionDenied))
|
||||
.err()
|
||||
.unwrap();
|
||||
assert_eq!(err.to_string(), "IO error (permission denied)");
|
||||
}
|
||||
|
||||
/// ### get_paths
|
||||
///
|
||||
/// Get paths for configuration and keys directory
|
||||
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||
let mut k: PathBuf = PathBuf::from(dir);
|
||||
let mut c: PathBuf = k.clone();
|
||||
k.push("ssh-keys/");
|
||||
c.push("config.toml");
|
||||
(c, k)
|
||||
}
|
||||
|
||||
/// ### create_tmp_dir
|
||||
///
|
||||
/// Create temporary directory
|
||||
fn create_tmp_dir() -> tempfile::TempDir {
|
||||
tempfile::TempDir::new().ok().unwrap()
|
||||
}
|
||||
|
||||
fn get_sample_rsa_key() -> String {
|
||||
format!(
|
||||
"-----BEGIN OPENSSH PRIVATE KEY-----\n{}\n-----END OPENSSH PRIVATE KEY-----",
|
||||
random_alphanumeric_with_len(2536)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -27,7 +27,7 @@
|
|||
extern crate dirs;
|
||||
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// ### get_config_dir
|
||||
///
|
||||
|
@ -56,6 +56,32 @@ pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
|
|||
}
|
||||
}
|
||||
|
||||
/// ### get_bookmarks_paths
|
||||
///
|
||||
/// Get paths for bookmarks client
|
||||
/// Returns: path of bookmarks.toml and path of key
|
||||
pub fn get_bookmarks_paths(config_dir: &Path) -> (PathBuf, PathBuf) {
|
||||
// Prepare paths
|
||||
let mut bookmarks_file: PathBuf = PathBuf::from(config_dir);
|
||||
bookmarks_file.push("bookmarks.toml");
|
||||
let mut key_file: PathBuf = PathBuf::from(config_dir);
|
||||
key_file.push(".bookmarks.key"); // key file is hidden
|
||||
(bookmarks_file, key_file)
|
||||
}
|
||||
|
||||
/// ### get_config_paths
|
||||
///
|
||||
/// Returns paths for config client
|
||||
/// Returns: path of config.toml and path for ssh keys
|
||||
pub fn get_config_paths(config_dir: &Path) -> (PathBuf, PathBuf) {
|
||||
// Prepare paths
|
||||
let mut bookmarks_file: PathBuf = PathBuf::from(config_dir);
|
||||
bookmarks_file.push("config.toml");
|
||||
let mut keys_dir: PathBuf = PathBuf::from(config_dir);
|
||||
keys_dir.push(".ssh/"); // Path where keys are stored
|
||||
(bookmarks_file, keys_dir)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
|
@ -92,4 +118,26 @@ mod tests {
|
|||
// Remove file
|
||||
assert!(std::fs::remove_file(conf_dir.as_path()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_environment_get_bookmarks_paths() {
|
||||
assert_eq!(
|
||||
get_bookmarks_paths(&Path::new("/home/omar/.config/termscp/")),
|
||||
(
|
||||
PathBuf::from("/home/omar/.config/termscp/bookmarks.toml"),
|
||||
PathBuf::from("/home/omar/.config/termscp/.bookmarks.key")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_environment_get_config_paths() {
|
||||
assert_eq!(
|
||||
get_config_paths(&Path::new("/home/omar/.config/termscp/")),
|
||||
(
|
||||
PathBuf::from("/home/omar/.config/termscp/config.toml"),
|
||||
PathBuf::from("/home/omar/.config/termscp/.ssh/")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -25,4 +25,6 @@
|
|||
|
||||
// modules
|
||||
pub mod bookmarks_client;
|
||||
pub mod config_client;
|
||||
pub mod environment;
|
||||
pub mod sshkey_storage;
|
||||
|
|
140
src/system/sshkey_storage.rs
Normal file
140
src/system/sshkey_storage.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
//! ## SshKeyStorage
|
||||
//!
|
||||
//! `SshKeyStorage` is the module which behaves a storage for ssh keys
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::config_client::ConfigClient;
|
||||
// Ext
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct SshKeyStorage {
|
||||
hosts: HashMap<String, PathBuf>, // Association between {user}@{host} and RSA key path
|
||||
}
|
||||
|
||||
impl SshKeyStorage {
|
||||
/// ### storage_from_config
|
||||
///
|
||||
/// Create a `SshKeyStorage` starting from a `ConfigClient`
|
||||
pub fn storage_from_config(cfg_client: &ConfigClient) -> Self {
|
||||
let mut hosts: HashMap<String, PathBuf> =
|
||||
HashMap::with_capacity(cfg_client.iter_ssh_keys().count());
|
||||
// Iterate over keys
|
||||
for key in cfg_client.iter_ssh_keys() {
|
||||
match cfg_client.get_ssh_key(key) {
|
||||
Ok(host) => match host {
|
||||
Some((addr, username, rsa_key_path)) => {
|
||||
let key_name: String = Self::make_mapkey(&addr, &username);
|
||||
hosts.insert(key_name, rsa_key_path);
|
||||
}
|
||||
None => continue,
|
||||
},
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
// Return storage
|
||||
SshKeyStorage { hosts }
|
||||
}
|
||||
|
||||
/// ### empty
|
||||
///
|
||||
/// Create an empty ssh key storage; used in case `ConfigClient` is not available
|
||||
pub fn empty() -> Self {
|
||||
SshKeyStorage {
|
||||
hosts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### resolve
|
||||
///
|
||||
/// Return RSA key path from host and username
|
||||
pub fn resolve(&self, host: &str, username: &str) -> Option<&PathBuf> {
|
||||
let key: String = Self::make_mapkey(host, username);
|
||||
self.hosts.get(&key)
|
||||
}
|
||||
|
||||
/// ### make_mapkey
|
||||
///
|
||||
/// Make mapkey from host and username
|
||||
fn make_mapkey(host: &str, username: &str) -> String {
|
||||
format!("{}@{}", username, host)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_system_sshkey_storage_new() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
// Add ssh key
|
||||
assert!(client
|
||||
.add_ssh_key("192.168.1.31", "pi", "piroporopero")
|
||||
.is_ok());
|
||||
// Create ssh key storage
|
||||
let storage: SshKeyStorage = SshKeyStorage::storage_from_config(&client);
|
||||
// Verify key exists
|
||||
let mut exp_key_path: PathBuf = key_path.clone();
|
||||
exp_key_path.push("pi@192.168.1.31.key");
|
||||
assert_eq!(
|
||||
*storage.resolve("192.168.1.31", "pi").unwrap(),
|
||||
exp_key_path
|
||||
);
|
||||
// Verify unexisting key
|
||||
assert!(storage.resolve("deskichup", "veeso").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_sshkey_storage_empty() {
|
||||
let storage: SshKeyStorage = SshKeyStorage::empty();
|
||||
assert_eq!(storage.hosts.len(), 0);
|
||||
}
|
||||
|
||||
/// ### get_paths
|
||||
///
|
||||
/// Get paths for configuration and keys directory
|
||||
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||
let mut k: PathBuf = PathBuf::from(dir);
|
||||
let mut c: PathBuf = k.clone();
|
||||
k.push("ssh-keys/");
|
||||
c.push("config.toml");
|
||||
(c, k)
|
||||
}
|
||||
|
||||
/// ### create_tmp_dir
|
||||
///
|
||||
/// Create temporary directory
|
||||
fn create_tmp_dir() -> tempfile::TempDir {
|
||||
tempfile::TempDir::new().ok().unwrap()
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -27,7 +27,7 @@
|
|||
extern crate dirs;
|
||||
|
||||
// Locals
|
||||
use super::{AuthActivity, Color, DialogYesNoOption, InputMode, PopupType};
|
||||
use super::{AuthActivity, Color, DialogYesNoOption, Popup};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::environment;
|
||||
|
||||
|
@ -89,7 +89,7 @@ impl AuthActivity {
|
|||
let port: u16 = match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
|
@ -98,7 +98,7 @@ impl AuthActivity {
|
|||
val as u16
|
||||
}
|
||||
Err(_) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
|
@ -174,7 +174,7 @@ impl AuthActivity {
|
|||
let port: u16 = match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
|
@ -183,7 +183,7 @@ impl AuthActivity {
|
|||
val as u16
|
||||
}
|
||||
Err(_) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
|
@ -208,7 +208,7 @@ impl AuthActivity {
|
|||
fn write_bookmarks(&mut self) {
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() {
|
||||
if let Err(err) = bookmarks_cli.write_bookmarks() {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not write bookmarks: {}", err),
|
||||
));
|
||||
|
@ -225,16 +225,13 @@ impl AuthActivity {
|
|||
Ok(path) => {
|
||||
// If some configure client, otherwise do nothing; don't bother users telling them that bookmarks are not supported on their system.
|
||||
if let Some(path) = path {
|
||||
// Prepare paths
|
||||
let mut bookmarks_file: PathBuf = path.clone();
|
||||
bookmarks_file.push("bookmarks.toml");
|
||||
let mut key_file: PathBuf = path;
|
||||
key_file.push(".bookmarks.key"); // key file is hidden
|
||||
// Initialize client
|
||||
let (bookmarks_file, key_file): (PathBuf, PathBuf) =
|
||||
environment::get_bookmarks_paths(path.as_path());
|
||||
// Initialize client
|
||||
match BookmarksClient::new(bookmarks_file.as_path(), key_file.as_path(), 16) {
|
||||
Ok(cli) => self.bookmarks_client = Some(cli),
|
||||
Err(err) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!(
|
||||
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
|
||||
|
@ -248,7 +245,7 @@ impl AuthActivity {
|
|||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not initialize configuration directory: {}", err),
|
||||
))
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -25,7 +25,7 @@
|
|||
|
||||
use super::{
|
||||
AuthActivity, DialogCallback, DialogYesNoOption, FileTransferProtocol, InputEvent, InputField,
|
||||
InputForm, InputMode, PopupType,
|
||||
InputForm, Popup,
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
|
@ -36,13 +36,13 @@ impl AuthActivity {
|
|||
///
|
||||
/// Handle input event, based on current input mode
|
||||
pub(super) fn handle_input_event(&mut self, ev: &InputEvent) {
|
||||
let popup: Option<PopupType> = match &self.input_mode {
|
||||
InputMode::Popup(ptype) => Some(ptype.clone()),
|
||||
let popup: Option<Popup> = match &self.popup {
|
||||
Some(ptype) => Some(ptype.clone()),
|
||||
_ => None,
|
||||
};
|
||||
match self.input_mode {
|
||||
InputMode::Form => self.handle_input_event_mode_form(ev),
|
||||
InputMode::Popup(_) => {
|
||||
match &self.popup {
|
||||
None => self.handle_input_event_mode_form(ev),
|
||||
Some(_) => {
|
||||
if let Some(ptype) = popup {
|
||||
self.handle_input_event_mode_popup(ev, ptype)
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ impl AuthActivity {
|
|||
/// ### handle_input_event_mode_form
|
||||
///
|
||||
/// Handler for input event when in form mode
|
||||
pub(super) fn handle_input_event_mode_form(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_form(&mut self, ev: &InputEvent) {
|
||||
match self.input_form {
|
||||
InputForm::AuthCredentials => self.handle_input_event_mode_form_auth(ev),
|
||||
InputForm::Bookmarks => self.handle_input_event_mode_form_bookmarks(ev),
|
||||
|
@ -64,12 +64,12 @@ impl AuthActivity {
|
|||
/// ### handle_input_event_mode_form_auth
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Auth
|
||||
pub(super) fn handle_input_event_mode_form_auth(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_form_auth(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Show quit dialog
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
|
@ -81,10 +81,8 @@ impl AuthActivity {
|
|||
// Check form
|
||||
// Check address
|
||||
if self.address.is_empty() {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
String::from("Invalid address"),
|
||||
));
|
||||
self.popup =
|
||||
Some(Popup::Alert(Color::Red, String::from("Invalid address")));
|
||||
return;
|
||||
}
|
||||
// Check port
|
||||
|
@ -92,7 +90,7 @@ impl AuthActivity {
|
|||
match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
|
@ -100,7 +98,7 @@ impl AuthActivity {
|
|||
}
|
||||
}
|
||||
Err(_) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
|
@ -157,13 +155,17 @@ impl AuthActivity {
|
|||
match ch {
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'C' | 'c' => {
|
||||
// Show setup
|
||||
self.setup = true;
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Default choice option to no
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// Save bookmark as...
|
||||
self.input_mode = InputMode::Popup(PopupType::SaveBookmark);
|
||||
self.popup = Some(Popup::SaveBookmark);
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
|
@ -216,12 +218,12 @@ impl AuthActivity {
|
|||
/// ### handle_input_event_mode_form_bookmarks
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Bookmarks
|
||||
pub(super) fn handle_input_event_mode_form_bookmarks(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_form_bookmarks(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Show quit dialog
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
|
@ -253,7 +255,7 @@ impl AuthActivity {
|
|||
}
|
||||
KeyCode::Delete => {
|
||||
// Ask if user wants to delete bookmark
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to delete the selected bookmark?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
|
@ -268,9 +270,13 @@ impl AuthActivity {
|
|||
self.selected_field = InputField::Password;
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'C' | 'c' => {
|
||||
// Show setup
|
||||
self.setup = true;
|
||||
}
|
||||
'E' | 'e' => {
|
||||
// Ask if user wants to delete bookmark; NOTE: same as <DEL>
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to delete the selected bookmark?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
|
@ -278,13 +284,13 @@ impl AuthActivity {
|
|||
}
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Default choice option to no
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// Save bookmark as...
|
||||
self.input_mode = InputMode::Popup(PopupType::SaveBookmark);
|
||||
self.popup = Some(Popup::SaveBookmark);
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
},
|
||||
|
@ -296,12 +302,12 @@ impl AuthActivity {
|
|||
/// ### handle_input_event_mode_form_recents
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Recents
|
||||
pub(super) fn handle_input_event_mode_form_recents(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_form_recents(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Show quit dialog
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
|
@ -333,7 +339,7 @@ impl AuthActivity {
|
|||
}
|
||||
KeyCode::Delete => {
|
||||
// Ask if user wants to delete bookmark
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to delete the selected host?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
|
@ -348,9 +354,13 @@ impl AuthActivity {
|
|||
self.selected_field = InputField::Password;
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'C' | 'c' => {
|
||||
// Show setup
|
||||
self.setup = true;
|
||||
}
|
||||
'E' | 'e' => {
|
||||
// Ask if user wants to delete bookmark; NOTE: same as <DEL>
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to delete the selected host?"),
|
||||
AuthActivity::callback_del_bookmark,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
|
@ -358,13 +368,13 @@ impl AuthActivity {
|
|||
}
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Default choice option to no
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// Save bookmark as...
|
||||
self.input_mode = InputMode::Popup(PopupType::SaveBookmark);
|
||||
self.popup = Some(Popup::SaveBookmark);
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
},
|
||||
|
@ -376,12 +386,12 @@ impl AuthActivity {
|
|||
/// ### handle_input_event_mode_text
|
||||
///
|
||||
/// Handler for input event when in popup mode
|
||||
pub(super) fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, ptype: PopupType) {
|
||||
fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, ptype: Popup) {
|
||||
match ptype {
|
||||
PopupType::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
|
||||
PopupType::Help => self.handle_input_event_mode_popup_help(ev),
|
||||
PopupType::SaveBookmark => self.handle_input_event_mode_popup_save_bookmark(ev),
|
||||
PopupType::YesNo(_, yes_cb, no_cb) => {
|
||||
Popup::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
|
||||
Popup::Help => self.handle_input_event_mode_popup_help(ev),
|
||||
Popup::SaveBookmark => self.handle_input_event_mode_popup_save_bookmark(ev),
|
||||
Popup::YesNo(_, yes_cb, no_cb) => {
|
||||
self.handle_input_event_mode_popup_yesno(ev, yes_cb, no_cb)
|
||||
}
|
||||
}
|
||||
|
@ -390,11 +400,11 @@ impl AuthActivity {
|
|||
/// ### handle_input_event_mode_popup_alert
|
||||
///
|
||||
/// Handle input event when the input mode is popup, and popup type is alert
|
||||
pub(super) fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) {
|
||||
// Only enter should be allowed here
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let KeyCode::Enter = key.code {
|
||||
self.input_mode = InputMode::Form; // Hide popup
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
self.popup = None; // Hide popup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -402,15 +412,12 @@ impl AuthActivity {
|
|||
/// ### handle_input_event_mode_popup_help
|
||||
///
|
||||
/// Input event handler for popup help
|
||||
pub(super) fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Enter | KeyCode::Esc => {
|
||||
// Set input mode back to form
|
||||
self.input_mode = InputMode::Form;
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
// Set input mode back to form
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -418,7 +425,7 @@ impl AuthActivity {
|
|||
/// ### handle_input_event_mode_popup_save_bookmark
|
||||
///
|
||||
/// Input event handler for SaveBookmark popup
|
||||
pub(super) fn handle_input_event_mode_popup_save_bookmark(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_popup_save_bookmark(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup, otherwise push chars to input
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
|
@ -427,7 +434,7 @@ impl AuthActivity {
|
|||
// Clear current input text
|
||||
self.input_txt.clear();
|
||||
// Set mode back to form
|
||||
self.input_mode = InputMode::Form;
|
||||
self.popup = None;
|
||||
// Reset choice option to yes
|
||||
self.choice_opt = DialogYesNoOption::Yes;
|
||||
}
|
||||
|
@ -437,7 +444,7 @@ impl AuthActivity {
|
|||
// Clear current input text
|
||||
self.input_txt.clear();
|
||||
// Set mode back to form BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.input_mode = InputMode::Form;
|
||||
self.popup = None;
|
||||
// Call cb
|
||||
self.callback_save_bookmark(input_text);
|
||||
// Reset choice option to yes
|
||||
|
@ -457,7 +464,7 @@ impl AuthActivity {
|
|||
/// ### handle_input_event_mode_popup_yesno
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
pub(super) fn handle_input_event_mode_popup_yesno(
|
||||
fn handle_input_event_mode_popup_yesno(
|
||||
&mut self,
|
||||
ev: &InputEvent,
|
||||
yes_cb: DialogCallback,
|
||||
|
@ -468,7 +475,7 @@ impl AuthActivity {
|
|||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
// @! Set input mode to Form BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.input_mode = InputMode::Form;
|
||||
self.popup = None;
|
||||
// Check if user selected yes or not
|
||||
match self.choice_opt {
|
||||
DialogYesNoOption::No => no_cb(self),
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -23,12 +23,13 @@
|
|||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::{
|
||||
AuthActivity, Context, DialogYesNoOption, FileTransferProtocol, InputField, InputForm,
|
||||
InputMode, PopupType,
|
||||
AuthActivity, Context, DialogYesNoOption, FileTransferProtocol, InputField, InputForm, Popup,
|
||||
};
|
||||
use crate::utils::fmt::align_text_center;
|
||||
|
||||
// Ext
|
||||
use std::string::ToString;
|
||||
use tui::{
|
||||
layout::{Constraint, Corner, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
|
@ -121,23 +122,23 @@ impl AuthActivity {
|
|||
f.render_stateful_widget(tab, bookmark_chunks[1], &mut recents_state);
|
||||
}
|
||||
// Draw popup
|
||||
if let InputMode::Popup(popup) = &self.input_mode {
|
||||
if let Some(popup) = &self.popup {
|
||||
// Calculate popup size
|
||||
let (width, height): (u16, u16) = match popup {
|
||||
PopupType::Alert(_, _) => (50, 10),
|
||||
PopupType::Help => (50, 70),
|
||||
PopupType::SaveBookmark => (20, 20),
|
||||
PopupType::YesNo(_, _, _) => (30, 10),
|
||||
Popup::Alert(_, _) => (50, 10),
|
||||
Popup::Help => (50, 70),
|
||||
Popup::SaveBookmark => (20, 20),
|
||||
Popup::YesNo(_, _, _) => (30, 10),
|
||||
};
|
||||
let popup_area: Rect = self.draw_popup_area(f.size(), width, height);
|
||||
f.render_widget(Clear, popup_area); //this clears out the background
|
||||
match popup {
|
||||
PopupType::Alert(color, txt) => f.render_widget(
|
||||
Popup::Alert(color, txt) => f.render_widget(
|
||||
self.draw_popup_alert(*color, txt.clone(), popup_area.width),
|
||||
popup_area,
|
||||
),
|
||||
PopupType::Help => f.render_widget(self.draw_popup_help(), popup_area),
|
||||
PopupType::SaveBookmark => {
|
||||
Popup::Help => f.render_widget(self.draw_popup_help(), popup_area),
|
||||
Popup::SaveBookmark => {
|
||||
let popup_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
|
@ -158,7 +159,7 @@ impl AuthActivity {
|
|||
popup_chunks[0].y + 1,
|
||||
)
|
||||
}
|
||||
PopupType::YesNo(txt, _, _) => {
|
||||
Popup::YesNo(txt, _, _) => {
|
||||
f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area)
|
||||
}
|
||||
}
|
||||
|
@ -291,8 +292,20 @@ impl AuthActivity {
|
|||
let (footer, h_style) = (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("<CTRL+H>", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to show keybindings"),
|
||||
Span::styled(
|
||||
"<CTRL+H>",
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Cyan),
|
||||
),
|
||||
Span::raw(" to show keybindings; "),
|
||||
Span::styled(
|
||||
"<CTRL+C>",
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Cyan),
|
||||
),
|
||||
Span::raw(" to enter setup"),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
);
|
||||
|
@ -304,7 +317,7 @@ impl AuthActivity {
|
|||
/// ### draw_local_explorer
|
||||
///
|
||||
/// Draw local explorer list
|
||||
pub(super) fn draw_bookmarks_tab(&self) -> Option<List> {
|
||||
fn draw_bookmarks_tab(&self) -> Option<List> {
|
||||
self.bookmarks_client.as_ref()?;
|
||||
let hosts: Vec<ListItem> = self
|
||||
.bookmarks_client
|
||||
|
@ -321,7 +334,7 @@ impl AuthActivity {
|
|||
ListItem::new(Span::from(format!(
|
||||
"{} ({}://{}@{}:{})",
|
||||
key,
|
||||
AuthActivity::protocol_to_str(entry.2),
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
|
@ -352,7 +365,7 @@ impl AuthActivity {
|
|||
/// ### draw_local_explorer
|
||||
///
|
||||
/// Draw local explorer list
|
||||
pub(super) fn draw_recents_tab(&self) -> Option<List> {
|
||||
fn draw_recents_tab(&self) -> Option<List> {
|
||||
self.bookmarks_client.as_ref()?;
|
||||
let hosts: Vec<ListItem> = self
|
||||
.bookmarks_client
|
||||
|
@ -368,7 +381,7 @@ impl AuthActivity {
|
|||
.unwrap();
|
||||
ListItem::new(Span::from(format!(
|
||||
"{}://{}@{}:{}",
|
||||
AuthActivity::protocol_to_str(entry.2),
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
|
@ -384,7 +397,7 @@ impl AuthActivity {
|
|||
List::new(hosts)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::TOP | Borders::BOTTOM | Borders::RIGHT)
|
||||
.borders(Borders::ALL)
|
||||
.border_style(match self.input_form {
|
||||
InputForm::Recents => Style::default().fg(Color::LightBlue),
|
||||
_ => Style::default(),
|
||||
|
@ -449,7 +462,7 @@ impl AuthActivity {
|
|||
/// ### draw_popup_input
|
||||
///
|
||||
/// Draw input popup
|
||||
pub(super) fn draw_popup_save_bookmark(&self) -> (Paragraph, Tabs) {
|
||||
fn draw_popup_save_bookmark(&self) -> (Paragraph, Tabs) {
|
||||
let input: Paragraph = Paragraph::new(self.input_txt.as_ref())
|
||||
.style(Style::default().fg(Color::White))
|
||||
.block(
|
||||
|
@ -483,7 +496,7 @@ impl AuthActivity {
|
|||
/// ### draw_popup_yesno
|
||||
///
|
||||
/// Draw yes/no select popup
|
||||
pub(super) fn draw_popup_yesno(&self, text: String) -> Tabs {
|
||||
fn draw_popup_yesno(&self, text: String) -> Tabs {
|
||||
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
||||
let index: usize = match self.choice_opt {
|
||||
DialogYesNoOption::Yes => 0,
|
||||
|
@ -505,10 +518,10 @@ impl AuthActivity {
|
|||
)
|
||||
}
|
||||
|
||||
/// ### draw_footer
|
||||
/// ### draw_popup_help
|
||||
///
|
||||
/// Draw authentication page footer
|
||||
pub(super) fn draw_popup_help(&self) -> List {
|
||||
/// Draw authentication page help popup
|
||||
fn draw_popup_help(&self) -> List {
|
||||
// Write header
|
||||
let cmds: Vec<ListItem> = vec![
|
||||
ListItem::new(Spans::from(vec![
|
||||
|
@ -581,6 +594,16 @@ impl AuthActivity {
|
|||
Span::raw(" "),
|
||||
Span::raw("Delete selected bookmark"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+C>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Enter setup"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+H>",
|
||||
|
@ -612,18 +635,4 @@ impl AuthActivity {
|
|||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
}
|
||||
|
||||
/// ### protocol_to_str
|
||||
///
|
||||
/// Convert protocol to str for layouts
|
||||
fn protocol_to_str(proto: FileTransferProtocol) -> &'static str {
|
||||
match proto {
|
||||
FileTransferProtocol::Ftp(secure) => match secure {
|
||||
true => "ftps",
|
||||
false => "ftp",
|
||||
},
|
||||
FileTransferProtocol::Scp => "scp",
|
||||
FileTransferProtocol::Sftp => "sftp",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -38,10 +38,13 @@ extern crate unicode_width;
|
|||
use super::{Activity, Context};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::environment;
|
||||
|
||||
// Includes
|
||||
use crossterm::event::Event as InputEvent;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::path::PathBuf;
|
||||
use tui::style::Color;
|
||||
|
||||
// Types
|
||||
|
@ -68,26 +71,17 @@ enum DialogYesNoOption {
|
|||
No,
|
||||
}
|
||||
|
||||
/// ### PopupType
|
||||
/// ### Popup
|
||||
///
|
||||
/// PopupType describes the type of the popup displayed
|
||||
/// Popup describes the type of the popup displayed
|
||||
#[derive(Clone)]
|
||||
enum PopupType {
|
||||
enum Popup {
|
||||
Alert(Color, String), // Show a message displaying text with the provided color
|
||||
Help, // Help page
|
||||
SaveBookmark,
|
||||
YesNo(String, DialogCallback, DialogCallback), // Yes, no callback
|
||||
}
|
||||
|
||||
/// ### InputMode
|
||||
///
|
||||
/// InputMode describes the current input mode
|
||||
/// Each input mode handle the input events in a different way
|
||||
enum InputMode {
|
||||
Form,
|
||||
Popup(PopupType),
|
||||
}
|
||||
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
/// ### InputForm
|
||||
///
|
||||
|
@ -109,10 +103,12 @@ pub struct AuthActivity {
|
|||
pub password: String,
|
||||
pub submit: bool, // becomes true after user has submitted fields
|
||||
pub quit: bool, // Becomes true if user has pressed esc
|
||||
pub setup: bool, // Becomes true if user has requested setup
|
||||
context: Option<Context>,
|
||||
bookmarks_client: Option<BookmarksClient>,
|
||||
config_client: Option<ConfigClient>,
|
||||
selected_field: InputField, // Selected field in AuthCredentials Form
|
||||
input_mode: InputMode,
|
||||
popup: Option<Popup>,
|
||||
input_form: InputForm,
|
||||
password_placeholder: String,
|
||||
redraw: bool, // Should ui actually be redrawned?
|
||||
|
@ -141,10 +137,12 @@ impl AuthActivity {
|
|||
password: String::new(),
|
||||
submit: false,
|
||||
quit: false,
|
||||
setup: false,
|
||||
context: None,
|
||||
bookmarks_client: None,
|
||||
config_client: None,
|
||||
selected_field: InputField::Address,
|
||||
input_mode: InputMode::Form,
|
||||
popup: None,
|
||||
input_form: InputForm::AuthCredentials,
|
||||
password_placeholder: String::new(),
|
||||
redraw: true, // True at startup
|
||||
|
@ -154,6 +152,42 @@ impl AuthActivity {
|
|||
recents_idx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### init_config_client
|
||||
///
|
||||
/// Initialize config client
|
||||
fn init_config_client(&mut self) {
|
||||
// Get config dir
|
||||
match environment::init_config_dir() {
|
||||
Ok(config_dir) => {
|
||||
if let Some(config_dir) = config_dir {
|
||||
// Get config client paths
|
||||
let (config_path, ssh_dir): (PathBuf, PathBuf) =
|
||||
environment::get_config_paths(config_dir.as_path());
|
||||
match ConfigClient::new(config_path.as_path(), ssh_dir.as_path()) {
|
||||
Ok(cli) => {
|
||||
// Set default protocol
|
||||
self.protocol = cli.get_default_protocol();
|
||||
// Set client
|
||||
self.config_client = Some(cli);
|
||||
}
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not initialize user configuration: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not initialize configuration directory: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity for AuthActivity {
|
||||
|
@ -169,11 +203,15 @@ impl Activity for AuthActivity {
|
|||
self.context.as_mut().unwrap().clear_screen();
|
||||
// Put raw mode on enabled
|
||||
let _ = enable_raw_mode();
|
||||
self.input_mode = InputMode::Form;
|
||||
self.popup = None;
|
||||
// Init bookmarks client
|
||||
if self.bookmarks_client.is_none() {
|
||||
self.init_bookmarks_client();
|
||||
}
|
||||
// init config client
|
||||
if self.config_client.is_none() {
|
||||
self.init_config_client();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_draw
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -19,8 +23,9 @@
|
|||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::{FileExplorerTab, FileTransferActivity, FsEntry, LogLevel};
|
||||
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
|
||||
impl FileTransferActivity {
|
||||
|
@ -70,8 +75,8 @@ impl FileTransferActivity {
|
|||
match self.tab {
|
||||
FileExplorerTab::Local => {
|
||||
// Get selected entry
|
||||
if self.local.files.get(self.local.index).is_some() {
|
||||
let entry: FsEntry = self.local.files.get(self.local.index).unwrap().clone();
|
||||
if self.local.get_current_file().is_some() {
|
||||
let entry: FsEntry = self.local.get_current_file().unwrap().clone();
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
match ctx.local.copy(&entry, dest_path.as_path()) {
|
||||
Ok(_) => {
|
||||
|
@ -103,8 +108,8 @@ impl FileTransferActivity {
|
|||
}
|
||||
FileExplorerTab::Remote => {
|
||||
// Get selected entry
|
||||
if self.remote.files.get(self.remote.index).is_some() {
|
||||
let entry: FsEntry = self.remote.files.get(self.remote.index).unwrap().clone();
|
||||
if self.remote.get_current_file().is_some() {
|
||||
let entry: FsEntry = self.remote.get_current_file().unwrap().clone();
|
||||
match self.client.as_mut().copy(&entry, dest_path.as_path()) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
|
@ -204,7 +209,7 @@ impl FileTransferActivity {
|
|||
dst_path = wrkdir;
|
||||
}
|
||||
// Check if file entry exists
|
||||
if let Some(entry) = self.local.files.get(self.local.index) {
|
||||
if let Some(entry) = self.local.get_current_file() {
|
||||
let full_path: PathBuf = entry.get_abs_path();
|
||||
// Rename file or directory and report status as popup
|
||||
match self
|
||||
|
@ -244,7 +249,7 @@ impl FileTransferActivity {
|
|||
}
|
||||
FileExplorerTab::Remote => {
|
||||
// Check if file entry exists
|
||||
if let Some(entry) = self.remote.files.get(self.remote.index) {
|
||||
if let Some(entry) = self.remote.get_current_file() {
|
||||
let full_path: PathBuf = entry.get_abs_path();
|
||||
// Rename file or directory and report status as popup
|
||||
let dst_path: PathBuf = PathBuf::from(input);
|
||||
|
@ -288,7 +293,7 @@ impl FileTransferActivity {
|
|||
match self.tab {
|
||||
FileExplorerTab::Local => {
|
||||
// Check if file entry exists
|
||||
if let Some(entry) = self.local.files.get(self.local.index) {
|
||||
if let Some(entry) = self.local.get_current_file() {
|
||||
let full_path: PathBuf = entry.get_abs_path();
|
||||
// Delete file or directory and report status as popup
|
||||
match self.context.as_mut().unwrap().local.remove(entry) {
|
||||
|
@ -317,7 +322,7 @@ impl FileTransferActivity {
|
|||
}
|
||||
FileExplorerTab::Remote => {
|
||||
// Check if file entry exists
|
||||
if let Some(entry) = self.remote.files.get(self.remote.index) {
|
||||
if let Some(entry) = self.remote.get_current_file() {
|
||||
let full_path: PathBuf = entry.get_abs_path();
|
||||
// Delete file
|
||||
match self.client.remove(entry) {
|
||||
|
@ -354,16 +359,16 @@ impl FileTransferActivity {
|
|||
// Get pwd
|
||||
let wrkdir: PathBuf = self.remote.wrkdir.clone();
|
||||
// Get file and clone (due to mutable / immutable stuff...)
|
||||
if self.local.files.get(self.local.index).is_some() {
|
||||
let file: FsEntry = self.local.files.get(self.local.index).unwrap().clone();
|
||||
if self.local.get_current_file().is_some() {
|
||||
let file: FsEntry = self.local.get_current_file().unwrap().clone();
|
||||
// Call upload; pass realfile, keep link name
|
||||
self.filetransfer_send(&file.get_realfile(), wrkdir.as_path(), Some(input));
|
||||
}
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
// Get file and clone (due to mutable / immutable stuff...)
|
||||
if self.remote.files.get(self.remote.index).is_some() {
|
||||
let file: FsEntry = self.remote.files.get(self.remote.index).unwrap().clone();
|
||||
if self.remote.get_current_file().is_some() {
|
||||
let file: FsEntry = self.remote.get_current_file().unwrap().clone();
|
||||
// Call upload; pass realfile, keep link name
|
||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
||||
self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(input));
|
||||
|
@ -371,4 +376,115 @@ impl FileTransferActivity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_new_file
|
||||
///
|
||||
/// Create a new file in current directory with `input` as name
|
||||
pub(super) fn callback_new_file(&mut self, input: String) {
|
||||
match self.tab {
|
||||
FileExplorerTab::Local => {
|
||||
// Check if file exists
|
||||
let mut file_exists: bool = false;
|
||||
for file in self.local.iter_files_all() {
|
||||
if input == file.get_name() {
|
||||
file_exists = true;
|
||||
}
|
||||
}
|
||||
if file_exists {
|
||||
self.log_and_alert(
|
||||
LogLevel::Warn,
|
||||
format!("File \"{}\" already exists", input,),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Create file
|
||||
let file_path: PathBuf = PathBuf::from(input.as_str());
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
if let Err(err) = ctx.local.open_file_write(file_path.as_path()) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create file \"{}\": {}", file_path.display(), err),
|
||||
);
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Created file \"{}\"", file_path.display()).as_str(),
|
||||
);
|
||||
// Reload files
|
||||
let path: PathBuf = self.local.wrkdir.clone();
|
||||
self.local_scan(path.as_path());
|
||||
}
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
// Check if file exists
|
||||
let mut file_exists: bool = false;
|
||||
for file in self.remote.iter_files_all() {
|
||||
if input == file.get_name() {
|
||||
file_exists = true;
|
||||
}
|
||||
}
|
||||
if file_exists {
|
||||
self.log_and_alert(
|
||||
LogLevel::Warn,
|
||||
format!("File \"{}\" already exists", input,),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Get path on remote
|
||||
let file_path: PathBuf = PathBuf::from(input.as_str());
|
||||
// Create file (on local)
|
||||
match tempfile::NamedTempFile::new() {
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create tempfile: {}", err),
|
||||
),
|
||||
Ok(tfile) => {
|
||||
// Stat tempfile
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
let local_file: FsEntry = match ctx.local.stat(tfile.path()) {
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not stat tempfile: {}", err),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Ok(f) => f,
|
||||
};
|
||||
if let FsEntry::File(local_file) = local_file {
|
||||
// Create file
|
||||
match self.client.send_file(&local_file, file_path.as_path()) {
|
||||
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),
|
||||
);
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Created file \"{}\"", file_path.display())
|
||||
.as_str(),
|
||||
);
|
||||
// Reload files
|
||||
let path: PathBuf = self.remote.wrkdir.clone();
|
||||
self.remote_scan(path.as_path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -19,13 +23,15 @@
|
|||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate tempfile;
|
||||
|
||||
// Local
|
||||
use super::{
|
||||
DialogCallback, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputEvent,
|
||||
InputField, InputMode, LogLevel, OnInputSubmitCallback, PopupType,
|
||||
InputField, LogLevel, OnInputSubmitCallback, Popup,
|
||||
};
|
||||
|
||||
use crate::fs::explorer::{FileExplorer, FileSorting};
|
||||
// Ext
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
@ -55,16 +61,16 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event
|
||||
///
|
||||
/// Handle input event based on current input mode
|
||||
pub(super) fn handle_input_event(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event(&mut self, ev: &InputEvent) {
|
||||
// NOTE: this is necessary due to this <https://github.com/rust-lang/rust/issues/59159>
|
||||
// NOTE: Do you want my opinion about that issue? It's a bs and doesn't make any sense.
|
||||
let popup: Option<PopupType> = match &self.input_mode {
|
||||
InputMode::Popup(ptype) => Some(ptype.clone()),
|
||||
let popup: Option<Popup> = match &self.popup {
|
||||
Some(ptype) => Some(ptype.clone()),
|
||||
_ => None,
|
||||
};
|
||||
match &self.input_mode {
|
||||
InputMode::Explorer => self.handle_input_event_mode_explorer(ev),
|
||||
InputMode::Popup(_) => {
|
||||
match &self.popup {
|
||||
None => self.handle_input_event_mode_explorer(ev),
|
||||
Some(_) => {
|
||||
if let Some(popup) = popup {
|
||||
self.handle_input_event_mode_popup(ev, popup);
|
||||
}
|
||||
|
@ -75,7 +81,7 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_explorer
|
||||
///
|
||||
/// Input event handler for explorer mode
|
||||
pub(super) fn handle_input_event_mode_explorer(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_explorer(&mut self, ev: &InputEvent) {
|
||||
// Match input field
|
||||
match self.input_field {
|
||||
InputField::Explorer => match self.tab {
|
||||
|
@ -90,53 +96,40 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_explorer_tab_local
|
||||
///
|
||||
/// Input event handler for explorer mode when localhost tab is selected
|
||||
pub(super) fn handle_input_event_mode_explorer_tab_local(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_explorer_tab_local(&mut self, ev: &InputEvent) {
|
||||
// Match events
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Handle quit event
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_disconnect_popup();
|
||||
self.popup = self.create_disconnect_popup();
|
||||
}
|
||||
KeyCode::Tab => self.switch_input_field(), // <TAB> switch tab
|
||||
KeyCode::Right => self.tab = FileExplorerTab::Remote, // <RIGHT> switch to right tab
|
||||
KeyCode::Up => {
|
||||
// Move index up; or move to the last element if 0
|
||||
self.local.index = match self.local.index {
|
||||
0 => self.local.files.len() - 1,
|
||||
_ => self.local.index - 1,
|
||||
};
|
||||
// Decrement index
|
||||
self.local.decr_index();
|
||||
}
|
||||
KeyCode::Down => {
|
||||
// Move index down
|
||||
if self.local.index + 1 < self.local.files.len() {
|
||||
self.local.index += 1;
|
||||
} else {
|
||||
self.local.index = 0; // Move at the beginning of the list
|
||||
}
|
||||
// Increment index
|
||||
self.local.incr_index();
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
// Move index up (fast)
|
||||
if self.local.index > 8 {
|
||||
self.local.index -= 8; // Decrease by `8` if possible
|
||||
} else {
|
||||
self.local.index = 0; // Set to 0 otherwise
|
||||
}
|
||||
// Decrement index by 8
|
||||
self.local.decr_index_by(8);
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
// Move index down (fast)
|
||||
if self.local.index + 8 >= self.local.files.len() {
|
||||
// If overflows, set to size
|
||||
self.local.index = self.local.files.len() - 1;
|
||||
} else {
|
||||
self.local.index += 8; // Increase by `8`
|
||||
}
|
||||
// Increment index by 8
|
||||
self.local.incr_index_by(8);
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Match selected file
|
||||
let local_files: Vec<FsEntry> = self.local.files.clone();
|
||||
if let Some(entry) = local_files.get(self.local.index) {
|
||||
let mut entry: Option<FsEntry> = None;
|
||||
if let Some(e) = self.local.get_current_file() {
|
||||
entry = Some(e.clone());
|
||||
}
|
||||
if let Some(entry) = entry {
|
||||
// If directory, enter directory, otherwise check if symlink
|
||||
match entry {
|
||||
FsEntry::Directory(dir) => {
|
||||
|
@ -162,14 +155,14 @@ impl FileTransferActivity {
|
|||
}
|
||||
KeyCode::Delete => {
|
||||
// Get file at index
|
||||
if let Some(entry) = self.local.files.get(self.local.index) {
|
||||
if let Some(entry) = self.local.get_current_file() {
|
||||
// Get file name
|
||||
let file_name: String = match entry {
|
||||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
};
|
||||
// Show delete prompt
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
format!("Delete file \"{}\"", file_name),
|
||||
FileTransferActivity::callback_delete_fsentry,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
|
@ -177,30 +170,38 @@ impl FileTransferActivity {
|
|||
}
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'a' | 'A' => {
|
||||
// Toggle hidden files
|
||||
self.local.toggle_hidden_files();
|
||||
}
|
||||
'b' | 'B' => {
|
||||
// Choose file sorting type
|
||||
self.popup = Some(Popup::FileSortingDialog);
|
||||
}
|
||||
'c' | 'C' => {
|
||||
// Copy
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("Insert destination name"),
|
||||
FileTransferActivity::callback_copy,
|
||||
));
|
||||
}
|
||||
'd' | 'D' => {
|
||||
// Make directory
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("Insert directory name"),
|
||||
FileTransferActivity::callback_mkdir,
|
||||
));
|
||||
}
|
||||
'e' | 'E' => {
|
||||
// Get file at index
|
||||
if let Some(entry) = self.local.files.get(self.local.index) {
|
||||
if let Some(entry) = self.local.get_current_file() {
|
||||
// Get file name
|
||||
let file_name: String = match entry {
|
||||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
};
|
||||
// Show delete prompt
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
format!("Delete file \"{}\"", file_name),
|
||||
FileTransferActivity::callback_delete_fsentry,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
|
@ -210,30 +211,36 @@ impl FileTransferActivity {
|
|||
'g' | 'G' => {
|
||||
// Goto
|
||||
// Show input popup
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("Change working directory"),
|
||||
FileTransferActivity::callback_change_directory,
|
||||
));
|
||||
}
|
||||
'h' | 'H' => {
|
||||
// Show help
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'i' | 'I' => {
|
||||
// Show file info
|
||||
self.input_mode = InputMode::Popup(PopupType::FileInfo);
|
||||
self.popup = Some(Popup::FileInfo);
|
||||
}
|
||||
'l' | 'L' => {
|
||||
// Reload file entries
|
||||
let pwd: PathBuf = self.local.wrkdir.clone();
|
||||
self.local_scan(pwd.as_path());
|
||||
}
|
||||
'n' | 'N' => {
|
||||
// New file
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("New file"),
|
||||
Self::callback_new_file,
|
||||
));
|
||||
}
|
||||
'o' | 'O' => {
|
||||
// Edit local file
|
||||
if self.local.files.get(self.local.index).is_some() {
|
||||
if self.local.get_current_file().is_some() {
|
||||
// Clone entry due to mutable stuff...
|
||||
let fsentry: FsEntry =
|
||||
self.local.files.get(self.local.index).unwrap().clone();
|
||||
let fsentry: FsEntry = self.local.get_current_file().unwrap().clone();
|
||||
// Check if file
|
||||
if fsentry.is_file() {
|
||||
self.log(
|
||||
|
@ -258,11 +265,11 @@ impl FileTransferActivity {
|
|||
}
|
||||
'q' | 'Q' => {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
self.popup = self.create_quit_popup();
|
||||
}
|
||||
'r' | 'R' => {
|
||||
// Rename
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("Insert new name"),
|
||||
FileTransferActivity::callback_rename,
|
||||
));
|
||||
|
@ -270,7 +277,7 @@ impl FileTransferActivity {
|
|||
's' | 'S' => {
|
||||
// Save as...
|
||||
// Ask for input
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("Save as..."),
|
||||
FileTransferActivity::callback_save_as,
|
||||
));
|
||||
|
@ -287,10 +294,9 @@ impl FileTransferActivity {
|
|||
// Get pwd
|
||||
let wrkdir: PathBuf = self.remote.wrkdir.clone();
|
||||
// Get file and clone (due to mutable / immutable stuff...)
|
||||
if self.local.files.get(self.local.index).is_some() {
|
||||
let file: FsEntry =
|
||||
self.local.files.get(self.local.index).unwrap().clone();
|
||||
let name: String = file.get_name();
|
||||
if self.local.get_current_file().is_some() {
|
||||
let file: FsEntry = self.local.get_current_file().unwrap().clone();
|
||||
let name: String = file.get_name().to_string();
|
||||
// Call upload; pass realfile, keep link name
|
||||
self.filetransfer_send(
|
||||
&file.get_realfile(),
|
||||
|
@ -309,53 +315,40 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_explorer_tab_local
|
||||
///
|
||||
/// Input event handler for explorer mode when remote tab is selected
|
||||
pub(super) fn handle_input_event_mode_explorer_tab_remote(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_explorer_tab_remote(&mut self, ev: &InputEvent) {
|
||||
// Match events
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Handle quit event
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_disconnect_popup();
|
||||
self.popup = self.create_disconnect_popup();
|
||||
}
|
||||
KeyCode::Tab => self.switch_input_field(), // <TAB> switch tab
|
||||
KeyCode::Left => self.tab = FileExplorerTab::Local, // <LEFT> switch to local tab
|
||||
KeyCode::Up => {
|
||||
// Move index up; or move to the last element if 0
|
||||
self.remote.index = match self.remote.index {
|
||||
0 => self.remote.files.len() - 1,
|
||||
_ => self.remote.index - 1,
|
||||
};
|
||||
// Decrement index
|
||||
self.remote.decr_index();
|
||||
}
|
||||
KeyCode::Down => {
|
||||
// Move index down
|
||||
if self.remote.index + 1 < self.remote.files.len() {
|
||||
self.remote.index += 1;
|
||||
} else {
|
||||
self.remote.index = 0; // Move at the beginning of the list
|
||||
}
|
||||
// Increment index
|
||||
self.remote.incr_index();
|
||||
}
|
||||
KeyCode::PageUp => {
|
||||
// Move index up (fast)
|
||||
if self.remote.index > 8 {
|
||||
self.remote.index -= 8; // Decrease by `8` if possible
|
||||
} else {
|
||||
self.remote.index = 0; // Set to 0 otherwise
|
||||
}
|
||||
// Decrement index by 8
|
||||
self.remote.decr_index_by(8);
|
||||
}
|
||||
KeyCode::PageDown => {
|
||||
// Move index down (fast)
|
||||
if self.remote.index + 8 >= self.remote.files.len() {
|
||||
// If overflows, set to size
|
||||
self.remote.index = self.remote.files.len() - 1;
|
||||
} else {
|
||||
self.remote.index += 8; // Increase by `8`
|
||||
}
|
||||
// Increment index by 8
|
||||
self.remote.incr_index_by(8);
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Match selected file
|
||||
let files: Vec<FsEntry> = self.remote.files.clone();
|
||||
if let Some(entry) = files.get(self.remote.index) {
|
||||
let mut entry: Option<FsEntry> = None;
|
||||
if let Some(e) = self.remote.get_current_file() {
|
||||
entry = Some(e.clone());
|
||||
}
|
||||
if let Some(entry) = entry {
|
||||
// If directory, enter directory; if file, check if is symlink
|
||||
match entry {
|
||||
FsEntry::Directory(dir) => {
|
||||
|
@ -381,14 +374,14 @@ impl FileTransferActivity {
|
|||
}
|
||||
KeyCode::Delete => {
|
||||
// Get file at index
|
||||
if let Some(entry) = self.remote.files.get(self.remote.index) {
|
||||
if let Some(entry) = self.remote.get_current_file() {
|
||||
// Get file name
|
||||
let file_name: String = match entry {
|
||||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
};
|
||||
// Show delete prompt
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
format!("Delete file \"{}\"", file_name),
|
||||
FileTransferActivity::callback_delete_fsentry,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
|
@ -396,30 +389,38 @@ impl FileTransferActivity {
|
|||
}
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'a' | 'A' => {
|
||||
// Toggle hidden files
|
||||
self.remote.toggle_hidden_files();
|
||||
}
|
||||
'b' | 'B' => {
|
||||
// Choose file sorting type
|
||||
self.popup = Some(Popup::FileSortingDialog);
|
||||
}
|
||||
'c' | 'C' => {
|
||||
// Copy
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("Insert destination name"),
|
||||
FileTransferActivity::callback_copy,
|
||||
));
|
||||
}
|
||||
'd' | 'D' => {
|
||||
// Make directory
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("Insert directory name"),
|
||||
FileTransferActivity::callback_mkdir,
|
||||
));
|
||||
}
|
||||
'e' | 'E' => {
|
||||
// Get file at index
|
||||
if let Some(entry) = self.remote.files.get(self.remote.index) {
|
||||
if let Some(entry) = self.remote.get_current_file() {
|
||||
// Get file name
|
||||
let file_name: String = match entry {
|
||||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
};
|
||||
// Show delete prompt
|
||||
self.input_mode = InputMode::Popup(PopupType::YesNo(
|
||||
self.popup = Some(Popup::YesNo(
|
||||
format!("Delete file \"{}\"", file_name),
|
||||
FileTransferActivity::callback_delete_fsentry,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
|
@ -429,29 +430,35 @@ impl FileTransferActivity {
|
|||
'g' | 'G' => {
|
||||
// Goto
|
||||
// Show input popup
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("Change working directory"),
|
||||
FileTransferActivity::callback_change_directory,
|
||||
));
|
||||
}
|
||||
'h' | 'H' => {
|
||||
// Show help
|
||||
self.input_mode = InputMode::Popup(PopupType::Help);
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'i' | 'I' => {
|
||||
// Show file info
|
||||
self.input_mode = InputMode::Popup(PopupType::FileInfo);
|
||||
self.popup = Some(Popup::FileInfo);
|
||||
}
|
||||
'l' | 'L' => {
|
||||
// Reload file entries
|
||||
self.reload_remote_dir();
|
||||
}
|
||||
'n' | 'N' => {
|
||||
// New file
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("New file"),
|
||||
Self::callback_new_file,
|
||||
));
|
||||
}
|
||||
'o' | 'O' => {
|
||||
// Edit remote file
|
||||
if self.remote.files.get(self.remote.index).is_some() {
|
||||
if self.remote.get_current_file().is_some() {
|
||||
// Clone entry due to mutable stuff...
|
||||
let fsentry: FsEntry =
|
||||
self.remote.files.get(self.remote.index).unwrap().clone();
|
||||
let fsentry: FsEntry = self.remote.get_current_file().unwrap().clone();
|
||||
// Check if file
|
||||
if let FsEntry::File(file) = fsentry {
|
||||
self.log(
|
||||
|
@ -469,17 +476,17 @@ impl FileTransferActivity {
|
|||
Err(err) => self.log_and_alert(LogLevel::Error, err),
|
||||
}
|
||||
// Put input mode back to normal
|
||||
self.input_mode = InputMode::Explorer;
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
'q' | 'Q' => {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
self.popup = self.create_quit_popup();
|
||||
}
|
||||
'r' | 'R' => {
|
||||
// Rename
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("Insert new name"),
|
||||
FileTransferActivity::callback_rename,
|
||||
));
|
||||
|
@ -487,7 +494,7 @@ impl FileTransferActivity {
|
|||
's' | 'S' => {
|
||||
// Save as...
|
||||
// Ask for input
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
self.popup = Some(Popup::Input(
|
||||
String::from("Save as..."),
|
||||
FileTransferActivity::callback_save_as,
|
||||
));
|
||||
|
@ -502,10 +509,9 @@ impl FileTransferActivity {
|
|||
}
|
||||
' ' => {
|
||||
// Get file and clone (due to mutable / immutable stuff...)
|
||||
if self.remote.files.get(self.remote.index).is_some() {
|
||||
let file: FsEntry =
|
||||
self.remote.files.get(self.remote.index).unwrap().clone();
|
||||
let name: String = file.get_name();
|
||||
if self.remote.get_current_file().is_some() {
|
||||
let file: FsEntry = self.remote.get_current_file().unwrap().clone();
|
||||
let name: String = file.get_name().to_string();
|
||||
// Call upload; pass realfile, keep link name
|
||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
||||
self.filetransfer_recv(
|
||||
|
@ -525,7 +531,7 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_explorer_log
|
||||
///
|
||||
/// Input even handler for explorer mode when log tab is selected
|
||||
pub(super) fn handle_input_event_mode_explorer_log(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_explorer_log(&mut self, ev: &InputEvent) {
|
||||
// Match event
|
||||
let records_block: usize = 16;
|
||||
if let InputEvent::Key(key) = ev {
|
||||
|
@ -533,7 +539,7 @@ impl FileTransferActivity {
|
|||
KeyCode::Esc => {
|
||||
// Handle quit event
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_disconnect_popup();
|
||||
self.popup = self.create_disconnect_popup();
|
||||
}
|
||||
KeyCode::Tab => self.switch_input_field(), // <TAB> switch tab
|
||||
KeyCode::Down => {
|
||||
|
@ -572,7 +578,7 @@ impl FileTransferActivity {
|
|||
KeyCode::Char(ch) => match ch {
|
||||
'q' | 'Q' => {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
self.popup = self.create_quit_popup();
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
},
|
||||
|
@ -584,16 +590,17 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_explorer
|
||||
///
|
||||
/// Input event handler for popup mode. Handler is then based on Popup type
|
||||
pub(super) fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, popup: PopupType) {
|
||||
fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, popup: Popup) {
|
||||
match popup {
|
||||
PopupType::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
|
||||
PopupType::FileInfo => self.handle_input_event_mode_popup_fileinfo(ev),
|
||||
PopupType::Help => self.handle_input_event_mode_popup_help(ev),
|
||||
PopupType::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev),
|
||||
PopupType::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb),
|
||||
PopupType::Progress(_) => self.handle_input_event_mode_popup_progress(ev),
|
||||
PopupType::Wait(_) => self.handle_input_event_mode_popup_wait(ev),
|
||||
PopupType::YesNo(_, yes_cb, no_cb) => {
|
||||
Popup::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
|
||||
Popup::FileInfo => self.handle_input_event_mode_popup_fileinfo(ev),
|
||||
Popup::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev),
|
||||
Popup::FileSortingDialog => self.handle_input_event_mode_popup_file_sorting(ev),
|
||||
Popup::Help => self.handle_input_event_mode_popup_help(ev),
|
||||
Popup::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb),
|
||||
Popup::Progress(_) => self.handle_input_event_mode_popup_progress(ev),
|
||||
Popup::Wait(_) => self.handle_input_event_mode_popup_wait(ev),
|
||||
Popup::YesNo(_, yes_cb, no_cb) => {
|
||||
self.handle_input_event_mode_popup_yesno(ev, yes_cb, no_cb)
|
||||
}
|
||||
}
|
||||
|
@ -602,12 +609,12 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_popup_alert
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
pub(super) fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let KeyCode::Enter = key.code {
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
// Set input mode back to explorer
|
||||
self.input_mode = InputMode::Explorer;
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -615,13 +622,61 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_popup_fileinfo
|
||||
///
|
||||
/// Input event handler for popup fileinfo
|
||||
pub(super) fn handle_input_event_mode_popup_fileinfo(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_popup_fileinfo(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
// Set input mode back to explorer
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_fatal
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
fn handle_input_event_mode_popup_fatal(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
// Set quit to true; since a fatal error happened
|
||||
self.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_file_sorting
|
||||
///
|
||||
/// Handle input event for file sorting dialog popup
|
||||
fn handle_input_event_mode_popup_file_sorting(&mut self, ev: &InputEvent) {
|
||||
// Match key code
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Enter | KeyCode::Esc => {
|
||||
// Set input mode back to explorer
|
||||
self.input_mode = InputMode::Explorer;
|
||||
KeyCode::Esc | KeyCode::Enter => {
|
||||
// Exit
|
||||
self.popup = None;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
// Update sorting mode
|
||||
match self.tab {
|
||||
FileExplorerTab::Local => {
|
||||
Self::move_sorting_mode_opt_right(&mut self.local);
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
Self::move_sorting_mode_opt_right(&mut self.remote);
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
// Update sorting mode
|
||||
match self.tab {
|
||||
FileExplorerTab::Local => {
|
||||
Self::move_sorting_mode_opt_left(&mut self.local);
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
Self::move_sorting_mode_opt_left(&mut self.remote);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
|
@ -631,28 +686,12 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_popup_help
|
||||
///
|
||||
/// Input event handler for popup help
|
||||
pub(super) fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Enter | KeyCode::Esc => {
|
||||
// Set input mode back to explorer
|
||||
self.input_mode = InputMode::Explorer;
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_fatal
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
pub(super) fn handle_input_event_mode_popup_fatal(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let KeyCode::Enter = key.code {
|
||||
// Set quit to true; since a fatal error happened
|
||||
self.disconnect();
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
// Set input mode back to explorer
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -660,11 +699,7 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_popup_input
|
||||
///
|
||||
/// Input event handler for input popup
|
||||
pub(super) fn handle_input_event_mode_popup_input(
|
||||
&mut self,
|
||||
ev: &InputEvent,
|
||||
cb: OnInputSubmitCallback,
|
||||
) {
|
||||
fn handle_input_event_mode_popup_input(&mut self, ev: &InputEvent, cb: OnInputSubmitCallback) {
|
||||
// If enter, close popup, otherwise push chars to input
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
|
@ -673,7 +708,7 @@ impl FileTransferActivity {
|
|||
// Clear current input text
|
||||
self.input_txt.clear();
|
||||
// Set mode back to explorer
|
||||
self.input_mode = InputMode::Explorer;
|
||||
self.popup = None;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Submit
|
||||
|
@ -681,7 +716,7 @@ impl FileTransferActivity {
|
|||
// Clear current input text
|
||||
self.input_txt.clear();
|
||||
// Set mode back to explorer BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.input_mode = InputMode::Explorer;
|
||||
self.popup = None;
|
||||
// Call cb
|
||||
cb(self, input_text);
|
||||
}
|
||||
|
@ -697,7 +732,7 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_popup_progress
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
pub(super) fn handle_input_event_mode_popup_progress(&mut self, ev: &InputEvent) {
|
||||
fn handle_input_event_mode_popup_progress(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let KeyCode::Char(ch) = key.code {
|
||||
// If is 'C' and CTRL
|
||||
|
@ -712,14 +747,14 @@ impl FileTransferActivity {
|
|||
/// ### handle_input_event_mode_popup_wait
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
pub(super) fn handle_input_event_mode_popup_wait(&mut self, _ev: &InputEvent) {
|
||||
fn handle_input_event_mode_popup_wait(&mut self, _ev: &InputEvent) {
|
||||
// There's nothing you can do here I guess... maybe ctrl+c in the future idk
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_yesno
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
pub(super) fn handle_input_event_mode_popup_yesno(
|
||||
fn handle_input_event_mode_popup_yesno(
|
||||
&mut self,
|
||||
ev: &InputEvent,
|
||||
yes_cb: DialogCallback,
|
||||
|
@ -730,7 +765,7 @@ impl FileTransferActivity {
|
|||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
// @! Set input mode to Explorer BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.input_mode = InputMode::Explorer;
|
||||
self.popup = None;
|
||||
// Check if user selected yes or not
|
||||
match self.choice_opt {
|
||||
DialogYesNoOption::No => no_cb(self),
|
||||
|
@ -745,4 +780,30 @@ impl FileTransferActivity {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### move_sorting_mode_opt_left
|
||||
///
|
||||
/// Perform <LEFT> on file sorting dialog
|
||||
fn move_sorting_mode_opt_left(explorer: &mut FileExplorer) {
|
||||
let curr_sorting: FileSorting = explorer.get_file_sorting();
|
||||
explorer.sort_by(match curr_sorting {
|
||||
FileSorting::BySize => FileSorting::ByCreationTime,
|
||||
FileSorting::ByCreationTime => FileSorting::ByModifyTime,
|
||||
FileSorting::ByModifyTime => FileSorting::ByName,
|
||||
FileSorting::ByName => FileSorting::BySize, // Wrap
|
||||
});
|
||||
}
|
||||
|
||||
/// ### move_sorting_mode_opt_left
|
||||
///
|
||||
/// Perform <RIGHT> on file sorting dialog
|
||||
fn move_sorting_mode_opt_right(explorer: &mut FileExplorer) {
|
||||
let curr_sorting: FileSorting = explorer.get_file_sorting();
|
||||
explorer.sort_by(match curr_sorting {
|
||||
FileSorting::ByName => FileSorting::ByModifyTime,
|
||||
FileSorting::ByModifyTime => FileSorting::ByCreationTime,
|
||||
FileSorting::ByCreationTime => FileSorting::BySize,
|
||||
FileSorting::BySize => FileSorting::ByName, // Wrap
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -19,17 +23,19 @@
|
|||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate bytesize;
|
||||
extern crate hostname;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
extern crate users;
|
||||
|
||||
// Local
|
||||
use super::{
|
||||
Context, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputField,
|
||||
InputMode, LogLevel, LogRecord, PopupType,
|
||||
LogLevel, LogRecord, Popup,
|
||||
};
|
||||
use crate::fs::explorer::{FileExplorer, FileSorting};
|
||||
use crate::utils::fmt::{align_text_center, fmt_time};
|
||||
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tui::{
|
||||
|
@ -70,10 +76,10 @@ impl FileTransferActivity {
|
|||
.split(chunks[0]);
|
||||
// Set localhost state
|
||||
let mut localhost_state: ListState = ListState::default();
|
||||
localhost_state.select(Some(self.local.index));
|
||||
localhost_state.select(Some(self.local.get_relative_index()));
|
||||
// Set remote state
|
||||
let mut remote_state: ListState = ListState::default();
|
||||
remote_state.select(Some(self.remote.index));
|
||||
remote_state.select(Some(self.remote.get_relative_index()));
|
||||
// Draw tabs
|
||||
f.render_stateful_widget(
|
||||
self.draw_local_explorer(tabs_chunks[0].width),
|
||||
|
@ -95,32 +101,36 @@ impl FileTransferActivity {
|
|||
&mut log_state,
|
||||
);
|
||||
// Draw popup
|
||||
if let InputMode::Popup(popup) = &self.input_mode {
|
||||
if let Some(popup) = &self.popup {
|
||||
// Calculate popup size
|
||||
let (width, height): (u16, u16) = match popup {
|
||||
PopupType::Alert(_, _) => (50, 10),
|
||||
PopupType::Fatal(_) => (50, 10),
|
||||
PopupType::FileInfo => (50, 50),
|
||||
PopupType::Help => (50, 70),
|
||||
PopupType::Input(_, _) => (40, 10),
|
||||
PopupType::Progress(_) => (40, 10),
|
||||
PopupType::Wait(_) => (50, 10),
|
||||
PopupType::YesNo(_, _, _) => (30, 10),
|
||||
Popup::Alert(_, _) => (50, 10),
|
||||
Popup::Fatal(_) => (50, 10),
|
||||
Popup::FileInfo => (50, 50),
|
||||
Popup::FileSortingDialog => (50, 10),
|
||||
Popup::Help => (50, 80),
|
||||
Popup::Input(_, _) => (40, 10),
|
||||
Popup::Progress(_) => (40, 10),
|
||||
Popup::Wait(_) => (50, 10),
|
||||
Popup::YesNo(_, _, _) => (30, 10),
|
||||
};
|
||||
let popup_area: Rect = self.draw_popup_area(f.size(), width, height);
|
||||
f.render_widget(Clear, popup_area); //this clears out the background
|
||||
match popup {
|
||||
PopupType::Alert(color, txt) => f.render_widget(
|
||||
Popup::Alert(color, txt) => f.render_widget(
|
||||
self.draw_popup_alert(*color, txt.clone(), popup_area.width),
|
||||
popup_area,
|
||||
),
|
||||
PopupType::Fatal(txt) => f.render_widget(
|
||||
Popup::Fatal(txt) => f.render_widget(
|
||||
self.draw_popup_fatal(txt.clone(), popup_area.width),
|
||||
popup_area,
|
||||
),
|
||||
PopupType::FileInfo => f.render_widget(self.draw_popup_fileinfo(), popup_area),
|
||||
PopupType::Help => f.render_widget(self.draw_popup_help(), popup_area),
|
||||
PopupType::Input(txt, _) => {
|
||||
Popup::FileInfo => f.render_widget(self.draw_popup_fileinfo(), popup_area),
|
||||
Popup::FileSortingDialog => {
|
||||
f.render_widget(self.draw_popup_file_sorting_dialog(), popup_area)
|
||||
}
|
||||
Popup::Help => f.render_widget(self.draw_popup_help(), popup_area),
|
||||
Popup::Input(txt, _) => {
|
||||
f.render_widget(self.draw_popup_input(txt.clone()), popup_area);
|
||||
// Set cursor
|
||||
f.set_cursor(
|
||||
|
@ -128,14 +138,14 @@ impl FileTransferActivity {
|
|||
popup_area.y + 1,
|
||||
)
|
||||
}
|
||||
PopupType::Progress(txt) => {
|
||||
Popup::Progress(txt) => {
|
||||
f.render_widget(self.draw_popup_progress(txt.clone()), popup_area)
|
||||
}
|
||||
PopupType::Wait(txt) => f.render_widget(
|
||||
Popup::Wait(txt) => f.render_widget(
|
||||
self.draw_popup_wait(txt.clone(), popup_area.width),
|
||||
popup_area,
|
||||
),
|
||||
PopupType::YesNo(txt, _, _) => {
|
||||
Popup::YesNo(txt, _, _) => {
|
||||
f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area)
|
||||
}
|
||||
}
|
||||
|
@ -158,8 +168,7 @@ impl FileTransferActivity {
|
|||
};
|
||||
let files: Vec<ListItem> = self
|
||||
.local
|
||||
.files
|
||||
.iter()
|
||||
.iter_files()
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
|
@ -199,8 +208,7 @@ impl FileTransferActivity {
|
|||
pub(super) fn draw_remote_explorer(&self, width: u16) -> List {
|
||||
let files: Vec<ListItem> = self
|
||||
.remote
|
||||
.files
|
||||
.iter()
|
||||
.iter_files()
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
|
@ -365,6 +373,44 @@ impl FileTransferActivity {
|
|||
.start_corner(Corner::TopLeft)
|
||||
.style(Style::default().fg(Color::Red))
|
||||
}
|
||||
|
||||
/// ### draw_popup_file_sorting_dialog
|
||||
///
|
||||
/// Draw FileSorting mode select popup
|
||||
pub(super) fn draw_popup_file_sorting_dialog(&self) -> Tabs {
|
||||
let choices: Vec<Spans> = vec![
|
||||
Spans::from("Name"),
|
||||
Spans::from("Modify time"),
|
||||
Spans::from("Creation time"),
|
||||
Spans::from("Size"),
|
||||
];
|
||||
let explorer: &FileExplorer = match self.tab {
|
||||
FileExplorerTab::Local => &self.local,
|
||||
FileExplorerTab::Remote => &self.remote,
|
||||
};
|
||||
let index: usize = match explorer.get_file_sorting() {
|
||||
FileSorting::ByCreationTime => 2,
|
||||
FileSorting::ByModifyTime => 1,
|
||||
FileSorting::ByName => 0,
|
||||
FileSorting::BySize => 3,
|
||||
};
|
||||
Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Sort files by"),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::LightMagenta)
|
||||
.fg(Color::DarkGray),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_popup_input
|
||||
///
|
||||
/// Draw input popup
|
||||
|
@ -468,12 +514,12 @@ impl FileTransferActivity {
|
|||
let fsentry: Option<&FsEntry> = match self.tab {
|
||||
FileExplorerTab::Local => {
|
||||
// Get selected file
|
||||
match self.local.files.get(self.local.index) {
|
||||
match self.local.get_current_file() {
|
||||
Some(entry) => Some(entry),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
FileExplorerTab::Remote => match self.remote.files.get(self.remote.index) {
|
||||
FileExplorerTab::Remote => match self.remote.get_current_file() {
|
||||
Some(entry) => Some(entry),
|
||||
None => None,
|
||||
},
|
||||
|
@ -483,10 +529,9 @@ impl FileTransferActivity {
|
|||
Some(fsentry) => {
|
||||
// Get name and path
|
||||
let abs_path: PathBuf = fsentry.get_abs_path();
|
||||
let name: String = fsentry.get_name();
|
||||
let name: String = fsentry.get_name().to_string();
|
||||
let ctime: String = fmt_time(fsentry.get_creation_time(), "%b %d %Y %H:%M:%S");
|
||||
let atime: String =
|
||||
fmt_time(fsentry.get_last_access_time(), "%b %d %Y %H:%M:%S");
|
||||
let atime: String = fmt_time(fsentry.get_last_access_time(), "%b %d %Y %H:%M:%S");
|
||||
let mtime: String = fmt_time(fsentry.get_creation_time(), "%b %d %Y %H:%M:%S");
|
||||
let (bsize, size): (ByteSize, usize) =
|
||||
(ByteSize(fsentry.get_size() as u64), fsentry.get_size());
|
||||
|
@ -716,6 +761,26 @@ impl FileTransferActivity {
|
|||
Span::raw(" "),
|
||||
Span::raw("Delete file"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<A>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Toggle hidden files"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<B>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Change file sorting mode"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<C>",
|
||||
|
@ -786,6 +851,26 @@ impl FileTransferActivity {
|
|||
Span::raw(" "),
|
||||
Span::raw("Reload directory content"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<N>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("New file"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<O>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Open text file"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<Q>",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -19,7 +19,14 @@
|
|||
*
|
||||
*/
|
||||
|
||||
use super::{Color, FileTransferActivity, InputField, InputMode, LogLevel, LogRecord, PopupType};
|
||||
// Locals
|
||||
use super::{Color, ConfigClient, FileTransferActivity, InputField, LogLevel, LogRecord, Popup};
|
||||
use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
|
||||
use crate::system::environment;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
// Ext
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### log
|
||||
|
@ -49,14 +56,14 @@ impl FileTransferActivity {
|
|||
LogLevel::Warn => Color::Yellow,
|
||||
};
|
||||
self.log(level, msg.as_str());
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(color, msg));
|
||||
self.popup = Some(Popup::Alert(color, msg));
|
||||
}
|
||||
|
||||
/// ### create_quit_popup
|
||||
///
|
||||
/// Create quit popup input mode (since must be shared between different input handlers)
|
||||
pub(super) fn create_disconnect_popup(&mut self) -> InputMode {
|
||||
InputMode::Popup(PopupType::YesNo(
|
||||
pub(super) fn create_disconnect_popup(&mut self) -> Option<Popup> {
|
||||
Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to disconnect?"),
|
||||
FileTransferActivity::disconnect,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
|
@ -66,8 +73,8 @@ impl FileTransferActivity {
|
|||
/// ### create_quit_popup
|
||||
///
|
||||
/// Create quit popup input mode (since must be shared between different input handlers)
|
||||
pub(super) fn create_quit_popup(&mut self) -> InputMode {
|
||||
InputMode::Popup(PopupType::YesNo(
|
||||
pub(super) fn create_quit_popup(&mut self) -> Option<Popup> {
|
||||
Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to quit?"),
|
||||
FileTransferActivity::disconnect_and_quit,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
|
@ -83,4 +90,65 @@ impl FileTransferActivity {
|
|||
InputField::Logs => InputField::Explorer,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### init_config_client
|
||||
///
|
||||
/// Initialize configuration client if possible.
|
||||
/// This function doesn't return errors.
|
||||
pub(super) fn init_config_client() -> Option<ConfigClient> {
|
||||
match environment::init_config_dir() {
|
||||
Ok(termscp_dir) => match termscp_dir {
|
||||
Some(termscp_dir) => {
|
||||
// Make configuration file path and ssh keys path
|
||||
let (config_path, ssh_keys_path): (PathBuf, PathBuf) =
|
||||
environment::get_config_paths(termscp_dir.as_path());
|
||||
match ConfigClient::new(config_path.as_path(), ssh_keys_path.as_path()) {
|
||||
Ok(config_client) => Some(config_client),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### make_ssh_storage
|
||||
///
|
||||
/// Make ssh storage from `ConfigClient` if possible, empty otherwise
|
||||
pub(super) fn make_ssh_storage(cli: Option<&ConfigClient>) -> SshKeyStorage {
|
||||
match cli {
|
||||
Some(cli) => SshKeyStorage::storage_from_config(cli),
|
||||
None => SshKeyStorage::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### build_explorer
|
||||
///
|
||||
/// Build explorer reading configuration from `ConfigClient`
|
||||
pub(super) fn build_explorer(cli: Option<&ConfigClient>) -> FileExplorer {
|
||||
match &cli {
|
||||
Some(cli) => FileExplorerBuilder::new() // Build according to current configuration
|
||||
.with_file_sorting(FileSorting::ByName)
|
||||
.with_group_dirs(cli.get_group_dirs())
|
||||
.with_hidden_files(cli.get_show_hidden_files())
|
||||
.with_stack_size(16)
|
||||
.build(),
|
||||
None => FileExplorerBuilder::new() // Build default
|
||||
.with_file_sorting(FileSorting::ByName)
|
||||
.with_group_dirs(Some(GroupDirs::First))
|
||||
.with_stack_size(16)
|
||||
.build(),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### setup_text_editor
|
||||
///
|
||||
/// Set text editor to use
|
||||
pub(super) fn setup_text_editor(&self) {
|
||||
if let Some(config_cli) = &self.config_cli {
|
||||
// Set text editor
|
||||
env::set_var("EDITOR", config_cli.get_text_editor());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -39,21 +39,20 @@ extern crate unicode_width;
|
|||
|
||||
// locals
|
||||
use super::{Activity, Context};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
|
||||
// File transfer
|
||||
use crate::filetransfer::ftp_transfer::FtpFileTransfer;
|
||||
use crate::filetransfer::scp_transfer::ScpFileTransfer;
|
||||
use crate::filetransfer::sftp_transfer::SftpFileTransfer;
|
||||
use crate::filetransfer::FileTransfer;
|
||||
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
||||
use crate::fs::explorer::FileExplorer;
|
||||
use crate::fs::FsEntry;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
|
||||
// Includes
|
||||
use chrono::{DateTime, Local};
|
||||
use crossterm::event::Event as InputEvent;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
use tui::style::Color;
|
||||
|
||||
|
@ -90,14 +89,15 @@ enum DialogYesNoOption {
|
|||
No,
|
||||
}
|
||||
|
||||
/// ## PopupType
|
||||
/// ## Popup
|
||||
///
|
||||
/// PopupType describes the type of popup
|
||||
/// Popup describes the type of popup
|
||||
#[derive(Clone)]
|
||||
enum PopupType {
|
||||
enum Popup {
|
||||
Alert(Color, String), // Block color; Block text
|
||||
Fatal(String), // Must quit after being hidden
|
||||
FileInfo, // Show info about current file
|
||||
FileSortingDialog, // Dialog for choosing file sorting type
|
||||
Help, // Show Help
|
||||
Input(String, OnInputSubmitCallback), // Input description; Callback for submit
|
||||
Progress(String), // Progress block text
|
||||
|
@ -105,69 +105,6 @@ enum PopupType {
|
|||
YesNo(String, DialogCallback, DialogCallback), // Yes, no callback
|
||||
}
|
||||
|
||||
/// ## InputMode
|
||||
///
|
||||
/// InputMode describes the current input mode
|
||||
/// Each input mode handle the input events in a different way
|
||||
#[derive(Clone)]
|
||||
enum InputMode {
|
||||
Explorer,
|
||||
Popup(PopupType),
|
||||
}
|
||||
|
||||
/// ## FileExplorer
|
||||
///
|
||||
/// File explorer states
|
||||
struct FileExplorer {
|
||||
pub wrkdir: PathBuf, // Current directory
|
||||
pub index: usize, // Selected file
|
||||
pub files: Vec<FsEntry>, // Files in directory
|
||||
dirstack: VecDeque<PathBuf>, // Stack of visited directory (max 16)
|
||||
}
|
||||
|
||||
impl FileExplorer {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new FileExplorer
|
||||
pub fn new() -> FileExplorer {
|
||||
FileExplorer {
|
||||
wrkdir: PathBuf::from("/"),
|
||||
index: 0,
|
||||
files: Vec::new(),
|
||||
dirstack: VecDeque::with_capacity(16),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### pushd
|
||||
///
|
||||
/// push directory to stack
|
||||
pub fn pushd(&mut self, dir: &Path) {
|
||||
// Check if stack overflows the size
|
||||
if self.dirstack.len() + 1 > 16 {
|
||||
self.dirstack.pop_back(); // Start cleaning events from back
|
||||
}
|
||||
// Eventually push front the new record
|
||||
self.dirstack.push_front(PathBuf::from(dir));
|
||||
}
|
||||
|
||||
/// ### popd
|
||||
///
|
||||
/// Pop directory from the stack and return the directory
|
||||
pub fn popd(&mut self) -> Option<PathBuf> {
|
||||
self.dirstack.pop_front()
|
||||
}
|
||||
|
||||
/// ### sort_files_by_name
|
||||
///
|
||||
/// Sort explorer files by their name
|
||||
pub fn sort_files_by_name(&mut self) {
|
||||
self.files.sort_by_key(|x: &FsEntry| match x {
|
||||
FsEntry::Directory(dir) => dir.name.as_str().to_lowercase(),
|
||||
FsEntry::File(file) => file.name.as_str().to_lowercase(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// ## FileExplorerTab
|
||||
///
|
||||
/// File explorer tab
|
||||
|
@ -266,7 +203,11 @@ impl TransferStates {
|
|||
// bytes_written : elapsed_secs = x : 1
|
||||
let elapsed_secs: u64 = self.started.elapsed().as_secs();
|
||||
match elapsed_secs {
|
||||
0 => 0, // NOTE: would divide by 0 :D
|
||||
0 => match self.bytes_written == self.bytes_total {
|
||||
// NOTE: would divide by 0 :D
|
||||
true => self.bytes_total as u64, // Download completed in less than 1 second
|
||||
false => 0, // 0 B/S
|
||||
},
|
||||
_ => self.bytes_written as u64 / elapsed_secs,
|
||||
}
|
||||
}
|
||||
|
@ -287,13 +228,14 @@ pub struct FileTransferActivity {
|
|||
context: Option<Context>, // Context holder
|
||||
params: FileTransferParams, // FT connection params
|
||||
client: Box<dyn FileTransfer>, // File transfer client
|
||||
config_cli: Option<ConfigClient>, // Config Client
|
||||
local: FileExplorer, // Local File explorer state
|
||||
remote: FileExplorer, // Remote File explorer state
|
||||
tab: FileExplorerTab, // Current selected tab
|
||||
log_index: usize, // Current log index entry selected
|
||||
log_records: VecDeque<LogRecord>, // Log records
|
||||
log_size: usize, // Log records size (max)
|
||||
input_mode: InputMode, // Current input mode
|
||||
popup: Option<Popup>, // Current input mode
|
||||
input_field: InputField, // Current selected input mode
|
||||
input_txt: String, // Input text
|
||||
choice_opt: DialogYesNoOption, // Dialog popup selected option
|
||||
|
@ -306,23 +248,30 @@ impl FileTransferActivity {
|
|||
/// Instantiates a new FileTransferActivity
|
||||
pub fn new(params: FileTransferParams) -> FileTransferActivity {
|
||||
let protocol: FileTransferProtocol = params.protocol;
|
||||
// Get config client
|
||||
let config_client: Option<ConfigClient> = Self::init_config_client();
|
||||
FileTransferActivity {
|
||||
disconnected: false,
|
||||
quit: false,
|
||||
context: None,
|
||||
client: match protocol {
|
||||
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new()),
|
||||
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
|
||||
Self::make_ssh_storage(config_client.as_ref()),
|
||||
)),
|
||||
FileTransferProtocol::Ftp(ftps) => Box::new(FtpFileTransfer::new(ftps)),
|
||||
FileTransferProtocol::Scp => Box::new(ScpFileTransfer::new()),
|
||||
FileTransferProtocol::Scp => Box::new(ScpFileTransfer::new(
|
||||
Self::make_ssh_storage(config_client.as_ref()),
|
||||
)),
|
||||
},
|
||||
params,
|
||||
local: FileExplorer::new(),
|
||||
remote: FileExplorer::new(),
|
||||
local: Self::build_explorer(config_client.as_ref()),
|
||||
remote: Self::build_explorer(config_client.as_ref()),
|
||||
config_cli: config_client,
|
||||
tab: FileExplorerTab::Local,
|
||||
log_index: 0,
|
||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||
log_size: 256, // Must match with capacity
|
||||
input_mode: InputMode::Explorer,
|
||||
popup: None,
|
||||
input_field: InputField::Explorer,
|
||||
input_txt: String::new(),
|
||||
choice_opt: DialogYesNoOption::Yes,
|
||||
|
@ -354,6 +303,8 @@ impl Activity for FileTransferActivity {
|
|||
// Get files at current wd
|
||||
self.local_scan(pwd.as_path());
|
||||
self.local.wrkdir = pwd;
|
||||
// Configure text editor
|
||||
self.setup_text_editor();
|
||||
}
|
||||
|
||||
/// ### on_draw
|
||||
|
@ -367,11 +318,10 @@ impl Activity for FileTransferActivity {
|
|||
if self.context.is_none() {
|
||||
return;
|
||||
}
|
||||
let is_explorer_mode: bool = matches!(self.input_mode, InputMode::Explorer);
|
||||
// Check if connected
|
||||
if !self.client.is_connected() && is_explorer_mode {
|
||||
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
|
||||
if !self.client.is_connected() && self.popup.is_none() {
|
||||
// Set init state to connecting popup
|
||||
self.input_mode = InputMode::Popup(PopupType::Wait(format!(
|
||||
self.popup = Some(Popup::Wait(format!(
|
||||
"Connecting to {}:{}...",
|
||||
self.params.address, self.params.port
|
||||
)));
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -26,10 +30,9 @@ extern crate crossterm;
|
|||
extern crate tempfile;
|
||||
|
||||
// Locals
|
||||
use super::{FileTransferActivity, InputMode, LogLevel, PopupType};
|
||||
use super::{FileTransferActivity, LogLevel, Popup};
|
||||
use crate::fs::{FsEntry, FsFile};
|
||||
use crate::utils::fmt::fmt_millis;
|
||||
use crate::utils::hash::hash_sha256_file;
|
||||
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
|
@ -37,7 +40,7 @@ use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
|||
use std::fs::OpenOptions;
|
||||
use std::io::{Read, Seek, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
use std::time::{Instant, SystemTime};
|
||||
use tui::style::Color;
|
||||
|
||||
impl FileTransferActivity {
|
||||
|
@ -65,12 +68,12 @@ impl FileTransferActivity {
|
|||
);
|
||||
}
|
||||
// Set state to explorer
|
||||
self.input_mode = InputMode::Explorer;
|
||||
self.popup = None;
|
||||
self.reload_remote_dir();
|
||||
}
|
||||
Err(err) => {
|
||||
// Set popup fatal error
|
||||
self.input_mode = InputMode::Popup(PopupType::Fatal(format!("{}", err)));
|
||||
self.popup = Some(Popup::Fatal(format!("{}", err)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +83,7 @@ impl FileTransferActivity {
|
|||
/// disconnect from remote
|
||||
pub(super) fn disconnect(&mut self) {
|
||||
// Show popup disconnecting
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Disconnecting from remote..."),
|
||||
));
|
||||
|
@ -126,7 +129,7 @@ impl FileTransferActivity {
|
|||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
};
|
||||
self.input_mode = InputMode::Popup(PopupType::Wait(format!("Uploading \"{}\"", file_name)));
|
||||
self.popup = Some(Popup::Wait(format!("Uploading \"{}\"", file_name)));
|
||||
// Draw
|
||||
self.draw();
|
||||
// Get remote path
|
||||
|
@ -208,9 +211,9 @@ impl FileTransferActivity {
|
|||
} else {
|
||||
// @! Successful
|
||||
// Eventually, Reset input mode to explorer (if input mode is wait or progress)
|
||||
if let InputMode::Popup(ptype) = &self.input_mode {
|
||||
if matches!(ptype, PopupType::Wait(_) | PopupType::Progress(_)) {
|
||||
self.input_mode = InputMode::Explorer
|
||||
if let Some(ptype) = &self.popup {
|
||||
if matches!(ptype, Popup::Wait(_) | Popup::Progress(_)) {
|
||||
self.popup = None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -232,8 +235,7 @@ impl FileTransferActivity {
|
|||
FsEntry::Directory(dir) => dir.name.clone(),
|
||||
FsEntry::File(file) => file.name.clone(),
|
||||
};
|
||||
self.input_mode =
|
||||
InputMode::Popup(PopupType::Wait(format!("Downloading \"{}\"...", file_name)));
|
||||
self.popup = Some(Popup::Wait(format!("Downloading \"{}\"...", file_name)));
|
||||
// Draw
|
||||
self.draw();
|
||||
// Match entry
|
||||
|
@ -349,7 +351,7 @@ impl FileTransferActivity {
|
|||
self.transfer.aborted = false;
|
||||
} else {
|
||||
// Eventually, Reset input mode to explorer
|
||||
self.input_mode = InputMode::Explorer;
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -378,10 +380,7 @@ impl FileTransferActivity {
|
|||
// Write remote file
|
||||
let mut total_bytes_written: usize = 0;
|
||||
// Set input state to popup progress
|
||||
self.input_mode = InputMode::Popup(PopupType::Progress(format!(
|
||||
"Uploading \"{}\"",
|
||||
local.name
|
||||
)));
|
||||
self.popup = Some(Popup::Progress(format!("Uploading \"{}\"", local.name)));
|
||||
// Reset transfer states
|
||||
self.transfer.reset();
|
||||
let mut last_progress_val: f64 = 0.0;
|
||||
|
@ -481,7 +480,7 @@ impl FileTransferActivity {
|
|||
match self.client.recv_file(remote) {
|
||||
Ok(mut rhnd) => {
|
||||
// Set popup progress
|
||||
self.input_mode = InputMode::Popup(PopupType::Progress(format!(
|
||||
self.popup = Some(Popup::Progress(format!(
|
||||
"Downloading \"{}\"...",
|
||||
remote.name,
|
||||
)));
|
||||
|
@ -600,17 +599,16 @@ impl FileTransferActivity {
|
|||
pub(super) fn local_scan(&mut self, path: &Path) {
|
||||
match self.context.as_ref().unwrap().local.scan_dir(path) {
|
||||
Ok(files) => {
|
||||
self.local.files = files;
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.local.set_files(files);
|
||||
// Set index; keep if possible, otherwise set to last item
|
||||
self.local.index = match self.local.files.get(self.local.index) {
|
||||
Some(_) => self.local.index,
|
||||
None => match self.local.files.len() {
|
||||
self.local.set_index(match self.local.get_current_file() {
|
||||
Some(_) => self.local.get_index(),
|
||||
None => match self.local.count() {
|
||||
0 => 0,
|
||||
_ => self.local.files.len() - 1,
|
||||
_ => self.local.count() - 1,
|
||||
},
|
||||
};
|
||||
// Sort files
|
||||
self.local.sort_files_by_name();
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
|
@ -627,17 +625,16 @@ impl FileTransferActivity {
|
|||
pub(super) fn remote_scan(&mut self, path: &Path) {
|
||||
match self.client.list_dir(path) {
|
||||
Ok(files) => {
|
||||
self.remote.files = files;
|
||||
// Set files and sort (sorting is implicit)
|
||||
self.remote.set_files(files);
|
||||
// Set index; keep if possible, otherwise set to last item
|
||||
self.remote.index = match self.remote.files.get(self.remote.index) {
|
||||
Some(_) => self.remote.index,
|
||||
None => match self.remote.files.len() {
|
||||
self.remote.set_index(match self.remote.get_current_file() {
|
||||
Some(_) => self.remote.get_index(),
|
||||
None => match self.remote.count() {
|
||||
0 => 0,
|
||||
_ => self.remote.files.len() - 1,
|
||||
_ => self.remote.count() - 1,
|
||||
},
|
||||
};
|
||||
// Sort files
|
||||
self.remote.sort_files_by_name();
|
||||
});
|
||||
}
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
|
@ -664,7 +661,7 @@ impl FileTransferActivity {
|
|||
// Reload files
|
||||
self.local_scan(path);
|
||||
// Reset index
|
||||
self.local.index = 0;
|
||||
self.local.set_index(0);
|
||||
// Set wrkdir
|
||||
self.local.wrkdir = PathBuf::from(path);
|
||||
// Push prev_dir to stack
|
||||
|
@ -695,7 +692,7 @@ impl FileTransferActivity {
|
|||
// Update files
|
||||
self.remote_scan(path);
|
||||
// Reset index
|
||||
self.remote.index = 0;
|
||||
self.remote.set_index(0);
|
||||
// Set wrkdir
|
||||
self.remote.wrkdir = PathBuf::from(path);
|
||||
// Push prev_dir to stack
|
||||
|
@ -781,13 +778,14 @@ impl FileTransferActivity {
|
|||
if let Err(err) = self.filetransfer_recv_file(tmpfile.path(), file) {
|
||||
return Err(err);
|
||||
}
|
||||
// Get current file hash
|
||||
let prev_hash: String = match hash_sha256_file(tmpfile.path()) {
|
||||
Ok(s) => s,
|
||||
// Get current file modification time
|
||||
let prev_mtime: SystemTime = match self.context.as_ref().unwrap().local.stat(tmpfile.path())
|
||||
{
|
||||
Ok(e) => e.get_last_change_time(),
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Could not get sha256 for \"{}\": {}",
|
||||
file.abs_path.display(),
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.path().display(),
|
||||
err
|
||||
))
|
||||
}
|
||||
|
@ -796,19 +794,20 @@ impl FileTransferActivity {
|
|||
if let Err(err) = self.edit_local_file(tmpfile.path()) {
|
||||
return Err(err);
|
||||
}
|
||||
// Check if file has changed
|
||||
let new_hash: String = match hash_sha256_file(tmpfile.path()) {
|
||||
Ok(s) => s,
|
||||
// Get local fs entry
|
||||
let tmpfile_entry: FsEntry = match self.context.as_ref().unwrap().local.stat(tmpfile.path())
|
||||
{
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
return Err(format!(
|
||||
"Could not get sha256 for \"{}\": {}",
|
||||
file.abs_path.display(),
|
||||
"Could not stat \"{}\": {}",
|
||||
tmpfile.path().display(),
|
||||
err
|
||||
))
|
||||
}
|
||||
};
|
||||
// If hash is different, write changes
|
||||
match new_hash != prev_hash {
|
||||
// Check if file has changed
|
||||
match prev_mtime != tmpfile_entry.get_last_change_time() {
|
||||
true => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -30,6 +30,7 @@ use super::context::Context;
|
|||
// Activities
|
||||
pub mod auth_activity;
|
||||
pub mod filetransfer_activity;
|
||||
pub mod setup_activity;
|
||||
|
||||
// Activity trait
|
||||
|
||||
|
|
152
src/ui/activities/setup_activity/callbacks.rs
Normal file
152
src/ui/activities/setup_activity/callbacks.rs
Normal file
|
@ -0,0 +1,152 @@
|
|||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::{Color, Popup, SetupActivity};
|
||||
// Ext
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::env;
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### callback_nothing_to_do
|
||||
///
|
||||
/// Self titled
|
||||
pub(super) fn callback_nothing_to_do(&mut self) {}
|
||||
|
||||
/// ### callback_save_config_and_quit
|
||||
///
|
||||
/// Save configuration and quit
|
||||
pub(super) fn callback_save_config_and_quit(&mut self) {
|
||||
match self.save_config() {
|
||||
Ok(_) => self.quit = true, // Quit after successful save
|
||||
Err(err) => self.popup = Some(Popup::Alert(Color::Red, err)), // Show error and don't quit
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_save_config
|
||||
///
|
||||
/// Save configuration callback
|
||||
pub(super) fn callback_save_config(&mut self) {
|
||||
if let Err(err) = self.save_config() {
|
||||
self.popup = Some(Popup::Alert(Color::Red, err)); // Show save error
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_reset_config_changes
|
||||
///
|
||||
/// Reset config changes callback
|
||||
pub(super) fn callback_reset_config_changes(&mut self) {
|
||||
if let Err(err) = self.reset_config_changes() {
|
||||
self.popup = Some(Popup::Alert(Color::Red, err)); // Show reset error
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_delete_ssh_key
|
||||
///
|
||||
/// Callback for performing the delete of a ssh key
|
||||
pub(super) fn callback_delete_ssh_key(&mut self) {
|
||||
// Get key
|
||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
||||
let key: Option<String> = match config_cli.iter_ssh_keys().nth(self.ssh_key_idx) {
|
||||
Some(k) => Some(k.clone()),
|
||||
None => None,
|
||||
};
|
||||
if let Some(key) = key {
|
||||
match config_cli.get_ssh_key(&key) {
|
||||
Ok(opt) => {
|
||||
if let Some((host, username, _)) = opt {
|
||||
if let Err(err) = self.delete_ssh_key(host.as_str(), username.as_str())
|
||||
{
|
||||
// Report error
|
||||
self.popup = Some(Popup::Alert(Color::Red, err));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not get ssh key \"{}\": {}", key, err),
|
||||
))
|
||||
} // Report error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_new_ssh_key
|
||||
///
|
||||
/// Create a new ssh key with provided parameters
|
||||
pub(super) fn callback_new_ssh_key(&mut self, host: String, username: String) {
|
||||
if let Some(cli) = self.config_cli.as_ref() {
|
||||
// Prepare text editor
|
||||
env::set_var("EDITOR", cli.get_text_editor());
|
||||
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
||||
// Put input mode back to normal
|
||||
let _ = disable_raw_mode();
|
||||
// Leave alternate mode
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
ctx.leave_alternate_screen();
|
||||
}
|
||||
// Re-enable raw mode
|
||||
let _ = enable_raw_mode();
|
||||
// Write key to file
|
||||
match edit::edit(placeholder.as_bytes()) {
|
||||
Ok(rsa_key) => {
|
||||
// Remove placeholder from `rsa_key`
|
||||
let rsa_key: String = rsa_key.as_str().replace(placeholder.as_str(), "");
|
||||
if rsa_key.is_empty() {
|
||||
// Report error: empty key
|
||||
self.popup = Some(Popup::Alert(Color::Red, "SSH Key is empty".to_string()));
|
||||
} else {
|
||||
// Add key
|
||||
if let Err(err) =
|
||||
self.add_ssh_key(host.as_str(), username.as_str(), rsa_key.as_str())
|
||||
{
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not create new private key: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not write private key to file: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
// Restore terminal
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
// Enter alternate mode
|
||||
ctx.enter_alternate_screen();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
194
src/ui/activities/setup_activity/config.rs
Normal file
194
src/ui/activities/setup_activity/config.rs
Normal file
|
@ -0,0 +1,194 @@
|
|||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::{ConfigClient, Popup, SetupActivity};
|
||||
use crate::system::environment;
|
||||
// Ext
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### init_config_dir
|
||||
///
|
||||
/// Initialize configuration directory
|
||||
pub(super) fn init_config_client(&mut self) {
|
||||
match environment::init_config_dir() {
|
||||
Ok(config_dir) => match config_dir {
|
||||
Some(config_dir) => {
|
||||
// Get paths
|
||||
let (config_file, ssh_dir): (PathBuf, PathBuf) =
|
||||
environment::get_config_paths(config_dir.as_path());
|
||||
// Create config client
|
||||
match ConfigClient::new(config_file.as_path(), ssh_dir.as_path()) {
|
||||
Ok(cli) => self.config_cli = Some(cli),
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Fatal(format!(
|
||||
"Could not initialize configuration client: {}",
|
||||
err
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
self.popup = Some(Popup::Fatal(
|
||||
"No configuration directory is available on your system".to_string(),
|
||||
))
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Fatal(format!(
|
||||
"Could not initialize configuration directory: {}",
|
||||
err
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### save_config
|
||||
///
|
||||
/// Save configuration
|
||||
pub(super) fn save_config(&mut self) -> Result<(), String> {
|
||||
match &self.config_cli {
|
||||
Some(cli) => match cli.write_config() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Could not save configuration: {}", err)),
|
||||
},
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### reset_config_changes
|
||||
///
|
||||
/// Reset configuration changes; pratically read config from file, overwriting any change made
|
||||
/// since last write action
|
||||
pub(super) fn reset_config_changes(&mut self) -> Result<(), String> {
|
||||
match self.config_cli.as_mut() {
|
||||
Some(cli) => match cli.read_config() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Could not restore configuration: {}", err)),
|
||||
},
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### delete_ssh_key
|
||||
///
|
||||
/// Delete ssh key from config cli
|
||||
pub(super) fn delete_ssh_key(&mut self, host: &str, username: &str) -> Result<(), String> {
|
||||
match self.config_cli.as_mut() {
|
||||
Some(cli) => match cli.del_ssh_key(host, username) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!(
|
||||
"Could not delete ssh key \"{}@{}\": {}",
|
||||
host, username, err
|
||||
)),
|
||||
},
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### edit_ssh_key
|
||||
///
|
||||
/// Edit selected ssh key
|
||||
pub(super) fn edit_ssh_key(&mut self) -> Result<(), String> {
|
||||
match self.config_cli.as_ref() {
|
||||
Some(cli) => {
|
||||
// Set text editor
|
||||
env::set_var("EDITOR", cli.get_text_editor());
|
||||
// Prepare terminal
|
||||
let _ = disable_raw_mode();
|
||||
// Leave alternate mode
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
ctx.leave_alternate_screen();
|
||||
}
|
||||
// Check if key exists
|
||||
match cli.iter_ssh_keys().nth(self.ssh_key_idx) {
|
||||
Some(key) => {
|
||||
// Get key path
|
||||
match cli.get_ssh_key(key) {
|
||||
Ok(ssh_key) => match ssh_key {
|
||||
None => Ok(()),
|
||||
Some((_, _, key_path)) => match edit::edit_file(key_path.as_path())
|
||||
{
|
||||
Ok(_) => {
|
||||
// Restore terminal
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
// Enter alternate mode
|
||||
ctx.enter_alternate_screen();
|
||||
}
|
||||
// Re-enable raw mode
|
||||
let _ = enable_raw_mode();
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
// Restore terminal
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
// Enter alternate mode
|
||||
ctx.enter_alternate_screen();
|
||||
}
|
||||
// Re-enable raw mode
|
||||
let _ = enable_raw_mode();
|
||||
Err(format!("Could not edit ssh key: {}", err))
|
||||
}
|
||||
},
|
||||
},
|
||||
Err(err) => Err(format!("Could not read ssh key: {}", err)),
|
||||
}
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### add_ssh_key
|
||||
///
|
||||
/// Add provided ssh key to config client
|
||||
pub(super) fn add_ssh_key(
|
||||
&mut self,
|
||||
host: &str,
|
||||
username: &str,
|
||||
rsa_key: &str,
|
||||
) -> Result<(), String> {
|
||||
match self.config_cli.as_mut() {
|
||||
Some(cli) => {
|
||||
// Add key to client
|
||||
match cli.add_ssh_key(host, username, rsa_key) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Could not add SSH key: {}", err)),
|
||||
}
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
526
src/ui/activities/setup_activity/input.rs
Normal file
526
src/ui/activities/setup_activity/input.rs
Normal file
|
@ -0,0 +1,526 @@
|
|||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::{
|
||||
InputEvent, OnChoiceCallback, Popup, QuitDialogOption, SetupActivity, SetupTab,
|
||||
UserInterfaceInputField, YesNoDialogOption,
|
||||
};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs;
|
||||
// Ext
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use std::path::PathBuf;
|
||||
use tui::style::Color;
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### handle_input_event
|
||||
///
|
||||
/// Handle input event, based on current input mode
|
||||
pub(super) fn handle_input_event(&mut self, ev: &InputEvent) {
|
||||
let popup: Option<Popup> = match &self.popup {
|
||||
Some(ptype) => Some(ptype.clone()),
|
||||
None => None,
|
||||
};
|
||||
match &self.popup {
|
||||
Some(_) => self.handle_input_event_popup(ev, popup.unwrap()),
|
||||
None => self.handle_input_event_forms(ev),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_forms
|
||||
///
|
||||
/// Handle input event when popup is not visible.
|
||||
/// InputEvent is handled based on current tab
|
||||
fn handle_input_event_forms(&mut self, ev: &InputEvent) {
|
||||
// Match tab
|
||||
match &self.tab {
|
||||
SetupTab::SshConfig => self.handle_input_event_forms_ssh_config(ev),
|
||||
SetupTab::UserInterface(_) => self.handle_input_event_forms_ui(ev),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_forms_ssh_config
|
||||
///
|
||||
/// Handle input event when in ssh config tab
|
||||
fn handle_input_event_forms_ssh_config(&mut self, ev: &InputEvent) {
|
||||
// Match input event
|
||||
if let InputEvent::Key(key) = ev {
|
||||
// Match key code
|
||||
match key.code {
|
||||
KeyCode::Esc => self.popup = Some(Popup::Quit), // Prompt quit
|
||||
KeyCode::Tab => {
|
||||
self.tab = SetupTab::UserInterface(UserInterfaceInputField::DefaultProtocol)
|
||||
} // Switch tab to user interface config
|
||||
KeyCode::Up => {
|
||||
if let Some(config_cli) = self.config_cli.as_ref() {
|
||||
// Move ssh key index up
|
||||
let ssh_key_size: usize = config_cli.iter_ssh_keys().count();
|
||||
if self.ssh_key_idx > 0 {
|
||||
// Decrement
|
||||
self.ssh_key_idx -= 1;
|
||||
} else {
|
||||
// Set ssh key index to `ssh_key_size -1`
|
||||
self.ssh_key_idx = ssh_key_size - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if let Some(config_cli) = self.config_cli.as_ref() {
|
||||
// Move ssh key index down
|
||||
let ssh_key_size: usize = config_cli.iter_ssh_keys().count();
|
||||
if self.ssh_key_idx + 1 < ssh_key_size {
|
||||
// Increment index
|
||||
self.ssh_key_idx += 1;
|
||||
} else {
|
||||
// Wrap to 0
|
||||
self.ssh_key_idx = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
// Prompt to delete selected key
|
||||
self.yesno_opt = YesNoDialogOption::No; // Default to no
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Delete key?"),
|
||||
Self::callback_delete_ssh_key,
|
||||
Self::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Edit selected key
|
||||
if let Err(err) = self.edit_ssh_key() {
|
||||
self.popup = Some(Popup::Alert(Color::Red, err)); // Report error
|
||||
}
|
||||
}
|
||||
KeyCode::Char(ch) => {
|
||||
// Check if <CTRL> is enabled
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Match char
|
||||
match ch {
|
||||
'e' | 'E' => {
|
||||
// Prompt to delete selected key
|
||||
self.yesno_opt = YesNoDialogOption::No; // Default to no
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Delete key?"),
|
||||
Self::callback_delete_ssh_key,
|
||||
Self::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
'h' | 'H' => {
|
||||
// Show help
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'n' | 'N' => {
|
||||
// New ssh key
|
||||
self.popup = Some(Popup::NewSshKey);
|
||||
}
|
||||
'r' | 'R' => {
|
||||
// Show reset changes dialog
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Reset changes?"),
|
||||
Self::callback_reset_config_changes,
|
||||
Self::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
's' | 'S' => {
|
||||
// Show save dialog
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Save changes to configuration?"),
|
||||
Self::callback_save_config,
|
||||
Self::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_forms_ui
|
||||
///
|
||||
/// Handle input event when in UserInterface config tab
|
||||
fn handle_input_event_forms_ui(&mut self, ev: &InputEvent) {
|
||||
// Get `UserInterfaceInputField`
|
||||
let field: UserInterfaceInputField = match &self.tab {
|
||||
SetupTab::UserInterface(field) => field.clone(),
|
||||
_ => return,
|
||||
};
|
||||
// Match input event
|
||||
if let InputEvent::Key(key) = ev {
|
||||
// Match key code
|
||||
match key.code {
|
||||
KeyCode::Esc => self.popup = Some(Popup::Quit), // Prompt quit
|
||||
KeyCode::Tab => self.tab = SetupTab::SshConfig, // Switch tab to ssh config
|
||||
KeyCode::Backspace => {
|
||||
// Pop character from selected input
|
||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
||||
// NOTE: replace with match if other text fields are added
|
||||
if matches!(field, UserInterfaceInputField::TextEditor) {
|
||||
// Pop from text editor
|
||||
let mut input: String = String::from(
|
||||
config_cli.get_text_editor().as_path().to_string_lossy(),
|
||||
);
|
||||
input.pop();
|
||||
// Update text editor value
|
||||
config_cli.set_text_editor(PathBuf::from(input.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
// Move left on fields which are tabs
|
||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
||||
match field {
|
||||
UserInterfaceInputField::DefaultProtocol => {
|
||||
// Move left
|
||||
config_cli.set_default_protocol(
|
||||
match config_cli.get_default_protocol() {
|
||||
FileTransferProtocol::Ftp(secure) => match secure {
|
||||
true => FileTransferProtocol::Ftp(false),
|
||||
false => FileTransferProtocol::Scp,
|
||||
},
|
||||
FileTransferProtocol::Scp => FileTransferProtocol::Sftp,
|
||||
FileTransferProtocol::Sftp => {
|
||||
FileTransferProtocol::Ftp(true)
|
||||
} // Wrap
|
||||
},
|
||||
);
|
||||
}
|
||||
UserInterfaceInputField::GroupDirs => {
|
||||
// Move left
|
||||
config_cli.set_group_dirs(match config_cli.get_group_dirs() {
|
||||
None => Some(GroupDirs::Last),
|
||||
Some(val) => match val {
|
||||
GroupDirs::Last => Some(GroupDirs::First),
|
||||
GroupDirs::First => None,
|
||||
},
|
||||
});
|
||||
}
|
||||
UserInterfaceInputField::ShowHiddenFiles => {
|
||||
// Move left
|
||||
config_cli.set_show_hidden_files(true);
|
||||
}
|
||||
_ => { /* Not a tab field */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
// Move right on fields which are tabs
|
||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
||||
match field {
|
||||
UserInterfaceInputField::DefaultProtocol => {
|
||||
// Move left
|
||||
config_cli.set_default_protocol(
|
||||
match config_cli.get_default_protocol() {
|
||||
FileTransferProtocol::Sftp => FileTransferProtocol::Scp,
|
||||
FileTransferProtocol::Scp => {
|
||||
FileTransferProtocol::Ftp(false)
|
||||
}
|
||||
FileTransferProtocol::Ftp(secure) => match secure {
|
||||
false => FileTransferProtocol::Ftp(true),
|
||||
true => FileTransferProtocol::Sftp, // Wrap
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
UserInterfaceInputField::GroupDirs => {
|
||||
// Move right
|
||||
config_cli.set_group_dirs(match config_cli.get_group_dirs() {
|
||||
Some(val) => match val {
|
||||
GroupDirs::First => Some(GroupDirs::Last),
|
||||
GroupDirs::Last => None,
|
||||
},
|
||||
None => Some(GroupDirs::First),
|
||||
});
|
||||
}
|
||||
UserInterfaceInputField::ShowHiddenFiles => {
|
||||
// Move right
|
||||
config_cli.set_show_hidden_files(false);
|
||||
}
|
||||
_ => { /* Not a tab field */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Change selected field
|
||||
self.tab = SetupTab::UserInterface(match field {
|
||||
UserInterfaceInputField::GroupDirs => {
|
||||
UserInterfaceInputField::ShowHiddenFiles
|
||||
}
|
||||
UserInterfaceInputField::ShowHiddenFiles => {
|
||||
UserInterfaceInputField::DefaultProtocol
|
||||
}
|
||||
UserInterfaceInputField::DefaultProtocol => {
|
||||
UserInterfaceInputField::TextEditor
|
||||
}
|
||||
UserInterfaceInputField::TextEditor => UserInterfaceInputField::GroupDirs, // Wrap
|
||||
});
|
||||
}
|
||||
KeyCode::Down => {
|
||||
// Change selected field
|
||||
self.tab = SetupTab::UserInterface(match field {
|
||||
UserInterfaceInputField::TextEditor => {
|
||||
UserInterfaceInputField::DefaultProtocol
|
||||
}
|
||||
UserInterfaceInputField::DefaultProtocol => {
|
||||
UserInterfaceInputField::ShowHiddenFiles
|
||||
}
|
||||
UserInterfaceInputField::ShowHiddenFiles => {
|
||||
UserInterfaceInputField::GroupDirs
|
||||
}
|
||||
UserInterfaceInputField::GroupDirs => UserInterfaceInputField::TextEditor, // Wrap
|
||||
});
|
||||
}
|
||||
KeyCode::Char(ch) => {
|
||||
// Check if <CTRL> is enabled
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// Match char
|
||||
match ch {
|
||||
'h' | 'H' => {
|
||||
// Show help
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'r' | 'R' => {
|
||||
// Show reset changes dialog
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Reset changes?"),
|
||||
Self::callback_reset_config_changes,
|
||||
Self::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
's' | 'S' => {
|
||||
// Show save dialog
|
||||
self.popup = Some(Popup::YesNo(
|
||||
String::from("Save changes to configuration?"),
|
||||
Self::callback_save_config,
|
||||
Self::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
} else {
|
||||
// Push character to input field
|
||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
||||
// NOTE: change to match if other fields are added
|
||||
if matches!(field, UserInterfaceInputField::TextEditor) {
|
||||
// Get current text editor and push character
|
||||
let mut input: String = String::from(
|
||||
config_cli.get_text_editor().as_path().to_string_lossy(),
|
||||
);
|
||||
input.push(ch);
|
||||
// Update text editor value
|
||||
config_cli.set_text_editor(PathBuf::from(input.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_popup
|
||||
///
|
||||
/// Handler for input event when popup is visible
|
||||
fn handle_input_event_popup(&mut self, ev: &InputEvent, ptype: Popup) {
|
||||
match ptype {
|
||||
Popup::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
|
||||
Popup::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev),
|
||||
Popup::Help => self.handle_input_event_mode_popup_help(ev),
|
||||
Popup::NewSshKey => self.handle_input_event_mode_popup_newsshkey(ev),
|
||||
Popup::Quit => self.handle_input_event_mode_popup_quit(ev),
|
||||
Popup::YesNo(_, yes_cb, no_cb) => {
|
||||
self.handle_input_event_mode_popup_yesno(ev, yes_cb, no_cb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_alert
|
||||
///
|
||||
/// Handle input event when the input mode is popup, and popup type is alert
|
||||
fn handle_input_event_mode_popup_alert(&mut self, ev: &InputEvent) {
|
||||
// Only enter should be allowed here
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
self.popup = None; // Hide popup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_fatal
|
||||
///
|
||||
/// Handle input event when the input mode is popup, and popup type is fatal
|
||||
fn handle_input_event_mode_popup_fatal(&mut self, ev: &InputEvent) {
|
||||
// Only enter should be allowed here
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
// Quit after acknowelding fatal error
|
||||
self.quit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_help
|
||||
///
|
||||
/// Input event handler for popup help
|
||||
fn handle_input_event_mode_popup_help(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||
self.popup = None; // Hide popup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_newsshkey
|
||||
///
|
||||
/// Handle input events for `Popup::NewSshKey`
|
||||
fn handle_input_event_mode_popup_newsshkey(&mut self, ev: &InputEvent) {
|
||||
// If enter, close popup, otherwise push chars to input
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Abort input
|
||||
// Clear buffer
|
||||
self.clear_user_input();
|
||||
// Hide popup
|
||||
self.popup = None;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Submit
|
||||
let address: String = self.user_input.get(0).unwrap().to_string();
|
||||
let username: String = self.user_input.get(1).unwrap().to_string();
|
||||
// Clear buffer
|
||||
self.clear_user_input();
|
||||
// Close popup BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.popup = None;
|
||||
// Reset user ptr
|
||||
self.user_input_ptr = 0;
|
||||
// Call cb
|
||||
self.callback_new_ssh_key(address, username);
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Move ptr up, or to maximum index (1)
|
||||
self.user_input_ptr = match self.user_input_ptr {
|
||||
1 => 0,
|
||||
_ => 1, // Wrap
|
||||
};
|
||||
}
|
||||
KeyCode::Down => {
|
||||
// Move ptr down, or to minimum index (0)
|
||||
self.user_input_ptr = match self.user_input_ptr {
|
||||
0 => 1,
|
||||
_ => 0, // Wrap
|
||||
}
|
||||
}
|
||||
KeyCode::Char(ch) => {
|
||||
// Get current input
|
||||
let input: &mut String = self.user_input.get_mut(self.user_input_ptr).unwrap();
|
||||
input.push(ch);
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
let input: &mut String = self.user_input.get_mut(self.user_input_ptr).unwrap();
|
||||
input.pop();
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_quit
|
||||
///
|
||||
/// Handle input events for `Popup::Quit`
|
||||
fn handle_input_event_mode_popup_quit(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
// Hide popup
|
||||
self.popup = None;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Perform enter, based on current choice
|
||||
match self.quit_opt {
|
||||
QuitDialogOption::Cancel => self.popup = None, // Hide popup
|
||||
QuitDialogOption::DontSave => self.quit = true, // Just quit
|
||||
QuitDialogOption::Save => self.callback_save_config_and_quit(), // Save and quit
|
||||
}
|
||||
// Reset choice
|
||||
self.quit_opt = QuitDialogOption::Save;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
// Change option
|
||||
self.quit_opt = match self.quit_opt {
|
||||
QuitDialogOption::Save => QuitDialogOption::DontSave,
|
||||
QuitDialogOption::DontSave => QuitDialogOption::Cancel,
|
||||
QuitDialogOption::Cancel => QuitDialogOption::Save, // Wrap
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
// Change option
|
||||
self.quit_opt = match self.quit_opt {
|
||||
QuitDialogOption::Cancel => QuitDialogOption::DontSave,
|
||||
QuitDialogOption::DontSave => QuitDialogOption::Save,
|
||||
QuitDialogOption::Save => QuitDialogOption::Cancel, // Wrap
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_yesno
|
||||
///
|
||||
/// Input event handler for popup alert
|
||||
fn handle_input_event_mode_popup_yesno(
|
||||
&mut self,
|
||||
ev: &InputEvent,
|
||||
yes_cb: OnChoiceCallback,
|
||||
no_cb: OnChoiceCallback,
|
||||
) {
|
||||
// If enter, close popup, otherwise move dialog option
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
// Hide popup BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.popup = None;
|
||||
// Check if user selected yes or not
|
||||
match self.yesno_opt {
|
||||
YesNoDialogOption::No => no_cb(self),
|
||||
YesNoDialogOption::Yes => yes_cb(self),
|
||||
}
|
||||
// Reset choice option to yes
|
||||
self.yesno_opt = YesNoDialogOption::Yes;
|
||||
}
|
||||
KeyCode::Right => self.yesno_opt = YesNoDialogOption::No, // Set to NO
|
||||
KeyCode::Left => self.yesno_opt = YesNoDialogOption::Yes, // Set to YES
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
706
src/ui/activities/setup_activity/layout.rs
Normal file
706
src/ui/activities/setup_activity/layout.rs
Normal file
|
@ -0,0 +1,706 @@
|
|||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{
|
||||
Context, Popup, QuitDialogOption, SetupActivity, SetupTab, UserInterfaceInputField,
|
||||
YesNoDialogOption,
|
||||
};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::fs::explorer::GroupDirs;
|
||||
use crate::utils::fmt::align_text_center;
|
||||
// Ext
|
||||
use tui::{
|
||||
layout::{Constraint, Corner, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### draw
|
||||
///
|
||||
/// Draw UI
|
||||
pub(super) fn draw(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal.draw(|f| {
|
||||
// Prepare main chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Current tab
|
||||
Constraint::Percentage(90), // Main body
|
||||
Constraint::Length(3), // Help footer
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// Prepare selected tab
|
||||
f.render_widget(self.draw_selected_tab(), chunks[0]);
|
||||
// Draw main layout
|
||||
match &self.tab {
|
||||
SetupTab::SshConfig => {
|
||||
// Draw ssh config
|
||||
// Create explorer chunks
|
||||
let sshcfg_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Percentage(100)].as_ref())
|
||||
.split(chunks[1]);
|
||||
if let Some(ssh_key_tab) = self.draw_ssh_keys_list() {
|
||||
// Create ssh list state
|
||||
let mut ssh_key_state: ListState = ListState::default();
|
||||
ssh_key_state.select(Some(self.ssh_key_idx));
|
||||
// Render ssh keys
|
||||
f.render_stateful_widget(ssh_key_tab, sshcfg_chunks[0], &mut ssh_key_state);
|
||||
}
|
||||
}
|
||||
SetupTab::UserInterface(form_field) => {
|
||||
// Create chunks
|
||||
let ui_cfg_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(1),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(chunks[1]);
|
||||
// Render input forms
|
||||
if let Some(field) = self.draw_text_editor_input() {
|
||||
f.render_widget(field, ui_cfg_chunks[0]);
|
||||
}
|
||||
if let Some(tab) = self.draw_default_protocol_tab() {
|
||||
f.render_widget(tab, ui_cfg_chunks[1]);
|
||||
}
|
||||
if let Some(tab) = self.draw_hidden_files_tab() {
|
||||
f.render_widget(tab, ui_cfg_chunks[2]);
|
||||
}
|
||||
if let Some(tab) = self.draw_default_group_dirs_tab() {
|
||||
f.render_widget(tab, ui_cfg_chunks[3]);
|
||||
}
|
||||
// Set cursor
|
||||
if let Some(cli) = &self.config_cli {
|
||||
if matches!(form_field, UserInterfaceInputField::TextEditor) {
|
||||
let editor_text: String =
|
||||
String::from(cli.get_text_editor().as_path().to_string_lossy());
|
||||
f.set_cursor(
|
||||
ui_cfg_chunks[0].x + editor_text.width() as u16 + 1,
|
||||
ui_cfg_chunks[0].y + 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Draw footer
|
||||
f.render_widget(self.draw_footer(), chunks[2]);
|
||||
// Draw popup
|
||||
if let Some(popup) = &self.popup {
|
||||
// Calculate popup size
|
||||
let (width, height): (u16, u16) = match popup {
|
||||
Popup::Alert(_, _) | Popup::Fatal(_) => (50, 10),
|
||||
Popup::Help => (50, 70),
|
||||
Popup::NewSshKey => (50, 20),
|
||||
Popup::Quit => (40, 10),
|
||||
Popup::YesNo(_, _, _) => (30, 10),
|
||||
};
|
||||
let popup_area: Rect = self.draw_popup_area(f.size(), width, height);
|
||||
f.render_widget(Clear, popup_area); //this clears out the background
|
||||
match popup {
|
||||
Popup::Alert(color, txt) => f.render_widget(
|
||||
self.draw_popup_alert(*color, txt.clone(), popup_area.width),
|
||||
popup_area,
|
||||
),
|
||||
Popup::Fatal(txt) => f.render_widget(
|
||||
self.draw_popup_fatal(txt.clone(), popup_area.width),
|
||||
popup_area,
|
||||
),
|
||||
Popup::Help => f.render_widget(self.draw_popup_help(), popup_area),
|
||||
Popup::NewSshKey => {
|
||||
let popup_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Address form
|
||||
Constraint::Length(3), // Username form
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_area);
|
||||
let (address_form, username_form): (Paragraph, Paragraph) =
|
||||
self.draw_popup_new_ssh_key();
|
||||
// Render parts
|
||||
f.render_widget(address_form, popup_chunks[0]);
|
||||
f.render_widget(username_form, popup_chunks[1]);
|
||||
// Set cursor to popup form
|
||||
if self.user_input_ptr < 2 {
|
||||
if let Some(selected_text) = self.user_input.get(self.user_input_ptr) {
|
||||
// Set cursor
|
||||
f.set_cursor(
|
||||
popup_chunks[self.user_input_ptr].x
|
||||
+ selected_text.width() as u16
|
||||
+ 1,
|
||||
popup_chunks[self.user_input_ptr].y + 1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Popup::Quit => f.render_widget(self.draw_popup_quit(), popup_area),
|
||||
Popup::YesNo(txt, _, _) => {
|
||||
f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.context = Some(ctx);
|
||||
}
|
||||
|
||||
/// ### draw_selecte_tab
|
||||
///
|
||||
/// Draw selected tab tab
|
||||
fn draw_selected_tab(&self) -> Tabs {
|
||||
let choices: Vec<Spans> = vec![Spans::from("User Interface"), Spans::from("SSH Keys")];
|
||||
let index: usize = match self.tab {
|
||||
SetupTab::UserInterface(_) => 0,
|
||||
SetupTab::SshConfig => 1,
|
||||
};
|
||||
Tabs::new(choices)
|
||||
.block(Block::default().borders(Borders::BOTTOM).title("Setup"))
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Yellow),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_footer
|
||||
///
|
||||
/// Draw authentication page footer
|
||||
fn draw_footer(&self) -> Paragraph {
|
||||
// Write header
|
||||
let (footer, h_style) = (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled(
|
||||
"<CTRL+H>",
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Cyan),
|
||||
),
|
||||
Span::raw(" to show keybindings"),
|
||||
],
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
);
|
||||
let mut footer_text = Text::from(Spans::from(footer));
|
||||
footer_text.patch_style(h_style);
|
||||
Paragraph::new(footer_text)
|
||||
}
|
||||
|
||||
/// ### draw_text_editor_input
|
||||
///
|
||||
/// Draw input text field for text editor parameter
|
||||
fn draw_text_editor_input(&self) -> Option<Paragraph> {
|
||||
match &self.config_cli {
|
||||
Some(cli) => Some(
|
||||
Paragraph::new(String::from(
|
||||
cli.get_text_editor().as_path().to_string_lossy(),
|
||||
))
|
||||
.style(Style::default().fg(match &self.tab {
|
||||
SetupTab::SshConfig => Color::White,
|
||||
SetupTab::UserInterface(field) => match field {
|
||||
UserInterfaceInputField::TextEditor => Color::LightGreen,
|
||||
_ => Color::White,
|
||||
},
|
||||
}))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Text Editor"),
|
||||
),
|
||||
),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### draw_default_protocol_tab
|
||||
///
|
||||
/// Draw default protocol input tab
|
||||
fn draw_default_protocol_tab(&self) -> Option<Tabs> {
|
||||
// Check if config client is some
|
||||
match &self.config_cli {
|
||||
Some(cli) => {
|
||||
let choices: Vec<Spans> = vec![
|
||||
Spans::from("SFTP"),
|
||||
Spans::from("SCP"),
|
||||
Spans::from("FTP"),
|
||||
Spans::from("FTPS"),
|
||||
];
|
||||
let index: usize = match cli.get_default_protocol() {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(secure) => match secure {
|
||||
false => 2,
|
||||
true => 3,
|
||||
},
|
||||
};
|
||||
let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab {
|
||||
SetupTab::UserInterface(field) => match field {
|
||||
UserInterfaceInputField::DefaultProtocol => {
|
||||
(Color::Cyan, Color::Black, Color::Cyan)
|
||||
}
|
||||
_ => (Color::Reset, Color::Cyan, Color::Reset),
|
||||
},
|
||||
_ => (Color::Reset, Color::Reset, Color::Reset),
|
||||
};
|
||||
Some(
|
||||
Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().fg(block_fg))
|
||||
.title("Default File Transfer Protocol"),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default().add_modifier(Modifier::BOLD).fg(fg).bg(bg),
|
||||
),
|
||||
)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### draw_default_protocol_tab
|
||||
///
|
||||
/// Draw default protocol input tab
|
||||
fn draw_hidden_files_tab(&self) -> Option<Tabs> {
|
||||
// Check if config client is some
|
||||
match &self.config_cli {
|
||||
Some(cli) => {
|
||||
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
||||
let index: usize = match cli.get_show_hidden_files() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab {
|
||||
SetupTab::UserInterface(field) => match field {
|
||||
UserInterfaceInputField::ShowHiddenFiles => {
|
||||
(Color::LightRed, Color::Black, Color::LightRed)
|
||||
}
|
||||
_ => (Color::Reset, Color::LightRed, Color::Reset),
|
||||
},
|
||||
_ => (Color::Reset, Color::Reset, Color::Reset),
|
||||
};
|
||||
Some(
|
||||
Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().fg(block_fg))
|
||||
.title("Show hidden files (by default)"),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default().add_modifier(Modifier::BOLD).fg(fg).bg(bg),
|
||||
),
|
||||
)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### draw_default_group_dirs_tab
|
||||
///
|
||||
/// Draw group dirs input tab
|
||||
fn draw_default_group_dirs_tab(&self) -> Option<Tabs> {
|
||||
// Check if config client is some
|
||||
match &self.config_cli {
|
||||
Some(cli) => {
|
||||
let choices: Vec<Spans> = vec![
|
||||
Spans::from("Display First"),
|
||||
Spans::from("Display Last"),
|
||||
Spans::from("No"),
|
||||
];
|
||||
let index: usize = match cli.get_group_dirs() {
|
||||
None => 2,
|
||||
Some(val) => match val {
|
||||
GroupDirs::First => 0,
|
||||
GroupDirs::Last => 1,
|
||||
},
|
||||
};
|
||||
let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab {
|
||||
SetupTab::UserInterface(field) => match field {
|
||||
UserInterfaceInputField::GroupDirs => {
|
||||
(Color::LightMagenta, Color::Black, Color::LightMagenta)
|
||||
}
|
||||
_ => (Color::Reset, Color::LightMagenta, Color::Reset),
|
||||
},
|
||||
_ => (Color::Reset, Color::Reset, Color::Reset),
|
||||
};
|
||||
Some(
|
||||
Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().fg(block_fg))
|
||||
.title("Group directories"),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default().add_modifier(Modifier::BOLD).fg(fg).bg(bg),
|
||||
),
|
||||
)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### draw_ssh_keys_list
|
||||
///
|
||||
/// Draw ssh keys list
|
||||
fn draw_ssh_keys_list(&self) -> Option<List> {
|
||||
// Check if config client is some
|
||||
match &self.config_cli {
|
||||
Some(cli) => {
|
||||
// Iterate over ssh keys
|
||||
let mut ssh_keys: Vec<ListItem> = Vec::with_capacity(cli.iter_ssh_keys().count());
|
||||
for key in cli.iter_ssh_keys() {
|
||||
if let Ok(host) = cli.get_ssh_key(key) {
|
||||
if let Some((addr, username, _)) = host {
|
||||
ssh_keys.push(ListItem::new(Span::from(format!(
|
||||
"{} at {}",
|
||||
username, addr,
|
||||
))));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Return list
|
||||
Some(
|
||||
List::new(ssh_keys)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::LightGreen))
|
||||
.title("SSH Keys"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### draw_popup_area
|
||||
///
|
||||
/// Draw popup area
|
||||
fn draw_popup_area(&self, area: Rect, width: u16, height: u16) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - height) / 2),
|
||||
Constraint::Percentage(height),
|
||||
Constraint::Percentage((100 - height) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - width) / 2),
|
||||
Constraint::Percentage(width),
|
||||
Constraint::Percentage((100 - width) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
|
||||
/// ### draw_popup_alert
|
||||
///
|
||||
/// Draw alert popup
|
||||
fn draw_popup_alert(&self, color: Color, text: String, width: u16) -> List {
|
||||
// Wraps texts
|
||||
let message_rows = textwrap::wrap(text.as_str(), width as usize);
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for msg in message_rows.iter() {
|
||||
lines.push(ListItem::new(Spans::from(align_text_center(msg, width))));
|
||||
}
|
||||
List::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(color))
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Alert"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.style(Style::default().fg(color))
|
||||
}
|
||||
|
||||
/// ### draw_popup_fatal
|
||||
///
|
||||
/// Draw fatal error popup
|
||||
fn draw_popup_fatal(&self, text: String, width: u16) -> List {
|
||||
self.draw_popup_alert(Color::Red, text, width)
|
||||
}
|
||||
|
||||
/// ### draw_popup_new_ssh_key
|
||||
///
|
||||
/// Draw new ssh key form popup
|
||||
fn draw_popup_new_ssh_key(&self) -> (Paragraph, Paragraph) {
|
||||
let address: Paragraph = Paragraph::new(self.user_input.get(0).unwrap().as_str())
|
||||
.style(Style::default().fg(match self.user_input_ptr {
|
||||
0 => Color::LightCyan,
|
||||
_ => Color::White,
|
||||
}))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::TOP | Borders::RIGHT | Borders::LEFT)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.title("Host name or address"),
|
||||
);
|
||||
let username: Paragraph = Paragraph::new(self.user_input.get(1).unwrap().as_str())
|
||||
.style(Style::default().fg(match self.user_input_ptr {
|
||||
1 => Color::LightMagenta,
|
||||
_ => Color::White,
|
||||
}))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.title("Username"),
|
||||
);
|
||||
(address, username)
|
||||
}
|
||||
|
||||
/// ### draw_popup_quit
|
||||
///
|
||||
/// Draw quit select popup
|
||||
fn draw_popup_quit(&self) -> Tabs {
|
||||
let choices: Vec<Spans> = vec![
|
||||
Spans::from("Save"),
|
||||
Spans::from("Don't save"),
|
||||
Spans::from("Cancel"),
|
||||
];
|
||||
let index: usize = match self.quit_opt {
|
||||
QuitDialogOption::Save => 0,
|
||||
QuitDialogOption::DontSave => 1,
|
||||
QuitDialogOption::Cancel => 2,
|
||||
};
|
||||
Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Exit setup?"),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD).fg(Color::Red))
|
||||
}
|
||||
|
||||
/// ### draw_popup_yesno
|
||||
///
|
||||
/// Draw yes/no select popup
|
||||
fn draw_popup_yesno(&self, text: String) -> Tabs {
|
||||
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
||||
let index: usize = match self.yesno_opt {
|
||||
YesNoDialogOption::Yes => 0,
|
||||
YesNoDialogOption::No => 1,
|
||||
};
|
||||
Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(text),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Yellow),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_popup_help
|
||||
///
|
||||
/// Draw authentication page help popup
|
||||
fn draw_popup_help(&self) -> List {
|
||||
// Write header
|
||||
let cmds: Vec<ListItem> = vec![
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<ESC>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Exit setup"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<TAB>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Change setup page"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<RIGHT/LEFT>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Change selected element in tab"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<UP/DOWN>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Change input field"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<ENTER>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Submit / Dismiss popup"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<DEL>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Delete entry"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+E>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Delete entry"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+H>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Show help"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+N>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("New SSH key"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+R>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Revert changes"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+S>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Save configuration"),
|
||||
])),
|
||||
];
|
||||
List::new(cmds)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default())
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Help"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
}
|
||||
}
|
38
src/ui/activities/setup_activity/misc.rs
Normal file
38
src/ui/activities/setup_activity/misc.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::SetupActivity;
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### clear_user_input
|
||||
///
|
||||
/// Clear user input buffers
|
||||
pub(super) fn clear_user_input(&mut self) {
|
||||
for s in self.user_input.iter_mut() {
|
||||
s.clear();
|
||||
}
|
||||
}
|
||||
}
|
205
src/ui/activities/setup_activity/mod.rs
Normal file
205
src/ui/activities/setup_activity/mod.rs
Normal file
|
@ -0,0 +1,205 @@
|
|||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Submodules
|
||||
mod callbacks;
|
||||
mod config;
|
||||
mod input;
|
||||
mod layout;
|
||||
mod misc;
|
||||
|
||||
// Deps
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
|
||||
// Locals
|
||||
use super::{Activity, Context};
|
||||
use crate::system::config_client::ConfigClient;
|
||||
// Ext
|
||||
use crossterm::event::Event as InputEvent;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use tui::style::Color;
|
||||
|
||||
// Types
|
||||
type OnChoiceCallback = fn(&mut SetupActivity);
|
||||
|
||||
/// ### UserInterfaceInputField
|
||||
///
|
||||
/// Input field selected in user interface
|
||||
#[derive(std::cmp::PartialEq, Clone)]
|
||||
enum UserInterfaceInputField {
|
||||
DefaultProtocol,
|
||||
TextEditor,
|
||||
ShowHiddenFiles,
|
||||
GroupDirs,
|
||||
}
|
||||
|
||||
/// ### SetupTab
|
||||
///
|
||||
/// Selected setup tab
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
enum SetupTab {
|
||||
UserInterface(UserInterfaceInputField),
|
||||
SshConfig,
|
||||
}
|
||||
|
||||
/// ### QuitDialogOption
|
||||
///
|
||||
/// Quit dialog options
|
||||
#[derive(std::cmp::PartialEq, Clone)]
|
||||
enum QuitDialogOption {
|
||||
Save,
|
||||
DontSave,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
/// ### YesNoDialogOption
|
||||
///
|
||||
/// YesNo dialog options
|
||||
#[derive(std::cmp::PartialEq, Clone)]
|
||||
enum YesNoDialogOption {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
/// ## Popup
|
||||
///
|
||||
/// Popup describes the type of popup
|
||||
#[derive(Clone)]
|
||||
enum Popup {
|
||||
Alert(Color, String), // Block color; Block text
|
||||
Fatal(String), // Must quit after being hidden
|
||||
Help, // Show Help
|
||||
NewSshKey, //
|
||||
Quit, // Quit dialog
|
||||
YesNo(String, OnChoiceCallback, OnChoiceCallback), // Yes/No Dialog
|
||||
}
|
||||
|
||||
/// ## SetupActivity
|
||||
///
|
||||
/// Setup activity states holder
|
||||
pub struct SetupActivity {
|
||||
pub quit: bool, // Becomes true when user requests the activity to terminate
|
||||
context: Option<Context>, // Context holder
|
||||
config_cli: Option<ConfigClient>, // Config client
|
||||
tab: SetupTab, // Current setup tab
|
||||
popup: Option<Popup>, // Active popup
|
||||
user_input: Vec<String>, // User input holder
|
||||
user_input_ptr: usize, // Selected user input
|
||||
quit_opt: QuitDialogOption, // Popup::Quit selected option
|
||||
yesno_opt: YesNoDialogOption, // Popup::YesNo selected option
|
||||
ssh_key_idx: usize, // Index of selected ssh key in list
|
||||
redraw: bool, // Redraw ui?
|
||||
}
|
||||
|
||||
impl Default for SetupActivity {
|
||||
fn default() -> Self {
|
||||
// Initialize user input
|
||||
let mut user_input_buffer: Vec<String> = Vec::with_capacity(16);
|
||||
for _ in 0..16 {
|
||||
user_input_buffer.push(String::new());
|
||||
}
|
||||
SetupActivity {
|
||||
quit: false,
|
||||
context: None,
|
||||
config_cli: None,
|
||||
tab: SetupTab::UserInterface(UserInterfaceInputField::TextEditor),
|
||||
popup: None,
|
||||
user_input: user_input_buffer, // Max 16
|
||||
user_input_ptr: 0,
|
||||
quit_opt: QuitDialogOption::Save,
|
||||
yesno_opt: YesNoDialogOption::Yes,
|
||||
ssh_key_idx: 0,
|
||||
redraw: true, // Draw at first `on_draw`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity for SetupActivity {
|
||||
/// ### on_create
|
||||
///
|
||||
/// `on_create` is the function which must be called to initialize the activity.
|
||||
/// `on_create` must initialize all the data structures used by the activity
|
||||
/// Context is taken from activity manager and will be released only when activity is destroyed
|
||||
fn on_create(&mut self, context: Context) {
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
self.context.as_mut().unwrap().clear_screen();
|
||||
// Put raw mode on enabled
|
||||
let _ = enable_raw_mode();
|
||||
// Initialize config client
|
||||
if self.config_cli.is_none() {
|
||||
self.init_config_client();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_draw
|
||||
///
|
||||
/// `on_draw` is the function which draws the graphical interface.
|
||||
/// This function must be called at each tick to refresh the interface
|
||||
fn on_draw(&mut self) {
|
||||
// Context must be something
|
||||
if self.context.is_none() {
|
||||
return;
|
||||
}
|
||||
// Read one event
|
||||
if let Ok(event) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
if let Some(event) = event {
|
||||
// Set redraw to true
|
||||
self.redraw = true;
|
||||
// Handle event
|
||||
self.handle_input_event(&event);
|
||||
}
|
||||
}
|
||||
// Redraw if necessary
|
||||
if self.redraw {
|
||||
// Draw
|
||||
self.draw();
|
||||
// Redraw back to false
|
||||
self.redraw = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_destroy
|
||||
///
|
||||
/// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity.
|
||||
/// This function must be called once before terminating the activity.
|
||||
/// This function finally releases the context
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Disable raw mode
|
||||
let _ = disable_raw_mode();
|
||||
self.context.as_ref()?;
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
Some(mut ctx) => {
|
||||
ctx.clear_screen();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
|
67
src/utils/crypto.rs
Normal file
67
src/utils/crypto.rs
Normal file
|
@ -0,0 +1,67 @@
|
|||
//! ## Crypto
|
||||
//!
|
||||
//! `crypto` is the module which provides utilities for crypting
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate magic_crypt;
|
||||
|
||||
// Ext
|
||||
use magic_crypt::MagicCryptTrait;
|
||||
|
||||
/// ### aes128_b64_crypt
|
||||
///
|
||||
/// Crypt a string using AES128; output is returned as a BASE64 string
|
||||
pub fn aes128_b64_crypt(key: &str, input: &str) -> String {
|
||||
let crypter = new_magic_crypt!(key.to_string(), 128);
|
||||
crypter.encrypt_str_to_base64(input.to_string())
|
||||
}
|
||||
|
||||
/// ### aes128_b64_decrypt
|
||||
///
|
||||
/// Decrypt a string using AES128
|
||||
pub fn aes128_b64_decrypt(key: &str, secret: &str) -> Result<String, magic_crypt::MagicCryptError> {
|
||||
let crypter = new_magic_crypt!(key.to_string(), 128);
|
||||
crypter.decrypt_base64_to_string(secret.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_utils_crypto_aes128() {
|
||||
let key: &str = "MYSUPERSECRETKEY";
|
||||
let input: &str = "Hello world!";
|
||||
let secret: String = aes128_b64_crypt(&key, input);
|
||||
assert_eq!(secret.as_str(), "z4Z6LpcpYqBW4+bkIok+5A==");
|
||||
assert_eq!(
|
||||
aes128_b64_decrypt(key, secret.as_str())
|
||||
.ok()
|
||||
.unwrap()
|
||||
.as_str(),
|
||||
input
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
//! ## Hash
|
||||
//!
|
||||
//! `hash` is the module which provides utilities for calculating digests
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
extern crate data_encoding;
|
||||
extern crate ring;
|
||||
|
||||
use data_encoding::HEXLOWER;
|
||||
use ring::digest::{Context, Digest, SHA256};
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::Path;
|
||||
|
||||
/// ### hash_sha256_file
|
||||
///
|
||||
/// Get SHA256 of provided path
|
||||
pub fn hash_sha256_file(file: &Path) -> Result<String, std::io::Error> {
|
||||
// Open file
|
||||
let mut reader: File = File::open(file)?;
|
||||
let mut context = Context::new(&SHA256);
|
||||
let mut buffer = [0; 8192];
|
||||
loop {
|
||||
let count = reader.read(&mut buffer)?;
|
||||
if count == 0 {
|
||||
break;
|
||||
}
|
||||
context.update(&buffer[..count]);
|
||||
}
|
||||
// Finish context
|
||||
let digest: Digest = context.finish();
|
||||
Ok(HEXLOWER.encode(digest.as_ref()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn test_utils_hash_sha256() {
|
||||
let tmp: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
// Write
|
||||
let mut fhnd: File = File::create(tmp.path()).unwrap();
|
||||
assert!(fhnd.write_all(b"Hello world!\n").is_ok());
|
||||
assert_eq!(
|
||||
*hash_sha256_file(tmp.path()).ok().as_ref().unwrap(),
|
||||
String::from("0ba904eae8773b70c75333db4de2f3ac45a8ad4ddba1b242f0b3cfc199391dd8")
|
||||
);
|
||||
// Bad file
|
||||
assert!(hash_sha256_file(Path::new("/tmp/oiojjt5ig/aiehgoiwg")).is_err());
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -24,6 +24,7 @@
|
|||
*/
|
||||
|
||||
// modules
|
||||
pub mod crypto;
|
||||
pub mod fmt;
|
||||
pub mod hash;
|
||||
pub mod parser;
|
||||
pub mod random;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
@ -27,10 +27,15 @@
|
|||
extern crate chrono;
|
||||
extern crate whoami;
|
||||
|
||||
// Locals
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::environment;
|
||||
|
||||
// Ext
|
||||
use chrono::format::ParseError;
|
||||
use chrono::prelude::*;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// ### parse_remote_opt
|
||||
|
@ -58,8 +63,22 @@ pub fn parse_remote_opt(
|
|||
let mut wrkstr: String = remote.to_string();
|
||||
let address: String;
|
||||
let mut port: u16 = 22;
|
||||
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
|
||||
let mut username: Option<String> = None;
|
||||
// Set protocol to default protocol
|
||||
let mut protocol: FileTransferProtocol = match environment::init_config_dir() {
|
||||
Ok(p) => match p {
|
||||
Some(p) => {
|
||||
// Create config client
|
||||
let (config_path, ssh_key_path) = environment::get_config_paths(p.as_path());
|
||||
match ConfigClient::new(config_path.as_path(), ssh_key_path.as_path()) {
|
||||
Ok(cli) => cli.get_default_protocol(),
|
||||
Err(_) => FileTransferProtocol::Sftp,
|
||||
}
|
||||
}
|
||||
None => FileTransferProtocol::Sftp,
|
||||
},
|
||||
Err(_) => FileTransferProtocol::Sftp,
|
||||
};
|
||||
// Split string by '://'
|
||||
let tokens: Vec<&str> = wrkstr.split("://").collect();
|
||||
// If length is > 1, then token[0] is protocol
|
||||
|
@ -67,33 +86,16 @@ pub fn parse_remote_opt(
|
|||
1 => {}
|
||||
2 => {
|
||||
// Parse protocol
|
||||
match tokens[0] {
|
||||
"sftp" => {
|
||||
// Set protocol to sftp
|
||||
protocol = FileTransferProtocol::Sftp;
|
||||
// Set port to default (22)
|
||||
port = 22;
|
||||
}
|
||||
"scp" => {
|
||||
// Set protocol to scp
|
||||
protocol = FileTransferProtocol::Scp;
|
||||
// Set port to default (22)
|
||||
port = 22;
|
||||
}
|
||||
"ftp" => {
|
||||
// Set protocol to fpt
|
||||
protocol = FileTransferProtocol::Ftp(false);
|
||||
// Set port to default (21)
|
||||
port = 21;
|
||||
}
|
||||
"ftps" => {
|
||||
// Set protocol to fpt
|
||||
protocol = FileTransferProtocol::Ftp(true);
|
||||
// Set port to default (21)
|
||||
port = 21;
|
||||
}
|
||||
_ => return Err(format!("Unknown protocol '{}'", tokens[0])),
|
||||
}
|
||||
let (m_protocol, m_port) = match FileTransferProtocol::from_str(tokens[0]) {
|
||||
Ok(proto) => match proto {
|
||||
FileTransferProtocol::Ftp(_) => (proto, 21),
|
||||
FileTransferProtocol::Scp => (proto, 22),
|
||||
FileTransferProtocol::Sftp => (proto, 22),
|
||||
},
|
||||
Err(_) => return Err(format!("Unknown protocol '{}'", tokens[0])),
|
||||
};
|
||||
protocol = m_protocol;
|
||||
port = m_port;
|
||||
wrkstr = String::from(tokens[1]); // Wrkstr becomes tokens[1]
|
||||
}
|
||||
_ => return Err(String::from("Bad syntax")), // Too many tokens...
|
||||
|
@ -179,10 +181,26 @@ pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result<SystemT
|
|||
.unwrap_or(SystemTime::UNIX_EPOCH))
|
||||
}
|
||||
|
||||
/// ### parse_datetime
|
||||
///
|
||||
/// Parse date time string representation and transform it into `SystemTime`
|
||||
pub fn parse_datetime(tm: &str, fmt: &str) -> Result<SystemTime, ParseError> {
|
||||
match NaiveDateTime::parse_from_str(tm, fmt) {
|
||||
Ok(dt) => {
|
||||
let sys_time: SystemTime = SystemTime::UNIX_EPOCH;
|
||||
Ok(sys_time
|
||||
.checked_add(Duration::from_secs(dt.timestamp() as u64))
|
||||
.unwrap_or(SystemTime::UNIX_EPOCH))
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
|
||||
#[test]
|
||||
fn test_utils_parse_remote_opt() {
|
||||
|
@ -277,22 +295,24 @@ mod tests {
|
|||
fn test_utils_parse_lstime() {
|
||||
// Good cases
|
||||
assert_eq!(
|
||||
parse_lstime("Nov 5 16:32", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1604593920)
|
||||
fmt_time(
|
||||
parse_lstime("Nov 5 16:32", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap(),
|
||||
"%m %d %M"
|
||||
)
|
||||
.as_str(),
|
||||
"11 05 32"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_lstime("Dec 2 21:32", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1606944720)
|
||||
fmt_time(
|
||||
parse_lstime("Dec 2 21:32", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap(),
|
||||
"%m %d %M"
|
||||
)
|
||||
.as_str(),
|
||||
"12 02 32"
|
||||
);
|
||||
assert_eq!(
|
||||
parse_lstime("Nov 5 2018", "%b %d %Y", "%b %d %H:%M")
|
||||
|
@ -317,4 +337,19 @@ mod tests {
|
|||
assert!(parse_lstime("Feb 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
|
||||
assert!(parse_lstime("Feb 15 25:32", "%b %d %Y", "%b %d %H:%M").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_parse_datetime() {
|
||||
assert_eq!(
|
||||
parse_datetime("04-08-14 03:09PM", "%d-%m-%y %I:%M%p")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1407164940)
|
||||
);
|
||||
// Not enough argument for datetime
|
||||
assert!(parse_datetime("04-08-14", "%d-%m-%y").is_err());
|
||||
}
|
||||
}
|
||||
|
|
52
src/utils/random.rs
Normal file
52
src/utils/random.rs
Normal file
|
@ -0,0 +1,52 @@
|
|||
//! ## Random
|
||||
//!
|
||||
//! `random` is the module which provides utilities for rand
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
* TermSCP is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* TermSCP is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate rand;
|
||||
// Ext
|
||||
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||
|
||||
/// ## random_alphanumeric_with_len
|
||||
///
|
||||
/// Generate a random alphanumeric string with provided length
|
||||
pub fn random_alphanumeric_with_len(len: usize) -> String {
|
||||
let mut rng = thread_rng();
|
||||
std::iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(len)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_utils_random_alphanumeric_with_len() {
|
||||
assert_eq!(random_alphanumeric_with_len(256).len(), 256);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue