Merge branch '0.3.0' into main

This commit is contained in:
veeso 2021-01-10 17:31:31 +01:00
commit eeed99b013
55 changed files with 5902 additions and 939 deletions

View file

@ -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

View 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
View file

@ -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",

View file

@ -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"

View file

@ -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
View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 689 KiB

View file

@ -3,3 +3,5 @@ ignore:
- src/lib.rs
- src/activity_manager.rs
- src/ui/
fixes:
- "/::"

View file

@ -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)
}
}

View file

@ -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"
*

View file

@ -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
View 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
View 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
}
}

View file

@ -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);

View file

@ -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]

View file

@ -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());

View file

@ -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
View 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
View 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
}),
}
}
}

View file

@ -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")
)
);

View file

@ -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"
*

View file

@ -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;

View file

@ -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;

View file

@ -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
View 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)
)
}
}

View file

@ -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/")
)
);
}
}

View file

@ -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;

View 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()
}
}

View file

@ -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),
))

View file

@ -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"
*

View file

@ -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),

View file

@ -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",
}
}
}

View file

@ -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

View file

@ -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());
}
}
}
}
}
}
}
}
}
}

View file

@ -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
});
}
}

View file

@ -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>",

View file

@ -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());
}
}
}

View file

@ -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
)));

View file

@ -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,

View file

@ -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

View 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();
}
}
}
}

View 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(()),
}
}
}

View 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 */ }
}
}
}
}

View 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)
}
}

View 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();
}
}
}

View 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,
}
}
}

View file

@ -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"
*

View file

@ -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"
*

View file

@ -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
View 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
);
}
}

View file

@ -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"
*

View file

@ -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());
}
}

View file

@ -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;

View file

@ -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
View 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);
}
}