Compare commits
240 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd99665d1c | ||
|
|
93bab299ec | ||
|
|
eb33f93322 | ||
|
|
d165b699f0 | ||
|
|
7817295d8e | ||
|
|
a986e531d9 | ||
|
|
8a3b652dcd | ||
|
|
efbea63154 | ||
|
|
61045fa548 | ||
|
|
da5e1f315d | ||
|
|
85c57ce027 | ||
|
|
6682c07eb6 | ||
|
|
4e887c3429 | ||
|
|
6435271be8 | ||
|
|
c9a77fa65d | ||
|
|
cc5399d36e | ||
|
|
ca1aa5675a | ||
|
|
e21bfbbd14 | ||
|
|
e948d598b0 | ||
|
|
0173d67a3b | ||
|
|
025547a3dc | ||
|
|
af830d603d | ||
|
|
669fd23868 | ||
|
|
4ff7fc079c | ||
|
|
7f24d6db5c | ||
|
|
8c8d01c29c | ||
|
|
d23e6bb60c | ||
|
|
87aa900bc6 | ||
|
|
e8e4cd22a1 | ||
|
|
780cf592e4 | ||
|
|
45db58a0b3 | ||
|
|
f5218bc582 | ||
|
|
c5e2e02415 | ||
|
|
e088772685 | ||
|
|
859daa3107 | ||
|
|
56c580fc80 | ||
|
|
7a9ee697ff | ||
|
|
c16a2f6441 | ||
|
|
b3c4385617 | ||
|
|
e92370bd05 | ||
|
|
da0d5231bf | ||
|
|
c1f6308795 | ||
|
|
54ab24fc0c | ||
|
|
d99efb9de4 | ||
|
|
0c9ed38eb7 | ||
|
|
ec801f1555 | ||
|
|
e0d8879961 | ||
|
|
3f2be65a6c | ||
|
|
a5a745e444 | ||
|
|
7ee142314e | ||
|
|
c412d98ec7 | ||
|
|
d7e5eacd79 | ||
|
|
367fb235f6 | ||
|
|
d68d63b978 | ||
|
|
23ca2baa8c | ||
|
|
ac02928e69 | ||
|
|
cb20589b01 | ||
|
|
1acbf89717 | ||
|
|
08d8a3621c | ||
|
|
0192b86422 | ||
|
|
0e4caaecfd | ||
|
|
215927d432 | ||
|
|
eee08bd623 | ||
|
|
76fdd9864c | ||
|
|
350443ec99 | ||
|
|
928fc1b450 | ||
|
|
03e1bf53d0 | ||
|
|
9330025d07 | ||
|
|
bf56a269e0 | ||
|
|
0393c1a850 | ||
|
|
69ece00ae2 | ||
|
|
97656536d4 | ||
|
|
94b78d85ef | ||
|
|
ba3a888d26 | ||
|
|
d62a6e98c8 | ||
|
|
eeed99b013 | ||
|
|
2b54326334 | ||
|
|
2bd3d33ff6 | ||
|
|
c54a9ef5c9 | ||
|
|
a7ae0159cc | ||
|
|
f981602221 | ||
|
|
fa5468be4a | ||
|
|
591182414f | ||
|
|
49790b4704 | ||
|
|
daa3b3e549 | ||
|
|
120dc8ecb4 | ||
|
|
06a2373776 | ||
|
|
32ae5cc182 | ||
|
|
6975beaf30 | ||
|
|
c141c6c44d | ||
|
|
68cd77a9b3 | ||
|
|
e20a78acef | ||
|
|
6dd4cfaa3c | ||
|
|
d756bf7786 | ||
|
|
65e7ff22f7 | ||
|
|
655084c5f6 | ||
|
|
09bc8a92a2 | ||
|
|
99fd0b199d | ||
|
|
740d906eb3 | ||
|
|
b137fecc12 | ||
|
|
14125f673a | ||
|
|
4911cc5410 | ||
|
|
e0d9ac2ed8 | ||
|
|
5b042e86ef | ||
|
|
8ccf5eb0bb | ||
|
|
c0fdc9b8f8 | ||
|
|
545544ebe2 | ||
|
|
644ea1566c | ||
|
|
f6d1f24b60 | ||
|
|
c9a4706c24 | ||
|
|
7e275a9075 | ||
|
|
22a9eb03b6 | ||
|
|
46ee01e073 | ||
|
|
16a011e81e | ||
|
|
90f28d9f27 | ||
|
|
2e4ff78124 | ||
|
|
9e66207bf7 | ||
|
|
226ad8cc50 | ||
|
|
00731d67d2 | ||
|
|
e354d17c70 | ||
|
|
264b5afad6 | ||
|
|
76c4f1b67f | ||
|
|
96a395615b | ||
|
|
82d8bd0342 | ||
|
|
39e8d1f704 | ||
|
|
6bf503331e | ||
|
|
920d3b4af4 | ||
|
|
f601a0451c | ||
|
|
841db02b30 | ||
|
|
1449b5a524 | ||
|
|
fbb60c7cbc | ||
|
|
f0144e3bc2 | ||
|
|
ce924bb294 | ||
|
|
797174446c | ||
|
|
9b9dc43a7f | ||
|
|
53ee0f618c | ||
|
|
c35a5590b5 | ||
|
|
d23fe09f86 | ||
|
|
e761a90826 | ||
|
|
b5abe4538f | ||
|
|
7202b19d45 | ||
|
|
88f9c8910b | ||
|
|
c9948162b9 | ||
|
|
4e8e0d1b41 | ||
|
|
95b7a773d7 | ||
|
|
07c924d6a0 | ||
|
|
50efebdd63 | ||
|
|
50b8f3cd71 | ||
|
|
093dc4f33d | ||
|
|
963ef88ae5 | ||
|
|
03e63e11ac | ||
|
|
9f478227a9 | ||
|
|
1d112b3f32 | ||
|
|
1a5bd394b6 | ||
|
|
3901ed54c6 | ||
|
|
08728bf55e | ||
|
|
3220d00b14 | ||
|
|
eb12da0308 | ||
|
|
77545ec87d | ||
|
|
3f6f03af33 | ||
|
|
47f9b39630 | ||
|
|
9171e0789f | ||
|
|
5eabaf8ac2 | ||
|
|
b8ad1e7feb | ||
|
|
4d7ea1cdb4 | ||
|
|
021f860ca6 | ||
|
|
d0774fd7ed | ||
|
|
5632ac6f0b | ||
|
|
7a115e5dc3 | ||
|
|
d95cda3dfc | ||
|
|
dd9f54acae | ||
|
|
6bc2bcb89e | ||
|
|
9c2e751e11 | ||
|
|
2a52a19552 | ||
|
|
1b99d63c47 | ||
|
|
33683bc8ce | ||
|
|
61b4a3b76e | ||
|
|
386db6278b | ||
|
|
47664b98ad | ||
|
|
898b57943b | ||
|
|
d37cc4f796 | ||
|
|
b3fed60d12 | ||
|
|
a3d1db3fa2 | ||
|
|
0a79fb3687 | ||
|
|
900d9ac3c6 | ||
|
|
50b523f9f4 | ||
|
|
0759651ee4 | ||
|
|
daedee3f66 | ||
|
|
61c58e227e | ||
|
|
3cd9fc407e | ||
|
|
dd6e2be75d | ||
|
|
38e015efe4 | ||
|
|
335bfc8460 | ||
|
|
344bf8604f | ||
|
|
562a1b3ae8 | ||
|
|
65c541ff2a | ||
|
|
14ddba022f | ||
|
|
10df5abae2 | ||
|
|
443789698b | ||
|
|
52df9bc73b | ||
|
|
8cb3637954 | ||
|
|
ee55d1fd31 | ||
|
|
dcc289153f | ||
|
|
940d8d94e5 | ||
|
|
c9f1086408 | ||
|
|
ff4f35e5f5 | ||
|
|
b865fed7e9 | ||
|
|
274c5e309b | ||
|
|
e7d53a7d00 | ||
|
|
99117e067e | ||
|
|
88e27ee8fe | ||
|
|
d3fe546264 | ||
|
|
982b3ec8d0 | ||
|
|
b8cf2bea77 | ||
|
|
a511cd4ac3 | ||
|
|
0bab9a77a2 | ||
|
|
1f9b616de7 | ||
|
|
71593e3ea7 | ||
|
|
db7ee624e3 | ||
|
|
0e7527fb3f | ||
|
|
50fcba63c4 | ||
|
|
8046f82214 | ||
|
|
c33045b4b8 | ||
|
|
bb2a60e776 | ||
|
|
04e2dc86e8 | ||
|
|
ad44f020a5 | ||
|
|
ad339aa959 | ||
|
|
3ea8188805 | ||
|
|
18aacef89f | ||
|
|
b8b29371ca | ||
|
|
bd99a0c2b2 | ||
|
|
c493ba45a8 | ||
|
|
e9d5093a52 | ||
|
|
7aa8a892f8 | ||
|
|
99a3b64833 | ||
|
|
d94c331569 | ||
|
|
e2e25e4ac1 | ||
|
|
2b8be4d1e9 | ||
|
|
db38a53172 | ||
|
|
e273192f19 |
21
.github/workflows/aur-pub.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: aur-pub
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
jobs:
|
||||
aur-publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Publish AUR package
|
||||
uses: KSXGitHub/github-actions-deploy-aur@v2.2.3
|
||||
with:
|
||||
pkgname: termscp
|
||||
pkgbuild: ./dist/pkgs/arch/PKGBUILD
|
||||
commit_username: ${{ secrets.AUR_USERNAME }}
|
||||
commit_email: ${{ secrets.AUR_EMAIL }}
|
||||
ssh_private_key: ${{ secrets.AUR_KEY }}
|
||||
commit_message: Update AUR package
|
||||
ssh_keyscan_types: rsa,dsa,ecdsa,ed25519
|
||||
30
.github/workflows/linux.yml
vendored
@@ -7,14 +7,28 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features --no-fail-fast
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
|
||||
RUSTDOCFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
- name: Coverage with grcov
|
||||
uses: actions-rs/grcov@v0.1
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
15
.github/workflows/macos.yml
vendored
@@ -7,14 +7,13 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose -- --test-threads 1
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
15
.github/workflows/windows.yml
vendored
@@ -7,14 +7,13 @@ env:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: windows-2019
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
- uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose -- --test-threads 1
|
||||
- name: Clippy
|
||||
run: cargo clippy
|
||||
|
||||
8
.gitignore
vendored
@@ -13,3 +13,11 @@
|
||||
**/*.rs.bk
|
||||
|
||||
# End of https://www.gitignore.io/api/rust
|
||||
|
||||
# Distributions
|
||||
*.rpm
|
||||
*.deb
|
||||
dist/pkgs/arch/*.tar.gz
|
||||
|
||||
# Macos
|
||||
.DS_Store
|
||||
|
||||
154
CHANGELOG.md
@@ -1,12 +1,166 @@
|
||||
# Changelog
|
||||
|
||||
- [Changelog](#changelog)
|
||||
- [0.3.3](#033)
|
||||
- [0.3.2](#032)
|
||||
- [0.3.1](#031)
|
||||
- [0.3.0](#030)
|
||||
- [0.2.0](#020)
|
||||
- [0.1.3](#013)
|
||||
- [0.1.2](#012)
|
||||
- [0.1.1](#011)
|
||||
- [0.1.0](#010)
|
||||
|
||||
---
|
||||
|
||||
## 0.3.3
|
||||
|
||||
Released on 28/02/2021
|
||||
|
||||
- **Format key attributes**:
|
||||
- Added `EXTRA` and `LENGTH` parameters to format keys.
|
||||
- Now keys are provided with this syntax `{KEY_NAME[:LEN[:EXTRA]}`
|
||||
- **Check for updates**:
|
||||
- TermSCP will now check for updates on startup and will show in the main page if there is a new version available
|
||||
- This feature may be disabled from setup (Check for updates => No)
|
||||
- Enhancements:
|
||||
- Default choice for deleting file set to "NO" (way too easy to delete files by mistake)
|
||||
- Added CLI options to set starting workind directory on both local and remote hosts
|
||||
- Parse remote host now uses a Regex to gather parts (increased stability).
|
||||
- Now bookmarks and recents are sorted in the UI (bookmarks are sorted by name; recents are sorted by connection datetime)
|
||||
- Improved stability
|
||||
|
||||
## 0.3.2
|
||||
|
||||
Released on 24/01/2021
|
||||
|
||||
- **Explorer Formatter**:
|
||||
- Added possibility to customize the format when listing files in the explorers (Read more on README)
|
||||
- Added `file_fmt` key to configuration (if missing, default will be used).
|
||||
- Added the text input to the Settings view to set the value for `file_fmt`.
|
||||
- Bugfix:
|
||||
- Solved file index in explorer files at start of termscp, in case the first entry is an hidden file
|
||||
- SCP File transfer: when listing directory entries, check if a symlink points to a directory or to a file
|
||||
- Dependencies:
|
||||
- updated `crossterm` to `0.19.0`
|
||||
- updated `rand` to `0.8.2`
|
||||
- updated `rpassword` to `5.0.1`
|
||||
- updated `serde` to `1.0.121`
|
||||
- updated `tui` to `0.14.0`
|
||||
- updated `whoami` to `1.1.0`
|
||||
|
||||
## 0.3.1
|
||||
|
||||
Released on 18/01/2021
|
||||
|
||||
- **Keyring to store secrets**
|
||||
- On both MacOS and Windows, the secret used to encrypt passwords in bookmarks it is now store in the OS secret vault. This provides much more security to store the password
|
||||
- Enhancements:
|
||||
- Added connection timeout to 30 seconds to SFTP/SCP clients and improved name lookup system.
|
||||
- Bugfix:
|
||||
- Solved index in explorer files list which was no more kept after 0.3.0
|
||||
- SCP file transfer: fixed possible wrong file size when sending file, due to a possible incoherent size between the file explorer and the actual file size.
|
||||
- Breaking changes: on **MacOS / Windows systems only**, the password you saved for bookmarks won't be working anymore if you have support for the keyring crate. Because of the migration to keyring, the previously used secret hasn't been migrated to the storage, instead a new secret will be used. To solve this, just save the bookmark again with the password.
|
||||
|
||||
## 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
|
||||
|
||||
> The Bookmarks Update
|
||||
|
||||
- **Bookmarks**
|
||||
- Bookmarks and recent connections are now displayed in the home page
|
||||
- Bookmarks are saved at
|
||||
- Linux: `/home/alice/.config/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
|
||||
- Keybindings:
|
||||
- `C`: Copy file/directory
|
||||
- `O`: Open text file in editor
|
||||
- Enhancements:
|
||||
- User interface
|
||||
- Collpased borders to make everything more *aesthetic*
|
||||
- Rounded input field boards
|
||||
- File explorer:
|
||||
- Log how long it took to upload/download a file and the transfer speed
|
||||
- Display in progress bar the transfer speed (bytes/seconds)
|
||||
- Bugfix:
|
||||
- File mode of file on remote is now reported on local file after being downloaded (unix, linux, macos only)
|
||||
- Scp: when username was not provided, it didn't fallback to current username
|
||||
- Explorer: fixed UID format in Windows
|
||||
|
||||
## 0.1.3
|
||||
|
||||
Released on 14/12/2020
|
||||
|
||||
- Enhancements:
|
||||
- File transfer:
|
||||
- Read buffer is now 65536 bytes long
|
||||
- File explorer:
|
||||
- Fixed color mismatch in local explorer
|
||||
- Explorer tabs have now 70% of layout height, while logging area is 30%
|
||||
- Highlight selected entry in tabs, only when the tab is active
|
||||
- Auth page:
|
||||
- align popup text to center
|
||||
- Keybindings:
|
||||
- `L`: Refresh directory content
|
||||
- Bugfix:
|
||||
- Fixed memory vulnerability in Windows version
|
||||
|
||||
## 0.1.2
|
||||
|
||||
Released on 13/12/2020
|
||||
|
||||
@@ -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)
|
||||
@@ -9,6 +9,7 @@ Please note we have a [code of conduct](./CODE_OF_CONDUCT.md), please follow it
|
||||
- [Developer contributions guide](#developer-contributions-guide)
|
||||
- [How TermSCP works](#how-termscp-works)
|
||||
- [Activities](#activities)
|
||||
- [Tests fails due to receivers](#tests-fails-due-to-receivers)
|
||||
- [Implementing File Transfers](#implementing-file-transfers)
|
||||
|
||||
---
|
||||
@@ -17,9 +18,8 @@ Please note we have a [code of conduct](./CODE_OF_CONDUCT.md), please follow it
|
||||
|
||||
At the moment, these kind of contributions are more appreciated and should be preferred:
|
||||
|
||||
- Fix for issues described in [Known Issues](./README.md#known-issues) or [issues reported by the community](https://github.com/ChristianVisintin/TermSCP/issues)
|
||||
- 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 +30,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
|
||||
@@ -66,6 +66,12 @@ This trait provides only 3 methods:
|
||||
|
||||
---
|
||||
|
||||
### Tests fails due to receivers
|
||||
|
||||
Yes. This happens quite often and is related to the fact that I'm using public SSH/SFTP/FTP server to test file receivers and sometimes this server go down for even a day or more. If your tests don't pass due to this, don't worry, submit the pull request and I'll take care of testing them by myself.
|
||||
|
||||
---
|
||||
|
||||
### Implementing File Transfers
|
||||
|
||||
This chapter describes how to implement a file transfer in TermSCP. A file transfer is a module which implements the `FileTransfer` trait. The file transfer provides different modules to interact with a remote server, which in addition to the most obvious methods, used to download and upload files, provides also methods to list files, delete files, create directories etc.
|
||||
|
||||
1035
Cargo.lock
generated
48
Cargo.toml
@@ -1,43 +1,51 @@
|
||||
[package]
|
||||
name = "termscp"
|
||||
version = "0.1.2"
|
||||
version = "0.3.3"
|
||||
authors = ["Christian Visintin"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0"
|
||||
keywords = ["scp-client", "sftp-client", "ftp-client", "winscp", "command-line-utility"]
|
||||
categories = ["command-line-utilities"]
|
||||
description = "TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal."
|
||||
homepage = "https://github.com/ChristianVisintin/TermSCP"
|
||||
repository = "https://github.com/ChristianVisintin/TermSCP"
|
||||
homepage = "https://github.com/veeso/termscp"
|
||||
repository = "https://github.com/veeso/termscp"
|
||||
documentation = "https://docs.rs/termscp"
|
||||
readme = "README.md"
|
||||
include = ["src/**/*", "LICENSE", "README.md", "CHANGELOG.md"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
crossterm = "0.18.2"
|
||||
ftp4 = { version = "^4.0.1", features = ["secure"] }
|
||||
getopts = "0.2.21"
|
||||
ssh2 = "0.9.0"
|
||||
tui = { version = "0.13.0", features = ["crossterm"], default-features = false }
|
||||
whoami = "1.0.0"
|
||||
rpassword = "5.0.0"
|
||||
unicode-width = "0.1.7"
|
||||
chrono = "0.4.19"
|
||||
bitflags = "1.2.1"
|
||||
bytesize = "1.0.1"
|
||||
textwrap = "0.13.0"
|
||||
regex = "1.4.2"
|
||||
lazy_static = "1.4.0"
|
||||
chrono = "0.4.19"
|
||||
content_inspector = "0.2.4"
|
||||
crossterm = "0.19.0"
|
||||
dirs = "3.0.1"
|
||||
edit = "0.1.2"
|
||||
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.8.2"
|
||||
regex = "1.4.2"
|
||||
rpassword = "5.0.1"
|
||||
serde = { version = "1.0.121", features = ["derive"] }
|
||||
ssh2 = "0.9.0"
|
||||
tempfile = "3.1.0"
|
||||
textwrap = "0.13.1"
|
||||
toml = "0.5.8"
|
||||
tui = { version = "0.14.0", features = ["crossterm"], default-features = false }
|
||||
unicode-width = "0.1.7"
|
||||
ureq = { version = "2.0.2", features = ["json"] }
|
||||
whoami = "1.1.0"
|
||||
|
||||
[target.'cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))'.dependencies]
|
||||
users = "0.11.0"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
#[patch.crates-io]
|
||||
#ftp = { git = "https://github.com/ChristianVisintin/rust-ftp" }
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
|
||||
keyring = "0.10.1"
|
||||
|
||||
[[bin]]
|
||||
name = "termscp"
|
||||
|
||||
4
LICENSE
@@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
TermSCP
|
||||
Copyright (C) 2020 Christian Visintin
|
||||
Copyright (C) 2020-2021 Christian Visintin
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
TermSCP Copyright (C) 2020 Christian Visintin
|
||||
TermSCP Copyright (C) 2020-2021 Christian Visintin
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
272
README.md
@@ -1,12 +1,12 @@
|
||||
# TermSCP
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://github.com/ChristianVisintin/TermSCP) [](https://github.com/ChristianVisintin/TermSCP/issues) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0) [](https://github.com/veeso/termscp) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
|
||||
|
||||
[](https://github.com/ChristianVisintin/TermSCP/actions) [](https://github.com/ChristianVisintin/TermSCP/actions) [](https://github.com/ChristianVisintin/TermSCP/actions)
|
||||
[](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://codecov.io/gh/veeso/termscp)
|
||||
|
||||
~ Basically, WinSCP on a terminal ~
|
||||
Developed by Christian Visintin
|
||||
Current version: 0.1.2 (13/12/2020)
|
||||
Current version: 0.3.3 (28/02/2021)
|
||||
|
||||
---
|
||||
|
||||
@@ -18,11 +18,19 @@ Current version: 0.1.2 (13/12/2020)
|
||||
- [Cargo 🦀](#cargo-)
|
||||
- [Deb package 📦](#deb-package-)
|
||||
- [RPM package 📦](#rpm-package-)
|
||||
- [AUR Package 🔼](#aur-package-)
|
||||
- [Chocolatey 🍫](#chocolatey-)
|
||||
- [Brew 🍻](#brew-)
|
||||
- [Usage ❓](#usage-)
|
||||
- [Address argument](#address-argument)
|
||||
- [How Password can be provided](#how-password-can-be-provided)
|
||||
- [Address argument 🌎](#address-argument-)
|
||||
- [How Password can be provided 🔐](#how-password-can-be-provided-)
|
||||
- [Bookmarks ⭐](#bookmarks-)
|
||||
- [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-)
|
||||
- [File Explorer Format](#file-explorer-format)
|
||||
- [Keybindings ⌨](#keybindings-)
|
||||
- [Documentation 📚](#documentation-)
|
||||
- [Known issues 🧻](#known-issues-)
|
||||
@@ -45,18 +53,27 @@ 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 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 🎁
|
||||
|
||||
- Different communication protocols
|
||||
- Different communication protocols support
|
||||
- SFTP
|
||||
- SCP
|
||||
- FTP and FTPS
|
||||
- Practical user interface to explore and operate on the remote and on the local machine file system
|
||||
- Compatible with Windows, Linux, BSD and MacOS
|
||||
- Handy 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
|
||||
- Customizations:
|
||||
- Custom file explorer format
|
||||
- Customizable text editor
|
||||
- Customizable file sorting
|
||||
- SSH key storage
|
||||
- Written in Rust
|
||||
- Easy to extend with new file transfers protocols
|
||||
- Developed keeping an eye on performance
|
||||
|
||||
---
|
||||
|
||||
@@ -72,10 +89,17 @@ If you want to contribute to this project, don't forget to check out our contrib
|
||||
cargo install termscp
|
||||
```
|
||||
|
||||
Requirements:
|
||||
|
||||
- Linux
|
||||
- pkg-config
|
||||
- libssh2
|
||||
- openssl
|
||||
|
||||
### Deb package 📦
|
||||
|
||||
Get `deb` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.2_amd64.deb)
|
||||
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.2_amd64.deb`
|
||||
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.3_amd64.deb)
|
||||
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.3_amd64.deb`
|
||||
|
||||
then install through dpkg:
|
||||
|
||||
@@ -87,8 +111,8 @@ gdebi termscp_*.deb
|
||||
|
||||
### RPM package 📦
|
||||
|
||||
Get `rpm` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.2-1.x86_64.rpm)
|
||||
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.2-1.x86_64.rpm`
|
||||
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.3-1.x86_64.rpm)
|
||||
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.3-1.x86_64.rpm`
|
||||
|
||||
then install through rpm:
|
||||
|
||||
@@ -96,6 +120,14 @@ then install through rpm:
|
||||
rpm -U termscp_*.rpm
|
||||
```
|
||||
|
||||
### AUR Package 🔼
|
||||
|
||||
On Arch Linux based distribution, you can install termscp using for istance [yay](https://github.com/Jguer/yay), which I recommend to install AUR packages.
|
||||
|
||||
```sh
|
||||
yay -S termscp
|
||||
```
|
||||
|
||||
### Chocolatey 🍫
|
||||
|
||||
You can install TermSCP on Windows using [chocolatey](https://chocolatey.org/)
|
||||
@@ -106,7 +138,7 @@ Start PowerShell as administrator and run
|
||||
choco install termscp
|
||||
```
|
||||
|
||||
Alternatively you can download the ZIP file from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp.0.1.2.nupkg)
|
||||
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.3.3.nupkg)
|
||||
|
||||
and then with PowerShell started with administrator previleges, run:
|
||||
|
||||
@@ -121,7 +153,7 @@ You can install TermSCP on MacOS using [brew](https://brew.sh/)
|
||||
From your terminal run
|
||||
|
||||
```sh
|
||||
brew tap ChristianVisintin/termscp
|
||||
brew tap veeso/termscp
|
||||
brew install termscp
|
||||
```
|
||||
|
||||
@@ -131,6 +163,8 @@ brew install termscp
|
||||
|
||||
TermSCP can be started with the following options:
|
||||
|
||||
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
|
||||
|
||||
- `-P, --password <password>` if address is provided, password will be this argument
|
||||
- `-v, --version` Print version info
|
||||
- `-h, --help` Print help page
|
||||
@@ -139,23 +173,25 @@ TermSCP can be started in two different mode, if no extra arguments is provided,
|
||||
|
||||
Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server.
|
||||
|
||||
### Address argument
|
||||
If address argument is provided you can also provide the start working directory for local host
|
||||
|
||||
### Address argument 🌎
|
||||
|
||||
The address argument has the following syntax:
|
||||
|
||||
```txt
|
||||
[protocol]://[username@]<address>[:port]
|
||||
[protocol://][username@]<address>[:port][:wrkdir]
|
||||
```
|
||||
|
||||
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 if not provided is default for the selected protocol (in this case depends on your configuration); 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; username is `root`
|
||||
|
||||
```sh
|
||||
termscp root@192.168.1.31
|
||||
@@ -167,7 +203,13 @@ Let's see some example of this particular syntax, since it's very comfortable an
|
||||
termscp scp://omar@192.168.1.31:4022
|
||||
```
|
||||
|
||||
#### How Password can be provided
|
||||
- Connect using scp to 192.168.1.31, port is 4022; username is `omar`. You will start in directory `/tmp`
|
||||
|
||||
```sh
|
||||
termscp scp://omar@192.168.1.31:4022:/tmp
|
||||
```
|
||||
|
||||
#### How Password can be provided 🔐
|
||||
|
||||
You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
|
||||
Password can be basically provided through 3 ways when address argument is provided:
|
||||
@@ -178,31 +220,147 @@ Password can be basically provided through 3 ways when address argument is provi
|
||||
|
||||
---
|
||||
|
||||
## Bookmarks ⭐
|
||||
|
||||
In TermSCP it is possible to save favourites hosts, which can be then loaded quickly from the main layout of termscp.
|
||||
TermSCP will also save the last 16 hosts you connected to.
|
||||
This feature allows you to load all the parameters required to connect to a certain remote, simply selecting the bookmark in the tab under the authentication form.
|
||||
|
||||
Bookmarks will be saved, if possible at:
|
||||
|
||||
- `$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.
|
||||
|
||||
> I was very undecided about storing passwords in termscp. The reason? Saving a password on your computer might give access to a hacker to any server you've registered. But I must admit by myself that for many machines typing the password everytime is really boring, also many times I have to work with machines in LAN, which wouldn't provide any advantage to an attacker, So I came out with a good compromise for passwords.
|
||||
|
||||
I warmly suggest you to follow these guidelines in order to decide whether you should or you shouldn't save passwords:
|
||||
|
||||
- **DON'T** save passwords for machines which are exposed on the internet, save passwords only for machines in LAN
|
||||
- Make sure your machine is protected by attackers. If possible encrypt your disk and don't leave your PC unlocked while you're away.
|
||||
- Preferably, save passwords only when a compromising of the target machine wouldn't be a problem.
|
||||
|
||||
To create a bookmark, just fulfill the authentication form and then input `<CTRL+S>`; you'll then be asked to give a name to your bookmark, and tadah, the bookmark has been created.
|
||||
If you go to [gallery](#gallery-), there is a GIF showing how bookmarks work 💪.
|
||||
|
||||
### Are my passwords Safe 😈
|
||||
|
||||
Well, kinda.
|
||||
As said before, bookmarks are saved in your configuration directory along with passwords. Passwords are obviously not plain text, they are encrypted with **AES-128**. Does this make them safe? Well, depends on your operating system:
|
||||
|
||||
On Windows and MacOS the passwords are stored, if possible (but should be), in respectively the Windows Vault and the Keychain. This is actually super-safe and is directly managed by your operating system.
|
||||
|
||||
On Linux and BSD, on the other hand, the key used to encrypt your passwords is stored on your drive (at $HOME/.config/termscp). It is then, still possible to retrieve the key to decrypt passwords. Luckily, the location of the key guarantees your key can't be read by users different from yours, but yeah, I still wouldn't save the password for a server exposed on the internet 😉.
|
||||
Actually [keyring-rs](https://github.com/hwchen/keyring-rs), supports Linux, but for different reasons I preferred not to make it available for this configuration. If you want to read more about my decision read [this issue](https://github.com/veeso/termscp/issues/2), while if you think this might have been implemented differently feel free to open an issue with your proposal.
|
||||
|
||||
---
|
||||
|
||||
## 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 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 to use, change it in termscp configuration. [View more](#configuration-️)
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
- **Check for updates**: if set to `yes`, termscp will fetch the Github API to check if there is a new version of termscp available.
|
||||
- **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.
|
||||
|
||||
### File Explorer Format
|
||||
|
||||
It is possible through configuration to define a custom format for the file explorer. This field, with name `File formatter syntax` will define how the file entries will be displayed in the file explorer.
|
||||
The syntax for the formatter is the following `{KEY1}... {KEY2:LENGTH}... {KEY3:LENGTH:EXTRA} {KEYn}...`.
|
||||
Each key in bracket will be replaced with the related attribute, while everything outside brackets will be left unchanged.
|
||||
|
||||
- The key name is mandatory and must be one of the keys below
|
||||
- The length describes the length reserved to display the field. Static attributes doesn't support this (GROUP, PEX, SIZE, USER)
|
||||
- Extra is supported only by some parameters and is an additional options. See keys to check if extra is supported.
|
||||
|
||||
These are the keys supported by the formatter:
|
||||
|
||||
- `ATIME`: Last access time (with default syntax `%b %d %Y %H:%M`); Extra might be provided as the time syntax (e.g. `{ATIME:8:%H:%M}`)
|
||||
- `CTIME`: Creation time (with syntax `%b %d %Y %H:%M`); Extra might be provided as the time syntax (e.g. `{CTIME:8:%H:%M}`)
|
||||
- `GROUP`: Owner group
|
||||
- `MTIME`: Last change time (with syntax `%b %d %Y %H:%M`); Extra might be provided as the time syntax (e.g. `{MTIME:8:%H:%M}`)
|
||||
- `NAME`: File name (Elided if longer than 24)
|
||||
- `PEX`: File permissions (UNIX format)
|
||||
- `SIZE`: File size (omitted for directories)
|
||||
- `SYMLINK`: Symlink (if any `-> {FILE_PATH}`)
|
||||
- `USER`: Owner user
|
||||
|
||||
If left empty, the default formatter syntax will be used: `{NAME:24} {PEX} {USER} {SIZE} {MTIME:17:%b %d %Y %H:%M}`
|
||||
|
||||
---
|
||||
|
||||
## Keybindings ⌨
|
||||
|
||||
| Key | Command |
|
||||
|---------------|-------------------------------------------------------|
|
||||
| `<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 |
|
||||
| `<D>` | Make directory |
|
||||
| `<E>` | Delete file (Same as `CANC`) |
|
||||
| `<G>` | Go to supplied path |
|
||||
| `<H>` | Show help |
|
||||
| `<I>` | Show info about selected file or directory |
|
||||
| `<Q>` | Quit TermSCP |
|
||||
| `<R>` | Rename file |
|
||||
| `<U>` | Go to parent directory |
|
||||
| `<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 | |
|
||||
|
||||
---
|
||||
|
||||
@@ -214,18 +372,17 @@ 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.
|
||||
- Sftp:
|
||||
- sftp is much slower than scp: Okay this is an annoying issue, and again: not my fault. It seems there is an issue with [ssh2-rs](https://github.com/alexcrichton/ssh2-rs) library. If you want to stay up to date with the status of this issue, subscribe to [this issue](https://github.com/alexcrichton/ssh2-rs/issues/206)
|
||||
- `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 🧪
|
||||
|
||||
- **Text viewer**: possibility to open and read file both on remote and on local host; this will also support syntax highlighting.
|
||||
- **New commands in file explorer** (0.4.0 - March 2021)
|
||||
- **Find**: search for files through directories, with built-in regex support
|
||||
- **Execute**: run a command on both local host and remote host in protocols where this is supported
|
||||
- SCP for sure
|
||||
- SFTP: might be a challenge, since I should start a SSH session, but I guess it's not impossible
|
||||
|
||||
---
|
||||
|
||||
@@ -247,7 +404,10 @@ TermSCP is powered by these aweseome projects:
|
||||
|
||||
- [bytesize](https://github.com/hyunsik/bytesize)
|
||||
- [crossterm](https://github.com/crossterm-rs/crossterm)
|
||||
- [edit](https://github.com/milkey-mouse/edit)
|
||||
- [keyring-rs](https://github.com/hwchen/keyring-rs)
|
||||
- [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)
|
||||
@@ -257,8 +417,22 @@ TermSCP is powered by these aweseome projects:
|
||||
|
||||
## Gallery 🎬
|
||||
|
||||
> Termscp Home
|
||||
|
||||

|
||||
|
||||
> Bookmarks
|
||||
|
||||

|
||||
|
||||
> Setup
|
||||
|
||||

|
||||
|
||||
> Text editor
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## License 📃
|
||||
|
||||
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 314 KiB |
BIN
assets/images/bookmarks.gif
Normal file
|
After Width: | Height: | Size: 292 KiB |
BIN
assets/images/config.gif
Normal file
|
After Width: | Height: | Size: 689 KiB |
BIN
assets/images/explorer.gif
Executable file → Normal file
|
Before Width: | Height: | Size: 632 KiB After Width: | Height: | Size: 635 KiB |
BIN
assets/images/text-editor.gif
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
7
codecov.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
ignore:
|
||||
- src/main.rs
|
||||
- src/lib.rs
|
||||
- src/activity_manager.rs
|
||||
- src/ui/
|
||||
fixes:
|
||||
- "/::"
|
||||
25
dist/build/README.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# Build with Docker
|
||||
|
||||
- [Build with Docker](#build-with-docker)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Build](#build)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker
|
||||
|
||||
## Build
|
||||
|
||||
1. Build x86_64
|
||||
|
||||
this will build termscp for:
|
||||
|
||||
- Linux x86_64 Deb packages
|
||||
- Linux x86_64 RPM packages
|
||||
- Windows x86_64 MSVC packages
|
||||
|
||||
```sh
|
||||
|
||||
```
|
||||
47
dist/build/deploy.sh
vendored
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: deploy.sh <version>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VERSION=$1
|
||||
|
||||
set -e # Don't fail
|
||||
|
||||
# Create pkgs directory
|
||||
cd ..
|
||||
PKGS_DIR=$(pwd)/pkgs
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/
|
||||
# Build x86_64_deb
|
||||
cd x86_64_debian9/
|
||||
docker build --tag termscp-${VERSION}-x86_64_debian9 .
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/deb/
|
||||
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_debian9 termscp-${VERSION}-x86_64_debian9)
|
||||
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}_amd64.deb ${PKGS_DIR}/deb/
|
||||
# Build x86_64_centos7
|
||||
cd x86_64_centos7/
|
||||
docker build --tag termscp-${VERSION}-x86_64_centos7 .
|
||||
cd -
|
||||
mkdir -p ${PKGS_DIR}/rpm/
|
||||
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_centos7 termscp-${VERSION}-x86_64_centos7)
|
||||
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/release/rpmbuild/RPMS/x86_64/termscp-${VERSION}-1.el7.x86_64.rpm ${PKGS_DIR}/rpm/termscp-${VERSION}-1.x86_64.rpm
|
||||
# Build x86_64_archlinux
|
||||
|
||||
##################### TEMP REMOVED ###################################
|
||||
# cd x86_64_archlinux/
|
||||
# docker build --tag termscp-${VERSION}-x86_64_archlinux .
|
||||
# # Create container and get AUR pkg
|
||||
# cd -
|
||||
# mkdir -p ${PKGS_DIR}/arch/
|
||||
# CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_archlinux termscp-${VERSION}-x86_64_archlinux)
|
||||
# docker cp ${CONTAINER_NAME}:/usr/src/termscp/termscp-${VERSION}-x86_64.tar.gz ${PKGS_DIR}/arch/
|
||||
# docker cp ${CONTAINER_NAME}:/usr/src/termscp/PKGBUILD ${PKGS_DIR}/arch/
|
||||
# docker cp ${CONTAINER_NAME}:/usr/src/termscp/.SRCINFO ${PKGS_DIR}/arch/
|
||||
# # Replace termscp-bin with termscp in PKGBUILD
|
||||
# sed -i 's/termscp-bin/termscp/g' ${PKGS_DIR}/arch/PKGBUILD
|
||||
##################### TEMP REMOVED ###################################
|
||||
|
||||
exit $?
|
||||
19
dist/build/x86_64/Dockerfile
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM rust:1.48.0 AS builder
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Add toolchains
|
||||
RUN rustup target add x86_64-unknown-linux-gnu
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y rpm
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/veeso/termscp.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/termscp/
|
||||
# Install cargo RPM/Deb
|
||||
RUN cargo install cargo-deb cargo-rpm
|
||||
# Build for x86_64
|
||||
RUN cargo build --release --target x86_64-unknown-linux-gnu
|
||||
# Build pkgs
|
||||
RUN cargo deb && cargo rpm init && cargo rpm build
|
||||
|
||||
CMD ["sh"]
|
||||
33
dist/build/x86_64_archlinux/Dockerfile
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
FROM archlinux:base-20210120.0.13969 as builder
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Install dependencies
|
||||
RUN pacman -Syu --noconfirm \
|
||||
git \
|
||||
gcc \
|
||||
openssl \
|
||||
pkg-config \
|
||||
sudo
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
chmod +x /tmp/rust.sh && \
|
||||
/tmp/rust.sh -y
|
||||
# Create build user
|
||||
RUN useradd build -m && \
|
||||
passwd -d build && \
|
||||
mkdir -p termscp && \
|
||||
chown -R build.build termscp/
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/veeso/termscp.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/termscp/
|
||||
# Install cargo arxch
|
||||
RUN source $HOME/.cargo/env && cargo install cargo-aur
|
||||
# Build for x86_64
|
||||
RUN source $HOME/.cargo/env && cargo build --release
|
||||
# Build pkgs
|
||||
RUN source $HOME/.cargo/env && cargo aur
|
||||
# Create SRCINFO
|
||||
RUN chown -R build.build ../termscp/ && sudo -u build bash -c 'makepkg --printsrcinfo > .SRCINFO'
|
||||
|
||||
CMD ["sh"]
|
||||
25
dist/build/x86_64_centos7/Dockerfile
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
FROM centos:centos7 as builder
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Install dependencies
|
||||
RUN yum -y install \
|
||||
git \
|
||||
gcc \
|
||||
openssl \
|
||||
pkgconfig \
|
||||
openssl-devel
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
chmod +x /tmp/rust.sh && \
|
||||
/tmp/rust.sh -y
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/veeso/termscp.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/termscp/
|
||||
# Install cargo arxch
|
||||
RUN source $HOME/.cargo/env && cargo install cargo-rpm
|
||||
# Build for x86_64
|
||||
RUN source $HOME/.cargo/env && cargo build --release
|
||||
# Build pkgs
|
||||
RUN source $HOME/.cargo/env && yum -y install rpm-build && cargo rpm init && cargo rpm build
|
||||
CMD ["sh"]
|
||||
28
dist/build/x86_64_debian8/Dockerfile
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM debian:jessie
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y \
|
||||
git \
|
||||
gcc \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libssh2-1-dev \
|
||||
curl
|
||||
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
chmod +x /tmp/rust.sh && \
|
||||
/tmp/rust.sh -y
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/veeso/termscp.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/termscp/
|
||||
# Install cargo deb
|
||||
RUN . $HOME/.cargo/env && cargo install cargo-deb
|
||||
# Build for x86_64
|
||||
RUN . $HOME/.cargo/env && cargo build --release
|
||||
# Build pkgs
|
||||
RUN . $HOME/.cargo/env && cargo deb
|
||||
|
||||
CMD ["sh"]
|
||||
28
dist/build/x86_64_debian9/Dockerfile
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
FROM debian:stretch
|
||||
|
||||
WORKDIR /usr/src/
|
||||
# Install dependencies
|
||||
RUN apt update && apt install -y \
|
||||
git \
|
||||
gcc \
|
||||
pkg-config \
|
||||
libssl-dev \
|
||||
libssh2-1-dev \
|
||||
curl
|
||||
|
||||
# Install rust
|
||||
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
|
||||
chmod +x /tmp/rust.sh && \
|
||||
/tmp/rust.sh -y
|
||||
# Clone repository
|
||||
RUN git clone https://github.com/veeso/termscp.git
|
||||
# Set workdir to termscp
|
||||
WORKDIR /usr/src/termscp/
|
||||
# Install cargo deb
|
||||
RUN . $HOME/.cargo/env && cargo install cargo-deb
|
||||
# Build for x86_64
|
||||
RUN . $HOME/.cargo/env && cargo build --release
|
||||
# Build pkgs
|
||||
RUN . $HOME/.cargo/env && cargo deb
|
||||
|
||||
CMD ["sh"]
|
||||
14
dist/pkgs/arch/.SRCINFO
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
pkgbase = termscp
|
||||
pkgdesc = TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal.
|
||||
pkgver = 0.3.3
|
||||
pkgrel = 1
|
||||
url = https://github.com/veeso/termscp
|
||||
arch = x86_64
|
||||
license = GPL-3.0
|
||||
provides = termscp
|
||||
options = strip
|
||||
source = https://github.com/veeso/termscp/releases/download/v0.3.3/termscp-0.3.3-x86_64.tar.gz
|
||||
sha256sums = 7a8c70add8306a2cb3f2ee1d075a00fef143fc9aad4199797c7462bab1649296
|
||||
|
||||
pkgname = termscp
|
||||
|
||||
16
dist/pkgs/arch/PKGBUILD
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Maintainer: Christian Visintin
|
||||
pkgname=termscp
|
||||
pkgver=0.3.3
|
||||
pkgrel=1
|
||||
pkgdesc="TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal."
|
||||
url="https://github.com/veeso/termscp"
|
||||
license=("GPL-3.0")
|
||||
arch=("x86_64")
|
||||
provides=("termscp")
|
||||
options=("strip")
|
||||
source=("https://github.com/veeso/termscp/releases/download/v$pkgver/termscp-$pkgver-x86_64.tar.gz")
|
||||
sha256sums=("7a8c70add8306a2cb3f2ee1d075a00fef143fc9aad4199797c7462bab1649296")
|
||||
|
||||
package() {
|
||||
install -Dm755 termscp -t "$pkgdir/usr/bin/"
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -27,11 +27,10 @@ use std::path::PathBuf;
|
||||
|
||||
// Deps
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::host::Localhost;
|
||||
use crate::host::{HostError, 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,14 +60,11 @@ 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, HostError> {
|
||||
// Prepare Context
|
||||
let host: Localhost = match Localhost::new(local_dir.clone()) {
|
||||
Ok(h) => h,
|
||||
Err(_) => return Err(()),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
let ctx: Context = Context::new(host);
|
||||
Ok(ActivityManager {
|
||||
@@ -87,6 +84,7 @@ impl ActivityManager {
|
||||
protocol: FileTransferProtocol,
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
entry_directory: Option<PathBuf>,
|
||||
) {
|
||||
self.ftparams = Some(FileTransferParams {
|
||||
address,
|
||||
@@ -94,6 +92,7 @@ impl ActivityManager {
|
||||
protocol,
|
||||
username,
|
||||
password,
|
||||
entry_directory,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -109,6 +108,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 +126,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 +145,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);
|
||||
@@ -160,7 +165,8 @@ impl ActivityManager {
|
||||
0 => None,
|
||||
_ => Some(activity.password.clone()),
|
||||
},
|
||||
protocol: activity.protocol.clone(),
|
||||
protocol: activity.protocol,
|
||||
entry_directory: None, // Has use only when accessing with address
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -189,7 +195,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 +220,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)
|
||||
}
|
||||
}
|
||||
|
||||
197
src/bookmarks/mod.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
//! ## Bookmarks
|
||||
//!
|
||||
//! `bookmarks` is the module which provides data types and de/serializer for bookmarks
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
pub mod serializer;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
||||
/// ## UserHosts
|
||||
///
|
||||
/// UserHosts contains all the hosts saved by the user in the data storage
|
||||
/// It contains both `Bookmark`
|
||||
pub struct UserHosts {
|
||||
pub bookmarks: HashMap<String, Bookmark>,
|
||||
pub recents: HashMap<String, Bookmark>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, std::fmt::Debug, PartialEq)]
|
||||
/// ## Bookmark
|
||||
///
|
||||
/// Bookmark describes a single bookmark entry in the user hosts storage
|
||||
pub struct Bookmark {
|
||||
pub address: String,
|
||||
pub port: u16,
|
||||
pub protocol: String,
|
||||
pub username: String,
|
||||
pub password: Option<String>, // Password is optional; base64, aes-128 encrypted password
|
||||
}
|
||||
|
||||
// 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 Default for UserHosts {
|
||||
fn default() -> Self {
|
||||
UserHosts {
|
||||
bookmarks: HashMap::new(),
|
||||
recents: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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::*;
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_bookmark_new() {
|
||||
let bookmark: Bookmark = Bookmark {
|
||||
address: String::from("192.168.1.1"),
|
||||
port: 22,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("root"),
|
||||
password: Some(String::from("password")),
|
||||
};
|
||||
let recent: Bookmark = Bookmark {
|
||||
address: String::from("192.168.1.2"),
|
||||
port: 22,
|
||||
protocol: String::from("SCP"),
|
||||
username: String::from("admin"),
|
||||
password: Some(String::from("password")),
|
||||
};
|
||||
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1);
|
||||
bookmarks.insert(String::from("test"), bookmark);
|
||||
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
|
||||
recents.insert(String::from("ISO20201218T181432"), recent);
|
||||
let hosts: UserHosts = UserHosts {
|
||||
bookmarks: bookmarks,
|
||||
recents: recents,
|
||||
};
|
||||
// Verify
|
||||
let bookmark: &Bookmark = hosts.bookmarks.get(&String::from("test")).unwrap();
|
||||
assert_eq!(bookmark.address, String::from("192.168.1.1"));
|
||||
assert_eq!(bookmark.port, 22);
|
||||
assert_eq!(bookmark.protocol, String::from("SFTP"));
|
||||
assert_eq!(bookmark.username, String::from("root"));
|
||||
assert_eq!(
|
||||
*bookmark.password.as_ref().unwrap(),
|
||||
String::from("password")
|
||||
);
|
||||
let bookmark: &Bookmark = hosts
|
||||
.recents
|
||||
.get(&String::from("ISO20201218T181432"))
|
||||
.unwrap();
|
||||
assert_eq!(bookmark.address, String::from("192.168.1.2"));
|
||||
assert_eq!(bookmark.port, 22);
|
||||
assert_eq!(bookmark.protocol, String::from("SCP"));
|
||||
assert_eq!(bookmark.username, String::from("admin"));
|
||||
assert_eq!(
|
||||
*bookmark.password.as_ref().unwrap(),
|
||||
String::from("password")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_bookmark_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")
|
||||
);
|
||||
}
|
||||
}
|
||||
220
src/bookmarks/serializer.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
//! ## Serializer
|
||||
//!
|
||||
//! `serializer` is the module which provides the serializer/deserializer for bookmarks
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{SerializerError, SerializerErrorKind, UserHosts};
|
||||
|
||||
use std::io::{Read, Write};
|
||||
|
||||
pub struct BookmarkSerializer {}
|
||||
|
||||
impl BookmarkSerializer {
|
||||
/// ### serialize
|
||||
///
|
||||
/// Serialize `UserHosts` into TOML and write content to writable
|
||||
pub fn serialize(
|
||||
&self,
|
||||
mut writable: Box<dyn Write>,
|
||||
hosts: &UserHosts,
|
||||
) -> Result<(), SerializerError> {
|
||||
// Serialize content
|
||||
let data: String = match toml::ser::to_string(hosts) {
|
||||
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<UserHosts, 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::super::Bookmark;
|
||||
use super::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Seek, SeekFrom};
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_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: BookmarkSerializer = BookmarkSerializer {};
|
||||
let hosts = deserializer.deserialize(Box::new(toml_file));
|
||||
assert!(hosts.is_ok());
|
||||
let hosts: UserHosts = hosts.ok().unwrap();
|
||||
// Verify hosts
|
||||
// Verify recents
|
||||
assert_eq!(hosts.recents.len(), 1);
|
||||
let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap();
|
||||
assert_eq!(host.address, String::from("172.16.104.10"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SCP"));
|
||||
assert_eq!(host.username, String::from("root"));
|
||||
assert_eq!(host.password, None);
|
||||
// Verify bookmarks
|
||||
assert_eq!(hosts.bookmarks.len(), 3);
|
||||
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
|
||||
assert_eq!(host.address, String::from("192.168.1.31"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SFTP"));
|
||||
assert_eq!(host.username, String::from("root"));
|
||||
assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword"));
|
||||
let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap();
|
||||
assert_eq!(host.address, String::from("192.168.1.30"));
|
||||
assert_eq!(host.port, 22);
|
||||
assert_eq!(host.protocol, String::from("SFTP"));
|
||||
assert_eq!(host.username, String::from("cvisintin"));
|
||||
assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret"));
|
||||
let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap();
|
||||
assert_eq!(host.address, String::from("51.23.67.12"));
|
||||
assert_eq!(host.port, 21);
|
||||
assert_eq!(host.protocol, String::from("FTPS"));
|
||||
assert_eq!(host.username, String::from("aws001"));
|
||||
assert_eq!(host.password, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_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: BookmarkSerializer = BookmarkSerializer {};
|
||||
assert!(deserializer.deserialize(Box::new(toml_file)).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bookmarks_serializer_serialize() {
|
||||
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(2);
|
||||
// Push two samples
|
||||
bookmarks.insert(
|
||||
String::from("raspberrypi2"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.31"),
|
||||
port: 22,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("root"),
|
||||
password: None,
|
||||
},
|
||||
);
|
||||
bookmarks.insert(
|
||||
String::from("msi-estrem"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.30"),
|
||||
port: 4022,
|
||||
protocol: String::from("SFTP"),
|
||||
username: String::from("cvisintin"),
|
||||
password: Some(String::from("password")),
|
||||
},
|
||||
);
|
||||
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
|
||||
recents.insert(
|
||||
String::from("ISO20201215T094000Z"),
|
||||
Bookmark {
|
||||
address: String::from("192.168.1.254"),
|
||||
port: 3022,
|
||||
protocol: String::from("SCP"),
|
||||
username: String::from("omar"),
|
||||
password: Some(String::from("aaa")),
|
||||
},
|
||||
);
|
||||
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
// Serialize
|
||||
let deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
let hosts: UserHosts = UserHosts { bookmarks, recents };
|
||||
assert!(deserializer.serialize(Box::new(tmpfile), &hosts).is_ok());
|
||||
}
|
||||
|
||||
fn create_good_toml() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" }
|
||||
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" }
|
||||
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
|
||||
|
||||
[recents]
|
||||
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
//write!(tmpfile, "[bookmarks]\nraspberrypi2 = {{ address = \"192.168.1.31\", port = 22, protocol = \"SFTP\", username = \"root\" }}\nmsi-estrem = {{ address = \"192.168.1.30\", port = 22, protocol = \"SFTP\", username = \"cvisintin\" }}\naws-server-prod1 = {{ address = \"51.23.67.12\", port = 21, protocol = \"FTPS\", username = \"aws001\" }}\n\n[recents]\nISO20201215T094000Z = {{ address = \"172.16.104.10\", port = 22, protocol = \"SCP\", username = \"root\" }}\n");
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_bad_toml() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[bookmarks]
|
||||
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root"}
|
||||
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP" }
|
||||
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
|
||||
|
||||
[recents]
|
||||
ISO20201215T094000Z = { address = "172.16.104.10", protocol = "SCP", username = "root", port = 22 }
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
}
|
||||
239
src/config/mod.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
//! ## Config
|
||||
//!
|
||||
//! `config` is the module which provides access to termscp configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// 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 check_for_updates: Option<bool>, // @! Since 0.3.3
|
||||
pub group_dirs: Option<String>,
|
||||
pub file_fmt: 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,
|
||||
check_for_updates: Some(true),
|
||||
group_dirs: None,
|
||||
file_fmt: 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,
|
||||
check_for_updates: Some(true),
|
||||
group_dirs: Some(String::from("first")),
|
||||
file_fmt: Some(String::from("{NAME}")),
|
||||
};
|
||||
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.check_for_updates, Some(true));
|
||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
|
||||
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
|
||||
}
|
||||
|
||||
#[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.user_interface.check_for_updates.unwrap(), true);
|
||||
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")
|
||||
);
|
||||
}
|
||||
}
|
||||
250
src/config/serializer.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
//! ## Serializer
|
||||
//!
|
||||
//! `serializer` is the module which provides the serializer/deserializer for configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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.check_for_updates.unwrap(), true);
|
||||
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
|
||||
assert_eq!(
|
||||
cfg.user_interface.file_fmt,
|
||||
Some(String::from("{NAME} {PEX}"))
|
||||
);
|
||||
// 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);
|
||||
assert!(cfg.user_interface.check_for_updates.is_none());
|
||||
assert_eq!(cfg.user_interface.file_fmt, 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
|
||||
check_for_updates = true
|
||||
group_dirs = "last"
|
||||
file_fmt = "{NAME} {PEX}"
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_good_toml_no_opts() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SCP"
|
||||
text_editor = "vim"
|
||||
show_hidden_files = true
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"192.168.1.32" = "/home/omar/.ssh/beaglebone.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
|
||||
fn create_bad_toml() -> tempfile::NamedTempFile {
|
||||
// Write
|
||||
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
|
||||
let file_content: &str = r#"
|
||||
[user_interface]
|
||||
default_protocol = "SFTP"
|
||||
|
||||
[remote.ssh_keys]
|
||||
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
|
||||
"#;
|
||||
tmpfile.write_all(file_content.as_bytes()).unwrap();
|
||||
tmpfile
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -30,15 +30,18 @@ extern crate regex;
|
||||
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::utils::lstime_to_systime;
|
||||
use crate::utils::parser::{parse_datetime, parse_lstime};
|
||||
|
||||
// Includes
|
||||
use ftp4::native_tls::TlsConnector;
|
||||
use ftp4::FtpStream;
|
||||
use regex::Regex;
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
ops::Range,
|
||||
};
|
||||
|
||||
/// ## FtpFileTransfer
|
||||
///
|
||||
@@ -60,6 +63,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! {
|
||||
@@ -86,63 +108,30 @@ impl FtpFileTransfer {
|
||||
if metadata.get(2).unwrap().as_str().len() < 9 {
|
||||
return Err(());
|
||||
}
|
||||
// Get unix pex
|
||||
let unix_pex: (u8, u8, u8) = {
|
||||
let owner_pex: u8 = {
|
||||
let mut count: u8 = 0;
|
||||
for (i, c) in metadata.get(2).unwrap().as_str()[0..3].chars().enumerate() {
|
||||
match c {
|
||||
'-' => {}
|
||||
_ => {
|
||||
count += match i {
|
||||
0 => 4,
|
||||
1 => 2,
|
||||
2 => 1,
|
||||
_ => 0,
|
||||
}
|
||||
|
||||
let pex = |range: Range<usize>| {
|
||||
let mut count: u8 = 0;
|
||||
for (i, c) in metadata.get(2).unwrap().as_str()[range].chars().enumerate() {
|
||||
match c {
|
||||
'-' => {}
|
||||
_ => {
|
||||
count += match i {
|
||||
0 => 4,
|
||||
1 => 2,
|
||||
2 => 1,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
};
|
||||
let group_pex: u8 = {
|
||||
let mut count: u8 = 0;
|
||||
for (i, c) in metadata.get(2).unwrap().as_str()[3..6].chars().enumerate() {
|
||||
match c {
|
||||
'-' => {}
|
||||
_ => {
|
||||
count += match i {
|
||||
0 => 4,
|
||||
1 => 2,
|
||||
2 => 1,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
};
|
||||
let others_pex: u8 = {
|
||||
let mut count: u8 = 0;
|
||||
for (i, c) in metadata.get(2).unwrap().as_str()[6..9].chars().enumerate() {
|
||||
match c {
|
||||
'-' => {}
|
||||
_ => {
|
||||
count += match i {
|
||||
0 => 4,
|
||||
1 => 2,
|
||||
2 => 1,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
};
|
||||
(owner_pex, group_pex, others_pex)
|
||||
}
|
||||
count
|
||||
};
|
||||
|
||||
// Get unix pex
|
||||
let unix_pex = (pex(0..3), pex(3..6), pex(6..9));
|
||||
|
||||
// Parse mtime and convert to SystemTime
|
||||
let mtime: SystemTime = match lstime_to_systime(
|
||||
let mtime: SystemTime = match parse_lstime(
|
||||
metadata.get(7).unwrap().as_str(),
|
||||
"%b %d %Y",
|
||||
"%b %d %H:%M",
|
||||
@@ -161,21 +150,24 @@ impl FtpFileTransfer {
|
||||
Err(_) => None,
|
||||
};
|
||||
// Get filesize
|
||||
let filesize: usize = match metadata.get(6).unwrap().as_str().parse::<usize>() {
|
||||
Ok(sz) => sz,
|
||||
Err(_) => 0,
|
||||
};
|
||||
let filesize: usize = metadata
|
||||
.get(6)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.parse::<usize>()
|
||||
.unwrap_or(0);
|
||||
let file_name: String = String::from(metadata.get(8).unwrap().as_str());
|
||||
// Check if file_name is '.' or '..'
|
||||
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
||||
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 +202,93 @@ 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) => val.as_str().parse::<usize>().unwrap_or(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 {
|
||||
@@ -344,6 +423,16 @@ impl FileTransfer for FtpFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### copy
|
||||
///
|
||||
/// Copy file to destination
|
||||
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> {
|
||||
// FTP doesn't support file copy
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### list_dir
|
||||
///
|
||||
/// List directory entries
|
||||
@@ -585,6 +674,7 @@ impl FileTransfer for FtpFileTransfer {
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
@@ -599,7 +689,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
|
||||
@@ -658,25 +748,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");
|
||||
@@ -730,6 +811,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);
|
||||
@@ -796,47 +966,78 @@ mod tests {
|
||||
assert!(ftp.disconnect().is_ok());
|
||||
}
|
||||
|
||||
/* NOTE: they don't work
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_list_dir() {
|
||||
fn test_filetransfer_sftp_copy() {
|
||||
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("speedtest.tele2.net"), 21, None, None)
|
||||
.is_ok());
|
||||
// Pwd
|
||||
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
|
||||
// Copy
|
||||
let file: FsFile = FsFile {
|
||||
name: String::from("readme.txt"),
|
||||
abs_path: PathBuf::from("/readme.txt"),
|
||||
last_change_time: SystemTime::UNIX_EPOCH,
|
||||
last_access_time: SystemTime::UNIX_EPOCH,
|
||||
creation_time: SystemTime::UNIX_EPOCH,
|
||||
size: 0,
|
||||
ftype: Some(String::from("txt")), // File type
|
||||
readonly: true,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
};
|
||||
assert!(ftp
|
||||
.copy(&FsEntry::File(file), &Path::new("/tmp/dest.txt"))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_list_dir_dos_syntax() {
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
// Connect
|
||||
assert!(ftp
|
||||
.connect(
|
||||
String::from("test.rebex.net"),
|
||||
21,
|
||||
Some(String::from("demo")),
|
||||
Some(String::from("password"))
|
||||
)
|
||||
.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);
|
||||
@@ -866,4 +1067,31 @@ mod tests {
|
||||
// Disconnect
|
||||
assert!(ftp.disconnect().is_ok());
|
||||
}*/
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_ftp_uninitialized() {
|
||||
let file: FsFile = FsFile {
|
||||
name: String::from("omar.txt"),
|
||||
abs_path: PathBuf::from("/omar.txt"),
|
||||
last_change_time: SystemTime::UNIX_EPOCH,
|
||||
last_access_time: SystemTime::UNIX_EPOCH,
|
||||
creation_time: SystemTime::UNIX_EPOCH,
|
||||
size: 0,
|
||||
ftype: Some(String::from("txt")), // File type
|
||||
readonly: true,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
};
|
||||
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
|
||||
assert!(ftp.change_dir(Path::new("/tmp")).is_err());
|
||||
assert!(ftp.disconnect().is_err());
|
||||
assert!(ftp.list_dir(Path::new("/tmp")).is_err());
|
||||
assert!(ftp.mkdir(Path::new("/tmp")).is_err());
|
||||
assert!(ftp.pwd().is_err());
|
||||
assert!(ftp.stat(Path::new("/tmp")).is_err());
|
||||
assert!(ftp.recv_file(&file).is_err());
|
||||
assert!(ftp.send_file(&file, Path::new("/tmp/omar.txt")).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -37,7 +37,7 @@ pub mod sftp_transfer;
|
||||
///
|
||||
/// This enum defines the different transfer protocol available in TermSCP
|
||||
|
||||
#[derive(std::cmp::PartialEq, std::fmt::Debug, std::clone::Clone)]
|
||||
#[derive(PartialEq, std::fmt::Debug, std::clone::Clone, Copy)]
|
||||
pub enum FileTransferProtocol {
|
||||
Sftp,
|
||||
Scp,
|
||||
@@ -78,10 +78,7 @@ impl FileTransferError {
|
||||
///
|
||||
/// Instantiates a new FileTransferError
|
||||
pub fn new(code: FileTransferErrorType) -> FileTransferError {
|
||||
FileTransferError {
|
||||
code,
|
||||
msg: None,
|
||||
}
|
||||
FileTransferError { code, msg: None }
|
||||
}
|
||||
|
||||
/// ### new_ex
|
||||
@@ -102,7 +99,7 @@ impl std::fmt::Display for FileTransferError {
|
||||
FileTransferErrorType::ConnectionError => String::from("Connection error"),
|
||||
FileTransferErrorType::DirStatFailed => String::from("Could not stat directory"),
|
||||
FileTransferErrorType::FileCreateDenied => String::from("Failed to create file"),
|
||||
FileTransferErrorType::IoErr(err) => format!("IO Error: {}", err),
|
||||
FileTransferErrorType::IoErr(err) => format!("IO error: {}", err),
|
||||
FileTransferErrorType::NoSuchFileOrDirectory => {
|
||||
String::from("No such file or directory")
|
||||
}
|
||||
@@ -160,6 +157,11 @@ pub trait FileTransfer {
|
||||
|
||||
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError>;
|
||||
|
||||
/// ### copy
|
||||
///
|
||||
/// Copy file to destination
|
||||
fn copy(&mut self, src: &FsEntry, dst: &Path) -> Result<(), FileTransferError>;
|
||||
|
||||
/// ### list_dir
|
||||
///
|
||||
/// List directory entries
|
||||
@@ -193,7 +195,11 @@ pub trait FileTransfer {
|
||||
/// File name is referred to the name of the file as it will be saved
|
||||
/// Data contains the file data
|
||||
/// Returns file and its size
|
||||
fn send_file(&mut self, local: &FsFile, file_name: &Path) -> Result<Box<dyn Write>, FileTransferError>;
|
||||
fn send_file(
|
||||
&mut self,
|
||||
local: &FsFile,
|
||||
file_name: &Path,
|
||||
) -> Result<Box<dyn Write>, FileTransferError>;
|
||||
|
||||
/// ### recv_file
|
||||
///
|
||||
@@ -219,3 +225,190 @@ pub trait FileTransfer {
|
||||
/// You must call this method each time you want to finalize the read of the remote file.
|
||||
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError>;
|
||||
}
|
||||
|
||||
// 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)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_mod_protocol() {
|
||||
assert_eq!(
|
||||
FileTransferProtocol::Ftp(true),
|
||||
FileTransferProtocol::Ftp(true)
|
||||
);
|
||||
assert_eq!(
|
||||
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]
|
||||
fn test_filetransfer_mod_error() {
|
||||
let err: FileTransferError = FileTransferError::new_ex(
|
||||
FileTransferErrorType::IoErr(std::io::Error::from(std::io::ErrorKind::AddrInUse)),
|
||||
String::from("non va una mazza"),
|
||||
);
|
||||
assert_eq!(*err.msg.as_ref().unwrap(), String::from("non va una mazza"));
|
||||
assert_eq!(
|
||||
format!("{}", err),
|
||||
String::from("IO error: address in use (non va una mazza)")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::AuthenticationFailed)
|
||||
),
|
||||
String::from("Authentication failed")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::BadAddress)
|
||||
),
|
||||
String::from("Bad address syntax")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::ConnectionError)
|
||||
),
|
||||
String::from("Connection error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::DirStatFailed)
|
||||
),
|
||||
String::from("Could not stat directory")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::FileCreateDenied)
|
||||
),
|
||||
String::from("Failed to create file")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::NoSuchFileOrDirectory)
|
||||
),
|
||||
String::from("No such file or directory")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::PexError)
|
||||
),
|
||||
String::from("Not enough permissions")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::ProtocolError)
|
||||
),
|
||||
String::from("Protocol error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::SslError)
|
||||
),
|
||||
String::from("SSL error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::UninitializedSession)
|
||||
),
|
||||
String::from("Uninitialized session")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
FileTransferError::new(FileTransferErrorType::UnsupportedFeature)
|
||||
),
|
||||
String::from("Unsupported feature")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -30,15 +30,17 @@ extern crate ssh2;
|
||||
// Locals
|
||||
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
use crate::utils::lstime_to_systime;
|
||||
use crate::system::sshkey_storage::SshKeyStorage;
|
||||
use crate::utils::parser::parse_lstime;
|
||||
|
||||
// Includes
|
||||
use regex::Regex;
|
||||
use ssh2::{Channel, Session};
|
||||
use std::io::{BufReader, BufWriter, Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
|
||||
use std::ops::Range;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// ## ScpFileTransfer
|
||||
///
|
||||
@@ -46,22 +48,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +83,8 @@ impl ScpFileTransfer {
|
||||
}
|
||||
// Collect metadata
|
||||
// Get if is directory and if is symlink
|
||||
let (is_dir, is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str() {
|
||||
let (mut is_dir, is_symlink): (bool, bool) = match metadata.get(1).unwrap().as_str()
|
||||
{
|
||||
"-" => (false, false),
|
||||
"l" => (false, true),
|
||||
"d" => (true, false),
|
||||
@@ -95,63 +94,30 @@ impl ScpFileTransfer {
|
||||
if metadata.get(2).unwrap().as_str().len() < 9 {
|
||||
return Err(());
|
||||
}
|
||||
// Get unix pex
|
||||
let unix_pex: (u8, u8, u8) = {
|
||||
let owner_pex: u8 = {
|
||||
let mut count: u8 = 0;
|
||||
for (i, c) in metadata.get(2).unwrap().as_str()[0..3].chars().enumerate() {
|
||||
match c {
|
||||
'-' => {}
|
||||
_ => {
|
||||
count += match i {
|
||||
0 => 4,
|
||||
1 => 2,
|
||||
2 => 1,
|
||||
_ => 0,
|
||||
}
|
||||
|
||||
let pex = |range: Range<usize>| {
|
||||
let mut count: u8 = 0;
|
||||
for (i, c) in metadata.get(2).unwrap().as_str()[range].chars().enumerate() {
|
||||
match c {
|
||||
'-' => {}
|
||||
_ => {
|
||||
count += match i {
|
||||
0 => 4,
|
||||
1 => 2,
|
||||
2 => 1,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
};
|
||||
let group_pex: u8 = {
|
||||
let mut count: u8 = 0;
|
||||
for (i, c) in metadata.get(2).unwrap().as_str()[3..6].chars().enumerate() {
|
||||
match c {
|
||||
'-' => {}
|
||||
_ => {
|
||||
count += match i {
|
||||
0 => 4,
|
||||
1 => 2,
|
||||
2 => 1,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
};
|
||||
let others_pex: u8 = {
|
||||
let mut count: u8 = 0;
|
||||
for (i, c) in metadata.get(2).unwrap().as_str()[6..9].chars().enumerate() {
|
||||
match c {
|
||||
'-' => {}
|
||||
_ => {
|
||||
count += match i {
|
||||
0 => 4,
|
||||
1 => 2,
|
||||
2 => 1,
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
count
|
||||
};
|
||||
(owner_pex, group_pex, others_pex)
|
||||
}
|
||||
count
|
||||
};
|
||||
|
||||
// Get unix pex
|
||||
let unix_pex = (pex(0..3), pex(3..6), pex(6..9));
|
||||
|
||||
// Parse mtime and convert to SystemTime
|
||||
let mtime: SystemTime = match lstime_to_systime(
|
||||
let mtime: SystemTime = match parse_lstime(
|
||||
metadata.get(7).unwrap().as_str(),
|
||||
"%b %d %Y",
|
||||
"%b %d %H:%M",
|
||||
@@ -170,33 +136,40 @@ impl ScpFileTransfer {
|
||||
Err(_) => None,
|
||||
};
|
||||
// Get filesize
|
||||
let filesize: usize = match metadata.get(6).unwrap().as_str().parse::<usize>() {
|
||||
Ok(sz) => sz,
|
||||
Err(_) => 0,
|
||||
};
|
||||
let filesize: usize = metadata
|
||||
.get(6)
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.parse::<usize>()
|
||||
.unwrap_or(0);
|
||||
// Get link and name
|
||||
let (file_name, symlink_path): (String, Option<PathBuf>) = match is_symlink {
|
||||
true => self.get_name_and_link(metadata.get(8).unwrap().as_str()),
|
||||
false => (String::from(metadata.get(8).unwrap().as_str()), None),
|
||||
};
|
||||
// Check if symlink points to a directory
|
||||
if let Some(symlink_path) = symlink_path.as_ref() {
|
||||
is_dir = symlink_path.is_dir();
|
||||
}
|
||||
// Get symlink
|
||||
let symlink: Option<Box<FsEntry>> = match symlink_path {
|
||||
None => None,
|
||||
Some(p) => match self.stat(p.as_path()) {
|
||||
Ok(e) => Some(Box::new(e)),
|
||||
Err(_) => None, // Ignore errors
|
||||
}
|
||||
},
|
||||
};
|
||||
// Check if file_name is '.' or '..'
|
||||
if file_name.as_str() == "." || file_name.as_str() == ".." {
|
||||
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 {
|
||||
@@ -313,12 +286,34 @@ impl FileTransfer for ScpFileTransfer {
|
||||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError> {
|
||||
// Setup tcp stream
|
||||
let tcp: TcpStream = match TcpStream::connect(format!("{}:{}", address, port)) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
let socket_addresses: Vec<SocketAddr> =
|
||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||
Ok(s) => s.collect(),
|
||||
Err(err) => {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::BadAddress,
|
||||
format!("{}", err),
|
||||
))
|
||||
}
|
||||
};
|
||||
let mut tcp: Option<TcpStream> = None;
|
||||
// Try addresses
|
||||
for socket_addr in socket_addresses.iter() {
|
||||
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
||||
Ok(stream) => {
|
||||
tcp = Some(stream);
|
||||
break;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
// If stream is None, return connection timeout
|
||||
let tcp: TcpStream = match tcp {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::BadAddress,
|
||||
format!("{}", err),
|
||||
FileTransferErrorType::ConnectionError,
|
||||
String::from("Connection timeout"),
|
||||
))
|
||||
}
|
||||
};
|
||||
@@ -345,17 +340,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
|
||||
@@ -468,6 +482,47 @@ impl FileTransfer for ScpFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### copy
|
||||
///
|
||||
/// Copy file to destination
|
||||
fn copy(&mut self, src: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
|
||||
match self.is_connected() {
|
||||
true => {
|
||||
// Run `cp -rf`
|
||||
let p: PathBuf = self.wrkdir.clone();
|
||||
match self.perform_shell_cmd_with_path(
|
||||
p.as_path(),
|
||||
format!(
|
||||
"cp -rf \"{}\" \"{}\"; echo $?",
|
||||
src.get_abs_path().display(),
|
||||
dst.display()
|
||||
)
|
||||
.as_str(),
|
||||
) {
|
||||
Ok(output) =>
|
||||
// Check if output is 0
|
||||
{
|
||||
match output.as_str().trim() == "0" {
|
||||
true => Ok(()), // File copied
|
||||
false => Err(FileTransferError::new_ex(
|
||||
// Could not copy file
|
||||
FileTransferErrorType::FileCreateDenied,
|
||||
format!("\"{}\"", dst.display()),
|
||||
)),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
format!("{}", err),
|
||||
)),
|
||||
}
|
||||
}
|
||||
false => Err(FileTransferError::new(
|
||||
FileTransferErrorType::UninitializedSession,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### list_dir
|
||||
///
|
||||
/// List directory entries
|
||||
@@ -713,8 +768,14 @@ 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))),
|
||||
// We need to get the size of local; NOTE: don't use the `size` attribute, since might be out of sync
|
||||
let file_size: u64 = match std::fs::metadata(local.abs_path.as_path()) {
|
||||
Ok(metadata) => metadata.len(),
|
||||
Err(_) => local.size as u64, // NOTE: fallback to fsentry size
|
||||
};
|
||||
// Send file
|
||||
match session.scp_send(file_name, mode, file_size, Some(times)) {
|
||||
Ok(channel) => Ok(Box::new(BufWriter::with_capacity(65536, channel))),
|
||||
Err(err) => Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::ProtocolError,
|
||||
format!("{}", err),
|
||||
@@ -737,7 +798,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),
|
||||
@@ -782,14 +843,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(
|
||||
@@ -808,7 +869,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"),
|
||||
@@ -821,7 +882,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());
|
||||
@@ -829,7 +890,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"),
|
||||
@@ -841,7 +902,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"),
|
||||
@@ -861,7 +922,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"),
|
||||
@@ -882,7 +943,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"),
|
||||
@@ -905,7 +966,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"),
|
||||
@@ -926,7 +987,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"),
|
||||
@@ -950,7 +1011,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"),
|
||||
@@ -982,7 +1043,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"),
|
||||
@@ -1017,7 +1078,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
|
||||
@@ -1029,4 +1090,31 @@ mod tests {
|
||||
assert!(client.disconnect().is_ok());
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_scp_uninitialized() {
|
||||
let file: FsFile = FsFile {
|
||||
name: String::from("omar.txt"),
|
||||
abs_path: PathBuf::from("/omar.txt"),
|
||||
last_change_time: SystemTime::UNIX_EPOCH,
|
||||
last_access_time: SystemTime::UNIX_EPOCH,
|
||||
creation_time: SystemTime::UNIX_EPOCH,
|
||||
size: 0,
|
||||
ftype: Some(String::from("txt")), // File type
|
||||
readonly: true,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
};
|
||||
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());
|
||||
assert!(scp.mkdir(Path::new("/tmp")).is_err());
|
||||
assert!(scp.pwd().is_err());
|
||||
assert!(scp.stat(Path::new("/tmp")).is_err());
|
||||
assert!(scp.recv_file(&file).is_err());
|
||||
assert!(scp.send_file(&file, Path::new("/tmp/omar.txt")).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -29,11 +29,12 @@ 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};
|
||||
use std::io::{BufReader, BufWriter, Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::net::{SocketAddr, TcpStream, ToSocketAddrs};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,12 +203,34 @@ impl FileTransfer for SftpFileTransfer {
|
||||
password: Option<String>,
|
||||
) -> Result<Option<String>, FileTransferError> {
|
||||
// Setup tcp stream
|
||||
let tcp: TcpStream = match TcpStream::connect(format!("{}:{}", address, port)) {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => {
|
||||
let socket_addresses: Vec<SocketAddr> =
|
||||
match format!("{}:{}", address, port).to_socket_addrs() {
|
||||
Ok(s) => s.collect(),
|
||||
Err(err) => {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::BadAddress,
|
||||
format!("{}", err),
|
||||
))
|
||||
}
|
||||
};
|
||||
let mut tcp: Option<TcpStream> = None;
|
||||
// Try addresses
|
||||
for socket_addr in socket_addresses.iter() {
|
||||
match TcpStream::connect_timeout(&socket_addr, Duration::from_secs(30)) {
|
||||
Ok(stream) => {
|
||||
tcp = Some(stream);
|
||||
break;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
// If stream is None, return connection timeout
|
||||
let tcp: TcpStream = match tcp {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return Err(FileTransferError::new_ex(
|
||||
FileTransferErrorType::BadAddress,
|
||||
format!("{}", err),
|
||||
FileTransferErrorType::ConnectionError,
|
||||
String::from("Connection timeout"),
|
||||
))
|
||||
}
|
||||
};
|
||||
@@ -238,17 +257,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
|
||||
@@ -348,6 +386,16 @@ impl FileTransfer for SftpFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### copy
|
||||
///
|
||||
/// Copy file to destination
|
||||
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> {
|
||||
// SFTP doesn't support file copy
|
||||
Err(FileTransferError::new(
|
||||
FileTransferErrorType::UnsupportedFeature,
|
||||
))
|
||||
}
|
||||
|
||||
/// ### list_dir
|
||||
///
|
||||
/// List directory entries
|
||||
@@ -552,7 +600,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),
|
||||
@@ -590,7 +638,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("~"));
|
||||
@@ -599,7 +647,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(
|
||||
@@ -621,7 +669,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"),
|
||||
@@ -634,7 +682,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());
|
||||
@@ -642,7 +690,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"),
|
||||
@@ -655,7 +703,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"),
|
||||
@@ -676,7 +724,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"),
|
||||
@@ -701,9 +749,44 @@ mod tests {
|
||||
assert!(client.disconnect().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_copy() {
|
||||
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());
|
||||
// Check session and sftp
|
||||
assert!(client.session.is_some());
|
||||
assert!(client.sftp.is_some());
|
||||
assert_eq!(client.wrkdir, PathBuf::from("/"));
|
||||
// Copy
|
||||
let file: FsFile = FsFile {
|
||||
name: String::from("readme.txt"),
|
||||
abs_path: PathBuf::from("/readme.txt"),
|
||||
last_change_time: SystemTime::UNIX_EPOCH,
|
||||
last_access_time: SystemTime::UNIX_EPOCH,
|
||||
creation_time: SystemTime::UNIX_EPOCH,
|
||||
size: 0,
|
||||
ftype: Some(String::from("txt")), // File type
|
||||
readonly: true,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
};
|
||||
assert!(client
|
||||
.copy(&FsEntry::File(file), &Path::new("/tmp/dest.txt"))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[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"),
|
||||
@@ -726,7 +809,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"),
|
||||
@@ -749,7 +832,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"),
|
||||
@@ -775,7 +858,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"),
|
||||
@@ -809,7 +892,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"),
|
||||
@@ -847,7 +930,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
|
||||
@@ -859,4 +942,31 @@ mod tests {
|
||||
assert!(client.disconnect().is_ok());
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_filetransfer_sftp_uninitialized() {
|
||||
let file: FsFile = FsFile {
|
||||
name: String::from("omar.txt"),
|
||||
abs_path: PathBuf::from("/omar.txt"),
|
||||
last_change_time: SystemTime::UNIX_EPOCH,
|
||||
last_access_time: SystemTime::UNIX_EPOCH,
|
||||
creation_time: SystemTime::UNIX_EPOCH,
|
||||
size: 0,
|
||||
ftype: Some(String::from("txt")), // File type
|
||||
readonly: true,
|
||||
symlink: None, // UNIX only
|
||||
user: Some(0), // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
};
|
||||
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());
|
||||
assert!(sftp.mkdir(Path::new("/tmp")).is_err());
|
||||
assert!(sftp.pwd().is_err());
|
||||
assert!(sftp.stat(Path::new("/tmp")).is_err());
|
||||
assert!(sftp.recv_file(&file).is_err());
|
||||
assert!(sftp.send_file(&file, Path::new("/tmp/omar.txt")).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
143
src/fs/explorer/builder.rs
Normal file
@@ -0,0 +1,143 @@
|
||||
//! ## Builder
|
||||
//!
|
||||
//! `builder` is the module which provides a builder for FileExplorer
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::formatter::Formatter;
|
||||
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
|
||||
}
|
||||
|
||||
/// ### with_formatter
|
||||
///
|
||||
/// Set formatter for FileExplorer
|
||||
pub fn with_formatter(&mut self, fmt_str: Option<&str>) -> &mut FileExplorerBuilder {
|
||||
if let Some(e) = self.explorer.as_mut() {
|
||||
if let Some(fmt_str) = fmt_str {
|
||||
e.fmt = Formatter::new(fmt_str);
|
||||
}
|
||||
}
|
||||
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)
|
||||
.with_formatter(Some("{NAME}"))
|
||||
.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);
|
||||
}
|
||||
}
|
||||
908
src/fs/explorer/formatter.rs
Normal file
@@ -0,0 +1,908 @@
|
||||
//! ## Formatter
|
||||
//!
|
||||
//! `formatter` is the module which provides formatting utilities for `FileExplorer`
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate bytesize;
|
||||
extern crate regex;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
extern crate users;
|
||||
// Locals
|
||||
use super::FsEntry;
|
||||
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
|
||||
// Ext
|
||||
use bytesize::ByteSize;
|
||||
use regex::Regex;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
use users::{get_group_by_gid, get_user_by_uid};
|
||||
// Types
|
||||
// FmtCallback: Formatter, fsentry: &FsEntry, cur_str, prefix, length, extra
|
||||
type FmtCallback = fn(&Formatter, &FsEntry, &str, &str, Option<&usize>, Option<&String>) -> String;
|
||||
|
||||
// Keys
|
||||
const FMT_KEY_ATIME: &str = "ATIME";
|
||||
const FMT_KEY_CTIME: &str = "CTIME";
|
||||
const FMT_KEY_GROUP: &str = "GROUP";
|
||||
const FMT_KEY_MTIME: &str = "MTIME";
|
||||
const FMT_KEY_NAME: &str = "NAME";
|
||||
const FMT_KEY_PEX: &str = "PEX";
|
||||
const FMT_KEY_SIZE: &str = "SIZE";
|
||||
const FMT_KEY_SYMLINK: &str = "SYMLINK";
|
||||
const FMT_KEY_USER: &str = "USER";
|
||||
// Default
|
||||
const FMT_DEFAULT_STX: &str = "{NAME} {PEX} {USER} {SIZE} {MTIME}";
|
||||
// Regex
|
||||
lazy_static! {
|
||||
/**
|
||||
* Regex matches:
|
||||
* - group 0: KEY NAME
|
||||
* - group 1?: LENGTH
|
||||
* - group 2?: EXTRA
|
||||
*/
|
||||
static ref FMT_KEY_REGEX: Regex = Regex::new(r"\{(.*?)\}").ok().unwrap();
|
||||
static ref FMT_ATTR_REGEX: Regex = Regex::new(r"(?:([A-Z]+))(:?([0-9]+))?(:?(.+))?").ok().unwrap();
|
||||
}
|
||||
|
||||
/// ## CallChainBlock
|
||||
///
|
||||
/// Call Chain block is a block in a chain of functions which are called in order to format the FsEntry.
|
||||
/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found
|
||||
/// a chain of function is made using the Formatters method.
|
||||
/// This method provides an extremely fast way to format fs entries
|
||||
struct CallChainBlock {
|
||||
func: FmtCallback,
|
||||
prefix: String,
|
||||
fmt_len: Option<usize>,
|
||||
fmt_extra: Option<String>,
|
||||
next_block: Option<Box<CallChainBlock>>,
|
||||
}
|
||||
|
||||
impl CallChainBlock {
|
||||
/// ### new
|
||||
///
|
||||
/// Create a new `CallChainBlock`
|
||||
pub fn new(
|
||||
func: FmtCallback,
|
||||
prefix: String,
|
||||
fmt_len: Option<usize>,
|
||||
fmt_extra: Option<String>,
|
||||
) -> Self {
|
||||
CallChainBlock {
|
||||
func,
|
||||
prefix,
|
||||
fmt_len,
|
||||
fmt_extra,
|
||||
next_block: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### next
|
||||
///
|
||||
/// Call next callback in the CallChain
|
||||
pub fn next(&self, fmt: &Formatter, fsentry: &FsEntry, cur_str: &str) -> String {
|
||||
// Call func
|
||||
let new_str: String = (self.func)(
|
||||
fmt,
|
||||
fsentry,
|
||||
cur_str,
|
||||
self.prefix.as_str(),
|
||||
self.fmt_len.as_ref(),
|
||||
self.fmt_extra.as_ref(),
|
||||
);
|
||||
// If next is some, call next, otherwise (END OF CHAIN) return new_str
|
||||
match &self.next_block {
|
||||
Some(block) => block.next(fmt, fsentry, new_str.as_str()),
|
||||
None => new_str,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### push
|
||||
///
|
||||
/// Push func to the last element in the Call chain
|
||||
pub fn push(
|
||||
&mut self,
|
||||
func: FmtCallback,
|
||||
prefix: String,
|
||||
fmt_len: Option<usize>,
|
||||
fmt_extra: Option<String>,
|
||||
) {
|
||||
// Call recursively until an element with next_block equal to None is found
|
||||
match &mut self.next_block {
|
||||
None => {
|
||||
self.next_block = Some(Box::new(CallChainBlock::new(
|
||||
func, prefix, fmt_len, fmt_extra,
|
||||
)))
|
||||
}
|
||||
Some(block) => block.push(func, prefix, fmt_len, fmt_extra),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ## Formatter
|
||||
///
|
||||
/// Formatter takes care of formatting FsEntries according to the provided keys.
|
||||
/// Formatting is performed using the `CallChainBlock`, which composed makes a Call Chain. This method is extremely fast compared to match the format groups
|
||||
/// at each fmt call.
|
||||
pub struct Formatter {
|
||||
call_chain: CallChainBlock,
|
||||
}
|
||||
|
||||
impl Default for Formatter {
|
||||
/// ### default
|
||||
///
|
||||
/// Instantiates a Formatter with the default fmt syntax
|
||||
fn default() -> Self {
|
||||
Formatter {
|
||||
call_chain: Self::make_callchain(FMT_DEFAULT_STX),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Formatter {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new `Formatter` with the provided format string
|
||||
pub fn new(fmt_str: &str) -> Self {
|
||||
Formatter {
|
||||
call_chain: Self::make_callchain(fmt_str),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt
|
||||
///
|
||||
/// Format fsentry
|
||||
pub fn fmt(&self, fsentry: &FsEntry) -> String {
|
||||
// Execute callchain blocks
|
||||
self.call_chain.next(self, fsentry, "")
|
||||
}
|
||||
|
||||
// Fmt methods
|
||||
|
||||
/// ### fmt_atime
|
||||
///
|
||||
/// Format last access time
|
||||
fn fmt_atime(
|
||||
&self,
|
||||
fsentry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get date (use extra args as format or default "%b %d %Y %H:%M")
|
||||
let datetime: String = fmt_time(
|
||||
fsentry.get_last_access_time(),
|
||||
match fmt_extra {
|
||||
Some(fmt) => fmt.as_ref(),
|
||||
None => "%b %d %Y %H:%M",
|
||||
},
|
||||
);
|
||||
// Add to cur str, prefix and the key value
|
||||
format!(
|
||||
"{}{}{:0width$}",
|
||||
cur_str,
|
||||
prefix,
|
||||
datetime,
|
||||
width = fmt_len.unwrap_or(&17)
|
||||
)
|
||||
}
|
||||
|
||||
/// ### fmt_ctime
|
||||
///
|
||||
/// Format creation time
|
||||
fn fmt_ctime(
|
||||
&self,
|
||||
fsentry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get date
|
||||
let datetime: String = fmt_time(
|
||||
fsentry.get_creation_time(),
|
||||
match fmt_extra {
|
||||
Some(fmt) => fmt.as_ref(),
|
||||
None => "%b %d %Y %H:%M",
|
||||
},
|
||||
);
|
||||
// Add to cur str, prefix and the key value
|
||||
format!(
|
||||
"{}{}{:0width$}",
|
||||
cur_str,
|
||||
prefix,
|
||||
datetime,
|
||||
width = fmt_len.unwrap_or(&17)
|
||||
)
|
||||
}
|
||||
|
||||
/// ### fmt_group
|
||||
///
|
||||
/// Format owner group
|
||||
fn fmt_group(
|
||||
&self,
|
||||
fsentry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get username
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
let group: String = match fsentry.get_group() {
|
||||
Some(gid) => match get_group_by_gid(gid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
None => gid.to_string(),
|
||||
},
|
||||
None => 0.to_string(),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let group: String = match fsentry.get_group() {
|
||||
Some(gid) => gid.to_string(),
|
||||
None => 0.to_string(),
|
||||
};
|
||||
// Add to cur str, prefix and the key value
|
||||
format!(
|
||||
"{}{}{:0width$}",
|
||||
cur_str,
|
||||
prefix,
|
||||
group,
|
||||
width = fmt_len.unwrap_or(&12)
|
||||
)
|
||||
}
|
||||
|
||||
/// ### fmt_mtime
|
||||
///
|
||||
/// Format last change time
|
||||
fn fmt_mtime(
|
||||
&self,
|
||||
fsentry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get date
|
||||
let datetime: String = fmt_time(
|
||||
fsentry.get_last_change_time(),
|
||||
match fmt_extra {
|
||||
Some(fmt) => fmt.as_ref(),
|
||||
None => "%b %d %Y %H:%M",
|
||||
},
|
||||
);
|
||||
// Add to cur str, prefix and the key value
|
||||
format!(
|
||||
"{}{}{:0width$}",
|
||||
cur_str,
|
||||
prefix,
|
||||
datetime,
|
||||
width = fmt_len.unwrap_or(&17)
|
||||
)
|
||||
}
|
||||
|
||||
/// ### fmt_name
|
||||
///
|
||||
/// Format file name
|
||||
fn fmt_name(
|
||||
&self,
|
||||
fsentry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get file name (or elide if too long)
|
||||
let file_len: usize = match fmt_len {
|
||||
Some(l) => *l,
|
||||
None => 24,
|
||||
};
|
||||
let name: &str = fsentry.get_name();
|
||||
let last_idx: usize = match fsentry.is_dir() {
|
||||
// NOTE: For directories is 19, since we push '/' to name
|
||||
true => file_len - 5,
|
||||
false => file_len - 4,
|
||||
};
|
||||
let mut name: String = match name.len() >= file_len {
|
||||
false => name.to_string(),
|
||||
true => format!("{}...", &name[0..last_idx]),
|
||||
};
|
||||
if fsentry.is_dir() {
|
||||
name.push('/');
|
||||
}
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:0width$}", cur_str, prefix, name, width = file_len)
|
||||
}
|
||||
|
||||
/// ### fmt_pex
|
||||
///
|
||||
/// Format file permissions
|
||||
fn fmt_pex(
|
||||
&self,
|
||||
fsentry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
_fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Create mode string
|
||||
let mut pex: String = String::with_capacity(10);
|
||||
let file_type: char = match fsentry.is_symlink() {
|
||||
true => 'l',
|
||||
false => match fsentry.is_dir() {
|
||||
true => 'd',
|
||||
false => '-',
|
||||
},
|
||||
};
|
||||
pex.push(file_type);
|
||||
match fsentry.get_unix_pex() {
|
||||
None => pex.push_str("?????????"),
|
||||
Some((owner, group, others)) => pex.push_str(fmt_pex(owner, group, others).as_str()),
|
||||
}
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:10}", cur_str, prefix, pex)
|
||||
}
|
||||
|
||||
/// ### fmt_size
|
||||
///
|
||||
/// Format file size
|
||||
fn fmt_size(
|
||||
&self,
|
||||
fsentry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
_fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
if fsentry.is_file() {
|
||||
// Get byte size
|
||||
let size: ByteSize = ByteSize(fsentry.get_size() as u64);
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:10}", cur_str, prefix, size.to_string())
|
||||
} else {
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{} ", cur_str, prefix)
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt_symlink
|
||||
///
|
||||
/// Format file symlink (if any)
|
||||
fn fmt_symlink(
|
||||
&self,
|
||||
fsentry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get file name (or elide if too long)
|
||||
let file_len: usize = match fmt_len {
|
||||
Some(l) => *l,
|
||||
None => 21,
|
||||
};
|
||||
// Replace `FMT_KEY_NAME` with name
|
||||
match fsentry.is_symlink() {
|
||||
false => format!("{}{} ", cur_str, prefix),
|
||||
true => format!(
|
||||
"{}{}-> {:0width$}",
|
||||
cur_str,
|
||||
prefix,
|
||||
fmt_path_elide(
|
||||
fsentry.get_realfile().get_abs_path().as_path(),
|
||||
file_len - 1
|
||||
),
|
||||
width = file_len
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### fmt_user
|
||||
///
|
||||
/// Format owner user
|
||||
fn fmt_user(
|
||||
&self,
|
||||
fsentry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
_fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Get username
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
let username: String = match fsentry.get_user() {
|
||||
Some(uid) => match get_user_by_uid(uid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
None => uid.to_string(),
|
||||
},
|
||||
None => 0.to_string(),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let username: String = match fsentry.get_user() {
|
||||
Some(uid) => uid.to_string(),
|
||||
None => 0.to_string(),
|
||||
};
|
||||
// Add to cur str, prefix and the key value
|
||||
format!("{}{}{:12}", cur_str, prefix, username)
|
||||
}
|
||||
|
||||
/// ### fmt_fallback
|
||||
///
|
||||
/// Fallback function in case the format key is unknown
|
||||
/// It does nothing, just returns cur_str
|
||||
fn fmt_fallback(
|
||||
&self,
|
||||
_fsentry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
_fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
// Add to cur str and prefix
|
||||
format!("{}{}", cur_str, prefix)
|
||||
}
|
||||
|
||||
// Static
|
||||
|
||||
/// ### make_callchain
|
||||
///
|
||||
/// Make a callchain starting from the fmt str
|
||||
fn make_callchain(fmt_str: &str) -> CallChainBlock {
|
||||
// Init chain block
|
||||
let mut callchain: Option<CallChainBlock> = None;
|
||||
// Track index of the last match found, to get the prefix for each token
|
||||
let mut last_index: usize = 0;
|
||||
// Match fmt str against regex
|
||||
for regex_match in FMT_KEY_REGEX.captures_iter(fmt_str) {
|
||||
// Get match index (unwrap is safe, since always exists)
|
||||
let index: usize = fmt_str.find(®ex_match[0]).unwrap();
|
||||
// Get prefix
|
||||
let prefix: String = String::from(&fmt_str[last_index..index]);
|
||||
// Increment last index (sum prefix lenght and the length of the key)
|
||||
last_index += prefix.len() + regex_match[0].len();
|
||||
// Match attributes
|
||||
match FMT_ATTR_REGEX.captures(®ex_match[1]) {
|
||||
Some(regex_match) => {
|
||||
// Match group 0 (which is name)
|
||||
let callback: FmtCallback = match ®ex_match.get(1) {
|
||||
Some(key) => match key.as_str() {
|
||||
FMT_KEY_ATIME => Self::fmt_atime,
|
||||
FMT_KEY_CTIME => Self::fmt_ctime,
|
||||
FMT_KEY_GROUP => Self::fmt_group,
|
||||
FMT_KEY_MTIME => Self::fmt_mtime,
|
||||
FMT_KEY_NAME => Self::fmt_name,
|
||||
FMT_KEY_PEX => Self::fmt_pex,
|
||||
FMT_KEY_SIZE => Self::fmt_size,
|
||||
FMT_KEY_SYMLINK => Self::fmt_symlink,
|
||||
FMT_KEY_USER => Self::fmt_user,
|
||||
_ => Self::fmt_fallback,
|
||||
},
|
||||
None => Self::fmt_fallback,
|
||||
};
|
||||
// Match format length: group 3
|
||||
let fmt_len: Option<usize> = match ®ex_match.get(3) {
|
||||
Some(len) => match len.as_str().parse::<usize>() {
|
||||
Ok(len) => Some(len),
|
||||
Err(_) => None,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
// Match format extra: group 2 + 1
|
||||
let fmt_extra: Option<String> = match ®ex_match.get(5) {
|
||||
Some(extra) => Some(extra.as_str().to_string()),
|
||||
None => None,
|
||||
};
|
||||
// Create a callchain or push new element to its back
|
||||
match callchain.as_mut() {
|
||||
None => {
|
||||
callchain =
|
||||
Some(CallChainBlock::new(callback, prefix, fmt_len, fmt_extra))
|
||||
}
|
||||
Some(chain_block) => chain_block.push(callback, prefix, fmt_len, fmt_extra),
|
||||
}
|
||||
}
|
||||
None => continue,
|
||||
}
|
||||
}
|
||||
// Finalize and return
|
||||
match callchain {
|
||||
Some(callchain) => callchain,
|
||||
None => CallChainBlock::new(Self::fmt_fallback, String::new(), None, None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::fs::{FsDirectory, FsFile};
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_callchain() {
|
||||
// Make a dummy formatter
|
||||
let dummy_formatter: Formatter = Formatter::new("");
|
||||
// Make a dummy entry
|
||||
let t_now: SystemTime = SystemTime::now();
|
||||
let dummy_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
|
||||
});
|
||||
let prefix: String = String::from("h");
|
||||
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
|
||||
assert!(callchain.next_block.is_none());
|
||||
assert_eq!(callchain.prefix, String::from("h"));
|
||||
// Execute
|
||||
assert_eq!(
|
||||
callchain.next(&dummy_formatter, &dummy_entry, ""),
|
||||
String::from("hA")
|
||||
);
|
||||
// Push 4 new blocks
|
||||
callchain.push(dummy_fmt, String::from("h"), None, None);
|
||||
callchain.push(dummy_fmt, String::from("h"), None, None);
|
||||
callchain.push(dummy_fmt, String::from("h"), None, None);
|
||||
callchain.push(dummy_fmt, String::from("h"), None, None);
|
||||
// Verify
|
||||
assert_eq!(
|
||||
callchain.next(&dummy_formatter, &dummy_entry, ""),
|
||||
String::from("hAhAhAhAhA")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_format_files() {
|
||||
// Make default
|
||||
let formatter: Formatter = Formatter::default();
|
||||
// Experiments :D
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
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
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -rw-r--r-- 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// Elide name
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("piroparoporoperoperupupu.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
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
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperu... -rw-r--r-- root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"piroparoporoperoperu... -rw-r--r-- 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No pex
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
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: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? root 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No user
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"bar.txt -????????? 0 8.2 KB {}",
|
||||
fmt_time(t, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_format_dirs() {
|
||||
// Make default
|
||||
let formatter: Formatter = Formatter::default();
|
||||
// Experiments :D
|
||||
let t_now: SystemTime = SystemTime::now();
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from("projects"),
|
||||
abs_path: PathBuf::from("/home/cvisintin/projects"),
|
||||
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
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ drwxr-xr-x root {}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ drwxr-xr-x 0 {}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
// No pex, no user
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from("projects"),
|
||||
abs_path: PathBuf::from("/home/cvisintin/projects"),
|
||||
last_change_time: t_now,
|
||||
last_access_time: t_now,
|
||||
creation_time: t_now,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: Some(0), // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ d????????? 0 {}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
#[cfg(target_os = "windows")]
|
||||
assert_eq!(
|
||||
formatter.fmt(&entry),
|
||||
format!(
|
||||
"projects/ d????????? 0 {}",
|
||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fs_explorer_formatter_all_together_now() {
|
||||
let formatter: Formatter =
|
||||
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
|
||||
// Directory (with symlink)
|
||||
let t: SystemTime = SystemTime::now();
|
||||
let pointer: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("project.info"),
|
||||
abs_path: PathBuf::from("/project.info"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from("projects"),
|
||||
abs_path: PathBuf::from("/home/cvisintin/project"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
readonly: false,
|
||||
symlink: Some(Box::new(pointer)), // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
// Directory without symlink
|
||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
||||
name: String::from("projects"),
|
||||
abs_path: PathBuf::from("/home/cvisintin/project"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
readonly: false,
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: Some((7, 5, 5)), // UNIX only
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"projects/ 0 0 drwxr-xr-x {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
// File with symlink
|
||||
let pointer: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("project.info"),
|
||||
abs_path: PathBuf::from("/project.info"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: None, // UNIX only
|
||||
});
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: Some(Box::new(pointer)), // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
// File without symlink
|
||||
let entry: FsEntry = FsEntry::File(FsFile {
|
||||
name: String::from("bar.txt"),
|
||||
abs_path: PathBuf::from("/bar.txt"),
|
||||
last_change_time: t,
|
||||
last_access_time: t,
|
||||
creation_time: t,
|
||||
size: 8192,
|
||||
readonly: false,
|
||||
ftype: Some(String::from("txt")),
|
||||
symlink: None, // UNIX only
|
||||
user: None, // UNIX only
|
||||
group: None, // UNIX only
|
||||
unix_pex: Some((6, 4, 4)), // UNIX only
|
||||
});
|
||||
assert_eq!(formatter.fmt(&entry), format!(
|
||||
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||
));
|
||||
}
|
||||
|
||||
/// ### dummy_fmt
|
||||
///
|
||||
/// Dummy formatter, just yelds an 'A' at the end of the current string
|
||||
fn dummy_fmt(
|
||||
_fmt: &Formatter,
|
||||
_entry: &FsEntry,
|
||||
cur_str: &str,
|
||||
prefix: &str,
|
||||
_fmt_len: Option<&usize>,
|
||||
_fmt_extra: Option<&String>,
|
||||
) -> String {
|
||||
format!("{}{}A", cur_str, prefix)
|
||||
}
|
||||
}
|
||||
1004
src/fs/explorer/mod.rs
Normal file
145
src/fs/mod.rs
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -23,17 +23,11 @@
|
||||
*
|
||||
*/
|
||||
|
||||
extern crate bytesize;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
extern crate users;
|
||||
|
||||
use crate::utils::{fmt_pex, time_to_str};
|
||||
|
||||
use bytesize::ByteSize;
|
||||
// Mod
|
||||
pub mod explorer;
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
use std::time::SystemTime;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
use users::get_user_by_uid;
|
||||
|
||||
/// ## FsEntry
|
||||
///
|
||||
@@ -97,10 +91,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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +195,20 @@ impl FsEntry {
|
||||
matches!(self, FsEntry::Directory(_))
|
||||
}
|
||||
|
||||
/// ### is_file
|
||||
///
|
||||
/// Returns whether a FsEntry is a File
|
||||
pub fn is_file(&self) -> bool {
|
||||
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`
|
||||
@@ -218,67 +226,6 @@ impl FsEntry {
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FsEntry {
|
||||
/// ### fmt_ls
|
||||
///
|
||||
/// Format File Entry as `ls` does
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
// Create mode string
|
||||
let mut mode: String = String::with_capacity(10);
|
||||
let file_type: char = match self.is_symlink() {
|
||||
true => 'l',
|
||||
false => match self.is_dir() {
|
||||
true => 'd',
|
||||
false => '-',
|
||||
},
|
||||
};
|
||||
mode.push(file_type);
|
||||
match self.get_unix_pex() {
|
||||
None => mode.push_str("?????????"),
|
||||
Some((owner, group, others)) => mode.push_str(fmt_pex(owner, group, others).as_str()),
|
||||
}
|
||||
// Get username
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
let username: String = match self.get_user() {
|
||||
Some(uid) => match get_user_by_uid(uid) {
|
||||
Some(user) => user.name().to_string_lossy().to_string(),
|
||||
None => uid.to_string(),
|
||||
},
|
||||
None => String::from("0"),
|
||||
};
|
||||
#[cfg(target_os = "windows")]
|
||||
let username: usize = match self.get_user() {
|
||||
Some(uid) => uid as usize,
|
||||
None => 0,
|
||||
};
|
||||
// Get group
|
||||
/*
|
||||
let group: String = match self.get_group() {
|
||||
Some(gid) => match get_group_by_gid(gid) {
|
||||
Some(group) => group.name().to_string_lossy().to_string(),
|
||||
None => gid.to_string(),
|
||||
},
|
||||
None => String::from("0"),
|
||||
};
|
||||
*/
|
||||
// Get byte size
|
||||
let size: ByteSize = ByteSize(self.get_size() as u64);
|
||||
// Get date
|
||||
let datetime: String = time_to_str(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]),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
|
||||
name, mode, username, size, datetime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -310,6 +257,7 @@ mod tests {
|
||||
assert_eq!(entry.get_group(), Some(0));
|
||||
assert_eq!(entry.is_symlink(), false);
|
||||
assert_eq!(entry.is_dir(), true);
|
||||
assert_eq!(entry.is_file(), false);
|
||||
assert_eq!(entry.get_unix_pex(), Some((7, 5, 5)));
|
||||
}
|
||||
|
||||
@@ -342,6 +290,55 @@ mod tests {
|
||||
assert_eq!(entry.get_unix_pex(), Some((6, 4, 4)));
|
||||
assert_eq!(entry.is_symlink(), false);
|
||||
assert_eq!(entry.is_dir(), false);
|
||||
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]
|
||||
|
||||
351
src/host/mod.rs
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -28,7 +28,9 @@ use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
// Metadata ext
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
use std::fs::set_permissions;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
|
||||
// Locals
|
||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
||||
@@ -74,7 +76,7 @@ impl std::fmt::Display for HostError {
|
||||
HostErrorType::NoSuchFileOrDirectory => "No such file or directory",
|
||||
HostErrorType::ReadonlyFile => "File is readonly",
|
||||
HostErrorType::DirNotAccessible => "Could not access directory",
|
||||
HostErrorType::FileNotAccessible => "Could not access directory",
|
||||
HostErrorType::FileNotAccessible => "Could not access file",
|
||||
HostErrorType::FileAlreadyExists => "File already exists",
|
||||
HostErrorType::CouldNotCreateFile => "Could not create file",
|
||||
HostErrorType::DeleteFailed => "Could not delete file",
|
||||
@@ -134,7 +136,8 @@ impl Localhost {
|
||||
/// ### change_wrkdir
|
||||
///
|
||||
/// Change working directory with the new provided directory
|
||||
pub fn change_wrkdir(&mut self, new_dir: PathBuf) -> Result<PathBuf, HostError> {
|
||||
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
|
||||
let new_dir: PathBuf = self.to_abs_path(new_dir);
|
||||
// Check whether directory exists
|
||||
if !self.file_exists(new_dir.as_path()) {
|
||||
return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None));
|
||||
@@ -166,14 +169,7 @@ impl Localhost {
|
||||
/// Extended option version of makedir.
|
||||
/// ignex: don't report error if directory already exists
|
||||
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
|
||||
let dir_path: PathBuf = match dir_name.is_absolute() {
|
||||
true => PathBuf::from(dir_name),
|
||||
false => {
|
||||
let mut dir_path: PathBuf = self.wrkdir.clone();
|
||||
dir_path.push(dir_name);
|
||||
dir_path
|
||||
}
|
||||
};
|
||||
let dir_path: PathBuf = self.to_abs_path(dir_name);
|
||||
// If dir already exists, return Error
|
||||
if dir_path.exists() {
|
||||
match ignex {
|
||||
@@ -185,10 +181,7 @@ impl Localhost {
|
||||
Ok(_) => {
|
||||
// Update dir
|
||||
if dir_name.is_relative() {
|
||||
self.files = match self.scan_dir(self.wrkdir.as_path()) {
|
||||
Ok(f) => f,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -210,10 +203,7 @@ impl Localhost {
|
||||
match std::fs::remove_dir_all(dir.abs_path.as_path()) {
|
||||
Ok(_) => {
|
||||
// Update dir
|
||||
self.files = match self.scan_dir(self.wrkdir.as_path()) {
|
||||
Ok(f) => f,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(HostError::new(HostErrorType::DeleteFailed, Some(err))),
|
||||
@@ -228,10 +218,7 @@ impl Localhost {
|
||||
match std::fs::remove_file(file.abs_path.as_path()) {
|
||||
Ok(_) => {
|
||||
// Update dir
|
||||
self.files = match self.scan_dir(self.wrkdir.as_path()) {
|
||||
Ok(f) => f,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(HostError::new(HostErrorType::DeleteFailed, Some(err))),
|
||||
@@ -248,22 +235,85 @@ impl Localhost {
|
||||
match std::fs::rename(abs_path.as_path(), dst_path) {
|
||||
Ok(_) => {
|
||||
// Scan dir
|
||||
self.files = match self.scan_dir(self.wrkdir.as_path()) {
|
||||
Ok(f) => f,
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err))),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### copy
|
||||
///
|
||||
/// Copy file to destination path
|
||||
pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> {
|
||||
// Get absolute path of dest
|
||||
let dst: PathBuf = self.to_abs_path(dst);
|
||||
// Match entry
|
||||
match entry {
|
||||
FsEntry::File(file) => {
|
||||
// Copy file
|
||||
// If destination path is a directory, push file name
|
||||
let dst: PathBuf = match dst.as_path().is_dir() {
|
||||
true => {
|
||||
let mut p: PathBuf = dst.clone();
|
||||
p.push(file.name.as_str());
|
||||
p
|
||||
}
|
||||
false => dst.clone(),
|
||||
};
|
||||
// Copy entry path to dst path
|
||||
if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) {
|
||||
return Err(HostError::new(HostErrorType::CouldNotCreateFile, Some(err)));
|
||||
}
|
||||
}
|
||||
FsEntry::Directory(dir) => {
|
||||
// If destination path doesn't exist, create destination
|
||||
if !dst.exists() {
|
||||
self.mkdir(dst.as_path())?;
|
||||
}
|
||||
// Scan dir
|
||||
let dir_files: Vec<FsEntry> = self.scan_dir(dir.abs_path.as_path())?;
|
||||
// Iterate files
|
||||
for dir_entry in dir_files.iter() {
|
||||
// Calculate dst
|
||||
let mut sub_dst: PathBuf = dst.clone();
|
||||
sub_dst.push(dir_entry.get_name());
|
||||
// Call function recursively
|
||||
self.copy(dir_entry, sub_dst.as_path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reload directory if dst is pwd
|
||||
match dst.is_dir() {
|
||||
true => {
|
||||
if dst == self.pwd().as_path() {
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
} else if let Some(parent) = dst.parent() {
|
||||
// If parent is pwd, scan directory
|
||||
if parent == self.pwd().as_path() {
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
false => {
|
||||
if let Some(parent) = dst.parent() {
|
||||
// If parent is pwd, scan directory
|
||||
if parent == self.pwd().as_path() {
|
||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// ### stat
|
||||
///
|
||||
/// Stat file and create a FsEntry
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
||||
let attr: Metadata = match fs::metadata(path) {
|
||||
let path: PathBuf = self.to_abs_path(path);
|
||||
let attr: Metadata = match fs::metadata(path.as_path()) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => return Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
|
||||
};
|
||||
@@ -272,12 +322,12 @@ impl Localhost {
|
||||
Ok(match path.is_dir() {
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
name: file_name,
|
||||
abs_path: PathBuf::from(path),
|
||||
abs_path: path.clone(),
|
||||
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
readonly: attr.permissions().readonly(),
|
||||
symlink: match fs::read_link(path) {
|
||||
symlink: match fs::read_link(path.as_path()) {
|
||||
Ok(p) => match self.stat(p.as_path()) {
|
||||
Ok(entry) => Some(Box::new(entry)),
|
||||
Err(_) => None,
|
||||
@@ -296,14 +346,14 @@ impl Localhost {
|
||||
};
|
||||
FsEntry::File(FsFile {
|
||||
name: file_name,
|
||||
abs_path: PathBuf::from(path),
|
||||
abs_path: path.clone(),
|
||||
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
readonly: attr.permissions().readonly(),
|
||||
size: attr.len() as usize,
|
||||
ftype: extension,
|
||||
symlink: match fs::read_link(path) {
|
||||
symlink: match fs::read_link(path.as_path()) {
|
||||
Ok(p) => match self.stat(p.as_path()) {
|
||||
Ok(entry) => Some(Box::new(entry)),
|
||||
Err(_) => None,
|
||||
@@ -324,7 +374,8 @@ impl Localhost {
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
||||
let attr: Metadata = match fs::metadata(path.clone()) {
|
||||
let path: PathBuf = self.to_abs_path(path);
|
||||
let attr: Metadata = match fs::metadata(path.as_path()) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(err) => return Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
|
||||
};
|
||||
@@ -333,12 +384,12 @@ impl Localhost {
|
||||
Ok(match path.is_dir() {
|
||||
true => FsEntry::Directory(FsDirectory {
|
||||
name: file_name,
|
||||
abs_path: PathBuf::from(path),
|
||||
abs_path: path.clone(),
|
||||
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
readonly: attr.permissions().readonly(),
|
||||
symlink: match fs::read_link(path) {
|
||||
symlink: match fs::read_link(path.as_path()) {
|
||||
Ok(p) => match self.stat(p.as_path()) {
|
||||
Ok(entry) => Some(Box::new(entry)),
|
||||
Err(_) => None, // Ignore errors
|
||||
@@ -357,14 +408,14 @@ impl Localhost {
|
||||
};
|
||||
FsEntry::File(FsFile {
|
||||
name: file_name,
|
||||
abs_path: PathBuf::from(path),
|
||||
abs_path: path.clone(),
|
||||
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||
readonly: attr.permissions().readonly(),
|
||||
size: attr.len() as usize,
|
||||
ftype: extension,
|
||||
symlink: match fs::read_link(path) {
|
||||
symlink: match fs::read_link(path.as_path()) {
|
||||
Ok(p) => match self.stat(p.as_path()) {
|
||||
Ok(entry) => Some(Box::new(entry)),
|
||||
Err(_) => None,
|
||||
@@ -379,18 +430,39 @@ impl Localhost {
|
||||
})
|
||||
}
|
||||
|
||||
/// ### chmod
|
||||
///
|
||||
/// Change file mode to file, according to UNIX permissions
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
pub fn chmod(&self, path: &Path, pex: (u8, u8, u8)) -> Result<(), HostError> {
|
||||
let path: PathBuf = self.to_abs_path(path);
|
||||
// Get metadta
|
||||
match fs::metadata(path.as_path()) {
|
||||
Ok(metadata) => {
|
||||
let mut mpex = metadata.permissions();
|
||||
mpex.set_mode(self.mode_to_u32(pex));
|
||||
match set_permissions(path.as_path(), mpex) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### open_file_read
|
||||
///
|
||||
/// Open file for read
|
||||
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> {
|
||||
if !self.file_exists(file) {
|
||||
let file: PathBuf = self.to_abs_path(file);
|
||||
if !self.file_exists(file.as_path()) {
|
||||
return Err(HostError::new(HostErrorType::NoSuchFileOrDirectory, None));
|
||||
}
|
||||
match OpenOptions::new()
|
||||
.create(false)
|
||||
.read(true)
|
||||
.write(false)
|
||||
.open(file)
|
||||
.open(file.as_path())
|
||||
{
|
||||
Ok(f) => Ok(f),
|
||||
Err(err) => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
|
||||
@@ -401,14 +473,15 @@ impl Localhost {
|
||||
///
|
||||
/// Open file for write
|
||||
pub fn open_file_write(&self, file: &Path) -> Result<File, HostError> {
|
||||
let file: PathBuf = self.to_abs_path(file);
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(file)
|
||||
.open(file.as_path())
|
||||
{
|
||||
Ok(f) => Ok(f),
|
||||
Err(err) => match self.file_exists(file) {
|
||||
Err(err) => match self.file_exists(file.as_path()) {
|
||||
true => Err(HostError::new(HostErrorType::ReadonlyFile, Some(err))),
|
||||
false => Err(HostError::new(HostErrorType::FileNotAccessible, Some(err))),
|
||||
},
|
||||
@@ -418,7 +491,7 @@ impl Localhost {
|
||||
/// ### file_exists
|
||||
///
|
||||
/// Returns whether provided file path exists
|
||||
fn file_exists(&self, path: &Path) -> bool {
|
||||
pub fn file_exists(&self, path: &Path) -> bool {
|
||||
path.exists()
|
||||
}
|
||||
|
||||
@@ -452,6 +525,29 @@ impl Localhost {
|
||||
let others: u8 = (mode & 0x7) as u8;
|
||||
(user, group, others)
|
||||
}
|
||||
|
||||
/// mode_to_u32
|
||||
///
|
||||
/// Convert owner,group,others to u32
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
fn mode_to_u32(&self, mode: (u8, u8, u8)) -> u32 {
|
||||
((mode.0 as u32) << 6) + ((mode.1 as u32) << 3) + mode.2 as u32
|
||||
}
|
||||
|
||||
/// ### to_abs_path
|
||||
///
|
||||
/// Convert path to absolute path
|
||||
fn to_abs_path(&self, p: &Path) -> PathBuf {
|
||||
// Convert to abs path
|
||||
match p.is_relative() {
|
||||
true => {
|
||||
let mut path: PathBuf = self.wrkdir.clone();
|
||||
path.push(p);
|
||||
path
|
||||
}
|
||||
false => PathBuf::from(p),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -534,7 +630,7 @@ mod tests {
|
||||
fn test_host_localhost_change_dir() {
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
|
||||
let new_dir: PathBuf = PathBuf::from("/dev");
|
||||
assert!(host.change_wrkdir(new_dir.clone()).is_ok());
|
||||
assert!(host.change_wrkdir(new_dir.as_path()).is_ok());
|
||||
// Verify new files
|
||||
// Scan dir
|
||||
let entries = std::fs::read_dir(PathBuf::from(new_dir).as_path()).unwrap();
|
||||
@@ -551,7 +647,7 @@ mod tests {
|
||||
fn test_host_localhost_change_dir_failed() {
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from("/bin")).ok().unwrap();
|
||||
let new_dir: PathBuf = PathBuf::from("/omar/gabber/123/456");
|
||||
assert!(host.change_wrkdir(new_dir.clone()).is_ok());
|
||||
assert!(host.change_wrkdir(new_dir.as_path()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -720,6 +816,171 @@ mod tests {
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
#[test]
|
||||
fn test_host_chmod() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
let file: tempfile::NamedTempFile = create_sample_file();
|
||||
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
// mode_to_u32
|
||||
assert_eq!(host.mode_to_u32((6, 4, 4)), 0o644);
|
||||
assert_eq!(host.mode_to_u32((7, 7, 5)), 0o775);
|
||||
// Chmod to file
|
||||
assert!(host.chmod(file.path(), (7, 7, 5)).is_ok());
|
||||
// Chmod to dir
|
||||
assert!(host.chmod(tmpdir.path(), (7, 5, 0)).is_ok());
|
||||
// Error
|
||||
assert!(host
|
||||
.chmod(Path::new("/tmp/krgiogoiegj/kwrgnoerig"), (7, 7, 7))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
#[test]
|
||||
fn test_host_copy_file_absolute() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
// Create file in tmpdir
|
||||
let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
|
||||
file1_path.push("foo.txt");
|
||||
// Write file 1
|
||||
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap();
|
||||
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
||||
// Get file 2 path
|
||||
let mut file2_path: PathBuf = PathBuf::from(tmpdir.path());
|
||||
file2_path.push("bar.txt");
|
||||
// Create host
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let file1_entry: FsEntry = host.files.get(0).unwrap().clone();
|
||||
assert_eq!(file1_entry.get_name(), String::from("foo.txt"));
|
||||
// Copy
|
||||
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
|
||||
// Verify host has two files
|
||||
assert_eq!(host.files.len(), 2);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
#[test]
|
||||
fn test_host_copy_file_relative() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
// Create file in tmpdir
|
||||
let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
|
||||
file1_path.push("foo.txt");
|
||||
// Write file 1
|
||||
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap();
|
||||
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
||||
// Get file 2 path
|
||||
let file2_path: PathBuf = PathBuf::from("bar.txt");
|
||||
// Create host
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let file1_entry: FsEntry = host.files.get(0).unwrap().clone();
|
||||
assert_eq!(file1_entry.get_name(), String::from("foo.txt"));
|
||||
// Copy
|
||||
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
|
||||
// Verify host has two files
|
||||
assert_eq!(host.files.len(), 2);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
#[test]
|
||||
fn test_host_copy_directory_absolute() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
// Create directory in tmpdir
|
||||
let mut dir_src: PathBuf = PathBuf::from(tmpdir.path());
|
||||
dir_src.push("test_dir/");
|
||||
assert!(std::fs::create_dir(dir_src.as_path()).is_ok());
|
||||
// Create file in src dir
|
||||
let mut file1_path: PathBuf = dir_src.clone();
|
||||
file1_path.push("foo.txt");
|
||||
// Write file 1
|
||||
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap();
|
||||
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
||||
// Copy dir src to dir ddest
|
||||
let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path());
|
||||
dir_dest.push("test_dest_dir/");
|
||||
// Create host
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone();
|
||||
assert_eq!(dir_src_entry.get_name(), String::from("test_dir"));
|
||||
// Copy
|
||||
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
|
||||
// Verify host has two files
|
||||
assert_eq!(host.files.len(), 2);
|
||||
// Verify dir_dest contains foo.txt
|
||||
let mut test_file_path: PathBuf = dir_dest.clone();
|
||||
test_file_path.push("foo.txt");
|
||||
assert!(host.stat(test_file_path.as_path()).is_ok());
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
#[test]
|
||||
fn test_host_copy_directory_relative() {
|
||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||
// Create directory in tmpdir
|
||||
let mut dir_src: PathBuf = PathBuf::from(tmpdir.path());
|
||||
dir_src.push("test_dir/");
|
||||
assert!(std::fs::create_dir(dir_src.as_path()).is_ok());
|
||||
// Create file in src dir
|
||||
let mut file1_path: PathBuf = dir_src.clone();
|
||||
file1_path.push("foo.txt");
|
||||
// Write file 1
|
||||
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap();
|
||||
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
||||
// Copy dir src to dir ddest
|
||||
let dir_dest: PathBuf = PathBuf::from("test_dest_dir/");
|
||||
// Create host
|
||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||
let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone();
|
||||
assert_eq!(dir_src_entry.get_name(), String::from("test_dir"));
|
||||
// Copy
|
||||
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
|
||||
// Verify host has two files
|
||||
assert_eq!(host.files.len(), 2);
|
||||
// Verify dir_dest contains foo.txt
|
||||
let mut test_file_path: PathBuf = dir_dest.clone();
|
||||
test_file_path.push("foo.txt");
|
||||
println!("{:?}", host.scan_dir(tmpdir.path()).ok().unwrap());
|
||||
assert!(host.stat(test_file_path.as_path()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_host_fmt_error() {
|
||||
let err: HostError = HostError::new(
|
||||
HostErrorType::CouldNotCreateFile,
|
||||
Some(std::io::Error::from(std::io::ErrorKind::AddrInUse)),
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", err),
|
||||
String::from("Could not create file: address in use")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::new(HostErrorType::DeleteFailed, None)),
|
||||
String::from("Could not delete file")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::new(HostErrorType::DirNotAccessible, None)),
|
||||
String::from("Could not access directory")
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
HostError::new(HostErrorType::NoSuchFileOrDirectory, None)
|
||||
),
|
||||
String::from("No such file or directory")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::new(HostErrorType::ReadonlyFile, None)),
|
||||
String::from("File is readonly")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::new(HostErrorType::FileNotAccessible, None)),
|
||||
String::from("Could not access file")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", HostError::new(HostErrorType::FileAlreadyExists, None)),
|
||||
String::from("File already exists")
|
||||
);
|
||||
}
|
||||
|
||||
/// ### create_sample_file
|
||||
///
|
||||
/// Create a sample file
|
||||
|
||||
12
src/lib.rs
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -19,11 +19,19 @@
|
||||
*
|
||||
*/
|
||||
|
||||
#[macro_use] extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate bitflags;
|
||||
#[macro_use]
|
||||
extern crate lazy_static;
|
||||
#[macro_use]
|
||||
extern crate magic_crypt;
|
||||
|
||||
pub mod activity_manager;
|
||||
pub mod bookmarks;
|
||||
pub mod config;
|
||||
pub mod filetransfer;
|
||||
pub mod fs;
|
||||
pub mod host;
|
||||
pub mod system;
|
||||
pub mod ui;
|
||||
pub mod utils;
|
||||
|
||||
45
src/main.rs
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -25,7 +25,11 @@ 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;
|
||||
extern crate rpassword;
|
||||
|
||||
// External libs
|
||||
@@ -36,9 +40,12 @@ use std::time::Duration;
|
||||
|
||||
// Include
|
||||
mod activity_manager;
|
||||
mod bookmarks;
|
||||
mod config;
|
||||
mod filetransfer;
|
||||
mod fs;
|
||||
mod host;
|
||||
mod system;
|
||||
mod ui;
|
||||
mod utils;
|
||||
|
||||
@@ -51,9 +58,11 @@ use filetransfer::FileTransferProtocol;
|
||||
/// Print usage
|
||||
|
||||
fn print_usage(opts: Options) {
|
||||
let brief = String::from("Usage: termscp [options]... [protocol://user@address:port]");
|
||||
let brief = String::from(
|
||||
"Usage: termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]",
|
||||
);
|
||||
print!("{}", opts.usage(&brief));
|
||||
println!("\nPlease, report issues to <https://github.com/ChristianVisintin/TermSCP>");
|
||||
println!("\nPlease, report issues to <https://github.com/veeso/termscp>");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@@ -63,6 +72,7 @@ fn main() {
|
||||
let mut port: u16 = 22; // Default port
|
||||
let mut username: Option<String> = None; // Default username
|
||||
let mut password: Option<String> = None; // Default password
|
||||
let mut remote_wrkdir: Option<PathBuf> = None;
|
||||
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
|
||||
let mut ticks: Duration = Duration::from_millis(10);
|
||||
//Process options
|
||||
@@ -113,15 +123,17 @@ fn main() {
|
||||
}
|
||||
// Check free args
|
||||
let extra_args: Vec<String> = matches.free;
|
||||
// Remote argument
|
||||
if let Some(remote) = extra_args.get(0) {
|
||||
// Parse address
|
||||
match utils::parse_remote_opt(remote) {
|
||||
Ok((addr, portn, proto, user)) => {
|
||||
match utils::parser::parse_remote_opt(remote) {
|
||||
Ok(host_opts) => {
|
||||
// Set params
|
||||
address = Some(addr);
|
||||
port = portn;
|
||||
protocol = proto;
|
||||
username = user;
|
||||
address = Some(host_opts.hostname);
|
||||
port = host_opts.port;
|
||||
protocol = host_opts.protocol;
|
||||
username = host_opts.username;
|
||||
remote_wrkdir = host_opts.wrkdir;
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("Bad address option: {}", err);
|
||||
@@ -130,6 +142,15 @@ fn main() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Local directory
|
||||
if let Some(localdir) = extra_args.get(1) {
|
||||
// Change working directory if local dir is set
|
||||
let localdir: PathBuf = PathBuf::from(localdir);
|
||||
if let Err(err) = env::set_current_dir(localdir.as_path()) {
|
||||
eprintln!("Bad working directory argument: {}", err);
|
||||
std::process::exit(255);
|
||||
}
|
||||
}
|
||||
// Get working directory
|
||||
let wrkdir: PathBuf = match env::current_dir() {
|
||||
Ok(dir) => dir,
|
||||
@@ -160,14 +181,14 @@ fn main() {
|
||||
// Create activity manager (and context too)
|
||||
let mut manager: ActivityManager = match ActivityManager::new(&wrkdir, ticks) {
|
||||
Ok(m) => m,
|
||||
Err(_) => {
|
||||
eprintln!("Invalid directory '{}'", wrkdir.display());
|
||||
Err(err) => {
|
||||
eprintln!("Could not start activity manager: {}", err);
|
||||
std::process::exit(255);
|
||||
}
|
||||
};
|
||||
// Set file transfer params if set
|
||||
if let Some(address) = address {
|
||||
manager.set_filetransfer_params(address, port, protocol, username, password);
|
||||
manager.set_filetransfer_params(address, port, protocol, username, password, remote_wrkdir);
|
||||
}
|
||||
// Run
|
||||
manager.run(start_activity);
|
||||
|
||||
668
src/system/bookmarks_client.rs
Normal file
@@ -0,0 +1,668 @@
|
||||
//! ## BookmarksClient
|
||||
//!
|
||||
//! `bookmarks_client` is the module which provides an API between the Bookmarks module and the system
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate whoami;
|
||||
// Crate
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
use super::keys::keyringstorage::KeyringStorage;
|
||||
use super::keys::{filestorage::FileStorage, KeyStorage, KeyStorageError};
|
||||
// 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 std::fs::OpenOptions;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::string::ToString;
|
||||
use std::time::SystemTime;
|
||||
|
||||
/// ## BookmarksClient
|
||||
///
|
||||
/// BookmarksClient provides a layer between the host system and the bookmarks module
|
||||
pub struct BookmarksClient {
|
||||
hosts: UserHosts,
|
||||
bookmarks_file: PathBuf,
|
||||
key: String,
|
||||
recents_size: usize,
|
||||
}
|
||||
|
||||
impl BookmarksClient {
|
||||
/// ### BookmarksClient
|
||||
///
|
||||
/// Instantiates a new BookmarksClient
|
||||
/// Bookmarks file path must be provided
|
||||
/// Storage path for file provider must be provided
|
||||
pub fn new(
|
||||
bookmarks_file: &Path,
|
||||
storage_path: &Path,
|
||||
recents_size: usize,
|
||||
) -> Result<BookmarksClient, SerializerError> {
|
||||
// Create default hosts
|
||||
let default_hosts: UserHosts = Default::default();
|
||||
// Make a key storage (windows / macos)
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
let username: String = whoami::username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
// Check if keyring storage is supported
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "termscp";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "termscp-test";
|
||||
match storage.is_supported() {
|
||||
true => (Box::new(storage), app_name),
|
||||
false => (Box::new(FileStorage::new(storage_path)), "bookmarks"),
|
||||
}
|
||||
};
|
||||
// Make a key storage (linux / unix)
|
||||
#[cfg(any(target_os = "linux", target_os = "unix"))]
|
||||
let (key_storage, service_id): (Box<dyn KeyStorage>, &str) = {
|
||||
#[cfg(not(test))]
|
||||
let app_name: &str = "bookmarks";
|
||||
#[cfg(test)] // NOTE: when running test, add -test
|
||||
let app_name: &str = "bookmarks-test";
|
||||
(Box::new(FileStorage::new(storage_path)), app_name)
|
||||
};
|
||||
// Load key
|
||||
let key: String = match key_storage.get_key(service_id) {
|
||||
Ok(k) => k,
|
||||
Err(e) => match e {
|
||||
KeyStorageError::NoSuchKey => {
|
||||
// If no such key, generate key and set it into the storage
|
||||
let key: String = Self::generate_key();
|
||||
if let Err(e) = key_storage.set_key(service_id, key.as_str()) {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
format!("Could not write key to storage: {}", e),
|
||||
));
|
||||
}
|
||||
// Return key
|
||||
key
|
||||
}
|
||||
_ => {
|
||||
return Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
format!("Could not get key from storage: {}", e),
|
||||
))
|
||||
}
|
||||
},
|
||||
};
|
||||
let mut client: BookmarksClient = BookmarksClient {
|
||||
hosts: default_hosts,
|
||||
bookmarks_file: PathBuf::from(bookmarks_file),
|
||||
key,
|
||||
recents_size,
|
||||
};
|
||||
// If bookmark file doesn't exist, initialize it
|
||||
if !bookmarks_file.exists() {
|
||||
if let Err(err) = client.write_bookmarks() {
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
// Load bookmarks from file
|
||||
if let Err(err) = client.read_bookmarks() {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
// Load key
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
/// ### iter_bookmarks
|
||||
///
|
||||
/// Iterate over bookmarks keys
|
||||
pub fn iter_bookmarks(&self) -> impl Iterator<Item = &String> + '_ {
|
||||
Box::new(self.hosts.bookmarks.keys())
|
||||
}
|
||||
|
||||
/// ### get_bookmark
|
||||
///
|
||||
/// Get bookmark associated to key
|
||||
pub fn get_bookmark(
|
||||
&self,
|
||||
key: &str,
|
||||
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
|
||||
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
|
||||
Some((
|
||||
entry.address.clone(),
|
||||
entry.port,
|
||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||
Ok(proto) => proto,
|
||||
Err(_) => FileTransferProtocol::Sftp, // Default
|
||||
},
|
||||
entry.username.clone(),
|
||||
match &entry.password {
|
||||
// Decrypted password if Some; if decryption fails return None
|
||||
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
|
||||
Ok(decrypted_pwd) => Some(decrypted_pwd),
|
||||
Err(_) => None,
|
||||
},
|
||||
None => None,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// ### add_recent
|
||||
///
|
||||
/// Add a new recent to bookmarks
|
||||
pub fn add_bookmark(
|
||||
&mut self,
|
||||
name: String,
|
||||
addr: String,
|
||||
port: u16,
|
||||
protocol: FileTransferProtocol,
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
) {
|
||||
if name.is_empty() {
|
||||
panic!("Bookmark name can't be empty");
|
||||
}
|
||||
// Make bookmark
|
||||
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
|
||||
self.hosts.bookmarks.insert(name, host);
|
||||
}
|
||||
|
||||
/// ### del_bookmark
|
||||
///
|
||||
/// Delete entry from bookmarks
|
||||
pub fn del_bookmark(&mut self, name: &str) {
|
||||
let _ = self.hosts.bookmarks.remove(name);
|
||||
}
|
||||
/// ### iter_recents
|
||||
///
|
||||
/// Iterate over recents keys
|
||||
pub fn iter_recents(&self) -> impl Iterator<Item = &String> + '_ {
|
||||
Box::new(self.hosts.recents.keys())
|
||||
}
|
||||
|
||||
/// ### get_recent
|
||||
///
|
||||
/// Get recent associated to key
|
||||
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
|
||||
// NOTE: password is not decrypted; recents will never have password
|
||||
let entry: &Bookmark = self.hosts.recents.get(key)?;
|
||||
Some((
|
||||
entry.address.clone(),
|
||||
entry.port,
|
||||
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
|
||||
Ok(proto) => proto,
|
||||
Err(_) => FileTransferProtocol::Sftp, // Default
|
||||
},
|
||||
entry.username.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
/// ### add_recent
|
||||
///
|
||||
/// Add a new recent to bookmarks
|
||||
pub fn add_recent(
|
||||
&mut self,
|
||||
addr: String,
|
||||
port: u16,
|
||||
protocol: FileTransferProtocol,
|
||||
username: String,
|
||||
) {
|
||||
// Make bookmark
|
||||
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, None);
|
||||
// Check if duplicated
|
||||
for recent_host in self.hosts.recents.values() {
|
||||
if *recent_host == host {
|
||||
// Don't save duplicates
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If hosts size is bigger than self.recents_size; pop last
|
||||
if self.hosts.recents.len() >= self.recents_size {
|
||||
// Get keys
|
||||
let mut keys: Vec<String> = Vec::with_capacity(self.hosts.recents.len());
|
||||
for key in self.hosts.recents.keys() {
|
||||
keys.push(key.clone());
|
||||
}
|
||||
// Sort keys; NOTE: most recent is the last element
|
||||
keys.sort();
|
||||
// Delete keys starting from the last one
|
||||
for key in keys.iter() {
|
||||
let _ = self.hosts.recents.remove(key);
|
||||
// If length is < self.recents_size; break
|
||||
if self.hosts.recents.len() < self.recents_size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
|
||||
self.hosts.recents.insert(name, host);
|
||||
}
|
||||
|
||||
/// ### del_recent
|
||||
///
|
||||
/// Delete entry from recents
|
||||
pub fn del_recent(&mut self, name: &str) {
|
||||
let _ = self.hosts.recents.remove(name);
|
||||
}
|
||||
|
||||
/// ### write_bookmarks
|
||||
///
|
||||
/// Write bookmarks to file
|
||||
pub fn write_bookmarks(&self) -> Result<(), SerializerError> {
|
||||
// Open file
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(self.bookmarks_file.as_path())
|
||||
{
|
||||
Ok(writer) => {
|
||||
let serializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
serializer.serialize(Box::new(writer), &self.hosts)
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### read_bookmarks
|
||||
///
|
||||
/// Read bookmarks from file
|
||||
fn read_bookmarks(&mut self) -> Result<(), SerializerError> {
|
||||
// Open bookmarks file for read
|
||||
match OpenOptions::new()
|
||||
.read(true)
|
||||
.open(self.bookmarks_file.as_path())
|
||||
{
|
||||
Ok(reader) => {
|
||||
// Deserialize
|
||||
let deserializer: BookmarkSerializer = BookmarkSerializer {};
|
||||
match deserializer.deserialize(Box::new(reader)) {
|
||||
Ok(hosts) => {
|
||||
self.hosts = hosts;
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::IoError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### generate_key
|
||||
///
|
||||
/// Generate a new AES key
|
||||
fn generate_key() -> String {
|
||||
// Generate 256 bytes (2048 bits) key
|
||||
random_alphanumeric_with_len(256)
|
||||
}
|
||||
|
||||
/// ### make_bookmark
|
||||
///
|
||||
/// Make bookmark from credentials
|
||||
fn make_bookmark(
|
||||
&self,
|
||||
addr: String,
|
||||
port: u16,
|
||||
protocol: FileTransferProtocol,
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
) -> Bookmark {
|
||||
Bookmark {
|
||||
address: addr,
|
||||
port,
|
||||
username,
|
||||
protocol: protocol.to_string(),
|
||||
password: match password {
|
||||
Some(p) => Some(self.encrypt_str(p.as_str())), // Encrypt password if provided
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// ### encrypt_str
|
||||
///
|
||||
/// Encrypt provided string using AES-128. Encrypted buffer is then converted to BASE64
|
||||
fn encrypt_str(&self, txt: &str) -> 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> {
|
||||
match crypto::aes128_b64_decrypt(self.key.as_str(), secret) {
|
||||
Ok(txt) => Ok(txt),
|
||||
Err(err) => Err(SerializerError::new_ex(
|
||||
SerializerErrorKind::SyntaxError,
|
||||
err.to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(not(target_os = "macos"))] // CI/CD blocks
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_new() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Verify client
|
||||
assert_eq!(client.hosts.bookmarks.len(), 0);
|
||||
assert_eq!(client.hosts.recents.len(), 0);
|
||||
assert_eq!(client.key.len(), 256);
|
||||
assert_eq!(client.bookmarks_file, cfg_path);
|
||||
assert_eq!(client.recents_size, 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "unix", target_os = "linux"))]
|
||||
fn test_system_bookmarks_new_err() {
|
||||
assert!(BookmarksClient::new(
|
||||
Path::new("/tmp/oifoif/omar"),
|
||||
Path::new("/tmp/efnnu/omar"),
|
||||
16
|
||||
)
|
||||
.is_err());
|
||||
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, _): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
assert!(
|
||||
BookmarksClient::new(cfg_path.as_path(), Path::new("/tmp/efnnu/omar"), 16).is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_new_from_existing() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add some bookmarks
|
||||
client.add_bookmark(
|
||||
String::from("raspberry"),
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
Some(String::from("mypassword")),
|
||||
);
|
||||
client.add_recent(
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
let recent_key: String = String::from(client.iter_recents().next().unwrap());
|
||||
assert!(client.write_bookmarks().is_ok());
|
||||
let key: String = client.key.clone();
|
||||
// Re-initialize a client
|
||||
let client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Verify it loaded parameters correctly
|
||||
assert_eq!(client.key, key);
|
||||
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) =
|
||||
client.get_bookmark(&String::from("raspberry")).unwrap();
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
assert_eq!(bookmark.1, 22);
|
||||
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(bookmark.3, String::from("pi"));
|
||||
assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword"));
|
||||
let bookmark: (String, u16, FileTransferProtocol, String) =
|
||||
client.get_recent(&recent_key).unwrap();
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
assert_eq!(bookmark.1, 22);
|
||||
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(bookmark.3, String::from("pi"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_manipulate_bookmarks() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
String::from("raspberry"),
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
Some(String::from("mypassword")),
|
||||
);
|
||||
client.add_bookmark(
|
||||
String::from("raspberry2"),
|
||||
String::from("192.168.1.32"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
Some(String::from("mypassword2")),
|
||||
);
|
||||
// Iter
|
||||
assert_eq!(client.iter_bookmarks().count(), 2);
|
||||
// Get bookmark
|
||||
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) =
|
||||
client.get_bookmark(&String::from("raspberry")).unwrap();
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
assert_eq!(bookmark.1, 22);
|
||||
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(bookmark.3, String::from("pi"));
|
||||
assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword"));
|
||||
// Write bookmarks
|
||||
assert!(client.write_bookmarks().is_ok());
|
||||
// Delete bookmark
|
||||
client.del_bookmark(&String::from("raspberry"));
|
||||
// Get unexisting bookmark
|
||||
assert!(client.get_bookmark(&String::from("raspberry")).is_none());
|
||||
// Write bookmarks
|
||||
assert!(client.write_bookmarks().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
|
||||
fn test_system_bookmarks_bad_bookmark_name() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
String::from(""),
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
Some(String::from("mypassword")),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_manipulate_recents() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_recent(
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
// Iter
|
||||
assert_eq!(client.iter_recents().count(), 1);
|
||||
let key: String = String::from(client.iter_recents().next().unwrap());
|
||||
// Get bookmark
|
||||
let bookmark: (String, u16, FileTransferProtocol, String) =
|
||||
client.get_recent(&key).unwrap();
|
||||
assert_eq!(bookmark.0, String::from("192.168.1.31"));
|
||||
assert_eq!(bookmark.1, 22);
|
||||
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(bookmark.3, String::from("pi"));
|
||||
// Write bookmarks
|
||||
assert!(client.write_bookmarks().is_ok());
|
||||
// Delete bookmark
|
||||
client.del_recent(&key);
|
||||
// Get unexisting bookmark
|
||||
assert!(client.get_bookmark(&key).is_none());
|
||||
// Write bookmarks
|
||||
assert!(client.write_bookmarks().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_dup_recent() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_recent(
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
client.add_recent(
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
// There should be only one recent
|
||||
assert_eq!(client.iter_recents().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
fn test_system_bookmarks_recents_more_than_limit() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2).unwrap();
|
||||
// Add recent, wait 1 second for each one (cause the name depends on time)
|
||||
// 1
|
||||
client.add_recent(
|
||||
String::from("192.168.1.1"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
sleep(Duration::from_secs(1));
|
||||
// 2
|
||||
client.add_recent(
|
||||
String::from("192.168.1.2"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
sleep(Duration::from_secs(1));
|
||||
// 3
|
||||
client.add_recent(
|
||||
String::from("192.168.1.3"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
);
|
||||
// Limit is 2
|
||||
assert_eq!(client.iter_recents().count(), 2);
|
||||
// Check that 192.168.1.1 has been removed
|
||||
let key: String = client.iter_recents().nth(0).unwrap().to_string();
|
||||
assert!(matches!(
|
||||
client.hosts.recents.get(&key).unwrap().address.as_str(),
|
||||
"192.168.1.2" | "192.168.1.3"
|
||||
));
|
||||
let key: String = client.iter_recents().nth(1).unwrap().to_string();
|
||||
assert!(matches!(
|
||||
client.hosts.recents.get(&key).unwrap().address.as_str(),
|
||||
"192.168.1.2" | "192.168.1.3"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
|
||||
fn test_system_bookmarks_add_bookmark_empty() {
|
||||
let tmp_dir: tempfile::TempDir = create_tmp_dir();
|
||||
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||
// Initialize a new bookmarks client
|
||||
let mut client: BookmarksClient =
|
||||
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
|
||||
// Add bookmark
|
||||
client.add_bookmark(
|
||||
String::from(""),
|
||||
String::from("192.168.1.31"),
|
||||
22,
|
||||
FileTransferProtocol::Sftp,
|
||||
String::from("pi"),
|
||||
Some(String::from("mypassword")),
|
||||
);
|
||||
}
|
||||
|
||||
/// ### get_paths
|
||||
///
|
||||
/// Get paths for configuration and key for bookmarks
|
||||
|
||||
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||
let k: PathBuf = PathBuf::from(dir);
|
||||
let mut c: PathBuf = k.clone();
|
||||
c.push("bookmarks.toml");
|
||||
(c, k)
|
||||
}
|
||||
|
||||
/// ### create_tmp_dir
|
||||
///
|
||||
/// Create temporary directory
|
||||
|
||||
fn create_tmp_dir() -> tempfile::TempDir {
|
||||
tempfile::TempDir::new().ok().unwrap()
|
||||
}
|
||||
}
|
||||
594
src/system/config_client.rs
Normal file
@@ -0,0 +1,594 @@
|
||||
//! ## ConfigClient
|
||||
//!
|
||||
//! `config_client` is the module which provides an API between the Config module and the system
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// 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_check_for_updates
|
||||
///
|
||||
/// Get value of `check_for_updates`
|
||||
pub fn get_check_for_updates(&self) -> bool {
|
||||
self.config.user_interface.check_for_updates.unwrap_or(true)
|
||||
}
|
||||
|
||||
/// ### set_check_for_updates
|
||||
///
|
||||
/// Set new value for `check_for_updates`
|
||||
pub fn set_check_for_updates(&mut self, value: bool) {
|
||||
self.config.user_interface.check_for_updates = Some(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()),
|
||||
};
|
||||
}
|
||||
|
||||
/// ### get_file_fmt
|
||||
///
|
||||
/// Get current file fmt
|
||||
pub fn get_file_fmt(&self) -> Option<String> {
|
||||
self.config.user_interface.file_fmt.clone()
|
||||
}
|
||||
|
||||
/// ### set_file_fmt
|
||||
///
|
||||
/// Set file fmt parameter
|
||||
pub fn set_file_fmt(&mut self, s: String) {
|
||||
self.config.user_interface.file_fmt = match s.is_empty() {
|
||||
true => None,
|
||||
false => Some(s),
|
||||
};
|
||||
}
|
||||
|
||||
// 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) -> impl 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_check_for_updates() {
|
||||
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();
|
||||
assert_eq!(client.get_check_for_updates(), true); // Null ?
|
||||
client.set_check_for_updates(true);
|
||||
assert_eq!(client.get_check_for_updates(), true);
|
||||
client.set_check_for_updates(false);
|
||||
assert_eq!(client.get_check_for_updates(), false);
|
||||
}
|
||||
|
||||
#[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_file_fmt() {
|
||||
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();
|
||||
assert_eq!(client.get_file_fmt(), None);
|
||||
client.set_file_fmt(String::from("{NAME}"));
|
||||
assert_eq!(client.get_file_fmt().unwrap(), String::from("{NAME}"));
|
||||
// Delete
|
||||
client.set_file_fmt(String::from(""));
|
||||
assert_eq!(client.get_file_fmt(), 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
138
src/system/environment.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
//! ## Environment
|
||||
//!
|
||||
//! `environment` is the module which provides Path and values for the system environment
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate dirs;
|
||||
|
||||
// Ext
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// ### get_config_dir
|
||||
///
|
||||
/// Get termscp configuration directory path.
|
||||
/// Returns None, if it's not possible to get it
|
||||
pub fn init_config_dir() -> Result<Option<PathBuf>, String> {
|
||||
// Get file
|
||||
lazy_static! {
|
||||
static ref CONF_DIR: Option<PathBuf> = dirs::config_dir();
|
||||
}
|
||||
if CONF_DIR.is_some() {
|
||||
// Get path of bookmarks
|
||||
let mut p: PathBuf = CONF_DIR.as_ref().unwrap().clone();
|
||||
// Append termscp dir
|
||||
p.push("termscp/");
|
||||
// If directory doesn't exist, create it
|
||||
match p.exists() {
|
||||
true => Ok(Some(p)),
|
||||
false => match std::fs::create_dir(p.as_path()) {
|
||||
Ok(_) => Ok(Some(p)),
|
||||
Err(err) => Err(err.to_string()),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// ### get_bookmarks_paths
|
||||
///
|
||||
/// Get paths for bookmarks client
|
||||
/// Returns: path of bookmarks.toml
|
||||
pub fn get_bookmarks_paths(config_dir: &Path) -> PathBuf {
|
||||
// Prepare paths
|
||||
let mut bookmarks_file: PathBuf = PathBuf::from(config_dir);
|
||||
bookmarks_file.push("bookmarks.toml");
|
||||
bookmarks_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 {
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn test_system_environment_get_config_dir() {
|
||||
// Create and get conf_dir
|
||||
let conf_dir: PathBuf = init_config_dir().ok().unwrap().unwrap();
|
||||
// Remove dir
|
||||
assert!(std::fs::remove_dir_all(conf_dir.as_path()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_environment_get_config_dir_err() {
|
||||
let mut conf_dir: PathBuf = dirs::config_dir().unwrap();
|
||||
conf_dir.push("termscp");
|
||||
// Create file
|
||||
let mut f: File = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.open(conf_dir.as_path())
|
||||
.ok()
|
||||
.unwrap();
|
||||
// Write
|
||||
assert!(writeln!(f, "Hello world!").is_ok());
|
||||
// Drop file
|
||||
drop(f);
|
||||
// Get config dir (will fail)
|
||||
assert!(init_config_dir().is_err());
|
||||
// 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"),
|
||||
);
|
||||
}
|
||||
|
||||
#[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/")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
163
src/system/keys/filestorage.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
//! ## FileStorage
|
||||
//!
|
||||
//! `filestorage` provides an implementation of the `KeyStorage` trait using a file
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Local
|
||||
use super::{KeyStorage, KeyStorageError};
|
||||
// Ext
|
||||
use std::fs::{OpenOptions, Permissions};
|
||||
use std::io::{Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// ## FileStorage
|
||||
///
|
||||
/// File storage is an implementation o the `KeyStorage` which uses a file to store the key
|
||||
pub struct FileStorage {
|
||||
dir_path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileStorage {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new `FileStorage`
|
||||
pub fn new(dir_path: &Path) -> Self {
|
||||
FileStorage {
|
||||
dir_path: PathBuf::from(dir_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### make_file_path
|
||||
///
|
||||
/// Make file path for key file from `dir_path` and the application id
|
||||
fn make_file_path(&self, storage_id: &str) -> PathBuf {
|
||||
let mut p: PathBuf = self.dir_path.clone();
|
||||
let file_name = format!(".{}.key", storage_id);
|
||||
p.push(file_name);
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStorage for FileStorage {
|
||||
/// ### get_key
|
||||
///
|
||||
/// Retrieve key from the key storage.
|
||||
/// The key might be acccess through an identifier, which identifies
|
||||
/// the key in the storage
|
||||
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError> {
|
||||
let key_file: PathBuf = self.make_file_path(storage_id);
|
||||
// Check if file exists
|
||||
if !key_file.exists() {
|
||||
return Err(KeyStorageError::NoSuchKey);
|
||||
}
|
||||
// Read key from file
|
||||
match OpenOptions::new().read(true).open(key_file.as_path()) {
|
||||
Ok(mut file) => {
|
||||
let mut key: String = String::new();
|
||||
match file.read_to_string(&mut key) {
|
||||
Ok(_) => Ok(key),
|
||||
Err(_) => Err(KeyStorageError::ProviderError),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(KeyStorageError::ProviderError),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_key
|
||||
///
|
||||
/// Set the key into the key storage
|
||||
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> {
|
||||
let key_file: PathBuf = self.make_file_path(storage_id);
|
||||
// Write key
|
||||
match OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(key_file.as_path())
|
||||
{
|
||||
Ok(mut file) => {
|
||||
// Write key to file
|
||||
if file.write_all(key.as_bytes()).is_err() {
|
||||
return Err(KeyStorageError::ProviderError);
|
||||
}
|
||||
// Set file to readonly
|
||||
let mut permissions: Permissions = file.metadata().unwrap().permissions();
|
||||
permissions.set_readonly(true);
|
||||
let _ = file.set_permissions(permissions);
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(KeyStorageError::ProviderError),
|
||||
}
|
||||
}
|
||||
|
||||
/// is_supported
|
||||
///
|
||||
/// Returns whether the key storage is supported on the host system
|
||||
fn is_supported(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_filestorage_make_dir() {
|
||||
let storage: FileStorage = FileStorage::new(&Path::new("/tmp/"));
|
||||
assert_eq!(
|
||||
storage.make_file_path("bookmarks").as_path(),
|
||||
Path::new("/tmp/.bookmarks.key")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_filestorage_ok() {
|
||||
let key_dir: tempfile::TempDir =
|
||||
tempfile::TempDir::new().expect("Could not create tempdir");
|
||||
let storage: FileStorage = FileStorage::new(key_dir.path());
|
||||
// Supported
|
||||
assert!(storage.is_supported());
|
||||
let app_name: &str = "termscp";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
// Secret should not exist
|
||||
assert_eq!(
|
||||
storage.get_key(app_name).err().unwrap(),
|
||||
KeyStorageError::NoSuchKey
|
||||
);
|
||||
// Write secret
|
||||
assert!(storage.set_key(app_name, secret).is_ok());
|
||||
// Get secret
|
||||
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_filestorage_err() {
|
||||
let bad_dir: &Path = Path::new("/piro/poro/pero/");
|
||||
let storage: FileStorage = FileStorage::new(bad_dir);
|
||||
let app_name: &str = "termscp";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
assert!(storage.set_key(app_name, secret).is_err());
|
||||
}
|
||||
}
|
||||
129
src/system/keys/keyringstorage.rs
Normal file
@@ -0,0 +1,129 @@
|
||||
//! ## KeyringStorage
|
||||
//!
|
||||
//! `keyringstorage` provides an implementation of the `KeyStorage` trait using the OS keyring
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate keyring;
|
||||
// Local
|
||||
use super::{KeyStorage, KeyStorageError};
|
||||
// Ext
|
||||
use keyring::{Keyring, KeyringError};
|
||||
|
||||
/// ## KeyringStorage
|
||||
///
|
||||
/// provides a `KeyStorage` implementation using the keyring crate
|
||||
pub struct KeyringStorage {
|
||||
username: String,
|
||||
}
|
||||
|
||||
impl KeyringStorage {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new KeyringStorage
|
||||
pub fn new(username: &str) -> Self {
|
||||
KeyringStorage {
|
||||
username: username.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyStorage for KeyringStorage {
|
||||
/// ### get_key
|
||||
///
|
||||
/// Retrieve key from the key storage.
|
||||
/// The key might be acccess through an identifier, which identifies
|
||||
/// the key in the storage
|
||||
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError> {
|
||||
let storage: Keyring = Keyring::new(storage_id, self.username.as_str());
|
||||
match storage.get_password() {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => match e {
|
||||
KeyringError::NoPasswordFound => Err(KeyStorageError::NoSuchKey),
|
||||
#[cfg(target_os = "windows")]
|
||||
KeyringError::WindowsVaultError => Err(KeyStorageError::NoSuchKey),
|
||||
#[cfg(target_os = "macos")]
|
||||
KeyringError::MacOsKeychainError(_) => Err(KeyStorageError::NoSuchKey),
|
||||
_ => panic!("{}", e),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_key
|
||||
///
|
||||
/// Set the key into the key storage
|
||||
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError> {
|
||||
let storage: Keyring = Keyring::new(storage_id, self.username.as_str());
|
||||
match storage.set_password(key) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(KeyStorageError::ProviderError),
|
||||
}
|
||||
}
|
||||
|
||||
/// is_supported
|
||||
///
|
||||
/// Returns whether the key storage is supported on the host system
|
||||
fn is_supported(&self) -> bool {
|
||||
let dummy: String = String::from("dummy-service");
|
||||
let storage: Keyring = Keyring::new(dummy.as_str(), self.username.as_str());
|
||||
// Check what kind of error is returned
|
||||
match storage.get_password() {
|
||||
Ok(_) => true,
|
||||
Err(err) => !matches!(err, KeyringError::NoBackendFound),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
extern crate whoami;
|
||||
use super::*;
|
||||
|
||||
use whoami::username;
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_keyringstorage() {
|
||||
let username: String = username();
|
||||
let storage: KeyringStorage = KeyringStorage::new(username.as_str());
|
||||
assert!(storage.is_supported());
|
||||
let app_name: &str = "termscp-test2";
|
||||
let secret: &str = "Th15-15/My-Супер-Секрет";
|
||||
let kring: Keyring = Keyring::new(app_name, username.as_str());
|
||||
let _ = kring.delete_password();
|
||||
drop(kring);
|
||||
// Secret should not exist
|
||||
assert_eq!(
|
||||
storage.get_key(app_name).err().unwrap(),
|
||||
KeyStorageError::NoSuchKey
|
||||
);
|
||||
// Write secret
|
||||
assert!(storage.set_key(app_name, secret).is_ok());
|
||||
// Get secret
|
||||
assert_eq!(storage.get_key(app_name).ok().unwrap().as_str(), secret);
|
||||
|
||||
// Delete the key manually...
|
||||
let kring: Keyring = Keyring::new(app_name, username.as_str());
|
||||
assert!(kring.delete_password().is_ok());
|
||||
}
|
||||
}
|
||||
90
src/system/keys/mod.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
//! ## KeyStorage
|
||||
//!
|
||||
//! `keystorage` provides the trait to manipulate to a KeyStorage
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Storages
|
||||
pub mod filestorage;
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
pub mod keyringstorage;
|
||||
|
||||
/// ## KeyStorageError
|
||||
///
|
||||
/// defines the error type for the `KeyStorage`
|
||||
#[derive(PartialEq, std::fmt::Debug)]
|
||||
pub enum KeyStorageError {
|
||||
//BadKey,
|
||||
ProviderError,
|
||||
NoSuchKey,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for KeyStorageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let err: String = String::from(match &self {
|
||||
//KeyStorageError::BadKey => "Bad key syntax",
|
||||
KeyStorageError::ProviderError => "Provider service error",
|
||||
KeyStorageError::NoSuchKey => "No such key",
|
||||
});
|
||||
write!(f, "{}", err)
|
||||
}
|
||||
}
|
||||
|
||||
/// ## KeyStorage
|
||||
///
|
||||
/// this traits provides the methods to communicate and interact with the key storage.
|
||||
pub trait KeyStorage {
|
||||
/// ### get_key
|
||||
///
|
||||
/// Retrieve key from the key storage.
|
||||
/// The key might be acccess through an identifier, which identifies
|
||||
/// the key in the storage
|
||||
fn get_key(&self, storage_id: &str) -> Result<String, KeyStorageError>;
|
||||
|
||||
/// ### set_key
|
||||
///
|
||||
/// Set the key into the key storage
|
||||
fn set_key(&self, storage_id: &str, key: &str) -> Result<(), KeyStorageError>;
|
||||
|
||||
/// is_supported
|
||||
///
|
||||
/// Returns whether the key storage is supported on the host system
|
||||
fn is_supported(&self) -> bool;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_system_keys_mod_errors() {
|
||||
assert_eq!(
|
||||
format!("{}", KeyStorageError::ProviderError),
|
||||
String::from("Provider service error")
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", KeyStorageError::NoSuchKey),
|
||||
String::from("No such key")
|
||||
);
|
||||
}
|
||||
}
|
||||
31
src/system/mod.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! ## System
|
||||
//!
|
||||
//! `system` is the module which contains functions and data types related to current system
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// modules
|
||||
pub mod bookmarks_client;
|
||||
pub mod config_client;
|
||||
pub mod environment;
|
||||
pub(crate) mod keys;
|
||||
pub mod sshkey_storage;
|
||||
140
src/system/sshkey_storage.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
//! ## SshKeyStorage
|
||||
//!
|
||||
//! `SshKeyStorage` is the module which behaves a storage for ssh keys
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
@@ -1,542 +0,0 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Dependencies
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
extern crate unicode_width;
|
||||
|
||||
// locals
|
||||
use super::{Activity, Context};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
|
||||
// Includes
|
||||
use crossterm::event::Event as InputEvent;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use tui::{
|
||||
layout::{Constraint, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, Borders, Clear, Paragraph, Tabs},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// ### InputField
|
||||
///
|
||||
/// InputField describes the current input field to edit
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
enum InputField {
|
||||
Address,
|
||||
Port,
|
||||
Protocol,
|
||||
Username,
|
||||
Password,
|
||||
}
|
||||
|
||||
/// ### InputMode
|
||||
///
|
||||
/// InputMode describes the current input mode
|
||||
/// Each input mode handle the input events in a different way
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
enum InputMode {
|
||||
Text,
|
||||
Popup,
|
||||
}
|
||||
|
||||
/// ### AuthActivity
|
||||
///
|
||||
/// AuthActivity is the data holder for the authentication activity
|
||||
pub struct AuthActivity {
|
||||
pub address: String,
|
||||
pub port: String,
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
pub submit: bool, // becomes true after user has submitted fields
|
||||
pub quit: bool, // Becomes true if user has pressed esc
|
||||
context: Option<Context>,
|
||||
selected_field: InputField,
|
||||
input_mode: InputMode,
|
||||
popup_message: Option<String>,
|
||||
password_placeholder: String,
|
||||
redraw: bool, // Should ui actually be redrawned?
|
||||
}
|
||||
|
||||
impl Default for AuthActivity {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new AuthActivity
|
||||
pub fn new() -> AuthActivity {
|
||||
AuthActivity {
|
||||
address: String::new(),
|
||||
port: String::from("22"),
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
submit: false,
|
||||
quit: false,
|
||||
context: None,
|
||||
selected_field: InputField::Address,
|
||||
input_mode: InputMode::Text,
|
||||
popup_message: None,
|
||||
password_placeholder: String::new(),
|
||||
redraw: true, // True at startup
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_input_mode
|
||||
///
|
||||
/// Update input mode based on current parameters
|
||||
fn select_input_mode(&mut self) -> InputMode {
|
||||
if self.popup_message.is_some() {
|
||||
return InputMode::Popup;
|
||||
}
|
||||
// Default to text
|
||||
InputMode::Text
|
||||
}
|
||||
|
||||
/// ### handle_input_event
|
||||
///
|
||||
/// Handle input event, based on current input mode
|
||||
fn handle_input_event(&mut self, ev: &InputEvent) {
|
||||
match self.input_mode {
|
||||
InputMode::Text => self.handle_input_event_mode_text(ev),
|
||||
InputMode::Popup => self.handle_input_event_mode_popup(ev),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_text
|
||||
///
|
||||
/// Handler for input event when in textmode
|
||||
fn handle_input_event_mode_text(&mut self, ev: &InputEvent) {
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Esc => {
|
||||
self.quit = true;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Handle submit
|
||||
// Check form
|
||||
// Check address
|
||||
if self.address.is_empty() {
|
||||
self.popup_message = Some(String::from("Invalid address"));
|
||||
return;
|
||||
}
|
||||
// Check port
|
||||
// Convert port to number
|
||||
match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.popup_message =
|
||||
Some(String::from("Specified port must be in range 0-65535"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
self.popup_message =
|
||||
Some(String::from("Specified port is not a number"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Check username
|
||||
//if self.username.len() == 0 {
|
||||
// self.popup_message = Some(String::from("Invalid username"));
|
||||
// return;
|
||||
//}
|
||||
// Everything OK, set enter
|
||||
self.submit = true;
|
||||
self.popup_message =
|
||||
Some(format!("Connecting to {}:{}...", self.address, self.port));
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
// Pop last char
|
||||
match self.selected_field {
|
||||
InputField::Address => {
|
||||
let _ = self.address.pop();
|
||||
}
|
||||
InputField::Password => {
|
||||
let _ = self.password.pop();
|
||||
}
|
||||
InputField::Username => {
|
||||
let _ = self.username.pop();
|
||||
}
|
||||
InputField::Port => {
|
||||
let _ = self.port.pop();
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
};
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Move item up
|
||||
self.selected_field = match self.selected_field {
|
||||
InputField::Address => InputField::Password, // End of list (wrap)
|
||||
InputField::Port => InputField::Address,
|
||||
InputField::Protocol => InputField::Port,
|
||||
InputField::Username => InputField::Protocol,
|
||||
InputField::Password => InputField::Username,
|
||||
}
|
||||
}
|
||||
KeyCode::Down | KeyCode::Tab => {
|
||||
// Move item down
|
||||
self.selected_field = match self.selected_field {
|
||||
InputField::Address => InputField::Port,
|
||||
InputField::Port => InputField::Protocol,
|
||||
InputField::Protocol => InputField::Username,
|
||||
InputField::Username => InputField::Password,
|
||||
InputField::Password => InputField::Address, // End of list (wrap)
|
||||
}
|
||||
}
|
||||
KeyCode::Char(ch) => {
|
||||
match self.selected_field {
|
||||
InputField::Address => self.address.push(ch),
|
||||
InputField::Password => self.password.push(ch),
|
||||
InputField::Username => self.username.push(ch),
|
||||
InputField::Port => {
|
||||
// Value must be numeric
|
||||
if ch.is_numeric() {
|
||||
self.port.push(ch);
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
// If current field is Protocol handle event... (move element left)
|
||||
if self.selected_field == InputField::Protocol {
|
||||
self.protocol = match self.protocol {
|
||||
FileTransferProtocol::Sftp => FileTransferProtocol::Ftp(true), // End of list (wrap)
|
||||
FileTransferProtocol::Scp => FileTransferProtocol::Sftp,
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => FileTransferProtocol::Scp,
|
||||
true => FileTransferProtocol::Ftp(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
// If current field is Protocol handle event... ( move element right )
|
||||
if self.selected_field == InputField::Protocol {
|
||||
self.protocol = match self.protocol {
|
||||
FileTransferProtocol::Sftp => FileTransferProtocol::Scp,
|
||||
FileTransferProtocol::Scp => FileTransferProtocol::Ftp(false),
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => FileTransferProtocol::Ftp(true),
|
||||
true => FileTransferProtocol::Sftp, // End of list (wrap)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_text
|
||||
///
|
||||
/// Handler for input event when in popup mode
|
||||
fn handle_input_event_mode_popup(&mut self, ev: &InputEvent) {
|
||||
// Only enter should be allowed here
|
||||
if let InputEvent::Key(key) = ev {
|
||||
if let KeyCode::Enter = key.code {
|
||||
self.popup_message = None; // Hide popup
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### draw_remote_address
|
||||
///
|
||||
/// Draw remote address block
|
||||
fn draw_remote_address(&self) -> Paragraph {
|
||||
Paragraph::new(self.address.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Address => Style::default().fg(Color::Yellow),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Remote address"),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_remote_port
|
||||
///
|
||||
/// Draw remote port block
|
||||
fn draw_remote_port(&self) -> Paragraph {
|
||||
Paragraph::new(self.port.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Port => Style::default().fg(Color::Cyan),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Remote port"))
|
||||
}
|
||||
|
||||
/// ### draw_protocol_select
|
||||
///
|
||||
/// Draw protocol select
|
||||
fn draw_protocol_select(&self) -> Tabs {
|
||||
let protocols: Vec<Spans> = vec![
|
||||
Spans::from("SFTP"),
|
||||
Spans::from("SCP"),
|
||||
Spans::from("FTP"),
|
||||
Spans::from("FTPS"),
|
||||
];
|
||||
let index: usize = match self.protocol {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => 2,
|
||||
true => 3,
|
||||
},
|
||||
};
|
||||
Tabs::new(protocols)
|
||||
.block(Block::default().borders(Borders::ALL).title("Protocol"))
|
||||
.select(index)
|
||||
.style(match self.selected_field {
|
||||
InputField::Protocol => Style::default().fg(Color::Green),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Green)
|
||||
.fg(Color::Black),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_protocol_username
|
||||
///
|
||||
/// Draw username block
|
||||
fn draw_protocol_username(&self) -> Paragraph {
|
||||
Paragraph::new(self.username.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Username => Style::default().fg(Color::Magenta),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Username"))
|
||||
}
|
||||
|
||||
/// ### draw_protocol_password
|
||||
///
|
||||
/// Draw password block
|
||||
fn draw_protocol_password(&mut self) -> Paragraph {
|
||||
// Create password secret
|
||||
self.password_placeholder = (0..self.password.width()).map(|_| "*").collect::<String>();
|
||||
Paragraph::new(self.password_placeholder.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Password => Style::default().fg(Color::LightBlue),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(Block::default().borders(Borders::ALL).title("Password"))
|
||||
}
|
||||
|
||||
/// ### draw_header
|
||||
///
|
||||
/// Draw header
|
||||
fn draw_header(&self) -> Paragraph {
|
||||
Paragraph::new(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")
|
||||
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD))
|
||||
}
|
||||
|
||||
/// ### draw_footer
|
||||
///
|
||||
/// Draw authentication page footer
|
||||
fn draw_footer(&self) -> Paragraph {
|
||||
// Write header
|
||||
let (footer, h_style) = (
|
||||
vec![
|
||||
Span::raw("Press "),
|
||||
Span::styled("<ESC>", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to exit, "),
|
||||
Span::styled("<UP,DOWN>", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to change input field, "),
|
||||
Span::styled("<ENTER>", Style::default().add_modifier(Modifier::BOLD)),
|
||||
Span::raw(" to submit form"),
|
||||
],
|
||||
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_popup
|
||||
///
|
||||
/// Draw popup block
|
||||
fn draw_popup(&self, r: Rect) -> (Paragraph, Rect) {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((80) / 2),
|
||||
Constraint::Percentage(20),
|
||||
Constraint::Percentage((80) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(r);
|
||||
let area: Rect = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((80) / 2),
|
||||
Constraint::Percentage(20),
|
||||
Constraint::Percentage((80) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_layout[1])[1];
|
||||
let popup: Paragraph = Paragraph::new(self.popup_message.as_ref().unwrap().as_ref())
|
||||
.style(Style::default().fg(Color::Red))
|
||||
.block(Block::default().borders(Borders::ALL).title("Alert"));
|
||||
(popup, area)
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity for AuthActivity {
|
||||
/// ### 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
|
||||
let _ = self.context.as_mut().unwrap().terminal.clear();
|
||||
// Put raw mode on enabled
|
||||
let _ = enable_raw_mode();
|
||||
}
|
||||
|
||||
/// ### 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;
|
||||
}
|
||||
// Start catching Input Events
|
||||
if let Ok(input_events) = self.context.as_ref().unwrap().input_hnd.fetch_events() {
|
||||
if !input_events.is_empty() {
|
||||
self.redraw = true; // Set redraw to true if there is at least one event
|
||||
}
|
||||
// Iterate over input events
|
||||
for event in input_events.iter() {
|
||||
self.handle_input_event(event);
|
||||
}
|
||||
}
|
||||
// Redraw if necessary
|
||||
if self.redraw {
|
||||
// Determine input mode
|
||||
self.input_mode = self.select_input_mode();
|
||||
// draw interface
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal.draw(|f| {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(2)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// Draw header
|
||||
f.render_widget(self.draw_header(), chunks[0]);
|
||||
// Draw input fields
|
||||
f.render_widget(self.draw_remote_address(), chunks[1]);
|
||||
f.render_widget(self.draw_remote_port(), chunks[2]);
|
||||
f.render_widget(self.draw_protocol_select(), chunks[3]);
|
||||
f.render_widget(self.draw_protocol_username(), chunks[4]);
|
||||
f.render_widget(self.draw_protocol_password(), chunks[5]);
|
||||
// Draw footer
|
||||
f.render_widget(self.draw_footer(), chunks[6]);
|
||||
if self.popup_message.is_some() {
|
||||
let (popup, popup_area): (Paragraph, Rect) = self.draw_popup(f.size());
|
||||
f.render_widget(Clear, popup_area); //this clears out the background
|
||||
f.render_widget(popup, popup_area);
|
||||
}
|
||||
// Set cursor
|
||||
match self.selected_field {
|
||||
InputField::Address => f.set_cursor(
|
||||
chunks[1].x + self.address.width() as u16 + 1,
|
||||
chunks[1].y + 1,
|
||||
),
|
||||
InputField::Port => {
|
||||
f.set_cursor(chunks[2].x + self.port.width() as u16 + 1, chunks[2].y + 1)
|
||||
}
|
||||
InputField::Username => f.set_cursor(
|
||||
chunks[4].x + self.username.width() as u16 + 1,
|
||||
chunks[4].y + 1,
|
||||
),
|
||||
InputField::Password => f.set_cursor(
|
||||
chunks[5].x + self.password_placeholder.width() as u16 + 1,
|
||||
chunks[5].y + 1,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
// Reset ctx
|
||||
self.context = Some(ctx);
|
||||
// Set redraw 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) => {
|
||||
let _ = ctx.terminal.clear();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
281
src/ui/activities/auth_activity/bookmarks.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Dependencies
|
||||
extern crate dirs;
|
||||
|
||||
// Locals
|
||||
use super::{AuthActivity, Color, DialogYesNoOption, Popup};
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::environment;
|
||||
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### del_bookmark
|
||||
///
|
||||
/// Delete bookmark
|
||||
pub(super) fn del_bookmark(&mut self, idx: usize) {
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
|
||||
// Iterate over kyes
|
||||
let name: Option<&String> = self.bookmarks_list.get(idx);
|
||||
if let Some(name) = name {
|
||||
bookmarks_cli.del_bookmark(&name);
|
||||
// Write bookmarks
|
||||
self.write_bookmarks();
|
||||
}
|
||||
// Delete element from vec
|
||||
self.recents_list.remove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### load_bookmark
|
||||
///
|
||||
/// Load selected bookmark (at index) to input fields
|
||||
pub(super) fn load_bookmark(&mut self, idx: usize) {
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() {
|
||||
// Iterate over bookmarks
|
||||
if let Some(key) = self.bookmarks_list.get(idx) {
|
||||
if let Some(bookmark) = bookmarks_cli.get_bookmark(&key) {
|
||||
// Load parameters
|
||||
self.address = bookmark.0;
|
||||
self.port = bookmark.1.to_string();
|
||||
self.protocol = bookmark.2;
|
||||
self.username = bookmark.3;
|
||||
if let Some(password) = bookmark.4 {
|
||||
self.password = password;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### save_bookmark
|
||||
///
|
||||
/// Save current input fields as a bookmark
|
||||
pub(super) fn save_bookmark(&mut self, name: String) {
|
||||
// Check port
|
||||
let port: u16 = match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
val as u16
|
||||
}
|
||||
Err(_) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
|
||||
// Check if password must be saved
|
||||
let password: Option<String> = match self.choice_opt {
|
||||
DialogYesNoOption::Yes => Some(self.password.clone()),
|
||||
DialogYesNoOption::No => None,
|
||||
};
|
||||
bookmarks_cli.add_bookmark(
|
||||
name.clone(),
|
||||
self.address.clone(),
|
||||
port,
|
||||
self.protocol,
|
||||
self.username.clone(),
|
||||
password,
|
||||
);
|
||||
// Save bookmarks
|
||||
self.write_bookmarks();
|
||||
// Push bookmark to list and sort
|
||||
self.bookmarks_list.push(name);
|
||||
self.sort_bookmarks();
|
||||
}
|
||||
}
|
||||
/// ### del_recent
|
||||
///
|
||||
/// Delete recent
|
||||
pub(super) fn del_recent(&mut self, idx: usize) {
|
||||
if let Some(client) = self.bookmarks_client.as_mut() {
|
||||
let name: Option<&String> = self.recents_list.get(idx);
|
||||
if let Some(name) = name {
|
||||
client.del_recent(&name);
|
||||
// Write bookmarks
|
||||
self.write_bookmarks();
|
||||
}
|
||||
// Delete element from vec
|
||||
self.recents_list.remove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
/// ### load_recent
|
||||
///
|
||||
/// Load selected recent (at index) to input fields
|
||||
pub(super) fn load_recent(&mut self, idx: usize) {
|
||||
if let Some(client) = self.bookmarks_client.as_ref() {
|
||||
// Iterate over bookmarks
|
||||
if let Some(key) = self.recents_list.get(idx) {
|
||||
if let Some(bookmark) = client.get_recent(key) {
|
||||
// Load parameters
|
||||
self.address = bookmark.0;
|
||||
self.port = bookmark.1.to_string();
|
||||
self.protocol = bookmark.2;
|
||||
self.username = bookmark.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### save_recent
|
||||
///
|
||||
/// Save current input fields as a "recent"
|
||||
pub(super) fn save_recent(&mut self) {
|
||||
// Check port
|
||||
let port: u16 = match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
val as u16
|
||||
}
|
||||
Err(_) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
|
||||
bookmarks_cli.add_recent(
|
||||
self.address.clone(),
|
||||
port,
|
||||
self.protocol,
|
||||
self.username.clone(),
|
||||
);
|
||||
// Save bookmarks
|
||||
self.write_bookmarks();
|
||||
}
|
||||
}
|
||||
|
||||
/// ### write_bookmarks
|
||||
///
|
||||
/// Write bookmarks to file
|
||||
fn write_bookmarks(&mut self) {
|
||||
if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() {
|
||||
if let Err(err) = bookmarks_cli.write_bookmarks() {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not write bookmarks: {}", err),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### init_bookmarks_client
|
||||
///
|
||||
/// Initialize bookmarks client
|
||||
pub(super) fn init_bookmarks_client(&mut self) {
|
||||
// Get config dir
|
||||
match environment::init_config_dir() {
|
||||
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(config_dir_path) = path {
|
||||
let bookmarks_file: PathBuf =
|
||||
environment::get_bookmarks_paths(config_dir_path.as_path());
|
||||
// Initialize client
|
||||
match BookmarksClient::new(
|
||||
bookmarks_file.as_path(),
|
||||
config_dir_path.as_path(),
|
||||
16,
|
||||
) {
|
||||
Ok(cli) => {
|
||||
// Load bookmarks into list
|
||||
let mut bookmarks_list: Vec<String> =
|
||||
Vec::with_capacity(cli.iter_bookmarks().count());
|
||||
for bookmark in cli.iter_bookmarks() {
|
||||
bookmarks_list.push(bookmark.clone());
|
||||
}
|
||||
// Load recents into list
|
||||
let mut recents_list: Vec<String> =
|
||||
Vec::with_capacity(cli.iter_recents().count());
|
||||
for recent in cli.iter_recents() {
|
||||
recents_list.push(recent.clone());
|
||||
}
|
||||
self.bookmarks_client = Some(cli);
|
||||
self.bookmarks_list = bookmarks_list;
|
||||
self.recents_list = recents_list;
|
||||
// Sort bookmark list
|
||||
self.sort_bookmarks();
|
||||
self.sort_recents();
|
||||
}
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!(
|
||||
"Could not initialize bookmarks (at \"{}\", \"{}\"): {}",
|
||||
bookmarks_file.display(),
|
||||
config_dir_path.display(),
|
||||
err
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not initialize configuration directory: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### sort_bookmarks
|
||||
///
|
||||
/// Sort bookmarks in list
|
||||
fn sort_bookmarks(&mut self) {
|
||||
// Conver to lowercase when sorting
|
||||
self.bookmarks_list
|
||||
.sort_by(|a, b| a.to_lowercase().as_str().cmp(b.to_lowercase().as_str()));
|
||||
}
|
||||
|
||||
/// ### sort_recents
|
||||
///
|
||||
/// Sort recents in list
|
||||
fn sort_recents(&mut self) {
|
||||
// Reverse order
|
||||
self.recents_list.sort_by(|a, b| b.cmp(a));
|
||||
}
|
||||
}
|
||||
60
src/ui/activities/auth_activity/callbacks.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{AuthActivity, InputForm};
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### callback_nothing_to_do
|
||||
///
|
||||
/// Self titled
|
||||
pub(super) fn callback_nothing_to_do(&mut self) {}
|
||||
|
||||
/// ### callback_quit
|
||||
///
|
||||
/// Self titled
|
||||
pub(super) fn callback_quit(&mut self) {
|
||||
self.quit = true;
|
||||
}
|
||||
|
||||
/// ### callback_del_bookmark
|
||||
///
|
||||
/// Callback which deletes recent or bookmark based on current form
|
||||
pub(super) fn callback_del_bookmark(&mut self) {
|
||||
match self.input_form {
|
||||
InputForm::Bookmarks => self.del_bookmark(self.bookmarks_idx),
|
||||
InputForm::Recents => self.del_recent(self.recents_idx),
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_save_bookmark
|
||||
///
|
||||
/// Callback used to save bookmark with name
|
||||
pub(super) fn callback_save_bookmark(&mut self, input: String) {
|
||||
if !input.is_empty() {
|
||||
self.save_bookmark(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
493
src/ui/activities/auth_activity/input.rs
Normal file
@@ -0,0 +1,493 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{
|
||||
AuthActivity, DialogCallback, DialogYesNoOption, FileTransferProtocol, InputEvent, InputField,
|
||||
InputForm, Popup,
|
||||
};
|
||||
|
||||
use crossterm::event::{KeyCode, KeyModifiers};
|
||||
use tui::style::Color;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### 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,
|
||||
};
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form
|
||||
///
|
||||
/// Handler for input event when in form mode
|
||||
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),
|
||||
InputForm::Recents => self.handle_input_event_mode_form_recents(ev),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form_auth
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Auth
|
||||
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.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Tab => self.input_form = InputForm::Bookmarks, // Move to bookmarks
|
||||
KeyCode::Enter => {
|
||||
// Handle submit
|
||||
// Check form
|
||||
// Check address
|
||||
if self.address.is_empty() {
|
||||
self.popup =
|
||||
Some(Popup::Alert(Color::Red, String::from("Invalid address")));
|
||||
return;
|
||||
}
|
||||
// Check port
|
||||
// Convert port to number
|
||||
match self.port.parse::<usize>() {
|
||||
Ok(val) => {
|
||||
if val > 65535 {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port must be in range 0-65535"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
String::from("Specified port is not a number"),
|
||||
));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Save recent
|
||||
self.save_recent();
|
||||
// Everything OK, set enter
|
||||
self.submit = true;
|
||||
}
|
||||
KeyCode::Backspace => {
|
||||
// Pop last char
|
||||
match self.selected_field {
|
||||
InputField::Address => {
|
||||
let _ = self.address.pop();
|
||||
}
|
||||
InputField::Password => {
|
||||
let _ = self.password.pop();
|
||||
}
|
||||
InputField::Username => {
|
||||
let _ = self.username.pop();
|
||||
}
|
||||
InputField::Port => {
|
||||
let _ = self.port.pop();
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
};
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Move item up
|
||||
self.selected_field = match self.selected_field {
|
||||
InputField::Address => InputField::Password, // End of list (wrap)
|
||||
InputField::Port => InputField::Address,
|
||||
InputField::Protocol => InputField::Port,
|
||||
InputField::Username => InputField::Protocol,
|
||||
InputField::Password => InputField::Username,
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
// Move item down
|
||||
self.selected_field = match self.selected_field {
|
||||
InputField::Address => InputField::Port,
|
||||
InputField::Port => InputField::Protocol,
|
||||
InputField::Protocol => InputField::Username,
|
||||
InputField::Username => InputField::Password,
|
||||
InputField::Password => InputField::Address, // End of list (wrap)
|
||||
}
|
||||
}
|
||||
KeyCode::Char(ch) => {
|
||||
// Check if Ctrl is enabled
|
||||
if key.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||
// If 'S', save bookmark as...
|
||||
match ch {
|
||||
'H' | 'h' => {
|
||||
// Show 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.popup = Some(Popup::SaveBookmark);
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
} else {
|
||||
match self.selected_field {
|
||||
InputField::Address => self.address.push(ch),
|
||||
InputField::Password => self.password.push(ch),
|
||||
InputField::Username => self.username.push(ch),
|
||||
InputField::Port => {
|
||||
// Value must be numeric
|
||||
if ch.is_numeric() {
|
||||
self.port.push(ch);
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Left => {
|
||||
// If current field is Protocol handle event... (move element left)
|
||||
if self.selected_field == InputField::Protocol {
|
||||
self.protocol = match self.protocol {
|
||||
FileTransferProtocol::Sftp => FileTransferProtocol::Ftp(true), // End of list (wrap)
|
||||
FileTransferProtocol::Scp => FileTransferProtocol::Sftp,
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => FileTransferProtocol::Scp,
|
||||
true => FileTransferProtocol::Ftp(false),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
KeyCode::Right => {
|
||||
// If current field is Protocol handle event... ( move element right )
|
||||
if self.selected_field == InputField::Protocol {
|
||||
self.protocol = match self.protocol {
|
||||
FileTransferProtocol::Sftp => FileTransferProtocol::Scp,
|
||||
FileTransferProtocol::Scp => FileTransferProtocol::Ftp(false),
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => FileTransferProtocol::Ftp(true),
|
||||
true => FileTransferProtocol::Sftp, // End of list (wrap)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form_bookmarks
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Bookmarks
|
||||
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.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Tab => self.input_form = InputForm::AuthCredentials, // Move to Auth credentials
|
||||
KeyCode::Right => self.input_form = InputForm::Recents, // Move to recents
|
||||
KeyCode::Up => {
|
||||
// Move bookmarks index up
|
||||
if self.bookmarks_idx > 0 {
|
||||
self.bookmarks_idx -= 1;
|
||||
} else if let Some(bookmarks_cli) = &self.bookmarks_client {
|
||||
// Put to last index (wrap)
|
||||
self.bookmarks_idx = bookmarks_cli.iter_bookmarks().count() - 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if let Some(bookmarks_cli) = &self.bookmarks_client {
|
||||
let size: usize = bookmarks_cli.iter_bookmarks().count();
|
||||
// Check if can move down
|
||||
if self.bookmarks_idx + 1 >= size {
|
||||
// Move bookmarks index down
|
||||
self.bookmarks_idx = 0;
|
||||
} else {
|
||||
// Set index to first element (wrap)
|
||||
self.bookmarks_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
// Ask if user wants to delete bookmark
|
||||
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,
|
||||
));
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Load bookmark
|
||||
self.load_bookmark(self.bookmarks_idx);
|
||||
// Set input form to Auth
|
||||
self.input_form = InputForm::AuthCredentials;
|
||||
// Set input field to password (very comfy)
|
||||
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.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,
|
||||
));
|
||||
}
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Default choice option to no
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// Save bookmark as...
|
||||
self.popup = Some(Popup::SaveBookmark);
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
},
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_form_recents
|
||||
///
|
||||
/// Handle input event when input mode is Form and Tab is Recents
|
||||
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.popup = Some(Popup::YesNo(
|
||||
String::from("Are you sure you want to quit termscp?"),
|
||||
AuthActivity::callback_quit,
|
||||
AuthActivity::callback_nothing_to_do,
|
||||
));
|
||||
}
|
||||
KeyCode::Tab => self.input_form = InputForm::AuthCredentials, // Move to Auth credentials
|
||||
KeyCode::Left => self.input_form = InputForm::Bookmarks, // Move to bookmarks
|
||||
KeyCode::Up => {
|
||||
// Move bookmarks index up
|
||||
if self.recents_idx > 0 {
|
||||
self.recents_idx -= 1;
|
||||
} else if let Some(bookmarks_cli) = &self.bookmarks_client {
|
||||
// Put to last index (wrap)
|
||||
self.recents_idx = bookmarks_cli.iter_recents().count() - 1;
|
||||
}
|
||||
}
|
||||
KeyCode::Down => {
|
||||
if let Some(bookmarks_cli) = &self.bookmarks_client {
|
||||
let size: usize = bookmarks_cli.iter_recents().count();
|
||||
// Check if can move down
|
||||
if self.recents_idx + 1 >= size {
|
||||
// Move bookmarks index down
|
||||
self.recents_idx = 0;
|
||||
} else {
|
||||
// Set index to first element (wrap)
|
||||
self.recents_idx += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Delete => {
|
||||
// Ask if user wants to delete bookmark
|
||||
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,
|
||||
));
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Load bookmark
|
||||
self.load_recent(self.recents_idx);
|
||||
// Set input form to Auth
|
||||
self.input_form = InputForm::AuthCredentials;
|
||||
// Set input field to password (very comfy)
|
||||
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.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,
|
||||
));
|
||||
}
|
||||
'H' | 'h' => {
|
||||
// Show help
|
||||
self.popup = Some(Popup::Help);
|
||||
}
|
||||
'S' | 's' => {
|
||||
// Default choice option to no
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// Save bookmark as...
|
||||
self.popup = Some(Popup::SaveBookmark);
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
},
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_text
|
||||
///
|
||||
/// Handler for input event when in popup mode
|
||||
fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, ptype: Popup) {
|
||||
match ptype {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### 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_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) {
|
||||
// Set input mode back to form
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_popup_save_bookmark
|
||||
///
|
||||
/// Input event handler for SaveBookmark popup
|
||||
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 {
|
||||
KeyCode::Esc => {
|
||||
// Abort input
|
||||
// Clear current input text
|
||||
self.input_txt.clear();
|
||||
// Set mode back to form
|
||||
self.popup = None;
|
||||
// Reset choice option to yes
|
||||
self.choice_opt = DialogYesNoOption::Yes;
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
// Submit
|
||||
let input_text: String = self.input_txt.clone();
|
||||
// Clear current input text
|
||||
self.input_txt.clear();
|
||||
// Set mode back to form BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.popup = None;
|
||||
// Call cb
|
||||
self.callback_save_bookmark(input_text);
|
||||
// Reset choice option to yes
|
||||
self.choice_opt = DialogYesNoOption::Yes;
|
||||
}
|
||||
KeyCode::Left => self.choice_opt = DialogYesNoOption::Yes, // Move yes/no with arrows
|
||||
KeyCode::Right => self.choice_opt = DialogYesNoOption::No, // Move yes/no with arrows
|
||||
KeyCode::Char(ch) => self.input_txt.push(ch),
|
||||
KeyCode::Backspace => {
|
||||
let _ = self.input_txt.pop();
|
||||
}
|
||||
_ => { /* 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: DialogCallback,
|
||||
no_cb: DialogCallback,
|
||||
) {
|
||||
// If enter, close popup, otherwise move dialog option
|
||||
if let InputEvent::Key(key) = ev {
|
||||
match key.code {
|
||||
KeyCode::Enter => {
|
||||
// @! Set input mode to Form BEFORE CALLBACKS!!! Callback can then overwrite this, clever uh?
|
||||
self.popup = None;
|
||||
// Check if user selected yes or not
|
||||
match self.choice_opt {
|
||||
DialogYesNoOption::No => no_cb(self),
|
||||
DialogYesNoOption::Yes => yes_cb(self),
|
||||
}
|
||||
// Reset choice option to yes
|
||||
self.choice_opt = DialogYesNoOption::Yes;
|
||||
}
|
||||
KeyCode::Right => self.choice_opt = DialogYesNoOption::No, // Set to NO
|
||||
KeyCode::Left => self.choice_opt = DialogYesNoOption::Yes, // Set to YES
|
||||
_ => { /* Nothing to do */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
651
src/ui/activities/auth_activity/layout.rs
Normal file
@@ -0,0 +1,651 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::{
|
||||
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},
|
||||
text::{Span, Spans, Text},
|
||||
widgets::{Block, BorderType, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### draw
|
||||
///
|
||||
/// Draw UI
|
||||
pub(super) fn draw(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let _ = ctx.terminal.draw(|f| {
|
||||
// Prepare chunks
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage(70), // Auth Form
|
||||
Constraint::Percentage(30), // Bookmarks
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(f.size());
|
||||
// Create explorer chunks
|
||||
let auth_chunks = Layout::default()
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(5),
|
||||
Constraint::Length(1), // Version
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.direction(Direction::Vertical)
|
||||
.split(chunks[0]);
|
||||
// Create bookmark chunks
|
||||
let bookmark_chunks = Layout::default()
|
||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
|
||||
.direction(Direction::Horizontal)
|
||||
.split(chunks[1]);
|
||||
// Draw header
|
||||
f.render_widget(self.draw_header(), auth_chunks[0]);
|
||||
f.render_widget(self.draw_new_version(), auth_chunks[1]);
|
||||
// Draw input fields
|
||||
f.render_widget(self.draw_remote_address(), auth_chunks[2]);
|
||||
f.render_widget(self.draw_remote_port(), auth_chunks[3]);
|
||||
f.render_widget(self.draw_protocol_select(), auth_chunks[4]);
|
||||
f.render_widget(self.draw_protocol_username(), auth_chunks[5]);
|
||||
f.render_widget(self.draw_protocol_password(), auth_chunks[6]);
|
||||
// Draw footer
|
||||
f.render_widget(self.draw_footer(), auth_chunks[7]);
|
||||
// Set cursor
|
||||
if let InputForm::AuthCredentials = self.input_form {
|
||||
match self.selected_field {
|
||||
InputField::Address => f.set_cursor(
|
||||
auth_chunks[2].x + self.address.width() as u16 + 1,
|
||||
auth_chunks[2].y + 1,
|
||||
),
|
||||
InputField::Port => f.set_cursor(
|
||||
auth_chunks[3].x + self.port.width() as u16 + 1,
|
||||
auth_chunks[3].y + 1,
|
||||
),
|
||||
InputField::Username => f.set_cursor(
|
||||
auth_chunks[5].x + self.username.width() as u16 + 1,
|
||||
auth_chunks[5].y + 1,
|
||||
),
|
||||
InputField::Password => f.set_cursor(
|
||||
auth_chunks[6].x + self.password_placeholder.width() as u16 + 1,
|
||||
auth_chunks[6].y + 1,
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
// Draw bookmarks
|
||||
if let Some(tab) = self.draw_bookmarks_tab() {
|
||||
let mut bookmarks_state: ListState = ListState::default();
|
||||
bookmarks_state.select(Some(self.bookmarks_idx));
|
||||
f.render_stateful_widget(tab, bookmark_chunks[0], &mut bookmarks_state);
|
||||
}
|
||||
if let Some(tab) = self.draw_recents_tab() {
|
||||
let mut recents_state: ListState = ListState::default();
|
||||
recents_state.select(Some(self.recents_idx));
|
||||
f.render_stateful_widget(tab, bookmark_chunks[1], &mut recents_state);
|
||||
}
|
||||
// Draw popup
|
||||
if let Some(popup) = &self.popup {
|
||||
// Calculate popup size
|
||||
let (width, height): (u16, u16) = match popup {
|
||||
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 {
|
||||
Popup::Alert(color, txt) => f.render_widget(
|
||||
self.draw_popup_alert(*color, txt.clone(), popup_area.width),
|
||||
popup_area,
|
||||
),
|
||||
Popup::Help => f.render_widget(self.draw_popup_help(), popup_area),
|
||||
Popup::SaveBookmark => {
|
||||
let popup_chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(3), // Input form
|
||||
Constraint::Length(2), // Yes/No
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_area);
|
||||
let (input, yes_no): (Paragraph, Tabs) = self.draw_popup_save_bookmark();
|
||||
// Render parts
|
||||
f.render_widget(input, popup_chunks[0]);
|
||||
f.render_widget(yes_no, popup_chunks[1]);
|
||||
// Set cursor
|
||||
f.set_cursor(
|
||||
popup_chunks[0].x + self.input_txt.width() as u16 + 1,
|
||||
popup_chunks[0].y + 1,
|
||||
)
|
||||
}
|
||||
Popup::YesNo(txt, _, _) => {
|
||||
f.render_widget(self.draw_popup_yesno(txt.clone()), popup_area)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
self.context = Some(ctx);
|
||||
}
|
||||
|
||||
/// ### draw_remote_address
|
||||
///
|
||||
/// Draw remote address block
|
||||
fn draw_remote_address(&self) -> Paragraph {
|
||||
Paragraph::new(self.address.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Address => Style::default().fg(Color::Yellow),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Remote address"),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_remote_port
|
||||
///
|
||||
/// Draw remote port block
|
||||
fn draw_remote_port(&self) -> Paragraph {
|
||||
Paragraph::new(self.port.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Port => Style::default().fg(Color::Cyan),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Remote port"),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_protocol_select
|
||||
///
|
||||
/// Draw protocol select
|
||||
fn draw_protocol_select(&self) -> Tabs {
|
||||
let protocols: Vec<Spans> = vec![
|
||||
Spans::from("SFTP"),
|
||||
Spans::from("SCP"),
|
||||
Spans::from("FTP"),
|
||||
Spans::from("FTPS"),
|
||||
];
|
||||
let index: usize = match self.protocol {
|
||||
FileTransferProtocol::Sftp => 0,
|
||||
FileTransferProtocol::Scp => 1,
|
||||
FileTransferProtocol::Ftp(ftps) => match ftps {
|
||||
false => 2,
|
||||
true => 3,
|
||||
},
|
||||
};
|
||||
Tabs::new(protocols)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Protocol"),
|
||||
)
|
||||
.select(index)
|
||||
.style(match self.selected_field {
|
||||
InputField::Protocol => Style::default().fg(Color::Green),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Green)
|
||||
.fg(Color::Black),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_protocol_username
|
||||
///
|
||||
/// Draw username block
|
||||
fn draw_protocol_username(&self) -> Paragraph {
|
||||
Paragraph::new(self.username.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Username => Style::default().fg(Color::Magenta),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Username"),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_protocol_password
|
||||
///
|
||||
/// Draw password block
|
||||
fn draw_protocol_password(&mut self) -> Paragraph {
|
||||
// Create password secret
|
||||
self.password_placeholder = (0..self.password.width()).map(|_| "*").collect::<String>();
|
||||
Paragraph::new(self.password_placeholder.as_ref())
|
||||
.style(match self.selected_field {
|
||||
InputField::Password => Style::default().fg(Color::LightBlue),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Password"),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_header
|
||||
///
|
||||
/// Draw header
|
||||
fn draw_header(&self) -> Paragraph {
|
||||
Paragraph::new(" _____ ____ ____ ____ \n|_ _|__ _ __ _ __ ___ / ___| / ___| _ \\ \n | |/ _ \\ '__| '_ ` _ \\\\___ \\| | | |_) |\n | | __/ | | | | | | |___) | |___| __/ \n |_|\\___|_| |_| |_| |_|____/ \\____|_| \n")
|
||||
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD))
|
||||
}
|
||||
|
||||
/// ### draw_new_version
|
||||
///
|
||||
/// Draw new version disclaimer
|
||||
fn draw_new_version(&self) -> Paragraph {
|
||||
let content: String = match self.new_version.as_ref() {
|
||||
Some(ver) => format!("TermSCP {} is now available! Download it from <https://github.com/veeso/termscp/releases/latest>", ver),
|
||||
None => String::new(),
|
||||
};
|
||||
Paragraph::new(content).style(
|
||||
Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### 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; "),
|
||||
Span::styled(
|
||||
"<CTRL+C>",
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Cyan),
|
||||
),
|
||||
Span::raw(" to enter setup"),
|
||||
],
|
||||
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_local_explorer
|
||||
///
|
||||
/// Draw local explorer list
|
||||
fn draw_bookmarks_tab(&self) -> Option<List> {
|
||||
self.bookmarks_client.as_ref()?;
|
||||
let hosts: Vec<ListItem> = self
|
||||
.bookmarks_list
|
||||
.iter()
|
||||
.map(|key: &String| {
|
||||
let entry: (String, u16, FileTransferProtocol, String, _) = self
|
||||
.bookmarks_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_bookmark(key)
|
||||
.unwrap();
|
||||
ListItem::new(Span::from(format!(
|
||||
"{} ({}://{}@{}:{})",
|
||||
key,
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
)))
|
||||
})
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.input_form {
|
||||
InputForm::Bookmarks => (Color::Black, Color::LightGreen),
|
||||
_ => (Color::Reset, Color::Reset),
|
||||
};
|
||||
Some(
|
||||
List::new(hosts)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(match self.input_form {
|
||||
InputForm::Bookmarks => Style::default().fg(Color::LightGreen),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.title("Bookmarks"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD)),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_local_explorer
|
||||
///
|
||||
/// Draw local explorer list
|
||||
fn draw_recents_tab(&self) -> Option<List> {
|
||||
self.bookmarks_client.as_ref()?;
|
||||
let hosts: Vec<ListItem> = self
|
||||
.recents_list
|
||||
.iter()
|
||||
.map(|key: &String| {
|
||||
let entry: (String, u16, FileTransferProtocol, String) = self
|
||||
.bookmarks_client
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_recent(key)
|
||||
.unwrap();
|
||||
ListItem::new(Span::from(format!(
|
||||
"{}://{}@{}:{}",
|
||||
entry.2.to_string().to_lowercase(),
|
||||
entry.3,
|
||||
entry.0,
|
||||
entry.1
|
||||
)))
|
||||
})
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.input_form {
|
||||
InputForm::Recents => (Color::Black, Color::LightBlue),
|
||||
_ => (Color::Reset, Color::Reset),
|
||||
};
|
||||
Some(
|
||||
List::new(hosts)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(match self.input_form {
|
||||
InputForm::Recents => Style::default().fg(Color::LightBlue),
|
||||
_ => Style::default(),
|
||||
})
|
||||
.title("Recent connections"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD)),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### 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_input
|
||||
///
|
||||
/// Draw input popup
|
||||
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(
|
||||
Block::default()
|
||||
.borders(Borders::TOP | Borders::RIGHT | Borders::LEFT)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Save bookmark as..."),
|
||||
);
|
||||
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
||||
let index: usize = match self.choice_opt {
|
||||
DialogYesNoOption::Yes => 0,
|
||||
DialogYesNoOption::No => 1,
|
||||
};
|
||||
let tabs: Tabs = Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Save password?"),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::LightRed),
|
||||
);
|
||||
(input, tabs)
|
||||
}
|
||||
|
||||
/// ### 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.choice_opt {
|
||||
DialogYesNoOption::Yes => 0,
|
||||
DialogYesNoOption::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("Quit TermSCP"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<TAB>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Switch input form and bookmarks"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<RIGHT/LEFT>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Change bookmark tab"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<UP/DOWN>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Move up/down in current tab"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<ENTER>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Submit"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<DEL>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Delete bookmark"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<E>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
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>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Show help"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+S>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Save bookmark"),
|
||||
])),
|
||||
];
|
||||
List::new(cmds)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default())
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Help"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
}
|
||||
}
|
||||
291
src/ui/activities/auth_activity/mod.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
//! ## AuthActivity
|
||||
//!
|
||||
//! `auth_activity` is the module which implements the authentication activity
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Sub modules
|
||||
mod bookmarks;
|
||||
mod callbacks;
|
||||
mod input;
|
||||
mod layout;
|
||||
|
||||
// Dependencies
|
||||
extern crate crossterm;
|
||||
extern crate tui;
|
||||
extern crate unicode_width;
|
||||
|
||||
// locals
|
||||
use super::{Activity, Context};
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
use crate::system::bookmarks_client::BookmarksClient;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::environment;
|
||||
use crate::utils::git;
|
||||
|
||||
// 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
|
||||
type DialogCallback = fn(&mut AuthActivity);
|
||||
|
||||
/// ### InputField
|
||||
///
|
||||
/// InputField describes the current input field to edit
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
enum InputField {
|
||||
Address,
|
||||
Port,
|
||||
Protocol,
|
||||
Username,
|
||||
Password,
|
||||
}
|
||||
|
||||
/// ### DialogYesNoOption
|
||||
///
|
||||
/// Current yes/no dialog option
|
||||
#[derive(std::cmp::PartialEq, Clone)]
|
||||
enum DialogYesNoOption {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
/// ### Popup
|
||||
///
|
||||
/// Popup describes the type of the popup displayed
|
||||
#[derive(Clone)]
|
||||
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
|
||||
}
|
||||
|
||||
#[derive(std::cmp::PartialEq)]
|
||||
/// ### InputForm
|
||||
///
|
||||
/// InputForm describes the selected input form
|
||||
enum InputForm {
|
||||
AuthCredentials,
|
||||
Bookmarks,
|
||||
Recents,
|
||||
}
|
||||
|
||||
/// ### AuthActivity
|
||||
///
|
||||
/// AuthActivity is the data holder for the authentication activity
|
||||
pub struct AuthActivity {
|
||||
pub address: String,
|
||||
pub port: String,
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub username: String,
|
||||
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
|
||||
popup: Option<Popup>,
|
||||
input_form: InputForm,
|
||||
password_placeholder: String,
|
||||
redraw: bool, // Should ui actually be redrawned?
|
||||
input_txt: String, // Input text
|
||||
choice_opt: DialogYesNoOption, // Dialog popup selected option
|
||||
bookmarks_idx: usize, // Index of selected bookmark
|
||||
bookmarks_list: Vec<String>, // List of bookmarks
|
||||
recents_idx: usize, // Index of selected recent
|
||||
recents_list: Vec<String>, // list of recents
|
||||
// misc
|
||||
new_version: Option<String>, // Contains new version of termscp
|
||||
}
|
||||
|
||||
impl Default for AuthActivity {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthActivity {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new AuthActivity
|
||||
pub fn new() -> AuthActivity {
|
||||
AuthActivity {
|
||||
address: String::new(),
|
||||
port: String::from("22"),
|
||||
protocol: FileTransferProtocol::Sftp,
|
||||
username: String::new(),
|
||||
password: String::new(),
|
||||
submit: false,
|
||||
quit: false,
|
||||
setup: false,
|
||||
context: None,
|
||||
bookmarks_client: None,
|
||||
config_client: None,
|
||||
selected_field: InputField::Address,
|
||||
popup: None,
|
||||
input_form: InputForm::AuthCredentials,
|
||||
password_placeholder: String::new(),
|
||||
redraw: true, // True at startup
|
||||
input_txt: String::new(),
|
||||
choice_opt: DialogYesNoOption::Yes,
|
||||
bookmarks_idx: 0,
|
||||
bookmarks_list: Vec::new(),
|
||||
recents_idx: 0,
|
||||
recents_list: Vec::new(),
|
||||
new_version: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### 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),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_create
|
||||
///
|
||||
/// If enabled in configuration, check for updates from Github
|
||||
fn check_for_updates(&mut self) {
|
||||
if let Some(client) = self.config_client.as_ref() {
|
||||
if client.get_check_for_updates() {
|
||||
// Send request
|
||||
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
|
||||
Ok(version) => self.new_version = version,
|
||||
Err(err) => {
|
||||
// Report error
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not check for new updates: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Activity for AuthActivity {
|
||||
/// ### 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();
|
||||
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();
|
||||
}
|
||||
// If check for updates is enabled, check for updates
|
||||
self.check_for_updates();
|
||||
}
|
||||
|
||||
/// ### 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(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
// Set redraw to true
|
||||
self.redraw = true;
|
||||
// Handle event
|
||||
self.handle_input_event(&event);
|
||||
}
|
||||
// Redraw if necessary
|
||||
if self.redraw {
|
||||
// Draw
|
||||
self.draw();
|
||||
// Set redraw 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -19,10 +23,10 @@
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{FileExplorerTab, FileTransferActivity, FsEntry, InputMode, LogLevel, PopupType};
|
||||
|
||||
// Locals
|
||||
use super::{FileExplorerTab, FileTransferActivity, FsEntry, LogLevel};
|
||||
// Ext
|
||||
use std::path::PathBuf;
|
||||
use tui::style::Color;
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### callback_nothing_to_do
|
||||
@@ -40,7 +44,7 @@ impl FileTransferActivity {
|
||||
// If path is relative, concat pwd
|
||||
let abs_dir_path: PathBuf = match dir_path.is_relative() {
|
||||
true => {
|
||||
let mut d: PathBuf = self.context.as_ref().unwrap().local.pwd();
|
||||
let mut d: PathBuf = self.local.wrkdir.clone();
|
||||
d.push(dir_path);
|
||||
d
|
||||
}
|
||||
@@ -51,19 +55,11 @@ impl FileTransferActivity {
|
||||
FileExplorerTab::Remote => {
|
||||
// If path is relative, concat pwd
|
||||
let abs_dir_path: PathBuf = match dir_path.is_relative() {
|
||||
true => match self.client.pwd() {
|
||||
Ok(mut wkrdir) => {
|
||||
wkrdir.push(dir_path);
|
||||
wkrdir
|
||||
}
|
||||
Err(err) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not retrieve current directory: {}", err),
|
||||
));
|
||||
return;
|
||||
}
|
||||
},
|
||||
true => {
|
||||
let mut wrkdir: PathBuf = self.remote.wrkdir.clone();
|
||||
wrkdir.push(dir_path);
|
||||
wrkdir
|
||||
}
|
||||
false => dir_path,
|
||||
};
|
||||
self.remote_changedir(abs_dir_path.as_path(), true);
|
||||
@@ -71,6 +67,77 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_copy
|
||||
///
|
||||
/// Callback for COPY command (both from local and remote)
|
||||
pub(super) fn callback_copy(&mut self, input: String) {
|
||||
let dest_path: PathBuf = PathBuf::from(input);
|
||||
match self.tab {
|
||||
FileExplorerTab::Local => {
|
||||
// Get selected entry
|
||||
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(_) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Copied \"{}\" to \"{}\"",
|
||||
entry.get_abs_path().display(),
|
||||
dest_path.display()
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
// Reload entries
|
||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
||||
self.local_scan(wrkdir.as_path());
|
||||
}
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not copy \"{}\" to \"{}\": {}",
|
||||
entry.get_abs_path().display(),
|
||||
dest_path.display(),
|
||||
err
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
// Get selected entry
|
||||
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(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Copied \"{}\" to \"{}\"",
|
||||
entry.get_abs_path().display(),
|
||||
dest_path.display()
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
self.reload_remote_dir();
|
||||
}
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not copy \"{}\" to \"{}\": {}",
|
||||
entry.get_abs_path().display(),
|
||||
dest_path.display(),
|
||||
err
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_mkdir
|
||||
///
|
||||
/// Callback for MKDIR command (supports both local and remote)
|
||||
@@ -90,24 +157,24 @@ impl FileTransferActivity {
|
||||
LogLevel::Info,
|
||||
format!("Created directory \"{}\"", input).as_ref(),
|
||||
);
|
||||
let wrkdir: PathBuf = self.context.as_ref().unwrap().local.pwd();
|
||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
||||
self.local_scan(wrkdir.as_path());
|
||||
}
|
||||
Err(err) => {
|
||||
// Report err
|
||||
self.log(
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create directory \"{}\": {}", input, err).as_ref(),
|
||||
);
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not create directory \"{}\": {}", input, err),
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
match self.client.mkdir(PathBuf::from(input.as_str()).as_path()) {
|
||||
match self
|
||||
.client
|
||||
.as_mut()
|
||||
.mkdir(PathBuf::from(input.as_str()).as_path())
|
||||
{
|
||||
Ok(_) => {
|
||||
// Reload files
|
||||
self.log(
|
||||
@@ -118,14 +185,10 @@ impl FileTransferActivity {
|
||||
}
|
||||
Err(err) => {
|
||||
// Report err
|
||||
self.log(
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create directory \"{}\": {}", input, err).as_ref(),
|
||||
);
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not create directory \"{}\": {}", input, err),
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,12 +204,12 @@ impl FileTransferActivity {
|
||||
let mut dst_path: PathBuf = PathBuf::from(input);
|
||||
// Check if path is relative
|
||||
if dst_path.as_path().is_relative() {
|
||||
let mut wrkdir: PathBuf = self.context.as_ref().unwrap().local.pwd();
|
||||
let mut wrkdir: PathBuf = self.local.wrkdir.clone();
|
||||
wrkdir.push(dst_path);
|
||||
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
|
||||
@@ -158,7 +221,8 @@ impl FileTransferActivity {
|
||||
{
|
||||
Ok(_) => {
|
||||
// Reload files
|
||||
self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
|
||||
let path: PathBuf = self.local.wrkdir.clone();
|
||||
self.local_scan(path.as_path());
|
||||
// Log
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
@@ -171,35 +235,29 @@ impl FileTransferActivity {
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log(
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not rename file \"{}\": {}",
|
||||
full_path.display(),
|
||||
err
|
||||
)
|
||||
.as_ref(),
|
||||
),
|
||||
);
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not rename file: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
match self.client.rename(entry, dst_path.as_path()) {
|
||||
match self.client.as_mut().rename(entry, dst_path.as_path()) {
|
||||
Ok(_) => {
|
||||
// Reload files
|
||||
if let Ok(path) = self.client.pwd() {
|
||||
self.remote_scan(path.as_path());
|
||||
}
|
||||
let path: PathBuf = self.remote.wrkdir.clone();
|
||||
self.remote_scan(path.as_path());
|
||||
// Log
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
@@ -212,19 +270,14 @@ impl FileTransferActivity {
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log(
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not rename file \"{}\": {}",
|
||||
full_path.display(),
|
||||
err
|
||||
)
|
||||
.as_ref(),
|
||||
),
|
||||
);
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not rename file: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,13 +293,14 @@ 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) {
|
||||
Ok(_) => {
|
||||
// Reload files
|
||||
self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
|
||||
let p: PathBuf = self.local.wrkdir.clone();
|
||||
self.local_scan(p.as_path());
|
||||
// Log
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
@@ -254,26 +308,21 @@ impl FileTransferActivity {
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log(
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not delete file \"{}\": {}",
|
||||
full_path.display(),
|
||||
err
|
||||
)
|
||||
.as_ref(),
|
||||
),
|
||||
);
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not delete file: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -285,19 +334,14 @@ impl FileTransferActivity {
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log(
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not delete file \"{}\": {}",
|
||||
full_path.display(),
|
||||
err
|
||||
)
|
||||
.as_ref(),
|
||||
),
|
||||
);
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not delete file: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,37 +357,132 @@ impl FileTransferActivity {
|
||||
match self.tab {
|
||||
FileExplorerTab::Local => {
|
||||
// Get pwd
|
||||
let wrkdir: PathBuf = match self.client.pwd() {
|
||||
Ok(p) => p,
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!("Could not get current remote path: {}", err).as_ref(),
|
||||
);
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not get current remote path: {}", err),
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
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
|
||||
self.filetransfer_recv(
|
||||
&file.get_realfile(),
|
||||
self.context.as_ref().unwrap().local.pwd().as_path(),
|
||||
Some(input),
|
||||
let wrkdir: PathBuf = self.local.wrkdir.clone();
|
||||
self.filetransfer_recv(&file.get_realfile(), wrkdir.as_path(), Some(input));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_new_file
|
||||
///
|
||||
/// Create a new file in current directory with `input` as name
|
||||
pub(super) fn callback_new_file(&mut self, input: String) {
|
||||
match self.tab {
|
||||
FileExplorerTab::Local => {
|
||||
// Check if file exists
|
||||
let mut file_exists: bool = false;
|
||||
for file in self.local.iter_files_all() {
|
||||
if input == file.get_name() {
|
||||
file_exists = true;
|
||||
}
|
||||
}
|
||||
if file_exists {
|
||||
self.log_and_alert(
|
||||
LogLevel::Warn,
|
||||
format!("File \"{}\" already exists", input,),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Create file
|
||||
let file_path: PathBuf = PathBuf::from(input.as_str());
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
if let Err(err) = ctx.local.open_file_write(file_path.as_path()) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create file \"{}\": {}", file_path.display(), err),
|
||||
);
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Created file \"{}\"", file_path.display()).as_str(),
|
||||
);
|
||||
// Reload files
|
||||
let path: PathBuf = self.local.wrkdir.clone();
|
||||
self.local_scan(path.as_path());
|
||||
}
|
||||
}
|
||||
FileExplorerTab::Remote => {
|
||||
// Check if file exists
|
||||
let mut file_exists: bool = false;
|
||||
for file in self.remote.iter_files_all() {
|
||||
if input == file.get_name() {
|
||||
file_exists = true;
|
||||
}
|
||||
}
|
||||
if file_exists {
|
||||
self.log_and_alert(
|
||||
LogLevel::Warn,
|
||||
format!("File \"{}\" already exists", input,),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Get path on remote
|
||||
let file_path: PathBuf = PathBuf::from(input.as_str());
|
||||
// Create file (on local)
|
||||
match tempfile::NamedTempFile::new() {
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not create tempfile: {}", err),
|
||||
),
|
||||
Ok(tfile) => {
|
||||
// Stat tempfile
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
let local_file: FsEntry = match ctx.local.stat(tfile.path()) {
|
||||
Err(err) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("Could not stat tempfile: {}", err),
|
||||
);
|
||||
return;
|
||||
}
|
||||
Ok(f) => f,
|
||||
};
|
||||
if let FsEntry::File(local_file) = local_file {
|
||||
// Create file
|
||||
match self.client.send_file(&local_file, file_path.as_path()) {
|
||||
Err(err) => self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"Could not create file \"{}\": {}",
|
||||
file_path.display(),
|
||||
err
|
||||
),
|
||||
),
|
||||
Ok(writer) => {
|
||||
// Finalize write
|
||||
if let Err(err) = self.client.on_sent(writer) {
|
||||
self.log_and_alert(
|
||||
LogLevel::Warn,
|
||||
format!("Could not finalize file: {}", err),
|
||||
);
|
||||
}
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Created file \"{}\"", file_path.display())
|
||||
.as_str(),
|
||||
);
|
||||
// Reload files
|
||||
let path: PathBuf = self.remote.wrkdir.clone();
|
||||
self.remote_scan(path.as_path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -19,14 +23,17 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// 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;
|
||||
use tui::style::Color;
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// ### read_input_event
|
||||
@@ -34,17 +41,11 @@ impl FileTransferActivity {
|
||||
/// Read one event.
|
||||
/// Returns whether at least one event has been handled
|
||||
pub(super) fn read_input_event(&mut self) -> bool {
|
||||
if let Ok(event) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
// Iterate over input events
|
||||
if let Some(event) = event {
|
||||
// Handle event
|
||||
self.handle_input_event(&event);
|
||||
// Return true
|
||||
true
|
||||
} else {
|
||||
// No event
|
||||
false
|
||||
}
|
||||
if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
// Handle event
|
||||
self.handle_input_event(&event);
|
||||
// Return true
|
||||
true
|
||||
} else {
|
||||
// Error
|
||||
false
|
||||
@@ -54,16 +55,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);
|
||||
}
|
||||
@@ -74,7 +75,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 {
|
||||
@@ -89,53 +90,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 = Some(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) => {
|
||||
@@ -161,14 +149,16 @@ 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(),
|
||||
};
|
||||
// Default choice to NO for delete!
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// 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,
|
||||
@@ -176,20 +166,40 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'q' | 'Q' => {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
'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.popup = Some(Popup::Input(
|
||||
String::from("Insert destination name"),
|
||||
FileTransferActivity::callback_copy,
|
||||
));
|
||||
}
|
||||
'd' | 'D' => {
|
||||
// Make directory
|
||||
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(),
|
||||
};
|
||||
// Default choice to NO for delete!
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// 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,
|
||||
@@ -199,29 +209,65 @@ 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,
|
||||
));
|
||||
}
|
||||
'd' | 'D' => {
|
||||
// Make directory
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert directory name"),
|
||||
FileTransferActivity::callback_mkdir,
|
||||
));
|
||||
}
|
||||
'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.get_current_file().is_some() {
|
||||
// Clone entry due to mutable stuff...
|
||||
let fsentry: FsEntry = self.local.get_current_file().unwrap().clone();
|
||||
// Check if file
|
||||
if fsentry.is_file() {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"Opening file \"{}\"...",
|
||||
fsentry.get_abs_path().display()
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
// Edit file
|
||||
match self.edit_local_file(fsentry.get_abs_path().as_path()) {
|
||||
Ok(_) => {
|
||||
// Reload directory
|
||||
let pwd: PathBuf = self.local.wrkdir.clone();
|
||||
self.local_scan(pwd.as_path());
|
||||
}
|
||||
Err(err) => self.log_and_alert(LogLevel::Error, err),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'q' | 'Q' => {
|
||||
// Create quit prompt dialog
|
||||
self.popup = Some(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,
|
||||
));
|
||||
@@ -229,7 +275,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,
|
||||
));
|
||||
@@ -237,32 +283,18 @@ impl FileTransferActivity {
|
||||
'u' | 'U' => {
|
||||
// Go to parent directory
|
||||
// Get pwd
|
||||
let path: PathBuf = self.context.as_ref().unwrap().local.pwd();
|
||||
let path: PathBuf = self.local.wrkdir.clone();
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.local_changedir(parent, true);
|
||||
}
|
||||
}
|
||||
' ' => {
|
||||
// Get pwd
|
||||
let wrkdir: PathBuf = match self.client.pwd() {
|
||||
Ok(p) => p,
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!("Could not get current remote path: {}", err).as_ref(),
|
||||
);
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not get current remote path: {}", err),
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
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(),
|
||||
@@ -281,53 +313,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 = Some(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) => {
|
||||
@@ -353,14 +372,16 @@ 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(),
|
||||
};
|
||||
// Default choice to NO for delete!
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// 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,
|
||||
@@ -368,20 +389,40 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'q' | 'Q' => {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
'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.popup = Some(Popup::Input(
|
||||
String::from("Insert destination name"),
|
||||
FileTransferActivity::callback_copy,
|
||||
));
|
||||
}
|
||||
'd' | 'D' => {
|
||||
// Make directory
|
||||
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(),
|
||||
};
|
||||
// Default choice to NO for delete!
|
||||
self.choice_opt = DialogYesNoOption::No;
|
||||
// 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,
|
||||
@@ -391,29 +432,63 @@ 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,
|
||||
));
|
||||
}
|
||||
'd' | 'D' => {
|
||||
// Make directory
|
||||
self.input_mode = InputMode::Popup(PopupType::Input(
|
||||
String::from("Insert directory name"),
|
||||
FileTransferActivity::callback_mkdir,
|
||||
));
|
||||
}
|
||||
'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.get_current_file().is_some() {
|
||||
// Clone entry due to mutable stuff...
|
||||
let fsentry: FsEntry = self.remote.get_current_file().unwrap().clone();
|
||||
// Check if file
|
||||
if let FsEntry::File(file) = fsentry {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("Opening file \"{}\"...", file.abs_path.display())
|
||||
.as_str(),
|
||||
);
|
||||
// Edit file
|
||||
match self.edit_remote_file(&file) {
|
||||
Ok(_) => {
|
||||
// Reload directory
|
||||
let pwd: PathBuf = self.remote.wrkdir.clone();
|
||||
self.remote_scan(pwd.as_path());
|
||||
}
|
||||
Err(err) => self.log_and_alert(LogLevel::Error, err),
|
||||
}
|
||||
// Put input mode back to normal
|
||||
self.popup = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
'q' | 'Q' => {
|
||||
// Create quit prompt dialog
|
||||
self.popup = Some(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,
|
||||
));
|
||||
@@ -421,38 +496,29 @@ 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,
|
||||
));
|
||||
}
|
||||
'u' | 'U' => {
|
||||
// Go to parent directory
|
||||
// Get pwd
|
||||
match self.client.pwd() {
|
||||
Ok(path) => {
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.remote_changedir(parent, true);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.input_mode = InputMode::Popup(PopupType::Alert(
|
||||
Color::Red,
|
||||
format!("Could not change working directory: {}", err),
|
||||
))
|
||||
}
|
||||
let path: PathBuf = self.remote.wrkdir.clone();
|
||||
// Go to parent directory
|
||||
if let Some(parent) = path.as_path().parent() {
|
||||
self.remote_changedir(parent, true);
|
||||
}
|
||||
}
|
||||
' ' => {
|
||||
// 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(
|
||||
&file.get_realfile(),
|
||||
self.context.as_ref().unwrap().local.pwd().as_path(),
|
||||
wrkdir.as_path(),
|
||||
Some(name),
|
||||
);
|
||||
}
|
||||
@@ -467,7 +533,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 {
|
||||
@@ -475,7 +541,7 @@ impl FileTransferActivity {
|
||||
KeyCode::Esc => {
|
||||
// Handle quit event
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_disconnect_popup();
|
||||
self.popup = Some(self.create_disconnect_popup());
|
||||
}
|
||||
KeyCode::Tab => self.switch_input_field(), // <TAB> switch tab
|
||||
KeyCode::Down => {
|
||||
@@ -514,7 +580,7 @@ impl FileTransferActivity {
|
||||
KeyCode::Char(ch) => match ch {
|
||||
'q' | 'Q' => {
|
||||
// Create quit prompt dialog
|
||||
self.input_mode = self.create_quit_popup();
|
||||
self.popup = Some(self.create_quit_popup());
|
||||
}
|
||||
_ => { /* Nothing to do */ }
|
||||
},
|
||||
@@ -526,16 +592,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)
|
||||
}
|
||||
}
|
||||
@@ -544,12 +611,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -557,13 +624,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 */ }
|
||||
}
|
||||
@@ -573,28 +688,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,11 +701,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 {
|
||||
@@ -615,7 +710,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
|
||||
@@ -623,7 +718,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);
|
||||
}
|
||||
@@ -636,10 +731,10 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_explorer_alert
|
||||
/// ### 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
|
||||
@@ -651,17 +746,17 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### handle_input_event_mode_explorer_alert
|
||||
/// ### 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_explorer_alert
|
||||
/// ### 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,
|
||||
@@ -672,7 +767,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),
|
||||
@@ -687,4 +782,30 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### move_sorting_mode_opt_left
|
||||
///
|
||||
/// Perform <LEFT> on file sorting dialog
|
||||
fn move_sorting_mode_opt_left(explorer: &mut FileExplorer) {
|
||||
let curr_sorting: FileSorting = explorer.get_file_sorting();
|
||||
explorer.sort_by(match curr_sorting {
|
||||
FileSorting::BySize => FileSorting::ByCreationTime,
|
||||
FileSorting::ByCreationTime => FileSorting::ByModifyTime,
|
||||
FileSorting::ByModifyTime => FileSorting::ByName,
|
||||
FileSorting::ByName => FileSorting::BySize, // Wrap
|
||||
});
|
||||
}
|
||||
|
||||
/// ### move_sorting_mode_opt_left
|
||||
///
|
||||
/// Perform <RIGHT> on file sorting dialog
|
||||
fn move_sorting_mode_opt_right(explorer: &mut FileExplorer) {
|
||||
let curr_sorting: FileSorting = explorer.get_file_sorting();
|
||||
explorer.sort_by(match curr_sorting {
|
||||
FileSorting::ByName => FileSorting::ByModifyTime,
|
||||
FileSorting::ByModifyTime => FileSorting::ByCreationTime,
|
||||
FileSorting::ByCreationTime => FileSorting::BySize,
|
||||
FileSorting::BySize => FileSorting::ByName, // Wrap
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
//! ## FileTransferActivity
|
||||
//!
|
||||
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -19,24 +23,28 @@
|
||||
*
|
||||
*/
|
||||
|
||||
// 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::utils::time_to_str;
|
||||
|
||||
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::{
|
||||
layout::{Constraint, Corner, Direction, Layout, Rect},
|
||||
style::{Color, Modifier, Style},
|
||||
text::{Span, Spans},
|
||||
widgets::{Block, Borders, Clear, Gauge, List, ListItem, ListState, Paragraph, Tabs},
|
||||
widgets::{
|
||||
Block, BorderType, Borders, Clear, Gauge, List, ListItem, ListState, Paragraph, Tabs,
|
||||
},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
|
||||
@@ -48,7 +56,6 @@ impl FileTransferActivity {
|
||||
/// Draw UI
|
||||
pub(super) fn draw(&mut self) {
|
||||
let mut ctx: Context = self.context.take().unwrap();
|
||||
let local_wrkdir: PathBuf = ctx.local.pwd();
|
||||
let _ = ctx.terminal.draw(|f| {
|
||||
// Prepare chunks
|
||||
let chunks = Layout::default()
|
||||
@@ -56,8 +63,8 @@ impl FileTransferActivity {
|
||||
.margin(1)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Length(20), // Explorer
|
||||
Constraint::Length(16), // Log
|
||||
Constraint::Percentage(70), // Explorer
|
||||
Constraint::Percentage(30), // Log
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
@@ -69,23 +76,18 @@ 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(local_wrkdir, tabs_chunks[0].width),
|
||||
self.draw_local_explorer(tabs_chunks[0].width),
|
||||
tabs_chunks[0],
|
||||
&mut localhost_state,
|
||||
);
|
||||
// Get pwd
|
||||
let remote_wrkdir: PathBuf = match self.client.pwd() {
|
||||
Ok(p) => p,
|
||||
Err(_) => PathBuf::from("/"),
|
||||
};
|
||||
f.render_stateful_widget(
|
||||
self.draw_remote_explorer(remote_wrkdir, tabs_chunks[1].width),
|
||||
self.draw_remote_explorer(tabs_chunks[1].width),
|
||||
tabs_chunks[1],
|
||||
&mut remote_state,
|
||||
);
|
||||
@@ -99,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(
|
||||
@@ -132,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)
|
||||
}
|
||||
}
|
||||
@@ -151,7 +157,7 @@ impl FileTransferActivity {
|
||||
/// ### draw_local_explorer
|
||||
///
|
||||
/// Draw local explorer list
|
||||
pub(super) fn draw_local_explorer(&self, local_wrkdir: PathBuf, width: u16) -> List {
|
||||
pub(super) fn draw_local_explorer(&self, width: u16) -> List {
|
||||
let hostname: String = match hostname::get() {
|
||||
Ok(h) => {
|
||||
let hostname: String = h.as_os_str().to_string_lossy().to_string();
|
||||
@@ -162,17 +168,21 @@ impl FileTransferActivity {
|
||||
};
|
||||
let files: Vec<ListItem> = self
|
||||
.local
|
||||
.files
|
||||
.iter()
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
|
||||
.iter_files()
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(self.local.fmt_file(entry))))
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.tab {
|
||||
FileExplorerTab::Local => (Color::Black, Color::LightYellow),
|
||||
_ => (Color::LightYellow, Color::Reset),
|
||||
};
|
||||
List::new(files)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(match self.input_field {
|
||||
InputField::Explorer => match self.tab {
|
||||
FileExplorerTab::Local => Style::default().fg(Color::Yellow),
|
||||
FileExplorerTab::Local => Style::default().fg(Color::LightYellow),
|
||||
_ => Style::default(),
|
||||
},
|
||||
_ => Style::default(),
|
||||
@@ -181,7 +191,7 @@ impl FileTransferActivity {
|
||||
"{}:{} ",
|
||||
hostname,
|
||||
FileTransferActivity::elide_wrkdir_path(
|
||||
local_wrkdir.as_path(),
|
||||
self.local.wrkdir.as_path(),
|
||||
hostname.as_str(),
|
||||
width
|
||||
)
|
||||
@@ -189,24 +199,23 @@ impl FileTransferActivity {
|
||||
)),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::LightYellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_style(Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD))
|
||||
}
|
||||
|
||||
/// ### draw_remote_explorer
|
||||
///
|
||||
/// Draw remote explorer list
|
||||
pub(super) fn draw_remote_explorer(&self, remote_wrkdir: PathBuf, width: u16) -> List {
|
||||
pub(super) fn draw_remote_explorer(&self, width: u16) -> List {
|
||||
let files: Vec<ListItem> = self
|
||||
.remote
|
||||
.files
|
||||
.iter()
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(format!("{}", entry))))
|
||||
.iter_files()
|
||||
.map(|entry: &FsEntry| ListItem::new(Span::from(self.remote.fmt_file(entry))))
|
||||
.collect();
|
||||
// Get colors to use; highlight element inverting fg/bg only when tab is active
|
||||
let (fg, bg): (Color, Color) = match self.tab {
|
||||
FileExplorerTab::Remote => (Color::Black, Color::LightBlue),
|
||||
_ => (Color::LightBlue, Color::Reset),
|
||||
};
|
||||
List::new(files)
|
||||
.block(
|
||||
Block::default()
|
||||
@@ -222,7 +231,7 @@ impl FileTransferActivity {
|
||||
"{}:{} ",
|
||||
self.params.address,
|
||||
FileTransferActivity::elide_wrkdir_path(
|
||||
remote_wrkdir.as_path(),
|
||||
self.remote.wrkdir.as_path(),
|
||||
self.params.address.as_str(),
|
||||
width
|
||||
)
|
||||
@@ -230,12 +239,7 @@ impl FileTransferActivity {
|
||||
)),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.bg(Color::LightBlue)
|
||||
.fg(Color::Black)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)
|
||||
.highlight_style(Style::default().bg(bg).fg(fg).add_modifier(Modifier::BOLD))
|
||||
}
|
||||
|
||||
/// ### draw_log_list
|
||||
@@ -334,15 +338,14 @@ impl FileTransferActivity {
|
||||
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(
|
||||
FileTransferActivity::align_text_center(msg, width),
|
||||
)));
|
||||
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)
|
||||
@@ -357,27 +360,69 @@ impl FileTransferActivity {
|
||||
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(
|
||||
FileTransferActivity::align_text_center(msg, width),
|
||||
)));
|
||||
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::Red))
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Fatal error"),
|
||||
)
|
||||
.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
|
||||
pub(super) fn draw_popup_input(&self, text: String) -> Paragraph {
|
||||
Paragraph::new(self.input_txt.as_ref())
|
||||
.style(Style::default().fg(Color::White))
|
||||
.block(Block::default().borders(Borders::ALL).title(text))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(text),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_popup_progress
|
||||
@@ -385,16 +430,22 @@ impl FileTransferActivity {
|
||||
/// Draw progress popup
|
||||
pub(super) fn draw_popup_progress(&self, text: String) -> Gauge {
|
||||
// Calculate ETA
|
||||
let elapsed_secs: u64 = self.transfer.started.elapsed().as_secs();
|
||||
let eta: String = match self.transfer.progress as u64 {
|
||||
0 => String::from("--:--"), // NOTE: would divide by 0 :D
|
||||
_ => {
|
||||
let elapsed_secs: u64 = self.transfer.started.elapsed().as_secs();
|
||||
let eta: u64 =
|
||||
((elapsed_secs * 100) / (self.transfer.progress as u64)) - elapsed_secs;
|
||||
format!("{:0width$}:{:0width$}", (eta / 60), (eta % 60), width = 2)
|
||||
}
|
||||
};
|
||||
let label = format!("{:.2}% - ETA {}", self.transfer.progress, eta);
|
||||
// Calculate bytes/s
|
||||
let label = format!(
|
||||
"{:.2}% - ETA {} ({}/s)",
|
||||
self.transfer.progress,
|
||||
eta,
|
||||
ByteSize(self.transfer.bytes_per_second())
|
||||
);
|
||||
Gauge::default()
|
||||
.block(Block::default().borders(Borders::ALL).title(text))
|
||||
.gauge_style(
|
||||
@@ -415,15 +466,14 @@ impl FileTransferActivity {
|
||||
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(
|
||||
FileTransferActivity::align_text_center(msg, width),
|
||||
)));
|
||||
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::White))
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Please wait"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
@@ -440,7 +490,12 @@ impl FileTransferActivity {
|
||||
DialogYesNoOption::No => 1,
|
||||
};
|
||||
Tabs::new(choices)
|
||||
.block(Block::default().borders(Borders::ALL).title(text))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(text),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
@@ -459,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,
|
||||
},
|
||||
@@ -474,11 +529,10 @@ impl FileTransferActivity {
|
||||
Some(fsentry) => {
|
||||
// Get name and path
|
||||
let abs_path: PathBuf = fsentry.get_abs_path();
|
||||
let name: String = fsentry.get_name();
|
||||
let ctime: String = time_to_str(fsentry.get_creation_time(), "%b %d %Y %H:%M:%S");
|
||||
let atime: String =
|
||||
time_to_str(fsentry.get_last_access_time(), "%b %d %Y %H:%M:%S");
|
||||
let mtime: String = time_to_str(fsentry.get_creation_time(), "%b %d %Y %H:%M:%S");
|
||||
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 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());
|
||||
let user: Option<u32> = fsentry.get_user();
|
||||
@@ -605,6 +659,7 @@ impl FileTransferActivity {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default())
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(file_name),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
@@ -624,7 +679,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("disconnect"),
|
||||
Span::raw("Disconnect"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -654,7 +709,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("change explorer tab"),
|
||||
Span::raw("Change explorer tab"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -664,7 +719,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("move up/down in list"),
|
||||
Span::raw("Move up/down in list"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -674,7 +729,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("scroll up/down in list quickly"),
|
||||
Span::raw("Scroll up/down in list quickly"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -684,7 +739,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("enter directory"),
|
||||
Span::raw("Enter directory"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -694,7 +749,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("upload/download file"),
|
||||
Span::raw("Upload/download file"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -704,7 +759,37 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("delete file"),
|
||||
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>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Copy"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -714,7 +799,17 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("make directory"),
|
||||
Span::raw("Make directory"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<E>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Same as <DEL>"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -724,7 +819,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("goto path"),
|
||||
Span::raw("Goto path"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -734,7 +829,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("show help"),
|
||||
Span::raw("Show help"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -744,7 +839,37 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("show info about the selected file or directory"),
|
||||
Span::raw("Show info about the selected file or directory"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<L>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
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(
|
||||
@@ -764,7 +889,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("rename file"),
|
||||
Span::raw("Rename file"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -774,7 +899,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("go to parent directory"),
|
||||
Span::raw("Go to parent directory"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
@@ -784,7 +909,7 @@ impl FileTransferActivity {
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("abort current file transfer"),
|
||||
Span::raw("Abort current file transfer"),
|
||||
])),
|
||||
];
|
||||
List::new(cmds)
|
||||
@@ -792,26 +917,12 @@ impl FileTransferActivity {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default())
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Help"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
}
|
||||
|
||||
/// align_text_center
|
||||
///
|
||||
/// Align text to center for a given width
|
||||
fn align_text_center(text: &str, width: u16) -> String {
|
||||
let indent_size: usize = match (width as usize) >= text.len() {
|
||||
// NOTE: The check prevents underflow
|
||||
true => (width as usize - text.len()) / 2,
|
||||
false => 0,
|
||||
};
|
||||
textwrap::indent(
|
||||
text,
|
||||
(0..indent_size).map(|_| " ").collect::<String>().as_str(),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### elide_wrkdir_path
|
||||
///
|
||||
/// Elide working directory path if longer than width + host.len
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -19,7 +19,14 @@
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{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
|
||||
@@ -38,26 +45,40 @@ impl FileTransferActivity {
|
||||
self.log_index = 0;
|
||||
}
|
||||
|
||||
/// ### create_quit_popup
|
||||
/// ### log_and_alert
|
||||
///
|
||||
/// 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(
|
||||
String::from("Are you sure you want to disconnect?"),
|
||||
FileTransferActivity::disconnect,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
))
|
||||
/// Add message to log events and also display it as an alert
|
||||
pub(super) fn log_and_alert(&mut self, level: LogLevel, msg: String) {
|
||||
// Set input mode
|
||||
let color: Color = match level {
|
||||
LogLevel::Error => Color::Red,
|
||||
LogLevel::Info => Color::Green,
|
||||
LogLevel::Warn => Color::Yellow,
|
||||
};
|
||||
self.log(level, msg.as_str());
|
||||
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_quit_popup(&mut self) -> InputMode {
|
||||
InputMode::Popup(PopupType::YesNo(
|
||||
pub(super) fn create_disconnect_popup(&mut self) -> Popup {
|
||||
Popup::YesNo(
|
||||
String::from("Are you sure you want to disconnect?"),
|
||||
FileTransferActivity::disconnect,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
)
|
||||
}
|
||||
|
||||
/// ### create_quit_popup
|
||||
///
|
||||
/// Create quit popup input mode (since must be shared between different input handlers)
|
||||
pub(super) fn create_quit_popup(&mut self) -> Popup {
|
||||
Popup::YesNo(
|
||||
String::from("Are you sure you want to quit?"),
|
||||
FileTransferActivity::disconnect_and_quit,
|
||||
FileTransferActivity::callback_nothing_to_do,
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
/// ### switch_input_field
|
||||
@@ -70,17 +91,65 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
/// ### set_progress
|
||||
/// ### init_config_client
|
||||
///
|
||||
/// Calculate progress percentage based on current progress
|
||||
pub(super) fn set_progress(&mut self, it: usize, sz: usize) {
|
||||
let mut prog: f64 = ((it as f64) * 100.0) / (sz as f64);
|
||||
// Check value
|
||||
if prog > 100.0 {
|
||||
prog = 100.0;
|
||||
} else if prog < 0.0 {
|
||||
prog = 0.0;
|
||||
/// 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)
|
||||
.with_formatter(cli.get_file_fmt().as_deref())
|
||||
.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());
|
||||
}
|
||||
self.transfer.progress = prog;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian 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;
|
||||
|
||||
@@ -70,6 +69,7 @@ pub struct FileTransferParams {
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
pub entry_directory: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// ### InputField
|
||||
@@ -90,14 +90,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,67 +106,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 index: usize,
|
||||
pub files: Vec<FsEntry>,
|
||||
dirstack: VecDeque<PathBuf>,
|
||||
}
|
||||
|
||||
impl FileExplorer {
|
||||
/// ### new
|
||||
///
|
||||
/// Instantiates a new FileExplorer
|
||||
pub fn new() -> FileExplorer {
|
||||
FileExplorer {
|
||||
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
|
||||
@@ -209,9 +149,11 @@ impl LogRecord {
|
||||
///
|
||||
/// TransferStates contains the states related to the transfer process
|
||||
struct TransferStates {
|
||||
pub progress: f64, // Current read/write progress (percentage)
|
||||
pub started: Instant, // Instant the transfer process started
|
||||
pub aborted: bool, // Describes whether the transfer process has been aborted
|
||||
pub progress: f64, // Current read/write progress (percentage)
|
||||
pub started: Instant, // Instant the transfer process started
|
||||
pub aborted: bool, // Describes whether the transfer process has been aborted
|
||||
pub bytes_written: usize, // Bytes written during transfer
|
||||
pub bytes_total: usize, // Total bytes to write
|
||||
}
|
||||
|
||||
impl TransferStates {
|
||||
@@ -223,6 +165,8 @@ impl TransferStates {
|
||||
progress: 0.0,
|
||||
started: Instant::now(),
|
||||
aborted: false,
|
||||
bytes_written: 0,
|
||||
bytes_total: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,6 +177,40 @@ impl TransferStates {
|
||||
self.progress = 0.0;
|
||||
self.started = Instant::now();
|
||||
self.aborted = false;
|
||||
self.bytes_written = 0;
|
||||
self.bytes_total = 0;
|
||||
}
|
||||
|
||||
/// ### set_progress
|
||||
///
|
||||
/// Calculate progress percentage based on current progress
|
||||
pub fn set_progress(&mut self, w: usize, sz: usize) {
|
||||
self.bytes_written = w;
|
||||
self.bytes_total = sz;
|
||||
let mut prog: f64 = ((self.bytes_written as f64) * 100.0) / (self.bytes_total as f64);
|
||||
// Check value
|
||||
if prog > 100.0 {
|
||||
prog = 100.0;
|
||||
} else if prog < 0.0 {
|
||||
prog = 0.0;
|
||||
}
|
||||
self.progress = prog;
|
||||
}
|
||||
|
||||
/// ### byte_per_second
|
||||
///
|
||||
/// Calculate bytes per second
|
||||
pub fn bytes_per_second(&self) -> u64 {
|
||||
// bytes_written : elapsed_secs = x : 1
|
||||
let elapsed_secs: u64 = self.started.elapsed().as_secs();
|
||||
match elapsed_secs {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,13 +229,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
|
||||
@@ -269,24 +248,31 @@ impl FileTransferActivity {
|
||||
///
|
||||
/// Instantiates a new FileTransferActivity
|
||||
pub fn new(params: FileTransferParams) -> FileTransferActivity {
|
||||
let protocol: FileTransferProtocol = params.protocol.clone();
|
||||
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,
|
||||
@@ -310,11 +296,18 @@ impl Activity for FileTransferActivity {
|
||||
// Set context
|
||||
self.context = Some(context);
|
||||
// Clear terminal
|
||||
let _ = self.context.as_mut().unwrap().terminal.clear();
|
||||
self.context.as_mut().unwrap().clear_screen();
|
||||
// Put raw mode on enabled
|
||||
let _ = enable_raw_mode();
|
||||
// Set working directory
|
||||
let pwd: PathBuf = self.context.as_ref().unwrap().local.pwd();
|
||||
// Get files at current wd
|
||||
self.local_scan(self.context.as_ref().unwrap().local.pwd().as_path());
|
||||
self.local_scan(pwd.as_path());
|
||||
self.local.wrkdir = pwd;
|
||||
// Index at first valid
|
||||
self.local.index_at_first();
|
||||
// Configure text editor
|
||||
self.setup_text_editor();
|
||||
}
|
||||
|
||||
/// ### on_draw
|
||||
@@ -322,16 +315,16 @@ impl Activity for FileTransferActivity {
|
||||
/// `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) {
|
||||
let mut redraw: bool = false; // Should ui actually be redrawned?
|
||||
// Context must be something
|
||||
// Should ui actually be redrawned?
|
||||
let mut redraw: bool = false;
|
||||
// Context must be something
|
||||
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
|
||||
)));
|
||||
@@ -364,7 +357,7 @@ impl Activity for FileTransferActivity {
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
Some(mut ctx) => {
|
||||
let _ = ctx.terminal.clear();
|
||||
ctx.clear_screen();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -30,6 +30,7 @@ use super::context::Context;
|
||||
// Activities
|
||||
pub mod auth_activity;
|
||||
pub mod filetransfer_activity;
|
||||
pub mod setup_activity;
|
||||
|
||||
// Activity trait
|
||||
|
||||
|
||||
152
src/ui/activities/setup_activity/callbacks.rs
Normal file
@@ -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-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Locals
|
||||
use super::{Color, Popup, SetupActivity};
|
||||
// Ext
|
||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||
use std::env;
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### callback_nothing_to_do
|
||||
///
|
||||
/// Self titled
|
||||
pub(super) fn callback_nothing_to_do(&mut self) {}
|
||||
|
||||
/// ### callback_save_config_and_quit
|
||||
///
|
||||
/// Save configuration and quit
|
||||
pub(super) fn callback_save_config_and_quit(&mut self) {
|
||||
match self.save_config() {
|
||||
Ok(_) => self.quit = true, // Quit after successful save
|
||||
Err(err) => self.popup = Some(Popup::Alert(Color::Red, err)), // Show error and don't quit
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_save_config
|
||||
///
|
||||
/// Save configuration callback
|
||||
pub(super) fn callback_save_config(&mut self) {
|
||||
if let Err(err) = self.save_config() {
|
||||
self.popup = Some(Popup::Alert(Color::Red, err)); // Show save error
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_reset_config_changes
|
||||
///
|
||||
/// Reset config changes callback
|
||||
pub(super) fn callback_reset_config_changes(&mut self) {
|
||||
if let Err(err) = self.reset_config_changes() {
|
||||
self.popup = Some(Popup::Alert(Color::Red, err)); // Show reset error
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_delete_ssh_key
|
||||
///
|
||||
/// Callback for performing the delete of a ssh key
|
||||
pub(super) fn callback_delete_ssh_key(&mut self) {
|
||||
// Get key
|
||||
if let Some(config_cli) = self.config_cli.as_mut() {
|
||||
let key: Option<String> = match config_cli.iter_ssh_keys().nth(self.ssh_key_idx) {
|
||||
Some(k) => Some(k.clone()),
|
||||
None => None,
|
||||
};
|
||||
if let Some(key) = key {
|
||||
match config_cli.get_ssh_key(&key) {
|
||||
Ok(opt) => {
|
||||
if let Some((host, username, _)) = opt {
|
||||
if let Err(err) = self.delete_ssh_key(host.as_str(), username.as_str())
|
||||
{
|
||||
// Report error
|
||||
self.popup = Some(Popup::Alert(Color::Red, err));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not get ssh key \"{}\": {}", key, err),
|
||||
))
|
||||
} // Report error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ### callback_new_ssh_key
|
||||
///
|
||||
/// Create a new ssh key with provided parameters
|
||||
pub(super) fn callback_new_ssh_key(&mut self, host: String, username: String) {
|
||||
if let Some(cli) = self.config_cli.as_ref() {
|
||||
// Prepare text editor
|
||||
env::set_var("EDITOR", cli.get_text_editor());
|
||||
let placeholder: String = format!("# Type private SSH key for {}@{}\n", username, host);
|
||||
// Put input mode back to normal
|
||||
let _ = disable_raw_mode();
|
||||
// Leave alternate mode
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
ctx.leave_alternate_screen();
|
||||
}
|
||||
// Re-enable raw mode
|
||||
let _ = enable_raw_mode();
|
||||
// Write key to file
|
||||
match edit::edit(placeholder.as_bytes()) {
|
||||
Ok(rsa_key) => {
|
||||
// Remove placeholder from `rsa_key`
|
||||
let rsa_key: String = rsa_key.as_str().replace(placeholder.as_str(), "");
|
||||
if rsa_key.is_empty() {
|
||||
// Report error: empty key
|
||||
self.popup = Some(Popup::Alert(Color::Red, "SSH Key is empty".to_string()));
|
||||
} else {
|
||||
// Add key
|
||||
if let Err(err) =
|
||||
self.add_ssh_key(host.as_str(), username.as_str(), rsa_key.as_str())
|
||||
{
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not create new private key: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
// Report error
|
||||
self.popup = Some(Popup::Alert(
|
||||
Color::Red,
|
||||
format!("Could not write private key to file: {}", err),
|
||||
))
|
||||
}
|
||||
}
|
||||
// Restore terminal
|
||||
if let Some(ctx) = self.context.as_mut() {
|
||||
// Clear screen
|
||||
ctx.clear_screen();
|
||||
// Enter alternate mode
|
||||
ctx.enter_alternate_screen();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
194
src/ui/activities/setup_activity/config.rs
Normal file
@@ -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-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// 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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
565
src/ui/activities/setup_activity/input.rs
Normal file
@@ -0,0 +1,565 @@
|
||||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// 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() {
|
||||
match 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()));
|
||||
}
|
||||
UserInterfaceInputField::FileFmt => {
|
||||
// Push char to current file fmt
|
||||
let mut file_fmt = config_cli.get_file_fmt().unwrap_or_default();
|
||||
// Pop from file fmt
|
||||
file_fmt.pop();
|
||||
// If len is 0, will become None
|
||||
config_cli.set_file_fmt(file_fmt);
|
||||
}
|
||||
_ => { /* Not a text field */ }
|
||||
}
|
||||
// NOTE: replace with match if other text fields are added
|
||||
if matches!(field, UserInterfaceInputField::TextEditor) {}
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
UserInterfaceInputField::CheckForUpdates => {
|
||||
// move left
|
||||
config_cli.set_check_for_updates(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);
|
||||
}
|
||||
UserInterfaceInputField::CheckForUpdates => {
|
||||
// move right
|
||||
config_cli.set_check_for_updates(false);
|
||||
}
|
||||
_ => { /* Not a tab field */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
KeyCode::Up => {
|
||||
// Change selected field
|
||||
self.tab = SetupTab::UserInterface(match field {
|
||||
UserInterfaceInputField::FileFmt => UserInterfaceInputField::GroupDirs,
|
||||
UserInterfaceInputField::GroupDirs => {
|
||||
UserInterfaceInputField::CheckForUpdates
|
||||
}
|
||||
UserInterfaceInputField::CheckForUpdates => {
|
||||
UserInterfaceInputField::ShowHiddenFiles
|
||||
}
|
||||
UserInterfaceInputField::ShowHiddenFiles => {
|
||||
UserInterfaceInputField::DefaultProtocol
|
||||
}
|
||||
UserInterfaceInputField::DefaultProtocol => {
|
||||
UserInterfaceInputField::TextEditor
|
||||
}
|
||||
UserInterfaceInputField::TextEditor => UserInterfaceInputField::FileFmt, // Wrap
|
||||
});
|
||||
}
|
||||
KeyCode::Down => {
|
||||
// Change selected field
|
||||
self.tab = SetupTab::UserInterface(match field {
|
||||
UserInterfaceInputField::TextEditor => {
|
||||
UserInterfaceInputField::DefaultProtocol
|
||||
}
|
||||
UserInterfaceInputField::DefaultProtocol => {
|
||||
UserInterfaceInputField::ShowHiddenFiles
|
||||
}
|
||||
UserInterfaceInputField::ShowHiddenFiles => {
|
||||
UserInterfaceInputField::CheckForUpdates
|
||||
}
|
||||
UserInterfaceInputField::CheckForUpdates => {
|
||||
UserInterfaceInputField::GroupDirs
|
||||
}
|
||||
UserInterfaceInputField::GroupDirs => UserInterfaceInputField::FileFmt,
|
||||
UserInterfaceInputField::FileFmt => 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
|
||||
match 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()));
|
||||
}
|
||||
UserInterfaceInputField::FileFmt => {
|
||||
// Push char to current file fmt
|
||||
let mut file_fmt =
|
||||
config_cli.get_file_fmt().unwrap_or_default();
|
||||
file_fmt.push(ch);
|
||||
// update value
|
||||
config_cli.set_file_fmt(file_fmt);
|
||||
}
|
||||
_ => { /* Not a text field */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => { /* 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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
786
src/ui/activities/setup_activity/layout.rs
Normal file
@@ -0,0 +1,786 @@
|
||||
//! ## SetupActivity
|
||||
//!
|
||||
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
||||
//! work on termscp configuration
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
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(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_check_for_updates_tab() {
|
||||
f.render_widget(tab, ui_cfg_chunks[3]);
|
||||
}
|
||||
if let Some(tab) = self.draw_default_group_dirs_tab() {
|
||||
f.render_widget(tab, ui_cfg_chunks[4]);
|
||||
}
|
||||
if let Some(tab) = self.draw_file_fmt_input() {
|
||||
f.render_widget(tab, ui_cfg_chunks[5]);
|
||||
}
|
||||
// Set cursor
|
||||
if let Some(cli) = &self.config_cli {
|
||||
match 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,
|
||||
);
|
||||
}
|
||||
UserInterfaceInputField::FileFmt => {
|
||||
let file_fmt: String = cli.get_file_fmt().unwrap_or_default();
|
||||
f.set_cursor(
|
||||
ui_cfg_chunks[4].x + file_fmt.width() as u16 + 1,
|
||||
ui_cfg_chunks[4].y + 1,
|
||||
);
|
||||
}
|
||||
_ => { /* Not a text field */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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_hidden_files_tab
|
||||
///
|
||||
/// Draw default hidden files 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_check_for_updates_tab
|
||||
///
|
||||
/// Draw check for updates tab
|
||||
fn draw_check_for_updates_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_check_for_updates() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
};
|
||||
let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab {
|
||||
SetupTab::UserInterface(field) => match field {
|
||||
UserInterfaceInputField::CheckForUpdates => {
|
||||
(Color::LightYellow, Color::Black, Color::LightYellow)
|
||||
}
|
||||
_ => (Color::Reset, Color::LightYellow, 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("Check for updates?"),
|
||||
)
|
||||
.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_file_fmt_input
|
||||
///
|
||||
/// Draw input text field for file fmt
|
||||
fn draw_file_fmt_input(&self) -> Option<Paragraph> {
|
||||
match &self.config_cli {
|
||||
Some(cli) => Some(
|
||||
Paragraph::new(cli.get_file_fmt().unwrap_or_default())
|
||||
.style(Style::default().fg(match &self.tab {
|
||||
SetupTab::SshConfig => Color::White,
|
||||
SetupTab::UserInterface(field) => match field {
|
||||
UserInterfaceInputField::FileFmt => Color::LightCyan,
|
||||
_ => Color::White,
|
||||
},
|
||||
}))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("File formatter syntax"),
|
||||
),
|
||||
),
|
||||
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(Some((addr, username, _))) = cli.get_ssh_key(key) {
|
||||
ssh_keys.push(ListItem::new(Span::from(format!(
|
||||
"{} at {}",
|
||||
username, addr,
|
||||
))));
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Return list
|
||||
Some(
|
||||
List::new(ssh_keys)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::LightGreen))
|
||||
.title("SSH Keys"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::LightGreen)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// ### draw_popup_area
|
||||
///
|
||||
/// Draw popup area
|
||||
fn draw_popup_area(&self, area: Rect, width: u16, height: u16) -> Rect {
|
||||
let popup_layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - height) / 2),
|
||||
Constraint::Percentage(height),
|
||||
Constraint::Percentage((100 - height) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(area);
|
||||
Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(
|
||||
[
|
||||
Constraint::Percentage((100 - width) / 2),
|
||||
Constraint::Percentage(width),
|
||||
Constraint::Percentage((100 - width) / 2),
|
||||
]
|
||||
.as_ref(),
|
||||
)
|
||||
.split(popup_layout[1])[1]
|
||||
}
|
||||
|
||||
/// ### draw_popup_alert
|
||||
///
|
||||
/// Draw alert popup
|
||||
fn draw_popup_alert(&self, color: Color, text: String, width: u16) -> List {
|
||||
// Wraps texts
|
||||
let message_rows = textwrap::wrap(text.as_str(), width as usize);
|
||||
let mut lines: Vec<ListItem> = Vec::new();
|
||||
for msg in message_rows.iter() {
|
||||
lines.push(ListItem::new(Spans::from(align_text_center(msg, width))));
|
||||
}
|
||||
List::new(lines)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(color))
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Alert"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
.style(Style::default().fg(color))
|
||||
}
|
||||
|
||||
/// ### draw_popup_fatal
|
||||
///
|
||||
/// Draw fatal error popup
|
||||
fn draw_popup_fatal(&self, text: String, width: u16) -> List {
|
||||
self.draw_popup_alert(Color::Red, text, width)
|
||||
}
|
||||
|
||||
/// ### draw_popup_new_ssh_key
|
||||
///
|
||||
/// Draw new ssh key form popup
|
||||
fn draw_popup_new_ssh_key(&self) -> (Paragraph, Paragraph) {
|
||||
let address: Paragraph = Paragraph::new(self.user_input.get(0).unwrap().as_str())
|
||||
.style(Style::default().fg(match self.user_input_ptr {
|
||||
0 => Color::LightCyan,
|
||||
_ => Color::White,
|
||||
}))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::TOP | Borders::RIGHT | Borders::LEFT)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.title("Host name or address"),
|
||||
);
|
||||
let username: Paragraph = Paragraph::new(self.user_input.get(1).unwrap().as_str())
|
||||
.style(Style::default().fg(match self.user_input_ptr {
|
||||
1 => Color::LightMagenta,
|
||||
_ => Color::White,
|
||||
}))
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::BOTTOM | Borders::RIGHT | Borders::LEFT)
|
||||
.border_type(BorderType::Rounded)
|
||||
.style(Style::default().fg(Color::White))
|
||||
.title("Username"),
|
||||
);
|
||||
(address, username)
|
||||
}
|
||||
|
||||
/// ### draw_popup_quit
|
||||
///
|
||||
/// Draw quit select popup
|
||||
fn draw_popup_quit(&self) -> Tabs {
|
||||
let choices: Vec<Spans> = vec![
|
||||
Spans::from("Save"),
|
||||
Spans::from("Don't save"),
|
||||
Spans::from("Cancel"),
|
||||
];
|
||||
let index: usize = match self.quit_opt {
|
||||
QuitDialogOption::Save => 0,
|
||||
QuitDialogOption::DontSave => 1,
|
||||
QuitDialogOption::Cancel => 2,
|
||||
};
|
||||
Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Exit setup?"),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(Style::default().add_modifier(Modifier::BOLD).fg(Color::Red))
|
||||
}
|
||||
|
||||
/// ### draw_popup_yesno
|
||||
///
|
||||
/// Draw yes/no select popup
|
||||
fn draw_popup_yesno(&self, text: String) -> Tabs {
|
||||
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
|
||||
let index: usize = match self.yesno_opt {
|
||||
YesNoDialogOption::Yes => 0,
|
||||
YesNoDialogOption::No => 1,
|
||||
};
|
||||
Tabs::new(choices)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(text),
|
||||
)
|
||||
.select(index)
|
||||
.style(Style::default())
|
||||
.highlight_style(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Yellow),
|
||||
)
|
||||
}
|
||||
|
||||
/// ### draw_popup_help
|
||||
///
|
||||
/// Draw authentication page help popup
|
||||
fn draw_popup_help(&self) -> List {
|
||||
// Write header
|
||||
let cmds: Vec<ListItem> = vec![
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<ESC>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Exit setup"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<TAB>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Change setup page"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<RIGHT/LEFT>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Change selected element in tab"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<UP/DOWN>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Change input field"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<ENTER>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Submit / Dismiss popup"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<DEL>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Delete entry"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+E>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Delete entry"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+H>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Show help"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+N>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("New SSH key"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+R>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Revert changes"),
|
||||
])),
|
||||
ListItem::new(Spans::from(vec![
|
||||
Span::styled(
|
||||
"<CTRL+S>",
|
||||
Style::default()
|
||||
.fg(Color::Cyan)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
Span::raw(" "),
|
||||
Span::raw("Save configuration"),
|
||||
])),
|
||||
];
|
||||
List::new(cmds)
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default())
|
||||
.border_type(BorderType::Rounded)
|
||||
.title("Help"),
|
||||
)
|
||||
.start_corner(Corner::TopLeft)
|
||||
}
|
||||
}
|
||||
38
src/ui/activities/setup_activity/misc.rs
Normal file
@@ -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-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::SetupActivity;
|
||||
|
||||
impl SetupActivity {
|
||||
/// ### clear_user_input
|
||||
///
|
||||
/// Clear user input buffers
|
||||
pub(super) fn clear_user_input(&mut self) {
|
||||
for s in self.user_input.iter_mut() {
|
||||
s.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
205
src/ui/activities/setup_activity/mod.rs
Normal file
@@ -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-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// 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,
|
||||
CheckForUpdates,
|
||||
GroupDirs,
|
||||
FileFmt,
|
||||
}
|
||||
|
||||
/// ### 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(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
|
||||
// Set redraw to true
|
||||
self.redraw = true;
|
||||
// Handle event
|
||||
self.handle_input_event(&event);
|
||||
}
|
||||
// Redraw if necessary
|
||||
if self.redraw {
|
||||
// Draw
|
||||
self.draw();
|
||||
// Redraw back to false
|
||||
self.redraw = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// ### on_destroy
|
||||
///
|
||||
/// `on_destroy` is the function which cleans up runtime variables and data before terminating the activity.
|
||||
/// This function must be called once before terminating the activity.
|
||||
/// This function finally releases the context
|
||||
fn on_destroy(&mut self) -> Option<Context> {
|
||||
// Disable raw mode
|
||||
let _ = disable_raw_mode();
|
||||
self.context.as_ref()?;
|
||||
// Clear terminal and return
|
||||
match self.context.take() {
|
||||
Some(mut ctx) => {
|
||||
ctx.clear_screen();
|
||||
Some(ctx)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -32,10 +32,10 @@ use super::input::InputHandler;
|
||||
use crate::host::Localhost;
|
||||
|
||||
// Includes
|
||||
use crossterm::execute;
|
||||
use crossterm::event::DisableMouseCapture;
|
||||
use crossterm::execute;
|
||||
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
|
||||
use std::io::{stdout, Stdout, Write};
|
||||
use std::io::{stdout, Stdout};
|
||||
use tui::backend::CrosstermBackend;
|
||||
use tui::Terminal;
|
||||
|
||||
@@ -59,9 +59,29 @@ impl Context {
|
||||
Context {
|
||||
local,
|
||||
input_hnd: InputHandler::new(),
|
||||
terminal: Terminal::new(CrosstermBackend::new(stdout)).unwrap()
|
||||
terminal: Terminal::new(CrosstermBackend::new(stdout)).unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enter_alternate_screen(&mut self) {
|
||||
let _ = execute!(
|
||||
self.terminal.backend_mut(),
|
||||
EnterAlternateScreen,
|
||||
DisableMouseCapture
|
||||
);
|
||||
}
|
||||
|
||||
pub fn leave_alternate_screen(&mut self) {
|
||||
let _ = execute!(
|
||||
self.terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
);
|
||||
}
|
||||
|
||||
pub fn clear_screen(&mut self) {
|
||||
let _ = self.terminal.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Context {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
@@ -45,6 +45,7 @@ impl InputHandler {
|
||||
/// ### fetch_events
|
||||
///
|
||||
/// Check if new events have been received from handler
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn fetch_events(&self) -> Result<Vec<Event>, ()> {
|
||||
let mut inbox: Vec<Event> = Vec::new();
|
||||
loop {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020 Christian Visintin - christian.visintin1997@gmail.com
|
||||
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
|
||||
*
|
||||
* This file is part of "TermSCP"
|
||||
*
|
||||
|
||||
395
src/utils.rs
@@ -1,395 +0,0 @@
|
||||
//! ## Utils
|
||||
//!
|
||||
//! `utils` is the module which provides utilities of different kind
|
||||
|
||||
/*
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Dependencies
|
||||
extern crate chrono;
|
||||
extern crate whoami;
|
||||
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
|
||||
use chrono::format::ParseError;
|
||||
use chrono::prelude::*;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// ### parse_remote_opt
|
||||
///
|
||||
/// Parse remote option string. Returns in case of success a tuple made of (address, port, protocol, username)
|
||||
/// For ssh if username is not provided, current user will be used.
|
||||
/// In case of error, message is returned
|
||||
/// If port is missing default port will be used for each protocol
|
||||
/// SFTP => 22
|
||||
/// FTP => 21
|
||||
/// The option string has the following syntax
|
||||
/// [protocol]://[username]@{address}:[port]
|
||||
/// The only argument which is mandatory is address
|
||||
/// NOTE: possible strings
|
||||
/// - 172.26.104.1
|
||||
/// - root@172.26.104.1
|
||||
/// - sftp://root@172.26.104.1
|
||||
/// - sftp://172.26.104.1:4022
|
||||
/// - sftp://172.26.104.1
|
||||
/// - ...
|
||||
///
|
||||
pub fn parse_remote_opt(
|
||||
remote: &str,
|
||||
) -> Result<(String, u16, FileTransferProtocol, Option<String>), String> {
|
||||
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;
|
||||
// Split string by '://'
|
||||
let tokens: Vec<&str> = wrkstr.split("://").collect();
|
||||
// If length is > 1, then token[0] is protocol
|
||||
match tokens.len() {
|
||||
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])),
|
||||
}
|
||||
wrkstr = String::from(tokens[1]); // Wrkstr becomes tokens[1]
|
||||
}
|
||||
_ => return Err(String::from("Bad syntax")), // Too many tokens...
|
||||
}
|
||||
// Set username to default if sftp
|
||||
if protocol == FileTransferProtocol::Sftp {
|
||||
// Set username to current username
|
||||
username = Some(whoami::username());
|
||||
}
|
||||
// Split wrkstring by '@'
|
||||
let tokens: Vec<&str> = wrkstr.split('@').collect();
|
||||
match tokens.len() {
|
||||
1 => {}
|
||||
2 => {
|
||||
// Username is first token
|
||||
username = Some(String::from(tokens[0]));
|
||||
// Update wrkstr
|
||||
wrkstr = String::from(tokens[1]);
|
||||
}
|
||||
_ => return Err(String::from("Bad syntax")), // Too many tokens...
|
||||
}
|
||||
// Split wrkstring by ':'
|
||||
let tokens: Vec<&str> = wrkstr.split(':').collect();
|
||||
match tokens.len() {
|
||||
1 => {
|
||||
// Address is wrkstr
|
||||
address = wrkstr.clone();
|
||||
}
|
||||
2 => {
|
||||
// Address is first token
|
||||
address = String::from(tokens[0]);
|
||||
// Port is second str
|
||||
port = match tokens[1].parse::<u16>() {
|
||||
Ok(val) => val,
|
||||
Err(_) => {
|
||||
return Err(format!(
|
||||
"Port must be a number in range [0-65535], but is '{}'",
|
||||
tokens[1]
|
||||
))
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => return Err(String::from("Bad syntax")), // Too many tokens...
|
||||
}
|
||||
Ok((address, port, protocol, username))
|
||||
}
|
||||
|
||||
/// ### fmt_pex
|
||||
///
|
||||
/// Convert 3 bytes of permissions value into ls notation (e.g. rwx-wx--x)
|
||||
pub fn fmt_pex(owner: u8, group: u8, others: u8) -> String {
|
||||
let mut mode: String = String::with_capacity(9);
|
||||
let read: u8 = (owner >> 2) & 0x1;
|
||||
let write: u8 = (owner >> 1) & 0x1;
|
||||
let exec: u8 = owner & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (group >> 2) & 0x1;
|
||||
let write: u8 = (group >> 1) & 0x1;
|
||||
let exec: u8 = group & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (others >> 2) & 0x1;
|
||||
let write: u8 = (others >> 1) & 0x1;
|
||||
let exec: u8 = others & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
mode
|
||||
}
|
||||
|
||||
/// ### instant_to_str
|
||||
///
|
||||
/// Format a `Instant` into a time string
|
||||
pub fn time_to_str(time: SystemTime, fmt: &str) -> String {
|
||||
let datetime: DateTime<Local> = time.into();
|
||||
format!("{}", datetime.format(fmt))
|
||||
}
|
||||
|
||||
/// ### lstime_to_systime
|
||||
///
|
||||
/// Convert ls syntax time to System Time
|
||||
/// ls time has two possible syntax:
|
||||
/// 1. if year is current: %b %d %H:%M (e.g. Nov 5 13:46)
|
||||
/// 2. else: %b %d %Y (e.g. Nov 5 2019)
|
||||
pub fn lstime_to_systime(
|
||||
tm: &str,
|
||||
fmt_year: &str,
|
||||
fmt_hours: &str,
|
||||
) -> Result<SystemTime, ParseError> {
|
||||
let datetime: NaiveDateTime = match NaiveDate::parse_from_str(tm, fmt_year) {
|
||||
Ok(date) => {
|
||||
// Case 2.
|
||||
// Return NaiveDateTime from NaiveDate with time 00:00:00
|
||||
date.and_hms(0, 0, 0)
|
||||
}
|
||||
Err(_) => {
|
||||
// Might be case 1.
|
||||
// We need to add Current Year at the end of the string
|
||||
let this_year: i32 = Utc::now().year();
|
||||
let date_time_str: String = format!("{} {}", tm, this_year);
|
||||
// Now parse
|
||||
match NaiveDateTime::parse_from_str(
|
||||
date_time_str.as_ref(),
|
||||
format!("{} %Y", fmt_hours).as_ref(),
|
||||
) {
|
||||
Ok(dt) => dt,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
};
|
||||
// Convert datetime to system time
|
||||
let sys_time: SystemTime = SystemTime::UNIX_EPOCH;
|
||||
Ok(sys_time
|
||||
.checked_add(Duration::from_secs(datetime.timestamp() as u64))
|
||||
.unwrap_or(SystemTime::UNIX_EPOCH))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_utils_parse_remote_opt() {
|
||||
// Base case
|
||||
let result: (String, u16, FileTransferProtocol, Option<String>) =
|
||||
parse_remote_opt(&String::from("172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.0, String::from("172.26.104.1"));
|
||||
assert_eq!(result.1, 22);
|
||||
assert_eq!(result.2, FileTransferProtocol::Sftp);
|
||||
assert!(result.3.is_some());
|
||||
// User case
|
||||
let result: (String, u16, FileTransferProtocol, Option<String>) =
|
||||
parse_remote_opt(&String::from("root@172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.0, String::from("172.26.104.1"));
|
||||
assert_eq!(result.1, 22);
|
||||
assert_eq!(result.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.3.unwrap(), String::from("root"));
|
||||
// User + port
|
||||
let result: (String, u16, FileTransferProtocol, Option<String>) =
|
||||
parse_remote_opt(&String::from("root@172.26.104.1:8022"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.0, String::from("172.26.104.1"));
|
||||
assert_eq!(result.1, 8022);
|
||||
assert_eq!(result.2, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.3.unwrap(), String::from("root"));
|
||||
// Port only
|
||||
let result: (String, u16, FileTransferProtocol, Option<String>) =
|
||||
parse_remote_opt(&String::from("172.26.104.1:4022"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.0, String::from("172.26.104.1"));
|
||||
assert_eq!(result.1, 4022);
|
||||
assert_eq!(result.2, FileTransferProtocol::Sftp);
|
||||
assert!(result.3.is_some());
|
||||
// Protocol
|
||||
let result: (String, u16, FileTransferProtocol, Option<String>) =
|
||||
parse_remote_opt(&String::from("ftp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.0, String::from("172.26.104.1"));
|
||||
assert_eq!(result.1, 21); // Fallback to ftp default
|
||||
assert_eq!(result.2, FileTransferProtocol::Ftp(false));
|
||||
assert!(result.3.is_none()); // Doesn't fall back
|
||||
// Protocol
|
||||
let result: (String, u16, FileTransferProtocol, Option<String>) =
|
||||
parse_remote_opt(&String::from("scp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.0, String::from("172.26.104.1"));
|
||||
assert_eq!(result.1, 22); // Fallback to scp default
|
||||
assert_eq!(result.2, FileTransferProtocol::Scp);
|
||||
assert!(result.3.is_none()); // Doesn't fall back
|
||||
// Protocol + user
|
||||
let result: (String, u16, FileTransferProtocol, Option<String>) =
|
||||
parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.0, String::from("172.26.104.1"));
|
||||
assert_eq!(result.1, 21); // Fallback to ftp default
|
||||
assert_eq!(result.2, FileTransferProtocol::Ftp(true));
|
||||
assert_eq!(result.3.unwrap(), String::from("anon"));
|
||||
// All together now
|
||||
let result: (String, u16, FileTransferProtocol, Option<String>) =
|
||||
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.0, String::from("172.26.104.1"));
|
||||
assert_eq!(result.1, 8021); // Fallback to ftp default
|
||||
assert_eq!(result.2, FileTransferProtocol::Ftp(false));
|
||||
assert_eq!(result.3.unwrap(), String::from("anon"));
|
||||
|
||||
// bad syntax
|
||||
assert!(parse_remote_opt(&String::from("://172.26.104.1")).is_err()); // Missing protocol
|
||||
assert!(parse_remote_opt(&String::from("omar://172.26.104.1")).is_err()); // Bad protocol
|
||||
assert!(parse_remote_opt(&String::from("172.26.104.1:abc")).is_err()); // Bad port
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_fmt_pex() {
|
||||
assert_eq!(fmt_pex(7, 7, 7), String::from("rwxrwxrwx"));
|
||||
assert_eq!(fmt_pex(7, 5, 5), String::from("rwxr-xr-x"));
|
||||
assert_eq!(fmt_pex(6, 6, 6), String::from("rw-rw-rw-"));
|
||||
assert_eq!(fmt_pex(6, 4, 4), String::from("rw-r--r--"));
|
||||
assert_eq!(fmt_pex(6, 0, 0), String::from("rw-------"));
|
||||
assert_eq!(fmt_pex(0, 0, 0), String::from("---------"));
|
||||
assert_eq!(fmt_pex(4, 4, 4), String::from("r--r--r--"));
|
||||
assert_eq!(fmt_pex(1, 2, 1), String::from("--x-w---x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_time_to_str() {
|
||||
let system_time: SystemTime = SystemTime::from(SystemTime::UNIX_EPOCH);
|
||||
assert_eq!(
|
||||
time_to_str(system_time, "%Y-%m-%d"),
|
||||
String::from("1970-01-01")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_lstime_to_systime() {
|
||||
// Good cases
|
||||
assert_eq!(
|
||||
lstime_to_systime("Nov 5 16:32", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1604593920)
|
||||
);
|
||||
assert_eq!(
|
||||
lstime_to_systime("Dec 2 21:32", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1606944720)
|
||||
);
|
||||
assert_eq!(
|
||||
lstime_to_systime("Nov 5 2018", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1541376000)
|
||||
);
|
||||
assert_eq!(
|
||||
lstime_to_systime("Mar 18 2018", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1521331200)
|
||||
);
|
||||
// bad cases
|
||||
assert!(lstime_to_systime("Oma 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
|
||||
assert!(lstime_to_systime("Feb 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
|
||||
assert!(lstime_to_systime("Feb 15 25:32", "%b %d %Y", "%b %d %H:%M").is_err());
|
||||
}
|
||||
}
|
||||
67
src/utils/crypto.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
//! ## Crypto
|
||||
//!
|
||||
//! `crypto` is the module which provides utilities for crypting
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// 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
|
||||
);
|
||||
}
|
||||
}
|
||||
218
src/utils/fmt.rs
Normal file
@@ -0,0 +1,218 @@
|
||||
//! ## Fmt
|
||||
//!
|
||||
//! `fmt` is the module which provides utilities for formatting
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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 chrono;
|
||||
extern crate textwrap;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
/// ### fmt_pex
|
||||
///
|
||||
/// Convert 3 bytes of permissions value into ls notation (e.g. rwx-wx--x)
|
||||
pub fn fmt_pex(owner: u8, group: u8, others: u8) -> String {
|
||||
let mut mode: String = String::with_capacity(9);
|
||||
let read: u8 = (owner >> 2) & 0x1;
|
||||
let write: u8 = (owner >> 1) & 0x1;
|
||||
let exec: u8 = owner & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (group >> 2) & 0x1;
|
||||
let write: u8 = (group >> 1) & 0x1;
|
||||
let exec: u8 = group & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
let read: u8 = (others >> 2) & 0x1;
|
||||
let write: u8 = (others >> 1) & 0x1;
|
||||
let exec: u8 = others & 0x1;
|
||||
mode.push_str(match read {
|
||||
1 => "r",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match write {
|
||||
1 => "w",
|
||||
_ => "-",
|
||||
});
|
||||
mode.push_str(match exec {
|
||||
1 => "x",
|
||||
_ => "-",
|
||||
});
|
||||
mode
|
||||
}
|
||||
|
||||
/// ### instant_to_str
|
||||
///
|
||||
/// Format a `Instant` into a time string
|
||||
pub fn fmt_time(time: SystemTime, fmt: &str) -> String {
|
||||
let datetime: DateTime<Local> = time.into();
|
||||
format!("{}", datetime.format(fmt))
|
||||
}
|
||||
|
||||
/// ### fmt_millis
|
||||
///
|
||||
/// Format duration as {secs}.{millis}
|
||||
pub fn fmt_millis(duration: Duration) -> String {
|
||||
let seconds: u128 = duration.as_millis() / 1000;
|
||||
let millis: u128 = duration.as_millis() % 1000;
|
||||
format!("{}.{:0width$}", seconds, millis, width = 3)
|
||||
}
|
||||
|
||||
/// align_text_center
|
||||
///
|
||||
/// Align text to center for a given width
|
||||
pub fn align_text_center(text: &str, width: u16) -> String {
|
||||
let indent_size: usize = match (width as usize) >= text.len() {
|
||||
// NOTE: The check prevents underflow
|
||||
true => (width as usize - text.len()) / 2,
|
||||
false => 0,
|
||||
};
|
||||
textwrap::indent(
|
||||
text,
|
||||
(0..indent_size).map(|_| " ").collect::<String>().as_str(),
|
||||
)
|
||||
.trim_end()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// ### elide_path
|
||||
///
|
||||
/// Elide a path if longer than width
|
||||
/// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME}
|
||||
pub fn fmt_path_elide(p: &Path, width: usize) -> String {
|
||||
let fmt_path: String = format!("{}", p.display());
|
||||
match fmt_path.len() > width as usize {
|
||||
false => fmt_path,
|
||||
true => {
|
||||
// Elide
|
||||
let ancestors_len: usize = p.ancestors().count();
|
||||
let mut ancestors = p.ancestors();
|
||||
let mut elided_path: PathBuf = PathBuf::new();
|
||||
// If ancestors_len's size is bigger than 2, push count - 2
|
||||
if ancestors_len > 2 {
|
||||
elided_path.push(ancestors.nth(ancestors_len - 2).unwrap());
|
||||
}
|
||||
// If ancestors_len is bigger than 3, push '...' and parent too
|
||||
if ancestors_len > 3 {
|
||||
elided_path.push("...");
|
||||
if let Some(parent) = p.ancestors().nth(1) {
|
||||
elided_path.push(parent.file_name().unwrap());
|
||||
}
|
||||
}
|
||||
// Push file_name
|
||||
if let Some(name) = p.file_name() {
|
||||
elided_path.push(name);
|
||||
}
|
||||
format!("{}", elided_path.display())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_utils_fmt_pex() {
|
||||
assert_eq!(fmt_pex(7, 7, 7), String::from("rwxrwxrwx"));
|
||||
assert_eq!(fmt_pex(7, 5, 5), String::from("rwxr-xr-x"));
|
||||
assert_eq!(fmt_pex(6, 6, 6), String::from("rw-rw-rw-"));
|
||||
assert_eq!(fmt_pex(6, 4, 4), String::from("rw-r--r--"));
|
||||
assert_eq!(fmt_pex(6, 0, 0), String::from("rw-------"));
|
||||
assert_eq!(fmt_pex(0, 0, 0), String::from("---------"));
|
||||
assert_eq!(fmt_pex(4, 4, 4), String::from("r--r--r--"));
|
||||
assert_eq!(fmt_pex(1, 2, 1), String::from("--x-w---x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_fmt_time() {
|
||||
let system_time: SystemTime = SystemTime::from(SystemTime::UNIX_EPOCH);
|
||||
assert_eq!(
|
||||
fmt_time(system_time, "%Y-%m-%d"),
|
||||
String::from("1970-01-01")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_align_text_center() {
|
||||
assert_eq!(
|
||||
align_text_center("hello world!", 24),
|
||||
String::from(" hello world!")
|
||||
);
|
||||
// Bad case
|
||||
assert_eq!(
|
||||
align_text_center("hello world!", 8),
|
||||
String::from("hello world!")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_utils_fmt_millis() {
|
||||
assert_eq!(
|
||||
fmt_millis(Duration::from_millis(2048)),
|
||||
String::from("2.048")
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_millis(Duration::from_millis(8192)),
|
||||
String::from("8.192")
|
||||
);
|
||||
assert_eq!(
|
||||
fmt_millis(Duration::from_millis(18192)),
|
||||
String::from("18.192")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(any(target_os = "unix", target_os = "linux", target_os = "macos"))]
|
||||
fn test_utils_fmt_path_elide() {
|
||||
let p: &Path = &Path::new("/develop/pippo");
|
||||
// Under max size
|
||||
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/pippo"));
|
||||
// Above max size, only one ancestor
|
||||
assert_eq!(fmt_path_elide(p, 8), String::from("/develop/pippo"));
|
||||
let p: &Path = &Path::new("/develop/pippo/foo/bar");
|
||||
assert_eq!(fmt_path_elide(p, 16), String::from("/develop/.../foo/bar"));
|
||||
}
|
||||
}
|
||||
85
src/utils/git.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
//! ## git
|
||||
//!
|
||||
//! `git` is the module which provides utilities to interact through the GIT API and to perform some stuff at git level
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Deps
|
||||
extern crate ureq;
|
||||
// Locals
|
||||
use super::parser::parse_semver;
|
||||
// Others
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TagInfo {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
/// ### check_for_updates
|
||||
///
|
||||
/// Check if there is a new version available for termscp.
|
||||
/// This is performed through the Github API
|
||||
/// In case of success returns Ok(Option<String>), where the Option is Some(new_version); otherwise if no version is available, return None
|
||||
/// In case of error returns Error with the error description
|
||||
|
||||
pub fn check_for_updates(current_version: &str) -> Result<Option<String>, String> {
|
||||
// Send request
|
||||
let github_version: Result<String, String> =
|
||||
match ureq::get("https://api.github.com/repos/veeso/termscp/releases/latest").call() {
|
||||
Ok(response) => match response.into_json::<TagInfo>() {
|
||||
Ok(tag_info) => Ok(tag_info.tag_name),
|
||||
Err(err) => Err(err.to_string()),
|
||||
},
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
// Check version
|
||||
match github_version {
|
||||
Err(err) => Err(err),
|
||||
Ok(version) => {
|
||||
// Parse version
|
||||
match parse_semver(version.as_str()) {
|
||||
Some(new_version) => {
|
||||
// Check if version is different
|
||||
if new_version.as_str() > current_version {
|
||||
Ok(Some(new_version)) // New version is available
|
||||
} else {
|
||||
Ok(None) // No new version
|
||||
}
|
||||
}
|
||||
None => Err(String::from("Got bad response from Github")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_utils_git_check_for_updates() {
|
||||
assert!(check_for_updates("100.0.0").ok().unwrap().is_none());
|
||||
assert!(check_for_updates("0.0.1").ok().unwrap().is_some());
|
||||
}
|
||||
}
|
||||
31
src/utils/mod.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
//! ## Utils
|
||||
//!
|
||||
//! `utils` is the module which provides utilities of different kind
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// modules
|
||||
pub mod crypto;
|
||||
pub mod fmt;
|
||||
pub mod git;
|
||||
pub mod parser;
|
||||
pub mod random;
|
||||
418
src/utils/parser.rs
Normal file
@@ -0,0 +1,418 @@
|
||||
//! ## Parser
|
||||
//!
|
||||
//! `parser` is the module which provides utilities for parsing different kind of stuff
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// Dependencies
|
||||
extern crate chrono;
|
||||
extern crate regex;
|
||||
extern crate whoami;
|
||||
|
||||
// Locals
|
||||
use crate::filetransfer::FileTransferProtocol;
|
||||
#[cfg(not(test))] // NOTE: don't use configuration during tests
|
||||
use crate::system::config_client::ConfigClient;
|
||||
#[cfg(not(test))] // NOTE: don't use configuration during tests
|
||||
use crate::system::environment;
|
||||
|
||||
// Ext
|
||||
use chrono::format::ParseError;
|
||||
use chrono::prelude::*;
|
||||
use regex::Regex;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
// Regex
|
||||
lazy_static! {
|
||||
/**
|
||||
* Regex matches:
|
||||
* - group 1: Some(protocol) | None
|
||||
* - group 2: Some(user) | None
|
||||
* - group 3: Address
|
||||
* - group 4: Some(port) | None
|
||||
* - group 5: Some(path) | None
|
||||
*/
|
||||
static ref REMOTE_OPT_REGEX: Regex = Regex::new(r"(?:([a-z]+)://)?(?:([^@]+)@)?(?:([^:]+))(?::((?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])))?(?::([^:]+))?").ok().unwrap();
|
||||
/**
|
||||
* Regex matches:
|
||||
* - group 1: Version
|
||||
* E.g. termscp-0.3.2 => 0.3.2
|
||||
* v0.4.0 => 0.4.0
|
||||
*/
|
||||
static ref SEMVER_REGEX: Regex = Regex::new(r".*(:?[0-9]\.[0-9]\.[0-9])").unwrap();
|
||||
}
|
||||
|
||||
pub struct RemoteOptions {
|
||||
pub hostname: String,
|
||||
pub port: u16,
|
||||
pub protocol: FileTransferProtocol,
|
||||
pub username: Option<String>,
|
||||
pub wrkdir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// ### parse_remote_opt
|
||||
///
|
||||
/// Parse remote option string. Returns in case of success a RemoteOptions struct
|
||||
/// For ssh if username is not provided, current user will be used.
|
||||
/// In case of error, message is returned
|
||||
/// If port is missing default port will be used for each protocol
|
||||
/// SFTP => 22
|
||||
/// FTP => 21
|
||||
/// The option string has the following syntax
|
||||
/// [protocol://][username@]{address}[:port][:path]
|
||||
/// The only argument which is mandatory is address
|
||||
/// NOTE: possible strings
|
||||
/// - 172.26.104.1
|
||||
/// - root@172.26.104.1
|
||||
/// - sftp://root@172.26.104.1
|
||||
/// - sftp://172.26.104.1:4022
|
||||
/// - sftp://172.26.104.1
|
||||
/// - ...
|
||||
///
|
||||
pub fn parse_remote_opt(remote: &str) -> Result<RemoteOptions, String> {
|
||||
// Set protocol to default protocol
|
||||
#[cfg(not(test))] // NOTE: don't use configuration during tests
|
||||
let mut protocol: FileTransferProtocol = match environment::init_config_dir() {
|
||||
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,
|
||||
};
|
||||
#[cfg(test)] // NOTE: during test set protocol just to Sftp
|
||||
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
|
||||
// Match against regex
|
||||
match REMOTE_OPT_REGEX.captures(remote) {
|
||||
Some(groups) => {
|
||||
// Match protocol
|
||||
let mut port: u16 = 22;
|
||||
if let Some(group) = groups.get(1) {
|
||||
// Set protocol from group
|
||||
let (m_protocol, m_port) = match FileTransferProtocol::from_str(group.as_str()) {
|
||||
Ok(proto) => match proto {
|
||||
FileTransferProtocol::Ftp(_) => (proto, 21),
|
||||
FileTransferProtocol::Scp => (proto, 22),
|
||||
FileTransferProtocol::Sftp => (proto, 22),
|
||||
},
|
||||
Err(_) => return Err(format!("Unknown protocol \"{}\"", group.as_str())),
|
||||
};
|
||||
// NOTE: tuple destructuring assignment is not supported yet :(
|
||||
protocol = m_protocol;
|
||||
port = m_port;
|
||||
}
|
||||
// Match user
|
||||
let username: Option<String> = match groups.get(2) {
|
||||
Some(group) => Some(group.as_str().to_string()),
|
||||
None => match protocol {
|
||||
// If group is empty, set to current user
|
||||
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
|
||||
Some(whoami::username())
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
};
|
||||
// Get address
|
||||
let hostname: String = match groups.get(3) {
|
||||
Some(group) => group.as_str().to_string(),
|
||||
None => return Err(String::from("Missing address")),
|
||||
};
|
||||
// Get port
|
||||
if let Some(group) = groups.get(4) {
|
||||
port = match group.as_str().parse::<u16>() {
|
||||
Ok(p) => p,
|
||||
Err(err) => return Err(format!("Bad port \"{}\": {}", group.as_str(), err)),
|
||||
};
|
||||
}
|
||||
// Get workdir
|
||||
let wrkdir: Option<PathBuf> = match groups.get(5) {
|
||||
Some(group) => Some(PathBuf::from(group.as_str())),
|
||||
None => None,
|
||||
};
|
||||
Ok(RemoteOptions {
|
||||
hostname,
|
||||
port,
|
||||
protocol,
|
||||
username,
|
||||
wrkdir,
|
||||
})
|
||||
}
|
||||
None => Err(String::from("Bad remote host syntax!")),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### parse_lstime
|
||||
///
|
||||
/// Convert ls syntax time to System Time
|
||||
/// ls time has two possible syntax:
|
||||
/// 1. if year is current: %b %d %H:%M (e.g. Nov 5 13:46)
|
||||
/// 2. else: %b %d %Y (e.g. Nov 5 2019)
|
||||
pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result<SystemTime, ParseError> {
|
||||
let datetime: NaiveDateTime = match NaiveDate::parse_from_str(tm, fmt_year) {
|
||||
Ok(date) => {
|
||||
// Case 2.
|
||||
// Return NaiveDateTime from NaiveDate with time 00:00:00
|
||||
date.and_hms(0, 0, 0)
|
||||
}
|
||||
Err(_) => {
|
||||
// Might be case 1.
|
||||
// We need to add Current Year at the end of the string
|
||||
let this_year: i32 = Utc::now().year();
|
||||
let date_time_str: String = format!("{} {}", tm, this_year);
|
||||
// Now parse
|
||||
match NaiveDateTime::parse_from_str(
|
||||
date_time_str.as_ref(),
|
||||
format!("{} %Y", fmt_hours).as_ref(),
|
||||
) {
|
||||
Ok(dt) => dt,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
};
|
||||
// Convert datetime to system time
|
||||
let sys_time: SystemTime = SystemTime::UNIX_EPOCH;
|
||||
Ok(sys_time
|
||||
.checked_add(Duration::from_secs(datetime.timestamp() as u64))
|
||||
.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),
|
||||
}
|
||||
}
|
||||
|
||||
/// ### parse_semver
|
||||
///
|
||||
/// Parse semver string
|
||||
pub fn parse_semver(haystack: &str) -> Option<String> {
|
||||
match SEMVER_REGEX.captures(haystack) {
|
||||
Some(groups) => match groups.get(1) {
|
||||
Some(version) => Some(version.as_str().to_string()),
|
||||
None => None,
|
||||
},
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::utils::fmt::fmt_time;
|
||||
|
||||
#[test]
|
||||
fn test_utils_parse_remote_opt() {
|
||||
// Base case
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some());
|
||||
// User case
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.username.unwrap(), String::from("root"));
|
||||
assert!(result.wrkdir.is_none());
|
||||
// User + port
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 8022);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.username.unwrap(), String::from("root"));
|
||||
assert!(result.wrkdir.is_none());
|
||||
// Port only
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1:4022"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 4022);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some());
|
||||
assert!(result.wrkdir.is_none());
|
||||
// Protocol
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("ftp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 21); // Fallback to ftp default
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
|
||||
assert!(result.username.is_none()); // Doesn't fall back
|
||||
assert!(result.wrkdir.is_none());
|
||||
// Protocol
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("sftp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22); // Fallback to sftp default
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some()); // Doesn't fall back
|
||||
assert!(result.wrkdir.is_none());
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("scp://172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22); // Fallback to scp default
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Scp);
|
||||
assert!(result.username.is_some()); // Doesn't fall back
|
||||
assert!(result.wrkdir.is_none());
|
||||
// Protocol + user
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 21); // Fallback to ftp default
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Ftp(true));
|
||||
assert_eq!(result.username.unwrap(), String::from("anon"));
|
||||
assert!(result.wrkdir.is_none());
|
||||
// Path
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 8022);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert_eq!(result.username.unwrap(), String::from("root"));
|
||||
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("/var"));
|
||||
// Port only
|
||||
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1:home"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 22);
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
|
||||
assert!(result.username.is_some());
|
||||
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("home"));
|
||||
// All together now
|
||||
let result: RemoteOptions =
|
||||
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp"))
|
||||
.ok()
|
||||
.unwrap();
|
||||
assert_eq!(result.hostname, String::from("172.26.104.1"));
|
||||
assert_eq!(result.port, 8021); // Fallback to ftp default
|
||||
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
|
||||
assert_eq!(result.username.unwrap(), String::from("anon"));
|
||||
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("/tmp"));
|
||||
// bad syntax
|
||||
assert!(parse_remote_opt(&String::from("omar://172.26.104.1")).is_err()); // Bad protocol
|
||||
assert!(parse_remote_opt(&String::from("omar://172.26.104.1:650000")).is_err());
|
||||
// Bad port
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_parse_lstime() {
|
||||
// Good cases
|
||||
assert_eq!(
|
||||
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!(
|
||||
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")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1541376000)
|
||||
);
|
||||
assert_eq!(
|
||||
parse_lstime("Mar 18 2018", "%b %d %Y", "%b %d %H:%M")
|
||||
.ok()
|
||||
.unwrap()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.ok()
|
||||
.unwrap(),
|
||||
Duration::from_secs(1521331200)
|
||||
);
|
||||
// bad cases
|
||||
assert!(parse_lstime("Oma 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
|
||||
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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utils_parse_semver() {
|
||||
assert_eq!(
|
||||
parse_semver("termscp-0.3.2").unwrap(),
|
||||
String::from("0.3.2")
|
||||
);
|
||||
assert_eq!(parse_semver("v0.4.1").unwrap(), String::from("0.4.1"),);
|
||||
assert_eq!(parse_semver("1.0.0").unwrap(), String::from("1.0.0"),);
|
||||
assert!(parse_semver("v1.1").is_none());
|
||||
}
|
||||
}
|
||||
52
src/utils/random.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! ## Random
|
||||
//!
|
||||
//! `random` is the module which provides utilities for rand
|
||||
|
||||
/*
|
||||
*
|
||||
* Copyright (C) 2020-2021 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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||