Merge branch 'main' of github.com:veeso/termscp into main

This commit is contained in:
veeso 2021-10-12 10:54:18 +02:00
commit 1fadc53743
112 changed files with 10886 additions and 2122 deletions

35
.github/ISSUE_TEMPLATE/copy.md vendored Normal file
View file

@ -0,0 +1,35 @@
---
name: Copy
about: Report a typo/error in a repository document
title: "[COPY] - ISSUE_TITLE"
labels: documentation
assignees: veeso
---
## Report
### DOCUMENT NAME
This sentence at row ROW_NUMBER doesn't seem right:
> Write down here the wrong sentence
and I think it should be changed to:
> Write down here the correct sentence
`Copy paste the template above for all the sentences to fix`
---
`Copy paste the template above for all the documents to fix`
## Additional information
> ❗ Report LANGUAGE checks only if it concerns the documents above
> ❗ If the documents concerns more than one language, copy paste the checks below for each check you want to report
> ❗ The PR mention regards the indicated language. If you check the box, I may add you to a PR where I need to translate a new section of the user manual/README or other documents. I promise I won't stress you anyway.
- [ ] I am C1/C2 speaker for this language: LANGUAGE
- [ ] You can mention me in a PR in case a review for translations is needed

View file

@ -9,7 +9,9 @@ ignore:
- src/main.rs
- src/lib.rs
- src/activity_manager.rs
- src/filetransfer/transfer/s3/mod.rs
- src/support.rs
- src/system/notifications.rs
- "src/ui/activities/*"
- src/ui/context.rs
- src/ui/input.rs

View file

@ -11,6 +11,8 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev
- name: Setup containers
run: docker-compose -f "tests/docker-compose.yml" up -d --build
- name: Setup nightly toolchain

View file

@ -12,7 +12,7 @@ jobs:
uses: vmactions/freebsd-vm@v0.1.4
with:
usesh: true
prepare: pkg install -y curl wget libssh gcc vim
prepare: pkg install -y curl wget libssh gcc vim dbus pkgconf
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rustup.sh && \
chmod +x /tmp/rustup.sh && \

View file

@ -11,6 +11,8 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev
- name: Setup containers
run: docker-compose -f "tests/docker-compose.yml" up -d --build
- uses: actions-rs/toolchain@v1

2
.gitignore vendored
View file

@ -21,3 +21,5 @@ dist/pkgs/arch/*.tar.gz
# Macos
.DS_Store
dist/pkgs/

View file

@ -1,6 +1,7 @@
# Changelog
- [Changelog](#changelog)
- [0.7.0](#070)
- [0.6.1](#061)
- [0.6.0](#060)
- [0.5.1](#051)
@ -20,6 +21,47 @@
---
## 0.7.0
Released on 12/10/2021
> 🍁 Autumn update 2021 🍇
- **Aws S3** 🪣
- Added support for the aws-s3 protocol.
- Operate on your bucket directly from the file explorer.
- You can also save your buckets as bookmarks.
- Aws s3 reads credentials directly from your credentials file at `$HOME/.aws/credentials` or from environment. Read more in the user manual.
- **Auto update** ⬇️
- Possibility to update termscp directly via GUI or CLI.
- Install update via CLI running `(sudo) termscp --update`.
- Install update via GUI from auth form: when the "new version message" is displayed press `<CTRL+R>`, then enter `YES` in the radio input asking whether to install the update.
- **Notifications** 📫
- termscp will now send Desktop notifications in these cases
- on transfer completed (minimum transfer size can be specified in configuration; default 512MB)
- on transfer error (same as above)
- on update available
- Added "notifications enabled" in configuration (Default enabled)
- Added "Notifications: minimum transfer size": if transfer size is greater or equal than the specified value, notifications for transfer will be displayed.
- **Prompt user when about to replace existing file on a file transfer**
- Whenever a file transfer is about to replace an existing file on local/remote host, you will be prompted if you're sure you really want to replace that file.
- You may want to disable this option. You can go to configuration and set "Prompt when replacing existing files?" to "NO"
- **❗ BREAKING CHANGES ❗**:
- Added a new key in themes: `misc_info_dialog`: if your theme won't load, just reload it. If you're using a customised theme, you can add to it the missing key via a text editor. Just edit the `theme.toml` in your `$CONFIG_DIR/termscp/theme.toml` and add `misc_info_dialog` (Read more in manual at Themes).
- Enhancements:
- Reuse mounts in UI, in order to reduce executable size
- File list can now be "rewinded", which means that moving with arrows will now allow you to go from top to bottom of the list pressing `<UP>` and viceversa pressing `<DOWN>`.
- Bugfix:
- Fixed [Issue 70](https://github.com/veeso/termscp/issues/70): Unable to type characters with `CTRL+ALT` (e.g. italian layout `CTRL+ALT+ò` => `@`) due to a crossterm issue. Fixed with tui-realm-stdlib `0.6.3`.
- Dependencies:
- Added `notify_rust 4.5.3`
- Added `rust-s3 0.27-rc4`
- Added `self_update 0.27.0`
- Updated `argh` to `0.1.6`
- Updated `dirs` to `4.0.0`
- Updated `tui-realm-stdlib` to `0.6.3`
- Removed `ureq`
## 0.6.1
Released on 31/08/2021

View file

@ -9,6 +9,7 @@ Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in
- [Open an issue](#open-an-issue)
- [Questions](#questions)
- [Bug reports](#bug-reports)
- [Copy error](#copy-error)
- [Feature requests](#feature-requests)
- [Preferred contributions](#preferred-contributions)
- [Pull Request Process](#pull-request-process)
@ -45,6 +46,7 @@ Open an issue when:
- You have questions or concerns regarding the project or the application itself.
- You have a bug to report.
- You have a copy error to report. (Error in documentation; bad translation or sentence)
- You have a feature or a suggestion to improve termscp to submit.
### Questions
@ -71,6 +73,15 @@ Maintainers will may add additional labels to your issue:
- **sorcery**: it is not possible to find out what's causing your bug, nor is reproducible on our test environments.
- **wontfix**: your bug has a very high ratio between the difficulty to fix it and the probability to encounter it, or it just isn't a bug, but a feature.
### Copy error
If you want to report a copy error, create an issue using the `copy` template.
The `Documentation` label should already be set and the issue should already be assigned to `veeso`.
If you want to fix the copy by yourself you can fork the project and open a PR, otherwise I will fix it by myself.
The copy issue is accepted **also if you're not a C1/C2 speaker**, but a speaker of that level in case the language is different from Italian/English is preferred.
Please fullfil the form on the bottom of the template if you want.
### Feature requests
Whenever you have a good idea which chould improve the project, it is a good idea to submit it to the project owner.
@ -97,9 +108,10 @@ Always mind that your suggestion, may be rejected: I'll always provide a feedbac
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/veeso/termscp/issues)
- New file transfers: for further details see [Implementing File Transfer](#implementing-file-transfers)
- Code optimizations: any optimization to the code is welcome
- **New file transfers**: for further details see [Implementing File Transfer](#implementing-file-transfers). ⚠️ this is going to be deprecated SOON! We're moving the file transfers into another library. Please see [remotefs-rs](https://github.com/veeso/remotefs-rs).
- **Code optimizations**: any optimization to the code is welcome
- See also features described in [Upcoming features](./README.md##upcoming-features-). Open an issue first though.
- README/User manual **translations**: really appreciated atm. Please just add a folder into `docs/` with the language code. Language code with two characters is used in case the language is understandable by all the countries where this language is spoken (e.g. `it-CH`, `it-IT` are not really different, so I just created `it`, but there is a big difference from `zh-CN` and `zh-TW` for instance). Don't worry about flags in README, I will implement that part if it's too complicated.
For any other kind of contribution, especially for new features, please submit a new issue first.

1341
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package]
authors = ["Christian Visintin"]
categories = ["command-line-utilities"]
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP"
description = "termscp is a feature rich terminal file transfer and explorer with support for SCP/SFTP/FTP/S3"
documentation = "https://docs.rs/termscp"
edition = "2018"
homepage = "https://veeso.github.io/termscp/"
@ -11,7 +11,7 @@ license = "MIT"
name = "termscp"
readme = "README.md"
repository = "https://github.com/veeso/termscp"
version = "0.6.1"
version = "0.7.0"
[package.metadata.rpm]
package = "termscp"
@ -22,28 +22,36 @@ buildflags = ["--release"]
[package.metadata.rpm.targets]
termscp = { path = "/usr/bin/termscp" }
[package.metadata.deb]
maintainer = "Christian Visintin <christian.visintin1997@gmail.com>"
copyright = "2021, Christian Visintin <christian.visintin1997@gmail.com>"
extended-description-file = "docs/misc/README.deb.txt"
[[bin]]
name = "termscp"
path = "src/main.rs"
[dependencies]
argh = "0.1.5"
argh = "0.1.6"
bitflags = "1.3.2"
bytesize = "1.1.0"
chrono = "0.4.19"
content_inspector = "0.2.4"
crossterm = "0.20"
dirs = "3.0.1"
dirs = "4.0.0"
edit = "0.1.3"
hostname = "0.3.1"
keyring = { version = "0.10.1", optional = true }
lazy_static = "1.4.0"
log = "0.4.14"
magic-crypt = "3.1.7"
notify-rust = { version = "4.5.3", default-features = false, features = [ "d" ] }
open = "2.0.1"
rand = "0.8.4"
regex = "1.5.4"
rpassword = "5.0.1"
rust-s3 = { version = "0.27.0-rc4", default-features = false, features = [ "sync-native-tls", "sync" ] }
self_update = { version = "0.27.0", features = [ "archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate" ] }
serde = { version = "^1.0.0", features = [ "derive" ] }
simplelog = "0.10.0"
ssh2 = "0.9.0"
@ -52,9 +60,8 @@ tempfile = "3.1.0"
textwrap = "0.14.2"
thiserror = "^1.0.0"
toml = "0.5.8"
tui-realm-stdlib = "0.6.0"
tui-realm-stdlib = "0.6.3"
tuirealm = "0.6.0"
ureq = { version = "2.1.0", features = [ "json" ] }
whoami = "1.1.1"
wildmatch = "2.0.0"
@ -63,7 +70,8 @@ pretty_assertions = "0.7.2"
[features]
default = [ "with-keyring" ]
github-actions = []
github-actions = [ ]
with-s3-ci = []
with-containers = []
with-keyring = [ "keyring" ]

181
README.md
View file

@ -13,18 +13,123 @@
<a href="https://veeso.github.io/termscp/#user-manual" target="_blank">User manual</a>
</p>
<p align="center">Developed by <a href="https://veeso.github.io/">@veeso</a></p>
<p align="center">Current version: 0.6.1 (31/08/2021)</p>
<p align="center">
<a href="https://github.com/veeso/termscp"
><img
height="20"
src="/assets/images/flags/us.png"
alt="English"
/></a>
&nbsp;
<a
href="/docs/de/README.md"
><img
height="20"
src="/assets/images/flags/de.png"
alt="Deutsch"
/></a>
&nbsp;
<a
href="/docs/es/README.md"
><img
height="20"
src="/assets/images/flags/es.png"
alt="Español"
/></a>
&nbsp;
<a
href="/docs/fr/README.md"
><img
height="20"
src="/assets/images/flags/fr.png"
alt="Français"
/></a>
&nbsp;
<a
href="/docs/it/README.md"
><img
height="20"
src="/assets/images/flags/it.png"
alt="Italiano"
/></a>
&nbsp;
<a
href="/docs/zh-CN/README.md"
><img
height="20"
src="/assets/images/flags/cn.png"
alt="简体中文"
/></a>
</p>
[![License: MIT](https://img.shields.io/badge/License-MIT-teal.svg)](https://opensource.org/licenses/MIT) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.6.1-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
<p align="center">Developed by <a href="https://veeso.github.io/" target="_blank">@veeso</a></p>
<p align="center">Current version: 0.7.0 (12/10/2021)</p>
[![Linux](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![MacOs](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Windows](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions) [![FreeBSD](https://github.com/veeso/termscp/workflows/FreeBSD/badge.svg)](https://github.com/veeso/termscp/actions) [![Coverage Status](https://coveralls.io/repos/github/veeso/termscp/badge.svg)](https://coveralls.io/github/veeso/termscp)
<p align="center">
<a href="https://opensource.org/licenses/MIT"
><img
src="https://img.shields.io/badge/License-MIT-teal.svg"
alt="License-MIT"
/></a>
<a href="https://github.com/veeso/termscp/stargazers"
><img
src="https://img.shields.io/github/stars/veeso/termscp.svg"
alt="Repo stars"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/d/termscp.svg"
alt="Downloads counter"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/v/termscp.svg"
alt="Latest version"
/></a>
<a href="https://www.buymeacoffee.com/veeso"
><img
src="https://img.shields.io/badge/Donate-BuyMeACoffee-yellow.svg"
alt="Buy me a coffee"
/></a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Linux/badge.svg"
alt="Linux CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/MacOS/badge.svg"
alt="MacOS CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
alt="Windows CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/FreeBSD/badge.svg"
alt="FreeBSD CI"
/></a>
<a href="https://coveralls.io/github/veeso/termscp"
><img
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
alt="Coveralls"
/></a>
<a href="https://docs.rs/termscp"
><img
src="https://docs.rs/termscp/badge.svg"
alt="Docs"
/></a>
</p>
---
## About termscp 🖥
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **BSD** and **Windows** compatible and supports SFTP, SCP, FTP and FTPS.
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/S3. So basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and to interact with the local file system. It is **Linux**, **MacOS**, **FreeBSD** and **Windows** compatible.
![Explorer](assets/images/explorer.gif)
@ -33,23 +138,25 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
## Features 🎁
- 📁 Different communication protocols
- SFTP
- SCP
- FTP and FTPS
- **SFTP**
- **SCP**
- **FTP** and **FTPS**
- **Aws S3**
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
- Create, remove, rename, search, view and edit files
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
- 📝 View and edit text files with your favourite text editor
- 💁 SFTP/SCP authentication through SSH keys and username/password
- 🐧 Compatible with Windows, Linux, BSD and MacOS
- ✏ Customizable
- 📝 View and edit files with your favourite applications
- 💁 SFTP/SCP authentication with SSH keys and username/password
- 🐧 Compatible with Windows, Linux, FreeBSD and MacOS
- 🎨 Make it yours!
- Themes
- Custom file explorer format
- Customizable text editor
- Customizable file sorting
- and many other parameters...
- 📫 Get notified via Desktop Notifications when a large file has been transferred
- 🔐 Save your password in your operating system key vault
- 🦀 Rust-powered
- 🤝 Easy to extend with new file transfers protocols
- 👀 Developed keeping an eye on performance
- 🦄 Frequent awesome updates
@ -58,7 +165,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
## Get started 🚀
If you're considering to install termscp I want to thank you 💜 ! I hope you will enjoy termscp!
If you want to contribute to this project, don't forget to check out our contribute guide. [Read More](CONTRIBUTING.md)
If you want to contribute to this project, don't forget to check out our [contribute guide](CONTRIBUTING.md).
If you are a Linux, a FreeBSD or a MacOS user this simple shell script will install termscp on your system with a single command:
@ -74,49 +181,52 @@ choco install termscp
For more information or other platforms, please visit [veeso.github.io](https://veeso.github.io/termscp/#get-started) to view all installation methods.
⚠️ If you're looking on how to update termscp just run termscp from CLI with: `(sudo) termscp --update` ⚠️
### Requirements ❗
- **Linux** users:
- libssh
- libdbus-1
- pkg-config
- **FreeBSD** users:
- libssh
- dbus
- pkgconf
### Optional Requirements ✔️
These requirements are not forcely required to run termscp, but to enjoy all of its features
These requirements are not forced required to run termscp, but to enjoy all of its features
- **Linux/BSD** users:
- **Linux/FreeBSD** users:
- To **open** files via `V` (at least one of these)
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- **Linux** users:
- A keyring manager: read more in the [User manual](docs/man_en.md#linux-keyring)
- A keyring manager: read more in the [User manual](docs/man.md#linux-keyring)
- **WSL** users
- To **open** files via `V` (at least one of these)
- [wslu](https://github.com/wslutilities/wslu)
---
## Buy me a coffee ☕
## Support me ☕
If you like termscp and you'd love to see the project to grow, please consider a little donation 🥳
If you like termscp and you'd love to see the project to grow and to improve, please consider a little donation to support me on **Buy me a coffee** 🥳
[![Buy-me-a-coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=&slug=veeso&button_colour=404040&font_colour=ffffff&font_family=Comic&outline_colour=ffffff&coffee_colour=FFDD00)](https://www.buymeacoffee.com/veeso)
or, if you prefer, you can also donate on PayPal:
[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.me/chrisintin)
---
## User manual and Documentation 📚
The user manual can be found on the [termscp's website](https://veeso.github.io/termscp/#user-manual) or on Github:
- [User manual](docs/man-en.md)
- [用户手册](docs/man-zh.md)
A translation of the user manual in other languages would be really appreciated 😉
The user manual can be found on the [termscp's website](https://veeso.github.io/termscp/#user-manual) or on [Github](docs/man.md).
The developer documentation can be found on Rust Docs at <https://docs.rs/termscp>
@ -132,16 +242,21 @@ The developer documentation can be found on Rust Docs at <https://docs.rs/termsc
Major termscp releases will now be seasonal, so expect 4 major updates during the year.
Planned for *🍁 Autumn update 🍇*:
Planned for *❄️ Winter update 2021 ⛄*:
- **Self-update ⬇️**: In order to increase users updating termscp, I want to provide the possibility to update termscp directly from application, when a new update is available.
- **AWS S3 support 🪣**: Already into the 0.7.0 backlog
- **Prompt before replacing files ☢️**: Possibility to configure whether a prompt should be displayed before replacing files.
- **File system watcher 🔭**: The feature consists in the possibility to track some files in order to automatically sync them with remote host. For the implementation [notify](https://github.com/notify-rs/notify) will be used.
- **Translations 🌐**: The feature consists in the possibility for the user to install the language pack for the language he prefers in order to replace the default English interface. The following language will be provided along to English:
- 🇨🇳 Chinese
- 🇫🇷 French
- 🇩🇪 German
- 🇮🇹 Italian
- 🇳🇱 Dutch
- 🇪🇸 Spanish
Planned for *❄️ Winter update ⛄*:
Planned for *🍓 Spring update 2022 🌹*:
- **SMB Support 🎉**: This will require a long time to be implemented, since I'm currently working on a Rust native SMB library, since I don't want to add new C-bindings. ~~Fear the 🦚~~
- **Configuration profile for bookmarks 📚**: Basically this feature adds the possibility to have a specific setup for a certain host, instead of having only one global configuration.
- **Configuration profile for bookmarks 📚**: Basically this feature adds the possibility to have a specific setup for a certain host, instead of having only one global configuration. (Maybe will be postponed to spring 2022).
Along to new features, termscp developments is now focused on UX and performance improvements, so if you have any suggestion, feel free to open an issue.
@ -166,7 +281,7 @@ View termscp's changelog [HERE](CHANGELOG.md)
## Powered by 💪
termscp is powered by these aweseome projects:
termscp is powered by these awesome projects:
- [bytesize](https://github.com/hyunsik/bytesize)
- [crossterm](https://github.com/crossterm-rs/crossterm)
@ -174,6 +289,8 @@ termscp is powered by these aweseome projects:
- [keyring-rs](https://github.com/hwchen/keyring-rs)
- [open-rs](https://github.com/Byron/open-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-s3](https://github.com/durch/rust-s3)
- [self_update](https://github.com/jaemk/self_update)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
- [suppaftp](https://github.com/veeso/suppaftp)
- [textwrap](https://github.com/mgeisler/textwrap)

BIN
assets/images/flags/br.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/images/flags/cn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
assets/images/flags/de.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/images/flags/dk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
assets/images/flags/es.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
assets/images/flags/fr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
assets/images/flags/it.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/images/flags/jp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
assets/images/flags/kr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/images/flags/nl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
assets/images/flags/ru.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
assets/images/flags/us.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

View file

@ -1,7 +1,7 @@
#!/bin/bash
if [ -z "$1" ]; then
echo "Usage: deploy.sh <version>"
echo "Usage: docker.sh <version>"
exit 1
fi
@ -19,14 +19,21 @@ 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)
mkdir -p ${PKGS_DIR}/x86_64-unknown-linux-gnu/
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_debian9 /bin/bash)
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}_amd64.deb ${PKGS_DIR}/deb/
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/release/termscp ${PKGS_DIR}/x86_64-unknown-linux-gnu/
# Make tar.gz
cd ${PKGS_DIR}/x86_64-unknown-linux-gnu/
tar cvzf termscp-v${VERSION}-x86_64-unknown-linux-gnu.tar.gz termscp
rm termscp
cd -
# 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)
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_centos7 /bin/bash)
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
exit $?

53
dist/build/freebsd.sh vendored Executable file
View file

@ -0,0 +1,53 @@
#!/bin/sh
if [ -z "$1" ]; then
echo "Usage: freebsd.sh <version>"
exit 1
fi
VERSION=$1
set -e # Don't fail
# Go to root dir
cd ../../
# Check if in correct directory
if [ ! -f Cargo.toml ]; then
echo "Please start freebsd.sh from dist/build/ directory"
exit 1
fi
# Build release
cargo build --release && cargo strip
# Make pkg
cd target/release/
PKG="termscp-v${VERSION}-x86_64-unknown-freebsd.tar.gz"
tar czf $PKG termscp
sha256sum $PKG
# Calc sha256 of exec and copy to path
HASH=`sha256sum termscp | cut -d ' ' -f1`
sudo cp termscp /usr/local/bin/termscp
mkdir -p ../../dist/pkgs/freebsd/
mv $PKG ../../dist/pkgs/freebsd/$PKG
cd ../../dist/pkgs/freebsd/
rm manifest
echo -e "name: \"termscp\"" > manifest
echo -e "version: $VERSION" >> manifest
echo -e "origin: veeso/termscp" >> manifest
echo -e "comment: \"A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/S3\"" >> manifest
echo -e "desc: <<EOD\n\
A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP/S3\n\
EOD\n\
arch: \"amd64\"\n\
www: \"https://veeso.github.io/termscp/\"\n\
maintainer: \"christian.visintin1997@gmail.com\"\n\
prefix: \"/usr/local/bin\"\n\
deps: {\n\
libssh: {origin: security/libssh, version: 0.9.5}\n\
}\n\
files: {\n\
/usr/local/bin/termscp: \"$HASH\"\n\
}\n\
" >> manifest
exit $?

30
dist/build/macos.sh vendored Executable file
View file

@ -0,0 +1,30 @@
#!/bin/sh
if [ -z "$1" ]; then
echo "Usage: macos.sh <version>"
exit 1
fi
VERSION=$1
set -e # Don't fail
# Go to root dir
cd ../../
# Check if in correct directory
if [ ! -f Cargo.toml ]; then
echo "Please start macos.sh from dist/build/ directory"
exit 1
fi
# Build release
cargo build --release && cargo strip
# Make pkg
cd target/release/
PKG="termscp-v${VERSION}-x86_64-apple-darwin.tar.gz"
tar czf $PKG termscp
sha256sum $PKG
mkdir -p ../../dist/pkgs/macos/
mv $PKG ../../dist/pkgs/macos/$PKG
exit $?

20
dist/build/windows.ps1 vendored Executable file
View file

@ -0,0 +1,20 @@
$ErrorActionPreference = 'Stop';
if ($args.Count -eq 0) {
Write-Output "Usage: windows.ps1 <version>"
exit 1
}
$version = $args[0]
# Go to root directory
Set-Location ..\..\
# Build
cargo build --release
# Make zip
$zipName = "termscp-v$version-x86_64-pc-windows-msvc.zip"
Set-Location .\target\release\
Compress-Archive termscp $zipName
# Get checksum
checksum.exe -t sha256 $zipName
Move-Item $zipName .\..\..\dist\pkgs\windows\$zipName

View file

@ -1,19 +1,24 @@
FROM rust:1.48.0 AS builder
FROM rust:1.55.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
RUN apt update && apt install -y \
git \
gcc \
pkg-config \
libssl-dev \
libssh2-1-dev \
libdbus-1-dev \
curl
# 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
RUN cargo install cargo-strip
# 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
RUN cargo build --release --target x86_64-unknown-linux-gnu && cargo strip
CMD ["sh"]

View file

@ -8,7 +8,8 @@ RUN yum -y install \
openssl \
pkgconfig \
dbus-devel \
openssl-devel
openssl-devel \
bash
# Install rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
chmod +x /tmp/rust.sh && \
@ -18,9 +19,9 @@ 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
RUN source $HOME/.cargo/env && cargo install cargo-rpm cargo-strip
# Build for x86_64
RUN source $HOME/.cargo/env && cargo build --release
RUN source $HOME/.cargo/env && cargo build --release && cargo strip
# Build pkgs
RUN source $HOME/.cargo/env && yum -y install rpm-build && cargo rpm init && cargo rpm build
CMD ["sh"]

View file

@ -20,9 +20,9 @@ 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
RUN . $HOME/.cargo/env && cargo install cargo-deb cargo-strip
# Build for x86_64
RUN . $HOME/.cargo/env && cargo build --release
RUN . $HOME/.cargo/env && cargo build --release && cargo strip
# Build pkgs
RUN . $HOME/.cargo/env && cargo deb

View file

@ -9,6 +9,7 @@ RUN apt update && apt install -y \
libssl-dev \
libssh2-1-dev \
libdbus-1-dev \
bash \
curl
# Install rust
@ -20,10 +21,10 @@ 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
RUN . $HOME/.cargo/env && cargo install cargo-deb cargo-strip
# Build for x86_64
RUN . $HOME/.cargo/env && cargo build --release
RUN . $HOME/.cargo/env && cargo build --release && cargo strip
# Build pkgs
RUN . $HOME/.cargo/env && cargo deb
CMD ["sh"]
CMD ["bash"]

View file

@ -1,17 +0,0 @@
name: "termscp"
version: 0.6.1
origin: veeso/termscp
comment: "A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP"
desc: <<EOD
A feature rich terminal UI file transfer and explorer with support for SCP/SFTP/FTP
EOD
arch: "amd64"
www: "https://veeso.github.io/termscp/"
maintainer: "christian.visintin1997@gmail.com"
prefix: "/usr/local/bin"
deps: {
libssh: {origin: security/libssh, version: 0.9.5}
}
files: {
/usr/local/bin/termscp: "87543d13b11b6e601ba8cdde9d704c80dc3515f1681fbf71fd0b31d7206efc09"
}

302
docs/de/README.md Normal file
View file

@ -0,0 +1,302 @@
# termscp
<p align="center">
<img src="/assets/images/termscp.svg" width="256" height="256" />
</p>
<p align="center">~ Eine funktionsreiche Terminal-Dateiübertragung ~</p>
<p align="center">
<a href="https://veeso.github.io/termscp/" target="_blank">Webseite</a>
·
<a href="https://veeso.github.io/termscp/#get-started" target="_blank">Installation</a>
·
<a href="https://veeso.github.io/termscp/#user-manual" target="_blank">Benutzerhandbuch</a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp"
><img
height="20"
src="/assets/images/flags/us.png"
alt="English"
/></a>
&nbsp;
<a
href="/docs/de/README.md"
><img
height="20"
src="/assets/images/flags/de.png"
alt="Deutsch"
/></a>
&nbsp;
<a
href="/docs/es/README.md"
><img
height="20"
src="/assets/images/flags/es.png"
alt="Español"
/></a>
&nbsp;
<a
href="/docs/fr/README.md"
><img
height="20"
src="/assets/images/flags/fr.png"
alt="Français"
/></a>
&nbsp;
<a
href="/docs/it/README.md"
><img
height="20"
src="/assets/images/flags/it.png"
alt="Italiano"
/></a>
&nbsp;
<a
href="/docs/zh-CN/README.md"
><img
height="20"
src="/assets/images/flags/cn.png"
alt="简体中文"
/></a>
</p>
<p align="center">Entwickelt von <a href="https://veeso.github.io/" target="_blank">@veeso</a></p>
<p align="center">Aktuelle Version: 0.7.0 (12/10/2021)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
><img
src="https://img.shields.io/badge/License-MIT-teal.svg"
alt="License-MIT"
/></a>
<a href="https://github.com/veeso/termscp/stargazers"
><img
src="https://img.shields.io/github/stars/veeso/termscp.svg"
alt="Repo stars"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/d/termscp.svg"
alt="Downloads counter"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/v/termscp.svg"
alt="Latest version"
/></a>
<a href="https://www.buymeacoffee.com/veeso"
><img
src="https://img.shields.io/badge/Donate-BuyMeACoffee-yellow.svg"
alt="Buy me a coffee"
/></a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Linux/badge.svg"
alt="Linux CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/MacOS/badge.svg"
alt="MacOS CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
alt="Windows CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/FreeBSD/badge.svg"
alt="FreeBSD CI"
/></a>
<a href="https://coveralls.io/github/veeso/termscp"
><img
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
alt="Coveralls"
/></a>
<a href="https://docs.rs/termscp"
><img
src="https://docs.rs/termscp/badge.svg"
alt="Docs"
/></a>
</p>
---
## Über termscp 🖥
Termscp ist ein funktionsreicher Terminal-Dateitransfer und Explorer mit Unterstützung für SCP/SFTP/FTP/S3. Im Grunde handelt es sich also um ein Terminal-Dienstprogramm mit einer TUI, um eine Verbindung zu einem Remote-Server herzustellen, um Dateien abzurufen und hochzuladen und mit dem lokalen Dateisystem zu interagieren. Es ist **Linux**, **MacOS**, **FreeBSD** und **Windows** kompatibel.
![Explorer](/assets/images/explorer.gif)
---
## Features 🎁
- 📁 Verschiedene Kommunikationsprotokolle
- **SFTP**
- **SCP**
- **FTP** und **FTPS**
- **Aws S3**
- 🖥 Erkunden und bedienen Sie das Dateisystem der Fernbedienung und des lokalen Computers mit einer praktischen Benutzeroberfläche
- Erstellen, Entfernen, Umbenennen, Suchen, Anzeigen und Bearbeiten von Dateien
- ⭐ Verbinden Sie sich über integrierte Lesezeichen und aktuelle Verbindungen mit Ihren Lieblingshosts
- 📝 Anzeigen und Bearbeiten von Dateien mit Ihren bevorzugten Anwendungen
- 💁 SFTP/SCP-Authentifizierung mit SSH-Schlüsseln und Benutzername/Passwort
- 🐧 Kompatibel mit Windows, Linux, FreeBSD und MacOS
- 🎨 Mach es zu deinem!
- Themen
- Benutzerdefiniertes Datei-Explorer-Format
- Anpassbarer Texteditor
- Anpassbare Dateisortierung
- und viele andere Parameter...
- 📫 Lassen Sie sich benachrichtigen, wenn eine große Datei übertragen wurde
- 🔐 Speichern Sie Ihr Passwort in Ihrem Betriebssystem-Schlüsseltresor
- 🦀 Rust-powered
- 👀 Entwickelt, um die Leistung im Auge zu behalten
- 🦄 Häufige tolle Updates
---
## Loslegen 🚀
Wenn Sie überlegen, termscp zu installieren, möchte ich Ihnen danken 💜 ! Ich hoffe, Sie werden Termscp genießen!
Wenn Sie zu diesem Projekt beitragen möchten, vergessen Sie nicht, unseren [Beitragsleitfaden](../../CONTRIBUTING.md) zu lesen.
Wenn Sie ein Linux-, FreeBSD- oder MacOS-Benutzer sind, installiert dieses einfache Shell-Skript termscp mit einem einzigen Befehl auf Ihrem System:
```sh
curl --proto '=https' --tlsv1.2 -sSLf "https://git.io/JBhDb" | sh
```
Wenn Sie ein Windows-Benutzer sind, können Sie termscp mit [Chocolatey](https://chocolatey.org/) installieren:
```sh
choco install termscp
```
Für weitere Informationen oder andere Plattformen besuchen Sie bitte [veeso.github.io](https://veeso.github.io/termscp/#get-started), um alle Installationsmethoden anzuzeigen.
⚠️ Wenn Sie wissen möchten, wie Sie termscp aktualisieren können, führen Sie einfach termscp über die CLI aus mit: `(sudo) termscp --update` ⚠️
### Softwareanforderungen ❗
- **Linux** Benutzer:
- libssh
- libdbus-1
- pkg-config
- **FreeBSD** Benutzer:
- libssh
- dbus
- pkgconf
### Optionale Softwareanforderungen ✔️
Diese Anforderungen sind nicht zwingend erforderlich, um termscp auszuführen, sondern um alle Funktionen nutzen zu können
- **Linux/FreeBSD** Benutzer:
- Um Dateien mit `V` zu **öffnen** (mindestens eines davon)
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- **Linux** Benutzer:
- Ein Keyring-manager: Lesen Sie mehr in der [Bedienungsanleitung](man.md#linux-keyring)
- **WSL** Benutzer
- Um Dateien mit `V` zu **öffnen** (mindestens eines davon)
- [wslu](https://github.com/wslutilities/wslu)
---
## Unterstütze mich ☕
Wenn Ihnen termscp gefällt und Sie gerne sehen würden, wie das Projekt wächst und sich verbessert, denken Sie bitte über eine kleine Spende nach, um mich bei **Buy me a coffee** zu unterstützen. 🥳
[![Buy-me-a-coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=&slug=veeso&button_colour=404040&font_colour=ffffff&font_family=Comic&outline_colour=ffffff&coffee_colour=FFDD00)](https://www.buymeacoffee.com/veeso)
oder, wenn Sie möchten, können Sie auch über PayPal spenden:
[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.me/chrisintin)
---
## User manual and Documentation 📚
Das Benutzerhandbuch finden Sie auf der [termscp-Website](https://veeso.github.io/termscp/#user-manual) oder auf [Github](man.md).
Die Entwicklerdokumentation finden Sie in Rust Docs unter <https://docs.rs/termscp>
---
## Known issues 🧻
- `NoSuchFileOrDirectory` auf verbinden (WSL1): Ich kenne dieses Problem und es ist ein Fehler von WSL, denke ich. Machen Sie sich keine Sorgen, verschieben Sie einfach die ausführbare Datei von termcp an einen anderen PATH-Speicherort, z. B. `/usr/bin`, oder installieren Sie sie über das entsprechende Paketformat (z. B. deb).
---
## Contributing and issues 🤝🏻
Beiträge, Fehlerberichte, neue Funktionen und Fragen sind willkommen! 😉
Wenn Sie Fragen oder Bedenken haben, eine neue Funktion vorschlagen oder einfach nur die Bedingungen verbessern möchten, können Sie ein Problem oder eine PR erstellen.
Bitte befolgen Sie [unsere Beitragsrichtlinien](../../CONTRIBUTING.md)
---
## Changelog ⏳
Änderungsprotokoll von termscp ansehen [HIER](../../CHANGELOG.md)
---
## Powered by 💪
termscp wird von diesen großartigen Projekten unterstützt:
- [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)
- [open-rs](https://github.com/Byron/open-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-s3](https://github.com/durch/rust-s3)
- [self_update](https://github.com/jaemk/self_update)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
- [suppaftp](https://github.com/veeso/suppaftp)
- [textwrap](https://github.com/mgeisler/textwrap)
- [tui-rs](https://github.com/fdehau/tui-rs)
- [tui-realm](https://github.com/veeso/tui-realm)
- [whoami](https://github.com/libcala/whoami)
- [wildmatch](https://github.com/becheran/wildmatch)
---
## Galerie 🎬
> Termscp Home
![Auth](/assets/images/auth.gif)
> Bookmarks
![Bookmarks](/assets/images/bookmarks.gif)
> Setup
![Setup](/assets/images/config.gif)
> Text editor
![TextEditor](/assets/images/text-editor.gif)
---
## License 📃
termscp ist unter der MIT-Lizenz lizenziert.
Du kannst die gesamte Lizenz [HIER](../../LICENSE) lesen

View file

@ -3,7 +3,9 @@
- [User manual 🎓](#user-manual-)
- [Usage ❓](#usage-)
- [Address argument 🌎](#address-argument-)
- [AWS S3 address argument](#aws-s3-address-argument)
- [How Password can be provided 🔐](#how-password-can-be-provided-)
- [Aws S3 credentials 🦊](#aws-s3-credentials-)
- [File explorer 📂](#file-explorer-)
- [Keybindings ⌨](#keybindings-)
- [Work on multiple files 🥷](#work-on-multiple-files-)
@ -17,13 +19,16 @@
- [SSH Key Storage 🔐](#ssh-key-storage-)
- [File Explorer Format](#file-explorer-format)
- [Themes 🎨](#themes-)
- [My theme won't load 😱](#my-theme-wont-load-)
- [Styles 💈](#styles-)
- [Authentication page](#authentication-page)
- [Transfer page](#transfer-page)
- [Misc](#misc)
- [Text Editor ✏](#text-editor-)
- [How do I configure the text editor 🦥](#how-do-i-configure-the-text-editor-)
- [Logging 🩺](#logging-)
- [Notifications 📫](#notifications-)
> ❗ I need a help to translate this manual into German. If you want to contribute to the translations, please open a PR 🙏
## Usage ❓
@ -35,6 +40,7 @@ termscp can be started with the following options:
- `-c, --config` Open termscp starting from the configuration page
- `-q, --quiet` Disable logging
- `-t, --theme <path>` Import specified theme
- `-u, --update` Update termscp to latest version
- `-v, --version` Print version info
- `-h, --help` Print help page
@ -78,6 +84,20 @@ Let's see some example of this particular syntax, since it's very comfortable an
termscp scp://omar@192.168.1.31:4022:/tmp
```
#### AWS S3 address argument
Aws S3 has a different syntax for CLI address argument, for obvious reasons, but I managed to keep it the more similar as possible to the generic address argument:
```txt
s3://<bucket-name>@<region>[:profile][:/wrkdir]
```
e.g.
```txt
s3://buckethead@eu-central-1:default:/assets
```
#### How Password can be provided 🔐
You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
@ -89,6 +109,30 @@ Password can be basically provided through 3 ways when address argument is provi
---
## Aws S3 credentials 🦊
In order to connect to an Aws S3 bucket you must obviously provide some credentials.
There are basically two ways to achieve this, and as you've probably already noticed you **can't** do that via the authentication form.
So these are the ways you can provide the credentials for s3:
1. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form.
2. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below:
These should always be mandatory:
- `AWS_ACCESS_KEY_ID`: aws access key ID (usually starts with `AKIA...`)
- `AWS_SECRET_ACCESS_KEY`: the secret access key
In case you've configured a stronger security, you *may* require these too:
- `AWS_SECURITY_TOKEN`: security token
- `AWS_SESSION_TOKEN`: session token
⚠️ Your credentials are safe: termscp won't manipulate these values directly! Your credentials are directly consumed by the **s3** crate.
In case you've got some concern regarding security, please contact the library author on [Github](https://github.com/durch/rust-s3) ⚠️
---
## File explorer 📂
When we refer to file explorers in termscp, we refer to the panels you can see after establishing a connection with the remote.
@ -155,9 +199,9 @@ All the actions are available when working with multiple files, but be aware tha
### Synchronized browsing ⏲️
When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels.
This means that whenever you'll change the working directory on one panel, the same action will be reproduced on the other panel. If you want to enable synchronized browsing just press `<Y>`; press twice to disable. While enabled, the synchronized browising state will be reported on the status bar on `ON`.
This means that whenever you'll change the working directory on one panel, the same action will be reproduced on the other panel. If you want to enable synchronized browsing just press `<Y>`; press twice to disable. While enabled, the synchronized browsing state will be reported on the status bar on `ON`.
*Warning*: at the moment, whenever you try to access an unexisting directory, you won't be prompted to create it. This might change in a future update.
> ❗ at the moment, whenever you try to access an unexisting directory, you won't be prompted to create it. This might change in a future update.
### Open and Open With 🚪
@ -191,14 +235,7 @@ Bookmarks will be saved, if possible at:
- `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.
If you're concerned about the security of the password saved for your bookmarks, please read the [chapter below 👀](#are-my-passwords-safe-).
In order to create a new bookmark, just follow these steps:
@ -214,12 +251,12 @@ whenever you want to use the previously saved connection, just press `<TAB>` to
### Are my passwords Safe 😈
Well, Yep 😉.
Sure 😉.
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? Absolutely! (except for BSD and WSL users 😢)
On **Windows**, **Linux** and **MacOS** the passwords are stored, if possible (but should be), respectively in the *Windows Vault*, in the *system keyring* and into the *Keychain*. This is actually super-safe and is directly managed by your operating system.
On **Windows**, **Linux** and **MacOS** the key used to encrypt passwords is stored, if possible (but should be), respectively in the *Windows Vault*, in the *system keyring* and into the *Keychain*. This is actually super-safe and is directly managed by your operating system.
❗ Please, notice that if you're a Linux user, you should really read the [chapter below 👀](#linux-keyring), because the keyring might not be enabled or supported on your system!
❗ Please, notice that if you're a Linux user, you'd better to read the [chapter below 👀](#linux-keyring), because the keyring might not be enabled or supported on your system!
On *BSD* and *WSL*, 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 😉.
@ -265,9 +302,12 @@ 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.
- **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.
- **Prompt when replacing existing files?**: If set to `yes`, termscp will prompt for confirmation you whenever a file transfer would cause an existing file on target host to be replaced.
- **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.
- **Remote File formatter syntax**: syntax to display file info for each file in the remote explorer. See [File explorer format](#file-explorer-format)
- **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format)
- **Enable notifications?**: If set to `Yes`, notifications will be displayed.
- **Notifications: minimum transfer size**: if transfer size is greater or equal than the specified value, notifications for transfer will be displayed. The accepted values are in format `{UNSIGNED} B/KB/MB/GB/TB/PB`
### SSH Key Storage 🔐
@ -298,7 +338,7 @@ These are the keys supported by the formatter:
- `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)
- `NAME`: File name (Elided if longer than LENGTH)
- `PEX`: File permissions (UNIX format)
- `SIZE`: File size (omitted for directories)
- `SYMLINK`: Symlink (if any `-> {FILE_PATH}`)
@ -320,12 +360,30 @@ In order to create your own customization from termscp, all you have to do so is
Here you can move with `<UP>` and `<DOWN>` to change the style you want to change, as shown in the gif below:
![Themes](../assets/images/themes.gif)
![Themes](https://github.com/veeso/termscp/blob/main/assets/images/themes.gif?raw=true)
termscp supports both the traditional explicit hex (`#rrggbb`) and rgb `rgb(r, g, b)` syntax to provide colors, but also **[css colors](https://www.w3schools.com/cssref/css_colors.asp)** (such as `crimson`) are accepted 😉. There is also a special keywork which is `Default`. Default means that the color used will be the default foreground or background color based on the situation (foreground for texts and lines, background for well, guess what).
As said before, you can also import theme files. You can take inspiration from or directly use one of the themes provided along with termscp, located in the `themes/` directory of this repository and import them running termscp as `termscp -t <theme_file>`. If everything was fine, it should tell you the theme has successfully been imported.
### My theme won't load 😱
This is probably due to a recent update which has broken the theme. Whenever I add a new key to themes, the saved theme won't load. To fix this issues there are two really quick-fix solutions:
1. Reload theme: whenever I release an update I will also patch the "official" themes, so you just have to download it from the repository again and re-import the theme via `-t` option
```sh
termscp -t <theme.toml>
```
2. Fix your theme: If you're using a custom theme, then you can edit via `vim` and add the missing key. The theme is located at `$CONFIG_DIR/termscp/theme.toml` where `$CONFIG_DIR` is:
- FreeBSD/GNU-Linux: `$HOME/.config/`
- MacOs: `$HOME/Library/Application Support`
- Windows: `%appdata%`
❗ Missing keys are reported in the CHANGELOG under `BREAKING CHANGES` for the version you've just installed.
### Styles 💈
You can find in the table below, the description for each style field.
@ -368,6 +426,7 @@ These styles applie to different part of the application.
| Key | Description |
|-------------------|---------------------------------------------|
| misc_error_dialog | Color for error messages |
| misc_info_dialog | Color for info dialogs |
| misc_input_dialog | Color for input dialogs (such as copy file) |
| misc_keys | Color of text for key strokes |
| misc_quit_dialog | Color for quit dialogs |
@ -383,10 +442,6 @@ In case the file is located on remote host, the file will be first downloaded in
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. [Read more](#configuration-)
---
## Logging 🩺
@ -407,7 +462,7 @@ No. The reason is quite simple: when an issue happens, you must be able to know
> If trace level is set for logging, is the file going to reach a huge size?
Probably not, unless you never quit termscp, but I think that's likely to happne. A long session may produce up to 10MB of log files (I said a long session), but I think a normal session won't exceed 2MB.
Probably not, unless you never quit termscp, but I think that's unlikely to happen. A long session may produce up to 10MB of log files (I said a long session), but I think a normal session won't exceed 2MB.
> I don't want logging, can I turn it off?
@ -416,3 +471,18 @@ Yes, you can. Just start termscp with `-q or --quiet` option. You can alias term
> Is logging safe?
If you're concerned about security, the log file doesn't contain any plain password, so don't worry and exposes the same information the sibling file `bookmarks` reports.
## Notifications 📫
Termscp will send Desktop notifications for these kind of events:
- on **Transfer completed**: The notification will be sent once a transfer has been successfully completed.
- ❗ The notification will be displayed only if the transfer total size is at least the specified `Notifications: minimum transfer size` in the configuration.
- on **Transfer failed**: The notification will be sent once a transfer has failed due to an error.
- ❗ The notification will be displayed only if the transfer total size is at least the specified `Notifications: minimum transfer size` in the configuration.
- on **Update available**: Whenever a new version of termscp is available, a notification will be displayed.
- on **Update installed**: Whenever a new version of termscp has been installed, a notification will be displayed.
- on **Update failed**: Whenever the installation of the update fails, a notification will be displayed.
❗ If you prefer to keep notifications turned off, you can just enter setup and set `Enable notifications?` to `No` 😉.
❗ If you want to change the minimum transfer size to display notifications, you can change the value in the configuration with key `Notifications: minimum transfer size` and set it to whatever suits better for you 🙂.

302
docs/es/README.md Normal file
View file

@ -0,0 +1,302 @@
# termscp
<p align="center">
<img src="/assets/images/termscp.svg" width="256" height="256" />
</p>
<p align="center">~ Una transferencia de archivos de terminal rica en funciones ~</p>
<p align="center">
<a href="https://veeso.github.io/termscp/" target="_blank">Sitio Web</a>
·
<a href="https://veeso.github.io/termscp/#get-started" target="_blank">Instalación</a>
·
<a href="https://veeso.github.io/termscp/#user-manual" target="_blank">Manual de usuario</a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp"
><img
height="20"
src="/assets/images/flags/us.png"
alt="English"
/></a>
&nbsp;
<a
href="/docs/de/README.md"
><img
height="20"
src="/assets/images/flags/de.png"
alt="Deutsch"
/></a>
&nbsp;
<a
href="/docs/es/README.md"
><img
height="20"
src="/assets/images/flags/es.png"
alt="Español"
/></a>
&nbsp;
<a
href="/docs/fr/README.md"
><img
height="20"
src="/assets/images/flags/fr.png"
alt="Français"
/></a>
&nbsp;
<a
href="/docs/it/README.md"
><img
height="20"
src="/assets/images/flags/it.png"
alt="Italiano"
/></a>
&nbsp;
<a
href="/docs/zh-CN/README.md"
><img
height="20"
src="/assets/images/flags/cn.png"
alt="简体中文"
/></a>
</p>
<p align="center">Desarrollado por <a href="https://veeso.github.io/" target="_blank">@veeso</a></p>
<p align="center">Versión actual: 0.7.0 (12/10/2021)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
><img
src="https://img.shields.io/badge/License-MIT-teal.svg"
alt="License-MIT"
/></a>
<a href="https://github.com/veeso/termscp/stargazers"
><img
src="https://img.shields.io/github/stars/veeso/termscp.svg"
alt="Repo stars"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/d/termscp.svg"
alt="Downloads counter"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/v/termscp.svg"
alt="Latest version"
/></a>
<a href="https://www.buymeacoffee.com/veeso"
><img
src="https://img.shields.io/badge/Donate-BuyMeACoffee-yellow.svg"
alt="Buy me a coffee"
/></a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Linux/badge.svg"
alt="Linux CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/MacOS/badge.svg"
alt="MacOS CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
alt="Windows CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/FreeBSD/badge.svg"
alt="FreeBSD CI"
/></a>
<a href="https://coveralls.io/github/veeso/termscp"
><img
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
alt="Coveralls"
/></a>
<a href="https://docs.rs/termscp"
><img
src="https://docs.rs/termscp/badge.svg"
alt="Docs"
/></a>
</p>
---
## Sobre termscp 🖥
Termscp es un explorador y transferencia de archivos de terminal rico en funciones, con apoyo para SCP/SFTP/FTP/S3. Básicamente, es una utilidad de terminal con una TUI para conectarse a un servidor remoto para recuperar y cargar archivos e interactuar con el sistema de archivos local. Es compatible con **Linux**, **MacOS**, **FreeBSD** y **Windows**.
![Explorer](/assets/images/explorer.gif)
---
## Características 🎁
- 📁 Diferentes protocolos de comunicación
- **SFTP**
- **SCP**
- **FTP** y **FTPS**
- **Aws S3**
- 🖥 Explore y opere en el sistema de archivos de la máquina local y remota con una interfaz de usuario práctica
- Cree, elimine, cambie el nombre, busque, vea y edite archivos
- ⭐ Conéctese a sus hosts favoritos y conexiones recientes
- 📝 Ver y editar archivos con sus aplicaciones favoritas
- 💁 Autenticación SFTP / SCP con claves SSH y nombre de usuario / contraseña
- 🐧 compatible con Linux, MacOS, FreeBSD y Windows
- 🎨 Haz lo tuyo!
- Temas
- Formato de explorador de archivos personalizado
- Editor de texto personalizable
- Clasificación de archivos personalizable
- y muchos otros parámetros ...
- 📫 Reciba una notificación cuando se haya transferido un archivo grande
- 🔐 Guarde su contraseña en el almacén de claves de su sistema operativo
- 🦀 Rust-powered
- 👀 Desarrollado sin perder de vista el rendimiento
- 🦄 Actualizaciones frecuentes
---
## Para iniciar 🚀
Si estás considerando instalar termscp, ¡quiero darte las gracias 💜! ¡Espero que disfrutes de termscp!
Si desea contribuir a este proyecto, no olvide consultar nuestra [guía de contribución](../../CONTRIBUTING.md).
Si tu eres un usuario de Linux, FreeBSD o MacOS, este sencillo script de shell instalará termscp en tu sistema con un solo comando:
```sh
curl --proto '=https' --tlsv1.2 -sSLf "https://git.io/JBhDb" | sh
```
mientras que si eres un usuario de Windows, puedes instalar termscp con [Chocolatey](https://chocolatey.org/):
```sh
choco install termscp
```
Para obtener más información u otras plataformas, visite [veeso.github.io](https://veeso.github.io/termscp/#get-started) para ver todos los métodos de instalación.
⚠️ Si estás buscando cómo actualizar termscp, simplemente ejecute termscp desde CLI con:: `(sudo) termscp --update` ⚠️
### Requisitos ❗
- Usuarios **Linux**:
- libssh
- libdbus-1
- pkg-config
- Usuarios **FreeBSD**:
- libssh
- dbus
- pkgconf
### Requisitos opcionales ✔️
These requirements are not forced required to run termscp, but to enjoy all of its features
- Usuarios **Linux/FreeBSD**:
- Para **abrir** archivos con `V` (al menos uno de estos)
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- Usuarios **Linux**:
- Un keyring manager: leer más en el [manual de usuario](man.md#linux-keyring)
- Usuarios **WSL**
- Para **abrir** archivos con `V` (al menos uno de estos)
- [wslu](https://github.com/wslutilities/wslu)
---
## Apoyame ☕
Si te gusta termscp y te encantaría que el proyecto crezca y mejore, considera una pequeña donación para apoyarme en **Buy me a coffee** 🥳
[![Buy-me-a-coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=&slug=veeso&button_colour=404040&font_colour=ffffff&font_family=Comic&outline_colour=ffffff&coffee_colour=FFDD00)](https://www.buymeacoffee.com/veeso)
o, si lo prefiere, también puede donar en PayPal:
[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.me/chrisintin)
---
## Manual de usuario y documentación 📚
El manual del usuario se puede encontrar en el [sitio web de termscp](https://veeso.github.io/termscp/#user-manual) o en [Github](man.md).
La documentación para desarrolladores se puede encontrar en Rust Docs en <https://docs.rs/termscp>
---
## Problemas conocidos 🧻
- `NoSuchFileOrDirectory` al conectar (WSL1): Conozco este problema y supongo que es un problema técnico de WSL. No se preocupe, simplemente mueva el ejecutable termscp a otra ubicación en el PATH, como `/usr/bin`, o instálelo a través del formato de paquete apropiado (por ejemplo, deb).
---
## Contribuir y problemas 🤝🏻
¡Las contribuciones, los informes de errores, las nuevas funciones y las preguntas son bienvenidas! 😉
Si tiene alguna pregunta o inquietud, o si desea sugerir una nueva función, o simplemente desea mejorar termscp, no dude en abrir un problema o un PR.
Sigue [nuestras pautas de contribución](../../CONTRIBUTING.md)
---
## Changelog ⏳
Ver registro de cambios de termscp [AQUÍ](../../CHANGELOG.md)
---
## Powered by 💪
termscp funciona con estos increíbles proyectos:
- [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)
- [open-rs](https://github.com/Byron/open-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-s3](https://github.com/durch/rust-s3)
- [self_update](https://github.com/jaemk/self_update)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
- [suppaftp](https://github.com/veeso/suppaftp)
- [textwrap](https://github.com/mgeisler/textwrap)
- [tui-rs](https://github.com/fdehau/tui-rs)
- [tui-realm](https://github.com/veeso/tui-realm)
- [whoami](https://github.com/libcala/whoami)
- [wildmatch](https://github.com/becheran/wildmatch)
---
## Galería 🎬
> Termscp Home
![Auth](/assets/images/auth.gif)
> Bookmarks
![Bookmarks](/assets/images/bookmarks.gif)
> Setup
![Setup](/assets/images/config.gif)
> Text editor
![TextEditor](/assets/images/text-editor.gif)
---
## Licencia 📃
termscp tiene la licencia MIT.
Puede leer la licencia completa [AQUÍ](../../LICENSE)

487
docs/es/man.md Normal file
View file

@ -0,0 +1,487 @@
# User manual 🎓
- [User manual 🎓](#user-manual-)
- [Uso ❓](#uso-)
- [Argumento dirección 🌎](#argumento-dirección-)
- [Argumento dirección por AWS S3](#argumento-dirección-por-aws-s3)
- [Cómo se puede proporcionar la contraseña 🔐](#cómo-se-puede-proporcionar-la-contraseña-)
- [Credenciales de AWS S3 🦊](#credenciales-de-aws-s3-)
- [Explorador de archivos 📂](#explorador-de-archivos-)
- [Keybindings ⌨](#keybindings-)
- [Trabaja en varios archivos 🥷](#trabaja-en-varios-archivos-)
- [Navegación sincronizada ⏲️](#navegación-sincronizada-)
- [Abierta y abierta con 🚪](#abierta-y-abierta-con-)
- [Marcadores ⭐](#marcadores-)
- [¿Son seguras mis contraseñas? 😈](#son-seguras-mis-contraseñas-)
- [Linux Keyring](#linux-keyring)
- [KeepassXC setup por termscp](#keepassxc-setup-por-termscp)
- [Configuración ⚙️](#configuración--)
- [SSH Key Storage 🔐](#ssh-key-storage-)
- [Formato del explorador de archivos](#formato-del-explorador-de-archivos)
- [Temas 🎨](#temas-)
- [Mi tema no se carga 😱](#mi-tema-no-se-carga-)
- [Estilos 💈](#estilos-)
- [Authentication page](#authentication-page)
- [Transfer page](#transfer-page)
- [Misc](#misc)
- [Text Editor ✏](#text-editor-)
- [Logging 🩺](#logging-)
- [Notificaciones 📫](#notificaciones-)
> ❗ Este documento ha sido traducido con Google Translator (y luego lo he revisado a grandes rasgos, pero no puedo hablar el idioma muy bien). Si habla l'idioma, abra un [issue](https://github.com/veeso/termscp/issues/new/choose) utilizando la label COPY o abra un PR 🙏
## Uso ❓
termscp se puede iniciar con las siguientes opciones:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
- `-P, --password <password>` si se proporciona la dirección, la contraseña será este argumento
- `-c, --config` Abrir termscp comenzando desde la página de configuración
- `-q, --quiet` Deshabilitar el registro
- `-t, --theme <path>` Importar tema especificado
- `-u, --update` Actualizar termscp a la última versión
- `-v, --version` Imprimir información de la versión
- `-h, --help` Imprimir página de ayuda
termscp se puede iniciar en dos modos diferentes, si no se proporcionan argumentos adicionales, termscp mostrará el formulario de autenticación, donde el usuario podrá proporcionar los parámetros necesarios para conectarse al par remoto.
Alternativamente, el usuario puede proporcionar una dirección como argumento para omitir el formulario de autenticación e iniciar directamente la conexión al servidor remoto.
Si se proporciona un argumento de dirección, también puede proporcionar el directorio de inicio de trabajo para el host local
### Argumento dirección 🌎
El argumento dirección tiene la siguiente sintaxis:
```txt
[protocol://][username@]<address>[:port][:wrkdir]
```
Veamos algún ejemplo de esta sintaxis en particular, ya que es muy cómoda y probablemente usarás esta en lugar de la otra ...
- Conéctese usando el protocolo predeterminado (*definido en la configuración*) a 192.168.1.31, el puerto, si no se proporciona, es el predeterminado para el protocolo seleccionado (en este caso, depende de su configuración); nombre de usuario es el nombre del usuario actual
```sh
termscp 192.168.1.31
```
- Conéctese usando el protocolo predeterminado (*definido en la configuración*) a 192.168.1.31; el nombre de usuario es `root`
```sh
termscp root@192.168.1.31
```
- Conéctese usando scp a 192.168.1.31, el puerto es 4022; nombre de usuario es `omar`
```sh
termscp scp://omar@192.168.1.31:4022
```
- Conéctese usando scp a 192.168.1.31, el puerto es 4022; El nombre de usuario es `omar`. Comenzará en el directorio `/ tmp`
```sh
termscp scp://omar@192.168.1.31:4022:/tmp
```
#### Argumento dirección por AWS S3
Aws S3 tiene una sintaxis diferente para el argumento de la dirección CLI, por razones obvias, pero logré mantenerlo lo más similar posible al argumento de la dirección genérica:
```txt
s3://<bucket-name>@<region>[:profile][:/wrkdir]
```
por ejemplo
```txt
s3://buckethead@eu-central-1:default:/assets
```
#### Cómo se puede proporcionar la contraseña 🔐
Probablemente haya notado que, al proporcionar la dirección como argumento, no hay forma de proporcionar la contraseña.
La contraseña se puede proporcionar básicamente a través de 3 formas cuando se proporciona un argumento de dirección:
- `-P, --password` opción: simplemente use esta opción CLI proporcionando la contraseña. No recomiendo este método, ya que es muy inseguro (ya que puede mantener la contraseña en el historial de shell)
- Con `sshpass`: puede proporcionar la contraseña a través de `sshpass`, p. ej. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Se te pedirá que ingreses: si no utilizas ninguno de los métodos anteriores, se te pedirá la contraseña, como ocurre con las herramientas más clásicas como `scp`, `ssh`, etc.
---
## Credenciales de AWS S3 🦊
Para conectarse a un bucket de Aws S3, obviamente debe proporcionar algunas credenciales.
Básicamente, hay dos formas de lograr esto, y como probablemente ya hayas notado, **no puedes** hacerlo a través del formulario de autenticación.
Entonces, estas son las formas en que puede proporcionar las credenciales para s3:
1. Use su archivo de credenciales: simplemente configure la cli de AWS a través de `aws configure` y sus credenciales ya deberían estar ubicadas en`~/.aws/credentials`. En caso de que esté usando un perfil diferente al "predeterminado", simplemente proporciónelo en el campo de perfil en el formulario de autenticación.
2. **Variables de entorno**: siempre puede proporcionar sus credenciales como variables de entorno. Tenga en cuenta que estas credenciales **siempre anularán** las credenciales ubicadas en el archivo `credentials`. Vea cómo configurar el entorno a continuación:
Estos siempre deben ser obligatorios:
- `AWS_ACCESS_KEY_ID`: aws access key ID (generalmente comienza con `AKIA...`)
- `AWS_SECRET_ACCESS_KEY`: la secret access key
En caso de que haya configurado una seguridad más fuerte, *puede* requerir estos también:
- `AWS_SECURITY_TOKEN`: security token
- `AWS_SESSION_TOKEN`: session token
⚠️ Sus credenciales están seguras: ¡termscp no manipulará estos valores directamente! Sus credenciales son consumidas directamente por la caja **s3**.
En caso de que tenga alguna inquietud con respecto a la seguridad, comuníquese con el autor de la biblioteca en [Github](https://github.com/durch/rust-s3) ⚠️
---
## Explorador de archivos 📂
Cuando nos referimos a exploradores de archivos en termscp, nos referimos a los paneles que puede ver después de establecer una conexión con el control remoto.
Estos paneles son básicamente 3:
- Panel del explorador local: se muestra a la izquierda de la pantalla y muestra las entradas del directorio actual para localhost
- Panel del explorador remoto: se muestra a la derecha de la pantalla y muestra las entradas del directorio actual para el host remoto.
- Panel de resultados de búsqueda: dependiendo de dónde esté buscando archivos (local / remoto), reemplazará el panel local o del explorador. Este panel muestra las entradas que coinciden con la consulta de búsqueda que realizó.
Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador remoto y `<RIGHT>` para volver al panel del explorador local. Siempre que se encuentre en el panel de resultados de búsqueda, debe presionar `<ESC>` para salir del panel y volver al panel anterior.
### Keybindings ⌨
| Key | Command | Reminder |
|---------------|-------------------------------------------------------|-------------|
| `<ESC>` | Desconecte; volver a la página de autenticación | |
| `<TAB>` | Cambiar entre la pestaña de registro y el explorador | |
| `<BACKSPACE>` | Ir al directorio anterior en la pila | |
| `<RIGHT>` | Mover a la pestaña del explorador remoto | |
| `<LEFT>` | Mover a la pestaña del explorador local | |
| `<UP>` | Subir en la lista seleccionada | |
| `<DOWN>` | Bajar en la lista seleccionada | |
| `<PGUP>` | Subir 8 filas en la lista seleccionada | |
| `<PGDOWN>` | Bajar 8 filas en la lista seleccionada | |
| `<ENTER>` | Entrar directorio | |
| `<SPACE>` | Cargar / descargar el archivo seleccionado | |
| `<A>` | Alternar archivos ocultos | All |
| `<B>` | Ordenar archivos por | Bubblesort? |
| `<C>` | Copiar archivo / directorio | Copy |
| `<D>` | Hacer directorio | Directory |
| `<E>` | Eliminar archivo (igual que `DEL`) | Erase |
| `<F>` | Búsqueda de archivos | Find |
| `<G>` | Ir a la ruta proporcionada | Go to |
| `<H>` | Mostrar ayuda | Help |
| `<I>` | Mostrar información sobre el archivo | Info |
| `<L>` | Recargar contenido del directorio / Borrar selección | List |
| `<M>` | Seleccione un archivo | Mark |
| `<N>` | Crear un nuevo archivo con el nombre proporcionado | New |
| `<O>` | Editar archivo | Open |
| `<Q>` | Salir de termscp | Quit |
| `<R>` | Renombrar archivo | Rename |
| `<S>` | Guardar archivo como... | Save |
| `<U>` | Ir al directorio principal | Upper |
| `<V>` | Abrir archivo con el programa predeterminado | View |
| `<W>` | Abrir archivo con el programa proporcionado | With |
| `<X>` | Ejecutar un comando | eXecute |
| `<Y>` | Alternar navegación sincronizada | sYnc |
| `<DEL>` | Eliminar archivo | |
| `<CTRL+A>` | Seleccionar todos los archivos | |
| `<CTRL+C>` | Abortar el proceso de transferencia de archivos | |
### Trabaja en varios archivos 🥷
Puede optar por trabajar en varios archivos, seleccionándolos presionando `<M>`, para seleccionar el archivo actual, o presionando `<CTRL + A>`, que seleccionará todos los archivos en el directorio de trabajo.
Una vez que un archivo está marcado para su selección, se mostrará con un `*` a la izquierda.
Al trabajar en la selección, solo se procesará el archivo seleccionado para las acciones, mientras que el elemento resaltado actual se ignorará.
También es posible trabajar en varios archivos desde el panel de resultados de búsqueda.
Todas las acciones están disponibles cuando se trabaja con varios archivos, pero tenga en cuenta que algunas acciones funcionan de forma ligeramente diferente. Vamos a sumergirnos en:
- *Copy*: cada vez que copie un archivo, se le pedirá que inserte el nombre de destino. Cuando se trabaja con varios archivos, este nombre se refiere al directorio de destino donde se copiarán todos estos archivos.
- *Rename*: igual que copiar, pero moverá archivos allí.
- *Save as*: igual que copiar, pero los escribirá allí.
### Navegación sincronizada ⏲️
Cuando está habilitada, la navegación sincronizada le permitirá sincronizar la navegación entre los dos paneles.
Esto significa que siempre que cambie el directorio de trabajo en un panel, la misma acción se reproducirá en el otro panel. Si desea habilitar la navegación sincronizada, simplemente presione `<Y>`; presione dos veces para deshabilitar. Mientras está habilitado, el estado de navegación sincronizada se informará en la barra de estado en "ON".
> ❗ Por el momento, cada vez que intente acceder a un directorio que no existe, no se le pedirá que lo cree. Esto podría cambiar en una actualización futura.
### Abierta y abierta con 🚪
Al abrir archivos con el comando Ver (`<V>`), se utilizará la aplicación predeterminada del sistema para el tipo de archivo. Para hacerlo, se utilizará el servicio del sistema operativo predeterminado, así que asegúrese de tener al menos uno de estos instalado en su sistema:
- Usuarios **Windows**: no tiene que preocuparse por eso, ya que la caja usará el comando `start`.
- Usuarios **MacOS**: tampoco tiene que preocuparse, ya que la caja usará `open`, que ya está instalado en su sistema.
- Usuarios **Linux**: uno de estos debe estar instalado
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- Usuarios **WSL**: *wslview* es obligatorio, debe instalar [wslu](https://github.com/wslutilities/wslu).
> Q: ¿Puedo editar archivos remotos usando el comando de vista?
> A: No, al menos no directamente desde el "panel remoto". Primero debe descargarlo en un directorio local, eso se debe al hecho de que cuando abre un archivo remoto, el archivo se descarga en un directorio temporal, pero no hay forma de crear un observador para que el archivo verifique cuándo el programa utilizado para abrirlo estaba cerrado, por lo que termscp no puede saber cuándo ha terminado de editar el archivo.
---
## Marcadores ⭐
En termscp es posible guardar hosts favoritos, que luego se pueden cargar rápidamente desde el diseño principal de termscp.
termscp también guardará los últimos 16 hosts a los que se conectó.
Esta función le permite cargar todos los parámetros necesarios para conectarse a un determinado control remoto, simplemente seleccionando el marcador en la pestaña debajo del formulario de autenticación.
Los marcadores se guardarán, si es posible, en:
- `$HOME/.config/termscp/` en Linux/BSD
- `$HOME/Library/Application Support/termscp` en MacOs
- `FOLDERID_RoamingAppData\termscp\` en Windows
Solo para marcadores (esto no se aplicará a hosts recientes) también es posible guardar la contraseña utilizada para autenticarse. La contraseña no se guarda de forma predeterminada y debe especificarse a través del indicador al guardar un nuevo marcador.
Si le preocupa la seguridad de la contraseña guardada para sus marcadores, lea el [capítulo siguiente 👀](#are-my-passwords-safe-).
Para crear un nuevo marcador, simplemente siga estos pasos:
1. Escriba en el formulario de autenticación los parámetros para conectarse a su servidor remoto
2. Presionar `<CTRL + S>`
3. Escriba el nombre que desea darle al marcador
4. Elija si recordar la contraseña o no
5. Presionar `<ENTER>`
siempre que desee utilizar la conexión previamente guardada, simplemente presione `<TAB>` para navegar a la lista de marcadores y cargue los parámetros del marcador en el formulario presionando `<ENTER>`.
![Bookmarks](https://github.com/veeso/termscp/blob/main/assets/images/bookmarks.gif?raw=true)
### ¿Son seguras mis contraseñas? 😈
Seguro 😉.
Como se dijo antes, los marcadores se guardan en su directorio de configuración junto con las contraseñas. Las contraseñas obviamente no son texto sin formato, están encriptadas con **AES-128**. ¿Esto los hace seguros? ¡Absolutamente! (excepto para usuarios de BSD y WSL 😢)
En **Windows**, **Linux** y **MacOS**, la clave utilizada para cifrar las contraseñas se almacena, si es posible (pero debería estar), respectivamente, en *Windows Vault*, en el *anillo de claves del sistema* y en el *Llavero*. Esto es realmente muy seguro y lo administra directamente su sistema operativo.
❗ Por favor, tenga en cuenta que si es un usuario de Linux, es mejor que lea el [capítulo siguiente 👀](#linux-keyring), ¡porque es posible que el llavero no esté habilitado o no sea compatible con su sistema!
En *BSD* y *WSL*, por otro lado, la clave utilizada para cifrar sus contraseñas se almacena en su disco (en `$HOME/.config/ termscp`). Entonces, todavía es posible recuperar la clave para descifrar las contraseñas. Afortunadamente, la ubicación de la clave garantiza que su clave no pueda ser leída por usuarios diferentes al suyo, pero sí, todavía no guardaría la contraseña para un servidor expuesto en Internet 😉.
#### Linux Keyring
A todos nos encanta Linux gracias a la libertad que ofrece a los usuarios. Básicamente, puede hacer lo que quiera como usuario de Linux, pero esto también tiene algunas desventajas, como el hecho de que a menudo no hay aplicaciones estándar en las diferentes distribuciones. Y esto también implica el llavero.
Esto significa que en Linux puede que no haya un llavero instalado en su sistema. Desafortunadamente, la biblioteca que usamos para trabajar con el almacenamiento de claves requiere un servicio que exponga `org.freedesktop.secrets` en D-BUS y el peor hecho es que solo hay dos servicios que lo exponen.
- ❗ Si usa GNOME como entorno de escritorio (por ejemplo, usuarios de ubuntu), ya debería estar bien, ya que `gnome-keyring` ya proporciona el llavero y todo debería estar funcionando.
- ❗ Para otros usuarios de entornos de escritorio, hay un buen programa que pueden usar para obtener un llavero que es [KeepassXC](https://keepassxc.org/), que utilizo en mi instalación de Manjaro (con KDE) y funciona bien. El único problema es que debe configurarlo para que se use junto con termscp (pero es bastante simple). Para comenzar con KeepassXC, lea más [aquí](#keepassxc-setup-por-termscp).
- ❗ ¿Qué pasa si no desea instalar ninguno de estos servicios? Bueno, ¡no hay problema! **termscp seguirá funcionando como de costumbre**, pero guardará la clave en un archivo, como suele hacer para BSD y WSL.
##### KeepassXC setup por termscp
Siga estos pasos para configurar keepassXC para termscp:
1. Instalar KeepassXC
2. Vaya a "tools" > "settings" en la barra de herramientas
3. Seleccione "Secret service integration" y abilita "Enable KeepassXC freedesktop.org secret service integration"
4. Cree una base de datos, si aún no tiene una: desde la barra de herramientas "Database" > "New database"
5. Desde la barra de herramientas: "Database" > "Database settings"
6. Seleccione "Secret service integration" y abilita "Expose entries under this group"
7. Seleccione el grupo de la lista donde desea que se mantenga el secreto de termscp. Recuerde que este grupo puede ser utilizado por cualquier otra aplicación para almacenar secretos a través de DBUS.
---
## Configuración ⚙️
termscp admite algunos parámetros definidos por el usuario, que se pueden definir en la configuración.
Underhood termscp tiene un archivo TOML y algunos otros directorios donde se guardarán todos los parámetros, pero no se preocupe, no tocará ninguno de estos archivos manualmente, ya que hice posible configurar termscp desde su interfaz de usuario por completo.
termscp, al igual que para los marcadores, solo requiere tener estas rutas accesibles:
- `$HOME/.config/termscp/` en Linux/BSD
- `$HOME/Library/Application Support/termscp` en MacOs
- `FOLDERID_RoamingAppData\termscp\` en Windows
Para acceder a la configuración, solo tiene que presionar `<CTRL + C>` desde el inicio de termscp.
Estos parámetros se pueden cambiar:
- **Text Editor**: l editor de texto a utilizar. Por defecto, termscp encontrará el editor predeterminado para usted; con esta opción puede forzar el uso de un editor (por ejemplo, `vim`). **También se admiten los editores de GUI**, a menos que hagan "nohup" del proceso principal.
- **Default Protocol**: el protocolo predeterminado es el valor predeterminado para el protocolo de transferencia de archivos que se utilizará en termscp. Esto se aplica a la página de inicio de sesión y al argumento de la CLI de la dirección.
- **Show Hidden Files**: seleccione si los archivos ocultos se mostrarán de forma predeterminada. Podrás decidir si mostrar o no archivos ocultos en tiempo de ejecución presionando "A" de todos modos.
- **Check for updates**: si se establece en `yes`, termscp buscará la API de Github para comprobar si hay una nueva versión de termscp disponible.
- **Prompt when replacing existing files?**: Si se establece en "sí", termscp le pedirá confirmación cada vez que una transferencia de archivo provoque la sustitución de un archivo existente en el host de destino.
- **Group Dirs**: seleccione si los directorios deben agruparse o no en los exploradores de archivos. Si se selecciona `Display first`, los directorios se ordenarán usando el método configurado pero se mostrarán antes de los archivos, y viceversa si se selecciona`Display last`.
- **Remote File formatter syntax**: sintaxis para mostrar información de archivo para cada archivo en el explorador remoto. Consulte [Formato del explorador de archivos](#formato-del-explorador-de-archivos).
- **Local File formatter syntax**: sintaxis para mostrar información de archivo para cada archivo en el explorador local. Consulte [Formato del explorador de archivos](#formato-del-explorador-de-archivos).
- **Enable notifications?**: Si se establece en "Sí", se mostrarán las notificaciones.
- **Notifications: minimum transfer size**: si el tamaño de la transferencia es mayor o igual que el valor especificado, se mostrarán notificaciones de transferencia. Los valores aceptados están en formato `{UNSIGNED} B/KB/MB/GB/TB/PB`
### SSH Key Storage 🔐
Junto con la configuración, termscp también proporciona una característica **esencial** para **clientes SFTP / SCP**: el almacenamiento de claves SSH.
Puede acceder al almacenamiento de claves SSH, desde la configuración pasando a la pestaña `Claves SSH`, una vez allí puede:
- **Agregar una nueva clave**: simplemente presione `<CTRL + N>` y se le pedirá que cree una nueva clave. Proporcione el nombre de host / dirección IP y el nombre de usuario asociado a la clave y finalmente se abrirá un editor de texto: pegue la clave ssh **PRIVATE** en el editor de texto, guarde y salga.
- **Eliminar una clave existente**: simplemente presione `<DEL>` o `<CTRL + E>` en la clave que desea eliminar, para eliminar persistentemente la clave de termscp.
- **Editar una clave existente**: simplemente presione `<ENTER>` en la clave que desea editar, para cambiar la clave privada.
> Q: Mi clave privada está protegida con contraseña, ¿puedo usarla?
> A: Por supuesto que puede. La contraseña proporcionada para la autenticación en termscp es válida tanto para la autenticación de nombre de usuario / contraseña como para la autenticación de clave RSA.
### Formato del explorador de archivos
Es posible a través de la configuración definir un formato personalizado para el explorador de archivos. Esto es posible tanto para el host local como para el remoto, por lo que puede tener dos sintaxis diferentes en uso. Estos campos, con el nombre `File formatter syntax (local)` y `File formatter syntax (remote)` definirán cómo se mostrarán las entradas del archivo en el explorador de archivos.
La sintaxis del formateador es la siguiente `{KEY1} ... {KEY2:LENGTH} ... {KEY3:LENGTH:EXTRA} {KEYn} ...`.
Cada clave entre corchetes se reemplazará con el atributo relacionado, mientras que todo lo que esté fuera de los corchetes se dejará sin cambios.
- El nombre de la clave es obligatorio y debe ser una de las claves siguientes
- La longitud describe la longitud reservada para mostrar el campo. Los atributos estáticos no admiten esto (GROUP, PEX, SIZE, USER)
- Extra es compatible solo con algunos parámetros y es una opción adicional. Consulte las claves para comprobar si se admite extra.
Estas son las claves admitidas por el formateador:
- `ATIME`: Hora del último acceso (con la sintaxis predeterminada`%b %d %Y %H:%M`); Se puede proporcionar un extra como la sintaxis de tiempo (por ejemplo, "{ATIME: 8:% H:% M}")
- `CTIME`: Hora de creación (con sintaxis`%b %d %Y %H:%M`); Se puede proporcionar un extra como sintaxis de tiempo (p. Ej., `{CTIME:8:%H:%M}`)
- `GROUP`: Grupo propietario
- `MTIME`: Hora del último cambio (con sintaxis`%b %d %Y %H:%M`); Se puede proporcionar extra como sintaxis de tiempo (p. Ej., `{MTIME: 8:% H:% M}`)
- `NAME`: nombre de archivo (se omite si es más largo que LENGTH)
- `PEX`: permisos de archivo (formato UNIX)
- `SIZE`: Tamaño del archivo (se omite para directorios)
- `SYMLINK`: Symlink (si existe` -> {FILE_PATH} `)
- `USER`: Usuario propietario
Si se deja vacío, se utilizará la sintaxis del formateador predeterminada: `{NAME:24} {PEX} {USER} {SIZE} {MTIME:17:%b %d %Y %H:%M}`
---
## Temas 🎨
Termscp le ofrece una característica asombrosa: la posibilidad de configurar los colores para varios componentes de la aplicación.
Si desea personalizar termscp, hay dos formas disponibles de hacerlo:
- Desde el **menú de configuración**
- Importando un **archivo de tema**
Para crear su propia personalización desde termscp, todo lo que tiene que hacer es ingresar a la configuración desde la actividad de autenticación, presionando `<CTRL + C>` y luego `<TAB>` dos veces. Deberías haberte movido ahora al panel de `temas`.
Aquí puede moverse con `<UP>` y `<DOWN>` para cambiar el estilo que desea cambiar, como se muestra en el siguiente gif:
![Themes](https://github.com/veeso/termscp/blob/main/assets/images/themes.gif?raw=true)
termscp admite la sintaxis tradicional hexadecimal explícita (`#rrggbb`) y rgb `rgb(r, g, b)` para proporcionar colores, pero se aceptan también **[colores css](https://www.w3schools.com/cssref/css_colors.asp)** (como `crimson`) 😉. También hay un teclado especial que es `Default`. Predeterminado significa que el color utilizado será el color de primer plano o de fondo predeterminado según la situación (primer plano para textos y líneas, fondo para bien, adivinen qué).
Como se dijo antes, también puede importar archivos de temas. Puede inspirarse o utilizar directamente uno de los temas proporcionados junto con termscp, ubicado en el directorio `themes/` de este repositorio e importarlos ejecutando termscp como `termscp -t <theme_file>`. Si todo estuvo bien, debería decirle que el tema se ha importado correctamente.
### Mi tema no se carga 😱
Esto probablemente se deba a una actualización reciente que ha roto el tema. Siempre que agrego una nueva clave a los temas, el tema guardado no se carga. Para solucionar este problema, existen dos soluciones realmente rápidas:
1. Recargar tema: cada vez que publique una actualización, también parchearé los temas "oficiales", por lo que solo tiene que descargarlo del repositorio nuevamente y volver a importar el tema a través de la opción `-t`
```sh
termscp -t <theme.toml>
```
2. Corrija su tema: si está utilizando un tema personalizado, puede editarlo a través de `vim` y agregar la clave que falta. El tema se encuentra en `$CONFIG_DIR/termscp/theme.toml` donde `$CONFIG_DIR` es:
- FreeBSD/GNU-Linux: `$HOME/.config/`
- MacOs: `$HOME/Library/Application Support`
- Windows: `%appdata%`
❗ Las claves que faltan se informan en el CAMBIO en `BREAKING CHANGES` para la versión que acaba de instalar.
### Estilos 💈
Puede encontrar en la tabla siguiente la descripción de cada campo de estilo.
Tenga en cuenta que **los estilos no se aplicarán a la página de configuración**, para que sea siempre accesible en caso de que lo estropee todo
#### Authentication page
| Key | Description |
|----------------|------------------------------------------|
| auth_address | Color of the input field for IP address |
| auth_bookmarks | Color of the bookmarks panel |
| auth_password | Color of the input field for password |
| auth_port | Color of the input field for port number |
| auth_protocol | Color of the radio group for protocol |
| auth_recents | Color of the recents panel |
| auth_username | Color of the input field for username |
#### Transfer page
| Key | Description |
|--------------------------------------|---------------------------------------------------------------------------|
| transfer_local_explorer_background | Background color of localhost explorer |
| transfer_local_explorer_foreground | Foreground coloor of localhost explorer |
| transfer_local_explorer_highlighted | Border and highlighted color for localhost explorer |
| transfer_remote_explorer_background | Background color of remote explorer |
| transfer_remote_explorer_foreground | Foreground coloor of remote explorer |
| transfer_remote_explorer_highlighted | Border and highlighted color for remote explorer |
| transfer_log_background | Background color for log panel |
| transfer_log_window | Window color for log panel |
| transfer_progress_bar_partial | Partial progress bar color |
| transfer_progress_bar_total | Total progress bar color |
| transfer_status_hidden | Color for status bar "hidden" label |
| transfer_status_sorting | Color for status bar "sorting" label; applies also to file sorting dialog |
| transfer_status_sync_browsing | Color for status bar "sync browsing" label |
#### Misc
These styles applie to different part of the application.
| Key | Description |
|-------------------|---------------------------------------------|
| misc_error_dialog | Color for error messages |
| misc_info_dialog | Color for info dialogs |
| misc_input_dialog | Color for input dialogs (such as copy file) |
| misc_keys | Color of text for key strokes |
| misc_quit_dialog | Color for quit dialogs |
| misc_save_dialog | Color for save dialogs |
| misc_warn_dialog | Color for warn dialogs |
---
## Text Editor ✏
termscp tiene, como habrás notado, muchas características, una de ellas es la posibilidad de ver y editar archivos de texto. No importa si el archivo está ubicado en el host local o en el host remoto, termscp brinda la posibilidad de abrir un archivo en su editor de texto favorito.
En caso de que el archivo esté ubicado en un host remoto, el archivo se descargará primero en su directorio de archivos temporales y luego, **solo** si se realizaron cambios en el archivo, se volverá a cargar en el host remoto. termscp comprueba si realizó cambios en el archivo verificando la última hora de modificación del archivo.
> ❗ Just a reminder: **you can edit only textual file**; binary files are not supported.
---
## Logging 🩺
termscp escribe un archivo de registro para cada sesión, que se escribe en
- `$HOME/.config/termscp/termscp.log` en Linux/BSD
- `$HOME/Library/Application Support/termscp/termscp.log` en MacOs
- `FOLDERID_RoamingAppData\termscp\termscp.log` en Windows
el registro no se rotará, sino que se truncará después de cada lanzamiento de termscp, por lo que si desea informar un problema y desea adjuntar su archivo de registro, recuerde guardar el archivo de registro en un lugar seguro antes de usar termscp de nuevo.
El archivo de registro siempre informa en el nivel de *seguimiento*, por lo que es un poco detallado.
Sé que es posible que tenga algunas preguntas sobre los archivos de registro, así que hice una especie de Q/A:
> ¿Es posible reducir la verbosidad?
No. La razón es bastante simple: cuando ocurre un problema, debe poder saber qué lo está causando y la única forma de hacerlo es tener el archivo de registro con el nivel de verbosidad máximo establecido.
> Si el nivel de seguimiento está configurado para el registro, ¿el archivo alcanzará un tamaño enorme?
Probablemente no, a menos que nunca cerra termscp, pero creo que es poco probable que eso suceda. Una sesión larga puede producir hasta 10 MB de archivos de registro (dije una sesión larga), pero creo que una sesión normal no excederá los 2 MB.
> No quiero el registro, ¿puedo apagarlo?
Sí tu puedes. Simplemente inicie termscp con la opción `-q o --quiet`. Puede alias termscp para que sea persistente. Recuerde que el registro se usa para diagnosticar problemas, por lo que, dado que detrás de cada proyecto de código abierto, siempre debe haber este tipo de ayuda mutua, mantener los archivos de registro puede ser su forma de respaldar el proyecto 😉.
> ¿Es seguro el registro?
Si le preocupa la seguridad, el archivo de registro no contiene ninguna contraseña simple, así que no se preocupe y expone la misma información que informa el archivo hermano `marcadores`.
## Notificaciones 📫
Termscp enviará notificaciones de escritorio para este tipo de eventos:
- en **Transferencia completada**: la notificación se enviará una vez que la transferencia se haya completado con éxito.
- ❗ La notificación se mostrará solo si el tamaño total de la transferencia es al menos el `Notifications: minimum transfer size` especificado en la configuración.
- en **Transferencia fallida**: la notificación se enviará una vez que la transferencia haya fallado debido a un error.
- ❗ La notificación se mostrará solo si el tamaño total de la transferencia es al menos el `Notifications: minimum transfer size` especificado en la configuración.
- en **Actualización disponible**: siempre que haya una nueva versión de termscp disponible, se mostrará una notificación.
- en **Actualización instalada**: siempre que se haya instalado una nueva versión de termscp, se mostrará una notificación.
- en **Actualización fallida**: siempre que falle la instalación de la actualización, se mostrará una notificación.
❗ Si prefiere mantener las notificaciones desactivadas, puede simplemente ingresar a la configuración y configurar `Enable notifications?` En `No` 😉.
❗ Si desea cambiar el tamaño mínimo de transferencia para mostrar notificaciones, puede cambiar el valor en la configuración con la tecla `Notifications: minimum transfer size` y configurarlo como mejor le convenga 🙂.

302
docs/fr/README.md Normal file
View file

@ -0,0 +1,302 @@
# termscp
<p align="center">
<img src="/assets/images/termscp.svg" width="256" height="256" />
</p>
<p align="center">~ Un file transfer de terminal riche en fonctionnalités ~</p>
<p align="center">
<a href="https://veeso.github.io/termscp/" target="_blank">Site internet</a>
·
<a href="https://veeso.github.io/termscp/#get-started" target="_blank">Installation</a>
·
<a href="https://veeso.github.io/termscp/#user-manual" target="_blank">Manuel de l'Utilisateur</a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp"
><img
height="20"
src="/assets/images/flags/us.png"
alt="English"
/></a>
&nbsp;
<a
href="/docs/de/README.md"
><img
height="20"
src="/assets/images/flags/de.png"
alt="Deutsch"
/></a>
&nbsp;
<a
href="/docs/es/README.md"
><img
height="20"
src="/assets/images/flags/es.png"
alt="Español"
/></a>
&nbsp;
<a
href="/docs/fr/README.md"
><img
height="20"
src="/assets/images/flags/fr.png"
alt="Français"
/></a>
&nbsp;
<a
href="/docs/it/README.md"
><img
height="20"
src="/assets/images/flags/it.png"
alt="Italiano"
/></a>
&nbsp;
<a
href="/docs/zh-CN/README.md"
><img
height="20"
src="/assets/images/flags/cn.png"
alt="简体中文"
/></a>
</p>
<p align="center">Développé par <a href="https://veeso.github.io/" target="_blank">@veeso</a></p>
<p align="center">Version actuelle: 0.7.0 (12/10/2021)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
><img
src="https://img.shields.io/badge/License-MIT-teal.svg"
alt="License-MIT"
/></a>
<a href="https://github.com/veeso/termscp/stargazers"
><img
src="https://img.shields.io/github/stars/veeso/termscp.svg"
alt="Repo stars"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/d/termscp.svg"
alt="Downloads counter"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/v/termscp.svg"
alt="Latest version"
/></a>
<a href="https://www.buymeacoffee.com/veeso"
><img
src="https://img.shields.io/badge/Donate-BuyMeACoffee-yellow.svg"
alt="Buy me a coffee"
/></a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Linux/badge.svg"
alt="Linux CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/MacOS/badge.svg"
alt="MacOS CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
alt="Windows CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/FreeBSD/badge.svg"
alt="FreeBSD CI"
/></a>
<a href="https://coveralls.io/github/veeso/termscp"
><img
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
alt="Coveralls"
/></a>
<a href="https://docs.rs/termscp"
><img
src="https://docs.rs/termscp/badge.svg"
alt="Docs"
/></a>
</p>
---
## À propos des termscp 🖥
Termscp est un file transfer et explorateur de fichiers de terminal riche en fonctionnalités, avec support pour SCP/SFTP/FTP/S3. Essentiellement c'est une utilitaire terminal avec une TUI pour se connecter à un serveur distant pour télécharger de fichiers et interagir avec le système de fichiers local. Il est compatible avec **Linux**, **MacOS**, **FreeBSD** et **Windows**.
![Explorer](/assets/images/explorer.gif)
---
## Fonctionnalités 🎁
- 📁 Différents protocoles de communication
- **SFTP**
- **SCP**
- **FTP** et **FTPS**
- **Aws S3**
- 🖥 Explorer et opérer sur le système de fichiers distant et local avec une interface utilisateur pratique.
- Créer, supprimer, renommer, rechercher, afficher et modifier des fichiers
- ⭐ Connectez-vous à vos hôtes préférés via des signets et des connexions récentes.
- 📝 Affichez et modifiez des fichiers avec vos applications préférées
- 💁 Authentication SFTP/SCP avec des clés SSH et nom/mot de passe
- 🐧 Compatible avec Windows, Linux, FreeBSD et MacOS
- 🎨 Faites en vôtre !
- thèmes
- format d'explorateur de fichiers personnalisé
- éditeur de texte personnalisable
- tri de fichiers personallisable
- et bien d'autres paramètres...
- 📫 Recevez une notification quande un gros fichier est télécharger.
- 🔐 Enregistre tes mots de passe dans le key vault du systeme.
- 🦀 Rust-powered
- 👀 Développé en gardant un œil sur les performances
- 🦄 Mises à jour fréquentes
---
## Pour commencer 🚀
Si tu envisage d'installer termscp, je veux te remercier 💜 ! J'espère que tu vas apprécier termscp !
Si tu veux contribuer à ce projet, n'oublié pas de consulter notre [guide de contribution](../../CONTRIBUTING.md).
Si tu es un utilisateur Linux, FreeBSD ou MacOS ce simple shell script installera termscp sur te système en un seule commande:
```sh
curl --proto '=https' --tlsv1.2 -sSLf "https://git.io/JBhDb" | sh
```
tandis que si tu es un utilisateur Windows, tu peux installer termscp avec [Chocolatey](https://chocolatey.org/):
```sh
choco install termscp
```
Pour plus d'informations sur les autres méthodes d'installation, veuillez visiter [veeso.github.io](https://veeso.github.io/termscp/#get-started).
⚠️ Si tu cherche comme de mettre à jour termscp, tu dois exécuter cette commande dans le terminal: `(sudo) termscp --update` ⚠️
### Requis ❗
- utilisateurs **Linux**:
- libssh
- libdbus-1
- pkg-config
- utilisateurs **FreeBSD**:
- libssh
- dbus
- pkgconf
### Requis facultatives ✔️
Ces requis ne sont pas obligatoires d'exécuter termscp, mais seulement à toutes ses fonctionnalités
- utilisateurs **Linux/FreeBSD**:
- Pour **ouvrir** les fichiers via `V` (au moins un de ces)
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- utilisateurs **Linux**:
- Un keyring manager: lire plus dans le [manuel d'utilisateur](man.md#linux-keyring)
- utilisateurs **WSL**
- Pour **ouvrir** les fichiers via `V` (au moins un de ces)
- [wslu](https://github.com/wslutilities/wslu)
---
## Me soutenir ☕
Si tu aime termscp et que tu aimerais voir le projet grandir et s'améliorer, voudrais considérer un petit don pour me soutenir sur **Buy me a coffee** 🥳
[![Buy-me-a-coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=&slug=veeso&button_colour=404040&font_colour=ffffff&font_family=Comic&outline_colour=ffffff&coffee_colour=FFDD00)](https://www.buymeacoffee.com/veeso)
ou, si tu préfére, tu peux également faire un don sur PayPal :
[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.me/chrisintin)
---
## Manuel d'utilisateur et Documentation 📚
Le manuel d'utilisateur peut être trouvé sur le [site de termscp](https://veeso.github.io/termscp/#user-manual) ou sur [Github](man.md).
La documentation peut être trouvé sur Rust Docs <https://docs.rs/termscp>
---
## Problèmes connus 🧻
- `NoSuchFileOrDirectory` à la connexion (WSL1): Je connais ce problème et c'est un problème de WSL je suppose. Ne te inquiéte pas, déplace simplement l'exécutable termscp dans un autre emplacement PATH, tel que `/usr/bin`, ou installe-le via le format de package approprié (par exemple, deb).
---
## Contribution et enjeux 🤝🏻
Les contributions, les rapports de bugs, les nouvelles fonctionnalités et les questions sont les bienvenus ! 😉
Si tu ai des questions ou des préoccupations, ou si tu souhaite suggérer une nouvelle fonctionnalité, ou si tu souhaite simplement améliorer les conditions de termscp, n'hésite pas à ouvrir un problème ou un PR.
Veuillez suivre [nos directives de contribution](../../CONTRIBUTING.md)
---
## Journal des modifications ⏳
Afficher le journal des modifications [ICI](../../CHANGELOG.md)
---
## Powered by 💪
termscp est soutenu par ces projets impressionnants:
- [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)
- [open-rs](https://github.com/Byron/open-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-s3](https://github.com/durch/rust-s3)
- [self_update](https://github.com/jaemk/self_update)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
- [suppaftp](https://github.com/veeso/suppaftp)
- [textwrap](https://github.com/mgeisler/textwrap)
- [tui-rs](https://github.com/fdehau/tui-rs)
- [tui-realm](https://github.com/veeso/tui-realm)
- [whoami](https://github.com/libcala/whoami)
- [wildmatch](https://github.com/becheran/wildmatch)
---
## Gallerie 🎬
> Termscp Home
![Auth](/assets/images/auth.gif)
> Bookmarks
![Bookmarks](/assets/images/bookmarks.gif)
> Setup
![Setup](/assets/images/config.gif)
> Text editor
![TextEditor](/assets/images/text-editor.gif)
---
## Licence 📃
termscp est sous licence MIT.
Vous pouvez lire l'intégralité de la licence [ICI](../../LICENSE)

485
docs/fr/man.md Normal file
View file

@ -0,0 +1,485 @@
# User manual 🎓
- [User manual 🎓](#user-manual-)
- [Usage ❓](#usage-)
- [Argument d'adresse 🌎](#argument-dadresse-)
- [Argument d'adresse AWS S3](#argument-dadresse-aws-s3)
- [Comment le mot de passe peut être fourni 🔐](#comment-le-mot-de-passe-peut-être-fourni-)
- [Identifiants AWS S3 🦊](#identifiants-aws-s3-)
- [Explorateur de fichiers 📂](#explorateur-de-fichiers-)
- [Raccourcis clavier ⌨](#raccourcis-clavier-)
- [Travailler sur plusieurs fichiers 🥷](#travailler-sur-plusieurs-fichiers-)
- [Navigation synchronisée ⏲️](#navigation-synchronisée-)
- [Ouvrir et ouvrir avec 🚪](#ouvrir-et-ouvrir-avec-)
- [Signets ⭐](#signets-)
- [Mes mots de passe sont-ils sûrs 😈](#mes-mots-de-passe-sont-ils-sûrs-)
- [Linux Keyring](#linux-keyring)
- [Configuration de KeepassXC pour termscp](#configuration-de-keepassxc-pour-termscp)
- [Configuration ⚙️](#configuration-)
- [SSH Key Storage 🔐](#ssh-key-storage-)
- [Format de l'explorateur de fichiers](#format-de-lexplorateur-de-fichiers)
- [Thèmes 🎨](#thèmes-)
- [Mon thème ne se charge pas 😱](#mon-thème-ne-se-charge-pas-)
- [Modes 💈](#modes-)
- [Authentication page](#authentication-page)
- [Transfer page](#transfer-page)
- [Misc](#misc)
- [Éditeur de texte ✏](#éditeur-de-texte-)
- [Enregistrement 🩺](#enregistrement-)
- [Notifications 📫](#notifications-)
## Usage ❓
termscp peut être démarré avec les options suivantes :
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
- `-P, --password <password>` si l'adresse est fournie, le mot de passe sera cet argument
- `-c, --config` Ouvrir termscp à partir de la page de configuration
- `-q, --quiet` Désactiver la journalisation
- `-t, --theme <path>` Importer le thème spécifié
- `-u, --update` Mettre à jour termscp vers la dernière version
- `-v, --version` Imprimer les informations sur la version
- `-h, --help` Imprimer la page d'aide
termscp peut être démarré dans deux modes différents, si aucun argument supplémentaire n'est fourni, termscp affichera le formulaire d'authentification, où l'utilisateur pourra fournir les paramètres requis pour se connecter au pair distant.
Alternativement, l'utilisateur peut fournir une adresse comme argument pour ignorer le formulaire d'authentification et démarrer directement la connexion au serveur distant.
Si l'argument d'adresse est fourni, vous pouvez également fournir le répertoire de démarrage de l'hôte local
### Argument d'adresse 🌎
L'argument adresse a la syntaxe suivante :
```txt
[protocole://][nom-utilisateur@]<adresse>[:port][:wrkdir]
```
Voyons un exemple de cette syntaxe particulière, car elle est très confortable et vous allez probablement l'utiliser à la place de l'autre...
- Se connecter en utilisant le protocole par défaut (*défini dans la configuration*) à 192.168.1.31, le port s'il n'est pas fourni est par défaut pour le protocole sélectionné (dans ce cas dépend de votre configuration) ; nom d'utilisateur est le nom de l'utilisateur actuel
```sh
termscp 192.168.1.31
```
- Se connecter en utilisant le protocole par défaut (*défini dans la configuration*) à 192.168.1.31 ; le nom d'utilisateur est "root"
```sh
termscp root@192.168.1.31
```
- Se connecter en utilisant scp à 192.168.1.31, le port est 4022 ; le nom d'utilisateur est "omar"
```sh
termscp scp://omar@192.168.1.31:4022
```
- Se connecter en utilisant scp à 192.168.1.31, le port est 4022 ; le nom d'utilisateur est "omar". Vous commencerez dans le répertoire `/tmp`
```sh
termscp scp://omar@192.168.1.31:4022:/tmp
```
#### Argument d'adresse AWS S3
Aws S3 a une syntaxe différente pour l'argument d'adresse CLI, pour des raisons évidentes, mais j'ai réussi à le garder le plus similaire possible à l'argument d'adresse générique :
```txt
s3://<bucket-name>@<region>[:profile][:/wrkdir]
```
e.g.
```txt
s3://buckethead@eu-central-1:default:/assets
```
#### Comment le mot de passe peut être fourni 🔐
Vous avez probablement remarqué que, lorsque vous fournissez l'adresse comme argument, il n'y a aucun moyen de fournir le mot de passe.
Le mot de passe peut être fourni de 3 manières lorsque l'argument d'adresse est fourni :
- `-P, --password` option : utilisez simplement cette option CLI en fournissant le mot de passe. Je déconseille fortement cette méthode, car elle n'est pas sécurisée (puisque vous pouvez conserver le mot de passe dans l'historique du shell)
- Avec `sshpass`: vous pouvez fournir un mot de passe via `sshpass`, par ex. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Il vous sera demandé : si vous n'utilisez aucune des méthodes précédentes, le mot de passe vous sera demandé, comme c'est le cas avec les outils plus classiques tels que `scp`, `ssh`, etc.
---
## Identifiants AWS S3 🦊
Afin de vous connecter à un compartiment Aws S3, vous devez évidemment fournir des informations d'identification.
Il existe essentiellement deux manières d'y parvenir, et comme vous l'avez probablement déjà remarqué, vous ne pouvez **pas** le faire via le formulaire d'authentification.
Voici donc les moyens de fournir les informations d'identification pour s3 :
1. Utilisez votre fichier d'informations d'identification : configurez simplement l'AWS cli via `aws configure` et vos informations d'identification doivent déjà se trouver dans `~/.aws/credentials`. Si vous utilisez un profil différent de "default", fournissez-le simplement dans le champ profile du formulaire d'authentification.
2. **Variables d'environnement** : vous pouvez toujours fournir vos informations d'identification en tant que variables d'environnement. Gardez à l'esprit que ces informations d'identification **remplaceront toujours** les informations d'identification situées dans le fichier « credentials ». Voir comment configurer l'environnement ci-dessous :
Ceux-ci devraient toujours être obligatoires:
- `AWS_ACCESS_KEY_ID`: aws access key ID (commence généralement par `AKIA...`)
- `AWS_SECRET_ACCESS_KEY`: la secret access key
Au cas où vous auriez configuré une sécurité renforcée, vous *pourriez* également en avoir besoin :
- `AWS_SECURITY_TOKEN`: security token
- `AWS_SESSION_TOKEN`: session token
⚠️ Vos identifiants sont en sécurité : les termscp ne manipuleront pas ces valeurs directement ! Vos identifiants sont directement consommés par la caisse **s3**.
Si vous avez des inquiétudes concernant la sécurité, veuillez contacter l'auteur de la bibliothèque sur [Github](https://github.com/durch/rust-s3) ⚠️
---
## Explorateur de fichiers 📂
Lorsque nous nous référons aux explorateurs de fichiers en termscp, nous nous référons aux panneaux que vous pouvez voir après avoir établi une connexion avec la télécommande.
Ces panneaux sont essentiellement 3 (oui, trois en fait):
- Panneau de l'explorateur local : il s'affiche sur la gauche de votre écran et affiche les entrées du répertoire en cours pour localhost
- Panneau de l'explorateur distant : il s'affiche à droite de votre écran et affiche les entrées du répertoire en cours pour l'hôte distant.
- Panneau de résultats de recherche : selon l'endroit où vous recherchez des fichiers (local/distant), il remplacera le panneau local ou l'explorateur. Ce panneau affiche les entrées correspondant à la requête de recherche que vous avez effectuée.
Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de l'explorateur distant et `<RIGHT>` pour revenir au panneau de l'explorateur local. Chaque fois que vous êtes dans le panneau des résultats de recherche, vous devez appuyer sur `<ESC>` pour quitter le panneau et revenir au panneau précédent.
### Raccourcis clavier ⌨
| Key | Command | Reminder |
|---------------|---------------------------------------------------------------------|-------------|
| `<ESC>` | Se Déconnecter de le serveur; retour à la page d'authentification | |
| `<TAB>` | Basculer entre l'onglet journal et l'explorateur | |
| `<BACKSPACE>` | Aller au répertoire précédent dans la pile | |
| `<RIGHT>` | Déplacer vers l'onglet explorateur distant | |
| `<LEFT>` | Déplacer vers l'onglet explorateur local | |
| `<UP>` | Remonter dans la liste sélectionnée | |
| `<DOWN>` | Descendre dans la liste sélectionnée | |
| `<PGUP>` | Remonter dans la liste sélectionnée de 8 lignes | |
| `<PGDOWN>` | Descendre dans la liste sélectionnée de 8 lignes | |
| `<ENTER>` | Entrer dans le directoire | |
| `<SPACE>` | Télécharger le fichier sélectionné | |
| `<A>` | Basculer les fichiers cachés | All |
| `<B>` | Trier les fichiers par | Bubblesort? |
| `<C>` | Copier le fichier/répertoire | Copy |
| `<D>` | Créer un dossier | Directory |
| `<E>` | Supprimer le fichier (Identique à `DEL`) | Erase |
| `<F>` | Rechercher des fichiers | Find |
| `<G>` | Aller au chemin fourni | Go to |
| `<H>` | Afficher l'aide | Help |
| `<I>` | Afficher les informations sur le fichier ou le dossier sélectionné | Info |
| `<L>` | Recharger le contenu du répertoire actuel / Effacer la sélection | List |
| `<M>` | Sélectionner un fichier | Mark |
| `<N>` | Créer un nouveau fichier avec le nom fourni | New |
| `<O>` | Modifier le fichier | Open |
| `<Q>` | Quitter termscp | Quit |
| `<R>` | Renommer le fichier | Rename |
| `<S>` | Enregistrer le fichier sous... | Save |
| `<U>` | Aller dans le répertoire parent | Upper |
| `<V>` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View |
| `<W>` | Ouvrir le fichier avec le programme spécifié | With |
| `<X>` | Exécuter une commande | eXecute |
| `<Y>` | Basculer la navigation synchronisée | sYnc |
| `<DEL>` | Supprimer le fichier | |
| `<CTRL+A>` | Sélectionner tous les fichiers | |
| `<CTRL+C>` | Abandonner le processus de transfert de fichiers | |
### Travailler sur plusieurs fichiers 🥷
Vous pouvez choisir de travailler sur plusieurs fichiers, en les sélectionnant en appuyant sur `<M>`, afin de sélectionner le fichier actuel, ou en appuyant sur `<CTRL+A>`, ce qui sélectionnera tous les fichiers dans le répertoire de travail.
Une fois qu'un fichier est marqué pour la sélection, il sera affiché avec un `*` sur la gauche.
Lorsque vous travaillez sur la sélection, seul le fichier sélectionné sera traité pour les actions, tandis que l'élément en surbrillance actuel sera ignoré.
Il est également possible de travailler sur plusieurs fichiers dans le panneau des résultats de recherche.
Toutes les actions sont disponibles lorsque vous travaillez avec plusieurs fichiers, mais sachez que certaines actions fonctionnent de manière légèrement différente. Plongeons dans:
- *Copy*: chaque fois que vous copiez un fichier, vous serez invité à insérer le nom de destination. Lorsque vous travaillez avec plusieurs fichiers, ce nom fait référence au répertoire de destination où tous ces fichiers seront copiés.
- *Rename*: identique à la copie, mais y déplacera les fichiers.
- *Save as*: identique à la copie, mais les y écrira.
### Navigation synchronisée ⏲️
Lorsqu'elle est activée, la navigation synchronisée vous permettra de synchroniser la navigation entre les deux panneaux.
Cela signifie que chaque fois que vous changerez de répertoire de travail sur un panneau, la même action sera reproduite sur l'autre panneau. Si vous souhaitez activer la navigation synchronisée, appuyez simplement sur `<Y>` ; appuyez deux fois pour désactiver. Lorsqu'il est activé, l'état de navigation synchronisé sera signalé dans la barre d'état sur `ON`
> ❗ pour le moment, chaque fois que vous essayez d'accéder à un répertoire inexistant, vous ne serez pas invité à le créer. Cela pourrait changer dans une future mise à jour.
### Ouvrir et ouvrir avec 🚪
Lors de l'ouverture de fichiers avec la commande Afficher (`<V>`), l'application par défaut du système pour le type de fichier sera utilisée. Pour ce faire, le service du système d'exploitation par défaut sera utilisé, alors assurez-vous d'avoir au moins l'un de ceux-ci installé sur votre système :
- Utilisateurs **Windows** : vous n'avez pas à vous en soucier, puisque la caisse utilisera la commande `start`.
- Utilisateurs **MacOS** : vous n'avez pas à vous inquiéter non plus, puisque le crate utilisera `open`, qui est déjà installé sur votre système.
- Utilisateurs **Linux** : l'un d'eux doit être installé
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- Utilisateurs **WSL** : *wslview* est requis, vous devez installer [wslu](https://github.com/wslutilities/wslu).
> Q: Puis-je modifier des fichiers distants à l'aide de la commande view ?
> A: Non, du moins pas directement depuis le "panneau distant". Vous devez d'abord le télécharger dans un répertoire local, cela est dû au fait que lorsque vous ouvrez un fichier distant, le fichier est téléchargé dans un répertoire temporaire, mais il n'y a aucun moyen de créer un observateur pour que le fichier vérifie quand le programme que vous utilisé pour l'ouvrir était fermé, donc termscp n'est pas en mesure de savoir quand vous avez fini de modifier le fichier.
---
## Signets ⭐
Dans termscp, il est possible de sauvegarder les hôtes favoris, qui peuvent ensuite être chargés rapidement à partir de la mise en page principale de termscp.
termscp enregistrera également les 16 derniers hôtes auxquels vous vous êtes connecté.
Cette fonctionnalité vous permet de charger tous les paramètres nécessaires pour vous connecter à une certaine télécommande, en sélectionnant simplement le signet dans l'onglet sous le formulaire d'authentification.
Les signets seront enregistrés, si possible à l'adresse :
- `$HOME/.config/termscp/` sous Linux/BSD
- `$HOME/Library/Application Support/termscp` sous MacOs
- `FOLDERID_RoamingAppData\termscp\` sous Windows
Pour les signets uniquement (cela ne s'appliquera pas aux hôtes récents), il est également possible de sauvegarder le mot de passe utilisé pour s'authentifier. Le mot de passe n'est pas enregistré par défaut et doit être spécifié via l'invite lors de l'enregistrement d'un nouveau signet.
Si vous êtes préoccupé par la sécurité du mot de passe enregistré pour vos favoris, veuillez lire le [chapitre ci-dessous 👀](#mes-mots-de-passe-sont-ils-sûrs-).
Pour créer un nouveau signet, suivez simplement ces étapes :
1. Tapez dans le formulaire d'authentification les paramètres pour vous connecter à votre serveur distant
2. Appuyez sur `<CTRL+S>`
3. Tapez le nom que vous souhaitez donner au signet
4. Choisissez de rappeler ou non le mot de passe
5. Appuyez sur `<ENTER>` pour soumettre
chaque fois que vous souhaitez utiliser la connexion précédemment enregistrée, appuyez simplement sur `<TAB>` pour accéder à la liste des signets et chargez les paramètres des signets dans le formulaire en appuyant sur `<ENTER>`.
![Bookmarks](https://github.com/veeso/termscp/blob/main/assets/images/bookmarks.gif?raw=true)
### Mes mots de passe sont-ils sûrs 😈
Bien sûr 😉.
Comme dit précédemment, les signets sont enregistrés dans votre répertoire de configuration avec les mots de passe. Les mots de passe ne sont évidemment pas en texte brut, ils sont cryptés avec **AES-128**. Est-ce que cela les sécurise ? Absolument! (sauf pour les utilisateurs BSD et WSL 😢)
Sous **Windows**, **Linux** et **MacOS**, la clé utilisée pour crypter les mots de passe est stockée, si possible (mais devrait l'être), respectivement dans le *Windows Vault*, dans le *porte-clés système* et dans le *Porte-clés*. Ceci est en fait super sûr et est directement géré par votre système d'exploitation.
❗ Veuillez noter que si vous êtes un utilisateur Linux, vous feriez mieux de lire le [chapitre ci-dessous 👀](#linux-keyring), car le trousseau peut ne pas être activé ou pris en charge sur votre système !
Sur *BSD* et *WSL*, en revanche, la clé utilisée pour crypter vos mots de passe est stockée sur votre disque (dans $HOME/.config/termscp). Il est alors, toujours possible de récupérer la clé pour déchiffrer les mots de passe. Heureusement, l'emplacement de la clé garantit que votre clé ne peut pas être lue par des utilisateurs différents du vôtre, mais oui, je n'enregistrerais toujours pas le mot de passe pour un serveur exposé sur Internet 😉.
#### Linux Keyring
Nous aimons tous Linux grâce à la liberté qu'il donne aux utilisateurs. En tant qu'utilisateur Linux, vous pouvez essentiellement faire tout ce que vous voulez, mais cela présente également des inconvénients, tels que le fait qu'il n'y a souvent pas d'applications standard dans différentes distributions. Et cela implique aussi un porte-clés.
Cela signifie que sous Linux, aucun trousseau de clés n'est peut-être installé sur votre système. Malheureusement, la bibliothèque que nous utilisons pour travailler avec le stockage des clés nécessite un service qui expose `org.freedesktop.secrets` sur D-BUS et le pire est qu'il n'y a que deux services qui l'exposent.
- ❗ Si vous utilisez GNOME comme environnement de bureau (par exemple, les utilisateurs d'ubuntu), ça devrait déjà aller, car le trousseau de clés est déjà fourni par `gnome-keyring` et tout devrait déjà fonctionner.
- ❗ Pour les autres utilisateurs d'environnement de bureau, il existe un programme sympa que vous pouvez utiliser pour obtenir un trousseau de clés qui est [KeepassXC](https://keepassxc.org/), que j'utilise sur mon installation Manjaro (avec KDE) et qui fonctionne bien. Le seul problème est que vous devez le configurer pour qu'il soit utilisé avec termscp (mais c'est assez simple). Pour commencer avec KeepassXC, lisez la suite [ici](#configuration-de-keepassxc-pour-termscp).
- ❗ Et si vous ne souhaitez installer aucun de ces services ? Eh bien, il n'y a pas de problème ! **termscp continuera à fonctionner comme d'habitude**, mais il enregistrera la clé dans un fichier, comme il le fait habituellement pour BSD et WSL.
##### Configuration de KeepassXC pour termscp
Suivez ces étapes afin de configurer keepassXC pour termscp :
1. Installer KeepassXC
2. Allez dans "outils" > "paramètres" dans la barre d'outils
3. Selectioner "Secret service integration" et basculer "Enable KeepassXC freedesktop.org secret service integration"
4. Creer une base de données, si vous n'en avez pas encore : à partir de la barre d'outils "Database" > "New database"
5. De la barre d'outils: "Database" > "Database settings"
6. Selectioner "Secret service integration" et basculer "Expose entries under this group"
7. Sélectionnez le groupe dans la liste où vous souhaitez conserver le secret du termscp. N'oubliez pas que ce groupe peut être utilisé par toute autre application pour stocker des secrets via DBUS.
---
## Configuration ⚙️
termscp prend en charge certains paramètres définis par l'utilisateur, qui peuvent être définis dans la configuration.
Underhood termscp a un fichier TOML et quelques autres répertoires où tous les paramètres seront enregistrés, mais ne vous inquiétez pas, vous ne toucherez à aucun de ces fichiers manuellement, car j'ai rendu possible la configuration complète de termscp à partir de son interface utilisateur.
termscp, comme pour les signets, nécessite juste d'avoir ces chemins accessibles :
- `$HOME/.config/termscp/` sous Linux/BSD
- `$HOME/Library/Application Support/termscp` sous MacOs
- `FOLDERID_RoamingAppData\termscp\` sous Windows
Pour accéder à la configuration, il vous suffit d'appuyer sur `<CTRL+C>` depuis l'accueil de termscp.
Ces paramètres peuvent être modifiés :
- **Text Editor**: l'éditeur de texte à utiliser. Par défaut, termscp trouvera l'éditeur par défaut pour vous ; avec cette option, vous pouvez forcer l'utilisation d'un éditeur (par exemple `vim`). **Les éditeurs d'interface graphique sont également pris en charge**, à moins qu'ils ne soient `nohup` à partir du processus parent.
- **Default Protocol**: le protocole par défaut est la valeur par défaut du protocole de transfert de fichiers à utiliser dans termscp. Cela s'applique à la page de connexion et à l'argument de l'adresse CLI.
- **Show Hidden Files**: sélectionnez si les fichiers cachés doivent être affichés par défaut. Vous pourrez décider d'afficher ou non les fichiers cachés au moment de l'exécution en appuyant sur `A` de toute façon.
- **Check for updates**: s'il est défini sur `yes`, Termscp récupère l'API Github pour vérifier si une nouvelle version de Termscp est disponible.
- **Prompt when replacing existing files?**: S'il est défini sur `yes`, Termscp vous demandera une confirmation chaque fois qu'un transfert de fichier entraînera le remplacement d'un fichier existant sur l'hôte cible.
- **Group Dirs**: sélectionnez si les répertoires doivent être regroupés ou non dans les explorateurs de fichiers. Si `Display first` est sélectionné, les répertoires seront triés en utilisant la méthode configurée mais affichés avant les fichiers, vice-versa si `Display last` est sélectionné.
- **Remote File formatter syntax**: syntaxe pour afficher les informations de fichier pour chaque fichier dans l'explorateur distant. Voir [File explorer format](#format-de-lexplorateur-de-fichiers)
- **Local File formatter syntax**: syntaxe pour afficher les informations de fichier pour chaque fichier dans l'explorateur local. Voir [File explorer format](#format-de-lexplorateur-de-fichiers)
- **Enable notifications?**: S'il est défini sur `Yes`, les notifications seront affichées.
- **Notifications: minimum transfer size**: si la taille du transfert est supérieure ou égale à la valeur spécifiée, les notifications de transfert seront affichées. Les valeurs acceptées sont au format `{UNSIGNED} B/KB/MB/GB/TB/PB`
### SSH Key Storage 🔐
n plus de la configuration, termscp fournit également une fonctionnalité **essentielle** pour les **clients SFTP/SCP** : le stockage de clés SSH.
Vous pouvez accéder au stockage des clés SSH, de la configuration à l'onglet « Clés SSH », une fois là-bas, vous pouvez :
- **Ajouter une neuf clé SSH**: appuyez simplement sur `<CTRL+N>` et vous serez invité à créer une nouvelle clé. Fournissez le nom d'hôte/l'adresse IP et le nom d'utilisateur associé à la clé et enfin un éditeur de texte s'ouvrira : collez la clé ssh **PRIVÉE** dans l'éditeur de texte, enregistrez et quittez.
- **Supprimer une clé existante**: appuyez simplement sur `<DEL>` ou `<CTRL+E>` sur la clé que vous souhaitez supprimer, pour supprimer de manière persistante la clé de termscp.
- **Modifier une clé existante**: appuyez simplement sur `<ENTER>` sur la clé que vous souhaitez modifier, pour changer la clé privée.
> Q: Ma clé privée est protégée par mot de passe, puis-je l'utiliser ?
> A: Bien sûr vous pouvez. Le mot de passe fourni pour l'authentification dans termscp est valide à la fois pour l'authentification par nom d'utilisateur/mot de passe et pour l'authentification par clé RSA.
### Format de l'explorateur de fichiers
Il est possible via la configuration de définir un format personnalisé pour l'explorateur de fichiers. Ceci est possible à la fois pour l'hôte local et distant, vous pouvez donc utiliser deux syntaxes différentes. Ces champs, nommés `File formatter syntax (local)` et `File formatter syntax (remote)` définiront comment les entrées de fichier seront affichées dans l'explorateur de fichiers.
La syntaxe du formateur est la suivante `{KEY1}... {KEY2:LENGTH}... {KEY3:LENGTH:EXTRA} {KEYn}...`.
Chaque clé entre crochets sera remplacée par l'attribut associé, tandis que tout ce qui se trouve en dehors des crochets restera inchangé.
- Le nom de la clé est obligatoire et doit être l'une des clés ci-dessous
- La longueur décrit la longueur réservée pour afficher le champ. Les attributs statiques ne prennent pas en charge cela (GROUP, PEX, SIZE, USER)
- Extra n'est pris en charge que par certains paramètres et constitue une option supplémentaire. Voir les touches pour vérifier si les extras sont pris en charge.
Voici les clés prises en charge par le formateur :
- `ATIME`: Heure du dernier accès (avec la syntaxe par défaut `%b %d %Y %H:%M`) ; Un supplément peut être fourni comme syntaxe de l'heure (par exemple, `{ATIME:8:%H:%M}`)
- `CTIME`: Heure de création (avec la syntaxe `%b %d %Y %H:%M`); Un supplément peut être fourni comme syntaxe de l'heure (par exemple, `{CTIME:8:%H:%M}`)
- `GROUP`: Groupe de propriétaires
- `MTIME`: Heure du dernier changement (avec la syntaxe `%b %d %Y %H:%M`); Un supplément peut être fourni comme syntaxe de l'heure (par exemple, `{MTIME:8:%H:%M}`)
- `NAME`: Nom du fichier (élidé si plus long que LENGTH)
- `PEX`: Autorisations de fichiers (format UNIX)
- `SIZE`: Taille du fichier (omis pour les répertoires)
- `SYMLINK`: Lien symbolique (le cas échéant `-> {FILE_PATH}`)
- `USER`: Utilisateur propriétaire
Si elle est laissée vide, la syntaxe par défaut du formateur sera utilisée : `{NAME:24} {PEX} {USER} {SIZE} {MTIME:17:%b %d %Y %H:%M}`
---
## Thèmes 🎨
Termscp vous offre une fonctionnalité géniale : la possibilité de définir les couleurs de plusieurs composants de l'application.
Si vous souhaitez personnaliser termscp, il existe deux manières de le faire :
- Depuis le **menu de configuration**
- Importation d'un **fichier de thème**
Afin de créer votre propre personnalisation à partir de termscp, il vous suffit de saisir la configuration à partir de l'activité d'authentification, en appuyant sur `<CTRL+C>` puis sur `<TAB>` deux fois. Vous devriez être maintenant passé au panneau `thèmes`.
Ici, vous pouvez vous déplacer avec `<UP>` et `<DOWN>` pour changer le style que vous souhaitez modifier, comme indiqué dans le gif ci-dessous :
![Themes](https://github.com/veeso/termscp/blob/main/assets/images/themes.gif?raw=true)
termscp prend en charge à la fois la syntaxe hexadécimale explicite traditionnelle (`#rrggbb`) et rgb `rgb(r, g, b)` pour fournir des couleurs, mais aussi **[couleurs css](https://www.w3schools.com/cssref/css_colors.asp)** (comme `crimson`) sont acceptés 😉. Il y a aussi un keywork spécial qui est `Default`. Par défaut signifie que la couleur utilisée sera la couleur de premier plan ou d'arrière-plan par défaut en fonction de la situation (premier plan pour les textes et les lignes, arrière-plan pour bien, devinez quoi)
Comme dit précédemment, vous pouvez également importer des fichiers de thème. Vous pouvez vous inspirer de ou utiliser directement l'un des thèmes fournis avec termscp, situé dans le répertoire `themes/` de ce référentiel et les importer en exécutant termscp en tant que `termscp -t <theme_file>`. Si tout allait bien, cela devrait vous dire que le thème a été importé avec succès.
### Mon thème ne se charge pas 😱
Cela est probablement dû à une mise à jour récente qui a cassé le thème. Chaque fois que j'ajoute une nouvelle clé aux thèmes, le thème enregistré ne se charge pas. Pour résoudre ces problèmes, il existe deux solutions vraiment rapides :
1. Recharger le thème : chaque fois que je publie une mise à jour, je corrige également les thèmes "officiels", il vous suffit donc de le télécharger à nouveau depuis le référentiel et de réimporter le thème via l'option `-t`
```sh
termscp -t <theme.toml>
```
2. Corrigez votre thème : si vous utilisez un thème personnalisé, vous pouvez le modifier via `vim` et ajouter la clé manquante. Le thème est situé dans `$CONFIG_DIR/termscp/theme.toml``$CONFIG_DIR` est :
- FreeBSD/GNU-Linux: `$HOME/.config/`
- MacOs: `$HOME/Library/Application Support`
- Windows: `%appdata%`
❗ Les clés manquantes sont signalées dans le CHANGELOG sous `BREAKING CHANGES` pour la version que vous venez d'installer.
### Modes 💈
Vous pouvez trouver dans le tableau ci-dessous, la description de chaque champ de style.
Veuillez noter que **les styles ne s'appliqueront pas à la page de configuration**, afin de la rendre toujours accessible au cas où vous gâcheriez tout
#### Authentication page
| Key | Description |
|----------------|------------------------------------------|
| auth_address | Couleur du champ pour adresse IP |
| auth_bookmarks | Couleur du panneau des signets |
| auth_password | Couleur du champ pour mot de passe |
| auth_port | Couleur du champ pour nombre de port |
| auth_protocol | Couleur du groupe radio pour protocole |
| auth_recents | Couleur du panneau récent |
| auth_username | Couleur du champ pour nom d'utilisateur |
#### Transfer page
| Key | Description |
|--------------------------------------|---------------------------------------------------------------------------|
| transfer_local_explorer_background | Couleur d'arrière-plan de l'explorateur localhost |
| transfer_local_explorer_foreground | Couleur de premier plan de l'explorateur localhost |
| transfer_local_explorer_highlighted | Bordure et couleur surlignée pour l'explorateur localhost |
| transfer_remote_explorer_background | Couleur d'arrière-plan de l'explorateur distant |
| transfer_remote_explorer_foreground | Couleur de premier plan de l'explorateur distant |
| transfer_remote_explorer_highlighted | Bordure et couleur en surbrillance pour l'explorateur distant |
| transfer_log_background | Couleur d'arrière-plan du panneau de journal |
| transfer_log_window | Couleur de la fenêtre du panneau de journal |
| transfer_progress_bar_partial | Couleur de la barre de progression partielle |
| transfer_progress_bar_total | Couleur de la barre de progression totale |
| transfer_status_hidden | Couleur de l'étiquette "hidden" de la barre d'état |
| transfer_status_sorting | Couleur de l'étiquette "sorting" de la barre d'état |
| transfer_status_sync_browsing | Couleur de l'étiquette "sync browsing" de la barre d'état |
#### Misc
These styles applie to different part of the application.
| Key | Description |
|-------------------|---------------------------------------------|
| misc_error_dialog | Couleur des messages d'erreur |
| misc_info_dialog | Couleur des messages d'info |
| misc_input_dialog | Couleur des messages de input |
| misc_keys | Couleur du texte pour les frappes de touches|
| misc_quit_dialog | Couleur des messages de quit |
| misc_save_dialog | Couleur des messages d'enregistrement |
| misc_warn_dialog | Couleur des messages de attention |
---
## Éditeur de texte ✏
termscp a, comme vous l'avez peut-être remarqué, de nombreuses fonctionnalités, l'une d'entre elles est la possibilité de visualiser et de modifier un fichier texte. Peu importe que le fichier se trouve sur l'hôte local ou sur l'hôte distant, termscp offre la possibilité d'ouvrir un fichier dans votre éditeur de texte préféré.
Si le fichier se trouve sur l'hôte distant, le fichier sera d'abord téléchargé dans votre répertoire de fichiers temporaires, puis **uniquement** si des modifications ont été apportées au fichier, rechargé sur l'hôte distant. termscp vérifie si vous avez apporté des modifications au fichier en vérifiant l'heure de la dernière modification du fichier.
> ❗ Juste un rappel : **vous ne pouvez éditer que des fichiers texte** ; les fichiers binaires ne sont pas pris en charge.
---
## Enregistrement 🩺
termscp écrit un fichier journal pour chaque session, qui est écrit à
- `$HOME/.config/termscp/termscp.log` sous Linux/BSD
- `$HOME/Library/Application Support/termscp/termscp.log` sous MacOs
- `FOLDERID_RoamingAppData\termscp\termscp.log` sous Windows
le journal ne sera pas tourné, mais sera simplement tronqué après chaque lancement de termscp, donc si vous souhaitez signaler un problème et que vous souhaitez joindre votre fichier journal, n'oubliez pas de sauvegarder le fichier journal dans un endroit sûr avant de l'utiliser termscp à nouveau.
Le fichier journal rapporte toujours au niveau *trace*, il est donc un peu détaillé.
Je sais que vous pourriez avoir des questions concernant les fichiers journaux, alors j'ai fait une sorte de Q/R :
> Est-il possible de réduire la verbosité ?
Non. La raison est assez simple : lorsqu'un problème survient, vous devez être capable de savoir ce qui en est la cause et la seule façon de le faire est d'avoir le fichier journal avec le niveau de verbosité maximum défini.
> Si le niveau de trace est défini pour la journalisation, le fichier va-t-il atteindre une taille énorme ?
Probablement pas, à moins que vous ne quittiez jamais termscp, mais je pense que cela est peu probable. Une longue session peut produire jusqu'à 10 MB de fichiers journaux (j'ai dit une longue session), mais je pense qu'une session normale ne dépassera pas 2 MB.
> Je ne veux pas me connecter, puis-je le désactiver ?
Oui, vous pouvez. Démarrez simplement termscp avec l'option `-q ou --quiet`. Vous pouvez créer un alias termcp pour le rendre persistant. N'oubliez pas que la journalisation est utilisée pour diagnostiquer les problèmes, donc puisque derrière chaque projet open source, il devrait toujours y avoir ce genre d'aide mutuelle, la conservation des fichiers journaux peut être votre moyen de soutenir le projet 😉. Je ne veux pas que tu te sentes coupable, mais juste pour dire.
> La journalisation est-elle sûre ?
Si vous êtes préoccupé par la sécurité, le fichier journal ne contient aucun mot de passe simple, alors ne vous inquiétez pas et expose les mêmes informations que le fichier frère "signets".
## Notifications 📫
Termscp enverra des notifications de bureau pour ce type d'événements :
- sur **Transfert terminé** : La notification sera envoyée une fois le transfert terminé avec succès.
- ❗ La notification ne s'affichera que si la taille totale du transfert est au moins la `Notifications: minimum transfer size` spécifiée dans la configuration.
- sur **Transfert échoué** : La notification sera envoyée une fois qu'un transfert a échoué en raison d'une erreur.
- ❗ La notification ne s'affichera que si la taille totale du transfert est au moins la `Notifications: minimum transfer size` spécifiée dans la configuration.
- sur **Mise à jour disponible** : chaque fois qu'une nouvelle version de Termscp est disponible, une notification s'affiche.
- sur **Mise à jour installée** : chaque fois qu'une nouvelle version de Termscp est installée, une notification s'affiche.
- sur **Échec de la mise à jour** : chaque fois que l'installation de la mise à jour échoue, une notification s'affiche.
❗ Si vous préférez désactiver les notifications, vous pouvez simplement accéder à la configuration et définir `Enable notifications?` sur `No` 😉.
❗ Si vous souhaitez modifier la taille de transfert minimale pour afficher les notifications, vous pouvez modifier la valeur dans la configuration avec la touche `Notifications: minimum transfer size` et la définir sur ce qui vous convient le mieux 🙂.

302
docs/it/README.md Normal file
View file

@ -0,0 +1,302 @@
# termscp
<p align="center">
<img src="/assets/images/termscp.svg" width="256" height="256" />
</p>
<p align="center">~ Un file transfer ricco di funzionalità ~</p>
<p align="center">
<a href="https://veeso.github.io/termscp/" target="_blank">Sito</a>
·
<a href="https://veeso.github.io/termscp/#get-started" target="_blank">Installazione</a>
·
<a href="https://veeso.github.io/termscp/#user-manual" target="_blank">Manuale utente</a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp"
><img
height="20"
src="/assets/images/flags/us.png"
alt="English"
/></a>
&nbsp;
<a
href="https://github.com/veeso/termscp/blob/main/docs/de/README.md"
><img
height="20"
src="/assets/images/flags/de.png"
alt="Deutsch"
/></a>
&nbsp;
<a
href="https://github.com/veeso/termscp/blob/main/docs/es/README.md"
><img
height="20"
src="/assets/images/flags/es.png"
alt="Español"
/></a>
&nbsp;
<a
href="https://github.com/veeso/termscp/blob/main/docs/fr/README.md"
><img
height="20"
src="/assets/images/flags/fr.png"
alt="Français"
/></a>
&nbsp;
<a
href="https://github.com/veeso/termscp/blob/main/docs/it/README.md"
><img
height="20"
src="/assets/images/flags/it.png"
alt="Italiano"
/></a>
&nbsp;
<a
href="https://github.com/veeso/termscp/blob/main/docs/zh-CN/README.md"
><img
height="20"
src="/assets/images/flags/cn.png"
alt="简体中文"
/></a>
</p>
<p align="center">Sviluppato da <a href="https://veeso.github.io/" target="_blank">@veeso</a></p>
<p align="center">Versione corrente: 0.7.0 (12/10/2021)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
><img
src="https://img.shields.io/badge/License-MIT-teal.svg"
alt="License-MIT"
/></a>
<a href="https://github.com/veeso/termscp/stargazers"
><img
src="https://img.shields.io/github/stars/veeso/termscp.svg"
alt="Repo stars"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/d/termscp.svg"
alt="Downloads counter"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/v/termscp.svg"
alt="Latest version"
/></a>
<a href="https://www.buymeacoffee.com/veeso"
><img
src="https://img.shields.io/badge/Donate-BuyMeACoffee-yellow.svg"
alt="Buy me a coffee"
/></a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Linux/badge.svg"
alt="Linux CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/MacOS/badge.svg"
alt="MacOS CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
alt="Windows CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/FreeBSD/badge.svg"
alt="FreeBSD CI"
/></a>
<a href="https://coveralls.io/github/veeso/termscp"
><img
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
alt="Coveralls"
/></a>
<a href="https://docs.rs/termscp"
><img
src="https://docs.rs/termscp/badge.svg"
alt="Docs"
/></a>
</p>
---
## Riguardo a termscp 🖥
Termscp è un file transfer ed explorer ricco di funzionalità, con supporto a SCP/SFTP/FTP/S3. Basicamente è un utility su terminale con una terminal user-interface per connettersi a server remoti per scambiare file ed interagire con il file system sia locale che remoto. È compatibile con **Linux**, **MacOS**, **FreeBSD** e **Windows**.
![Explorer](/assets/images/explorer.gif)
---
## Funzionalità 🎁
- 📁 Diversi protocolli di comunicazione
- **SFTP**
- **SCP**
- **FTP** and **FTPS**
- **Aws S3**
- 🖥 Esplora e opera sia sul file system locale che su quello remoto con una UI di facile utilizzo.
- Crea, rimuove, rinomina, cerca, visualizza e modifica file
- ⭐ Connettiti ai tuoi host preferiti tramite la funzionalità integrata dei segnalibri e delle connessioni recenti.
- 📝 Visualizza e modifica i file tramite le tue applicazioni preferite.
- 💁 Autenticazione su server SFTP/SCP tramite chiavi SSH e/o username/password
- 🐧 Compatibile con Windows, Linux, FreeBSD e MacOS
- 🎨 Customizzalo!
- Temi
- Formattazione dell'explorer
- Impostazione del text editor predefinito
- Imposta l'ordinamento di file e cartelle
- e tanto altro...
- 📫 Ricevi notifiche desktop quando un file di cospicue dimensioni è stato trasferito
- 🔐 Salva le password degli host remoti nel keyring predefinito dal tuo sistema operativo
- 🦀 Rust-powered
- 👀 Progettato tenendo conto delle performance
- 🦄 Aggiornamenti frequenti con nuove funzionalità
---
## Per iniziare 🚀
Intanto se stai considerando di installare termscp, ti voglio ringraziare 💜 e spero che termscp ti piacerà!
Se vuoi contribuire al progetto, non dimenticarti di leggere la [contribute guide](../../CONTRIBUTING.md).
Se sei un utente che utilizza Linux, FreeBSD o MacOS, questo shell script installerà termscp sul tuo sistema con un comando secco:
```sh
curl --proto '=https' --tlsv1.2 -sSLf "https://git.io/JBhDb" | sh
```
mentre se sei un utente Windows, puoi installare termscp con [Chocolatey](https://chocolatey.org/):
```sh
choco install termscp
```
Per ulteriori informazioni sui metodi di installazione su altre piattaforme, visita [veeso.github.io](https://veeso.github.io/termscp/#get-started).
⚠️ Se stavi cercando come aggiornare la tua versione di termscp, puoi semplicemente lanciare termscp con questi argomenti: `(sudo) termscp --update` ⚠️
### Requisiti ❗
- Utenti **Linux**:
- libssh
- libdbus-1
- pkg-config
- Utenti **FreeBSD**:
- libssh
- dbus
- pkgconf
### Requisiti opzionali ✔️
Questi requisiti non sono per forza necessari, ma lo sono per sfruttare tutte le sue funzionalità:
- Utenti **Linux/FreeBSD**:
- Per **aprire** i file con `V` (almeno uno di questi)
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- Utenti **Linux**:
- Un keyring manager: Approfondisci nel [Manuale](man.md#linux-keyring)
- Utenti **WSL**
- Per **aprire** i file con `V` (almeno uno di questi)
- [wslu](https://github.com/wslutilities/wslu)
---
## Supportami ☕
Se ti piace termscp e ti piacerebbe vedere il progetto crescere e migliorare, considera una piccola donazione su **Buy me a coffee** 🥳.
[![Buy-me-a-coffee](https://img.buymeacoffee.com/button-api/?text=Buy%20me%20a%20coffee&emoji=&slug=veeso&button_colour=404040&font_colour=ffffff&font_family=Comic&outline_colour=ffffff&coffee_colour=FFDD00)](https://www.buymeacoffee.com/veeso)
o, se lo preferisci, puoi anche fare una donazione tramite PayPal:
[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.me/chrisintin)
---
## Manuale e documentazione 📚
Il manuale utente lo puoi trovare sul [sito di termscp](https://veeso.github.io/termscp/#user-manual) o su [Github](man.md).
La documentazione per sviluppatori la puoi trovare su Rust Docs <https://docs.rs/termscp>.
---
## Problemi noti 🧻
- `NoSuchFileOrDirectory` alla connessione (WSL1): Dovrebbe essere sufficiente non installare termscp con Cargo, ma piuttosto con uno dei pacchetti forniti tramite lo script di installazione `install.sh`. Si tratta di un problema legato esclusivamente a WSL1 e non è chiaro cosa accada alla connessione, ma è sicuramente legato al percorso del file.
---
## Contributi e issues 🤝🏻
Contributi, report di bug, nuove funzionalità e domande sono i benvenuti! 😉
Se hai qualche domanda o dubbio o vuoi suggerire una nuova funzionalità, sentiti libero di aprire un issue o una PR.
Per favore segui le nostre [contributing guidelines](../../CONTRIBUTING.md)
---
## Changelog ⏳
Visualizza [Qui](../../CHANGELOG.md) il changelog
---
## Un grazie a questi progetti 💪
se termscp esiste, è anche grazie a questi fantastici progetti:
- [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)
- [open-rs](https://github.com/Byron/open-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-s3](https://github.com/durch/rust-s3)
- [self_update](https://github.com/jaemk/self_update)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
- [suppaftp](https://github.com/veeso/suppaftp)
- [textwrap](https://github.com/mgeisler/textwrap)
- [tui-rs](https://github.com/fdehau/tui-rs)
- [tui-realm](https://github.com/veeso/tui-realm)
- [whoami](https://github.com/libcala/whoami)
- [wildmatch](https://github.com/becheran/wildmatch)
---
## Galleria 🎬
> Termscp Home
![Auth](/assets/images/auth.gif)
> Bookmarks
![Bookmarks](/assets/images/bookmarks.gif)
> Configurazione
![Setup](/assets/images/config.gif)
> Text editor
![TextEditor](/assets/images/text-editor.gif)
---
## Licenza 📃
termscp è fornito sotto licenza MIT.
Puoi leggere l'intero documento di licenza [Qui](../../LICENSE)

484
docs/it/man.md Normal file
View file

@ -0,0 +1,484 @@
# Manuale utente 🎓
- [Manuale utente 🎓](#manuale-utente-)
- [Argomenti da linea di comando ❓](#argomenti-da-linea-di-comando-)
- [Argomento indirizzo 🌎](#argomento-indirizzo-)
- [Argomento indirizzo per AWS S3](#argomento-indirizzo-per-aws-s3)
- [Come fornire la password 🔐](#come-fornire-la-password-)
- [Credenziali Aws S3 🦊](#credenziali-aws-s3-)
- [File explorer 📂](#file-explorer-)
- [Abbinamento tasti ⌨](#abbinamento-tasti-)
- [Lavora su più file 🥷](#lavora-su-più-file-)
- [Synchronized browsing ⏲️](#synchronized-browsing-)
- [Apri e apri con 🚪](#apri-e-apri-con-)
- [Segnalibri ⭐](#segnalibri-)
- [Le mie password sono al sicuro 😈](#le-mie-password-sono-al-sicuro-)
- [Linux Keyring](#linux-keyring)
- [KeepassXC setup per termscp](#keepassxc-setup-per-termscp)
- [Configurazione ⚙️](#configurazione-)
- [SSH Key Storage 🔐](#ssh-key-storage-)
- [File Explorer Format](#file-explorer-format)
- [Temi 🎨](#temi-)
- [Il tema non carica 😱](#il-tema-non-carica-)
- [Stili 💈](#stili-)
- [Pagina autenticazione](#pagina-autenticazione)
- [Pagina explorer e trasferimento](#pagina-explorer-e-trasferimento)
- [Misc](#misc)
- [Editor di testo ✏](#editor-di-testo-)
- [Logging 🩺](#logging-)
- [Notifiche 📫](#notifiche-)
## Argomenti da linea di comando ❓
termscp può essere lanciato con questi argomenti:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
- `-P, --password <password>` Se viene fornito l'argomento indirizzo, questa sarà la password utilizzata per autenticarsi
- `-c, --config` Apri la configurazione di termscp
- `-q, --quiet` Disabilita i log
- `-t, --theme <path>` Importa il tema al percorso fornito
- `-u, --update` Aggiorna termscp all'ultima versione
- `-v, --version` Mostra a video le informazioni sulla versione attualmente installata
- `-h, --help` Mostra la pagina di aiuto.
termscp può venire lanciato in due modalità diverse. Se nessun argomento posizionale viene fornito, termscp mostrerà il form di autenticazione, dove l'utente potrà fornire i parametri di connessione necessari.
Alternativamente, l'utente può fornire l'argomento posizionale "indirizzo" per connettersi direttamente all'host fornito.
Se viene fornito anche il secondo argomento posizionale, ovvero la directory locale, termscp avvierà l'explorer locale sul percorso fornito.
### Argomento indirizzo 🌎
L'argomento indirizzo ha la sintassi seguente:
```txt
[protocollo://][username@]<indirizzo>[:porta][:wrkdir]
```
Vediamo qualche esempio per questa sintassi, dal momento che risulta molto comodo connettersi tramite questa modalità:
- Connessione utilizzando il protocollo di default (definito in configurazione) a 192.168.1.31, la porta sarà quella di default per il protocollo di default. Il nome utente è quello attualmente attivo sulla propria macchina:
```sh
termscp 192.168.1.31
```
- Connessione con protocollo di default a 192.168.1.31, utente è `root`:
```sh
termscp root@192.168.1.31
```
- Connessione usando `scp`, la porta è 4022, l'utente è `omar`:
```sh
termscp scp://omar@192.168.1.31:4022
```
- Connessione via `scp`, porta 4022, utente `omar`, l'explorer si avvierà in `/tmp`:
```sh
termscp scp://omar@192.168.1.31:4022:/tmp
```
#### Argomento indirizzo per AWS S3
Aws S3 ha una sintassi differente dal classico argomento indirizzo, per ovvie ragioni, in quanto S3 non ha la porta o l'host o l'utente. Ho deciso però di mantenere una sintassi il più simile possibile a quella "tradizionale":
```txt
s3://<bucket-name>@<region>[:profile][:/wrkdir]
```
e.g.
```txt
s3://buckethead@eu-central-1:default:/assets
```
#### Come fornire la password 🔐
Quando si usa l'argomento indirizzo non è possibile fornire la password direttamente nell'argomento, esistono però altri metodi per farlo:
- Argomento `-P, --password <password>`: Passa direttamente la password nell'argomento. Non lo consiglio particolarmente questo metodo, in quanto la password rimarrebbe nella history della shell in chiaro.
- Tramite `sshpass`: puoi fornire la password tramite l'applicazione GNU/Linux sshpass `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- Forniscila quando richiesta: se non la fornisci tramite nessun metodo precedente, alla connessione ti verrà richiesto di fornirla in un prompt che la oscurerà (come avviene con sudo tipo).
---
## Credenziali Aws S3 🦊
Per connettersi ad un bucket S3 devi come già saprai fornire le credenziali fornite da AWS.
Ci sono due modi per passare queste credenziali a termscp e come avrai già notato **non puoi** farlo dal form di autenticazione.
Questi sono quindi i due modi per passare le chiavi:
1. Utilizza il file delle credenziali s3: configurando aws via `aws configure` le tue credenziali dovrebbero già venir salvate in `~/.aws/credentials`. Nel caso tu debba usare un profile diverso da `default`, puoi fornire un profilo diverso nell'authentication form.
2. **Variabili d'ambiente**: nel caso il primo metodo non sia utilizzabile, puoi comunque fornirle come variabili d'ambiente. Considera però che queste variabili sovrascriveranno sempre le credenziali situate nel file credentials. Vediamo come impostarle:
Queste sono sempre obbligatorie:
- `AWS_ACCESS_KEY_ID`: aws access key ID (di solito inizia per `AKIA...`)
- `AWS_SECRET_ACCESS_KEY`: la secret access key
nel caso tu abbia impostato un maggiore livello di sicurezza, potrebbero servirti anche queste:
- `AWS_SECURITY_TOKEN`: security token
- `AWS_SESSION_TOKEN`: session token
⚠️ le tue credenziali sono al sicuro: termscp non manipola direttamente questi dati! Le credenziali sono direttamente lette dal crate di **s3**. Nel caso tu abbia dei dubbi sulla sicurezza, puoi contattare l'autore della libreria su [Github](https://github.com/durch/rust-s3) ⚠️
---
## File explorer 📂
Quando ci riferiamo al file explorer in termscp, intendiamo i pannelli che puoi vedere quando stabilisci una connessione con il server remoto.
Questi pannelli sono 3 (e non 2 come sembra):
- Pannello locale: viene visualizzato sulla sinistra del tuo schermo e mostra la cartella sul file system locale.
- Pannello remoto: viene visualizzato sulla destra del tuo schermo e mostra la cartella sul file system remoto.
- Pannello di ricerca: viene visualizzato a destra o a sinistra in base a dove stai cercando dei file. Questo pannello mostra i file che matchano al pattern cercato sull'host.
Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pannello locale e `<RIGHT>` per andare su quello remoto. Attenzione che quando è attivo il pannello ricerca non puoi spostarti sugli altri pannelli e devi prima chiuderlo con `<ESC>`.
### Abbinamento tasti ⌨
| Key | Command | Reminder |
|---------------|-------------------------------------------------------|-------------|
| `<ESC>` | Disconnettiti; chiudi popup | |
| `<TAB>` | Cambia tra explorer e pannello di log | |
| `<BACKSPACE>` | Vai alla directory precedente | |
| `<RIGHT>` | Vai al pannello remoto | |
| `<LEFT>` | Vai al pannello locale | |
| `<UP>` | Muovi il cursore verso l'alto | |
| `<DOWN>` | Muovi il cursore verso il basso | |
| `<PGUP>` | Muovi il cursore verso l'alto di 8 | |
| `<PGDOWN>` | Muovi il cursore verso il basso di 8 | |
| `<ENTER>` | Entra nella directory | |
| `<SPACE>` | Upload / download file selezionato/i | |
| `<A>` | Mostra/nascondi file nascosti | All |
| `<B>` | Ordina file per | Bubblesort? |
| `<C>` | Copia file/directory | Copy |
| `<D>` | Crea directory | Directory |
| `<E>` | Elimina file (Come `DEL`) | Erase |
| `<F>` | Cerca file (wild match supportato) | Find |
| `<G>` | Vai al percorso indicato | Go to |
| `<H>` | Mostra help | Help |
| `<I>` | Mostra informazioni per il file selezionato | Info |
| `<L>` | Ricarica posizione corrente / pulisci selezione file | List |
| `<M>` | Seleziona file | Mark |
| `<N>` | Crea nuovo file con il nome fornito | New |
| `<O>` | Modifica file; Vedi text editor | Open |
| `<Q>` | Termina termscp | Quit |
| `<R>` | Rinomina file | Rename |
| `<S>` | Salva file con nome | Save |
| `<U>` | Vai alla directory padre | Upper |
| `<V>` | Apri il file con il programma definito dal sistema | View |
| `<W>` | Apri il file con il programma specificato | With |
| `<X>` | Esegui comando shell | eXecute |
| `<Y>` | Abilita/disabilita Sync-Browsing | sYnc |
| `<DEL>` | Rimuovi file | |
| `<CTRL+A>` | Seleziona tutti i file | |
| `<CTRL+C>` | Annulla trasferimento file | |
### Lavora su più file 🥷
Puoi lavorare su una selezione di file, marcandoli come selezionati tramite `<M>`, per selezionare il file corrente o con `<CTRL+A` per selezionarli tutti.
Una volta che un file è marcato, sarà visualizzato con un `*` prima del nome.
Quando lavori con una selezioni, solo i file selezionati saranno presi in considerazione (l'eventuale file evidenziato sarà ignorato).
È possibile operare su più file anche nel pannello di ricerca.
Tutte le azioni sono disponibili quando si lavora sulle selezioni, ma occhio, che alcune azioni si comporteranno in maniera leggermente differente. Vediamo quali e come:
- *Copia*: Se copi un file, ti verrà richiesto di inserire il nome delle destinazione, ma quando lavori con la selezione, il nome si riferisce alla directory di destinazione, mentre il nome del file rimarrà inviariato.
- *Rinomina*: Come il copia, ma li sposterà.
- *Salva con nome*: Come il copia, ma li trasferirà.
### Synchronized browsing ⏲️
Quando abilitato, ti permetterà di sincronizzare la navigazione tra i due pannelli.
Ciò comporta che quando cambierai directory in uno dei due pannelli, lo stesso verrà fatto nell'altro. Per abilitare la modalità è sufficiente premere `<Y>`; fai lo stesso per disabilitarlo. Mentre abilitato, sull'interfaccia dovrebbe essere visualizzato `Sync Browsing: ON` nella barra di stato.
> ❗ Al momento, se provi ad accedere ad una cartella non esistente su uno dei due host, mentre il sync browsing è attivo, non ti verrà chiesto di crearla, ma semplicemente fallirà. Questo sarà risolto in un aggiornamento futuro.
### Apri e apri con 🚪
I comandi "apri" e "apri con" sono forniti da [open-rs](https://docs.rs/crate/open/2.1.0).
Quando apri un file (`<V>`), l'applicazione predefinita di sistema sarà utilizzata per aprire il file. Per fare ciò, sul tuo sistema dovrà essere usato il servizio di default del sistema.
- **Windows**: non devi installare niente, è già presente sul sistema.
- **MacOS**: non devi installare niente, è già presente sul sistema.
- **Linux**: uno di questi dev'essere presente (potrebbe già esserlo):
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- **WSL**: *wslview* è richiesto, lo puoi installare tramite questa suite [wslu](https://github.com/wslutilities/wslu).
> Q: Posso modificare i file su remoto tramite la funzionalità "apri" / "apri con"?
> A: No, almeno non direttamente dal pannello remoto. Devi prima scaricarlo in locale, modificarlo e poi ricaricarlo. Questo perché il file remoto viene scaricato come file temporaneo in locale, ma non esiste poi un modo per sapere quando è stato modificato e quando l'utente ha effettivamente finito di lavorarci.
---
## Segnalibri ⭐
In termscp è possibile salvare i tuoi host preferiti tramite i segnalibri al fine di connettersi velocemente ad essi.
Termscp salverà anche gli ultimi 16 host ai quali ti sei connesso.
Questa funzionalità ti permette di caricare tutti i parametri necessari per connettersi ad un certo host, semplicemente selezioandolo dal tab dei preferiti nel form di autenticazione.
I preferiti saranno salvati se possibile presso:
- `$HOME/.config/termscp/` su Linux/BSD
- `$HOME/Library/Application Support/termscp` su MacOs
- `FOLDERID_RoamingAppData\termscp\` su Windows
Per i segnalibri (ma non per le connessioni recenti), è anche possibile salvare la password. La password non viene salvata di default e deve essere specificato tramite apposita opzione, al momento della creazione del segnalibro stesso.
Se sei preoccupato riguardo alla sicurezza della password per i segnalibri, dai un'occhiata al capitolo qui sotto 👀.
Per creare un segnalibro, segui questa procedura:
1. Inserisci i parametri per connetterti all'host che vuoi inserire come segnalibro nell'authentication form.
2. Premi `<CTRL+S>`
3. Inserisci il nome che vuoi dare al bookmark
4. Seleziona nel radio button se salvare la password
5. Premi `<ENTER>` per salvare
Quando vuoi caricare un segnalibro, premi `<TAB>` e naviga nella lista dei segnalibri fino al segnalibro che vuoi caricare, quindi premi `<ENTER>`.
![Bookmarks](https://github.com/veeso/termscp/blob/main/assets/images/bookmarks.gif?raw=true)
### Le mie password sono al sicuro 😈
Certo 😉.
Come detto in precedenza, i segnalibri sono salvati nella cartella delle configurazioni insieme alle password. Le password però non sono in chiaro, ma bensì sono criptate con **AES-128**. Questo le rende sicure? Sì! Does this make them safe? (salvo che per gli utenti di FreeBSD e WSL 😢)
In **Windows**, **Linux** and **MacOS** la chiave per criptare le password è salvata, se possibile, rispettivamente nel *Windows Vault*, nel *system keyring* e nel *Keychain*. Questo sistema è super-sicuro, in quanto garantito direttamente dal tuo sistema operativo.
❗ Attenzione che se sei un utente Linux, dovresti leggere il capitolo qui sotto riguardante il linux keyring 👀, questo perché il keyring potrebbe non essere ancora presente sul tuo sistema.
Su *FreeBSD* e *WSL*, d'altro canto, la chiave utilizzata per criptare le password è salvata su file presso (at $HOME/.config/termscp). È quindi possibile per un malintenzionato ottenere la chiave. Per fortuna essendo sotto la tua home, non dovrebbe essere possibile accedere al file, se non con il tuo utente, ma comunque per sicurezza ti consiglio di non salvare dati sensibili 😉.
#### Linux Keyring
Tutti gli amanti di Linux lo preferiscono per la libertà che questo dà agli utenti nella personalizzazione. Allo stesso tempo però questo spesso comporta degli effetti collaterali, tra cui la mancanza spesso di un'imposizione da parte dei creatori delle distro di standard e applicazioni e questo fatto coinvolge anche la questione del keyring.
Su alcuni sistemi di default, non c'è nessun provider di keyring, perché la distro dà all'utente la possibilità di sceglierne uno.
termscp richiede un servizio D-BUS che fornisce `org.freedesktop.secrets` e purtroppo ci sono ad oggi solo due servizi mantenuti che lo supportano.
- ❗ Se usi GNOME come Desktop environment (come gli utenti Ubuntu), dovresti già averne uno installato sul sistema, chiamato `gnome-keyring` e quindi dovrebbe già funzionare tutto.
- ❗ Se invece usi un altro DE, dovresti installare [KeepassXC](https://keepassxc.org/), che io per esempio utilizzo sul mio Manjaro Linux (con KDE) e funziona piuttosto bene. L'unico problema è che dovrai fare il setup per farlo funzionare. Per farlo puoi leggere il tutorial [qui](#keepassxc-setup-per-termscp)
- ❗ Se non volessi installare uno di questi servizi, termscp funzionerà come sempre, l'unica differenza sarà che salverà la chiave di crittazione su un file, come fa per FreeBSD e WSL.
##### KeepassXC setup per termscp
Questo tutorial spiega come impostare KeepassXC per termscp.
1. Installa KeepassXC dal sito ufficiale <https://keepassxc.org/>
2. Una volta avviato, vai su "strumenti" > "impostazioni" nella toolbar
3. Seleziona "Secret service integration" e abilita "Enable KeepassXC freedesktop.org secret service integration"
4. Crea un database se non ne hai già uno: dalla toolbar "Database" > "Nuovo database"
5. Dalla toolbar: "Database" > "Impostazioni database"
6. Seleziona "Secret service integration" e abilita "Expose entries under this group"
7. Seleziona il gruppo in cui vuoi salvare le chiavi di termscp. Attenzione che questo gruppo sarà utilizzato da tutte le altre eventuali applicazioni che salvano le password via D-BUS.
---
## Configurazione ⚙️
termscp supporta diversi parametri definiti dall'utente, che possono essere impostati nella configurazione.
termscp usa un file TOML e altre directory per salvare tutti i parametri, ma non preoccuparti, tutto può essere comodamente configurato da interfaccia grafica.
Per la configurazione, termscp richiede che i seguenti percorsi siano accessibili (termscp proverà a crearli per te):
- `$HOME/.config/termscp/` su Linux/BSD
- `$HOME/Library/Application Support/termscp` su MacOs
- `FOLDERID_RoamingAppData\termscp\` su Windows
Per accedere alla configurazione è sufficiente premere `<CTRL+C>` dall'authentication form.
Questi parametri possono essere impostati:
- **Text Editor**: l'editor di testo da utilizzare per aprire i file. Di default termscp userà quello definito nella variabile `EDITOR` od il primo che troverà installato tra quelli più popolari. Puoi tuttavia definire quello che vuoi (ad esempio `vim`). **Anche gli editor GUI sono supportati**, a meno che loro non partano in `nohup` dal processo padre.
- **Default Protocol**: il protocollo di default da visualizzare come prima opzione nell'authentication form. Questa opzione sarà anche utilizzata quando si usa l'argomento indirizzo da CLI e non si specifica un protocollo.
- **Show Hidden Files**: seleziona se mostrare di default i file nascosti. A runtime potrai comunque scegliere se visualizzarli o meno premendo `<A>`.
- **Check for updates**: se impostato a `YES` all'avvio termscp controllerà l'eventuale presenza di aggiornamenti. Per farlo utilizzerà una chiamata GET all'API di Github.
- **Prompt when replacing existing files?**: se impostato a `yes`, termscp ti chiederà una conferma prima di sovrascrivere un file a seguito di un download/upload.
- **Group Dirs**: seleziona se e come raggruppare le cartelle negli explorer. Se `Display first` è impostato, le directory verranno ordinate secondo quanto stabilito nel `sort by`, ma verranno messe prima dei file, viceversa se `Display last` è utilizzato. Se invece metti `no`, le cartelle verrano messe in ordine assieme ad i file.
- **Remote File formatter syntax**: La formattazione da usare per formattare i file sull'explorer remoto. Vedi [File explorer format](#file-explorer-format)
- **Local File formatter syntax**: La formattazione da usare per formattare i file sull'explorer locale. Vedi [File explorer format](#file-explorer-format)
- **Enable notifications?**: Se impostato a `yes`, le notifiche desktop saranno abilitate.
- **Notifications: minimum transfer size**: se la dimensione di un trasferimento supera o è uguale al valore impostato, al termine del trasferimento riceverai una notifica desktop (se queste sono abilitate). Il formato del valore dev'essere `{UNSIGNED} B/KB/MB/GB/TB/PB`
### SSH Key Storage 🔐
Assieme alla configurazione termscp supporta anche una feature essenziale per i client **SFTP/SCP**: lo storage di chiavi SSH.
Puoi accedere allo storage muovendoti nel tab delle chiavi SSH tramite `<TAB>` dalla configurazione.
- **Aggiungere chiavi**: premi `<CTRL+N>` e ti verrà chiesto di creare una nuova chiave. Inserisci l'hostname/indirizzo ed il nome utente, infine una volta che premerai invio, ti si aprirà l'editor di testo: incolla la chiave SSH **PRIVATA**, salva ed esci.
- **Rimuovi una chiave esistente**: premi `<DEL>` o `<CTRL+E>` selezionando la chiave da rimuovere.
- **Aggiorna una chiave esistente**: premi `<ENTER>` sulla chiave che vuoi modificare.
> Q: Se la mia chiave è protetta da password, posso comunque usarla?
> A: Sì, certo. In questo caso dovrai fornire la password come faresti per autenticarti con utente/password, ma in questo caso la password sarà usata per decrittare la chiave.
### File Explorer Format
È possibile dalla configurazione impostare la formattazione dei file sull'explorer. È possibile sia farlo per il pannello locale, che per quello remoto; quindi puoi avere due sintassi diverse. Questi campi, con nome `File formatter syntax (local)` and `File formatter syntax (remote)` definiranno come i file devono essere formattati sull'explorer.
La sintassi è la seguente `{KEY1}... {KEY2:LENGTH}... {KEY3:LENGTH:EXTRA} {KEYn}...`.
Ogni chiave sarà rimpiazzata dal formatter con il relativo attributo, mentre tutto ciò che è fuori dalle parentesi graffe rimarrà inviariato (quindi puoi metterci del testo arbitratio).
- Il nome della chiave è obbligatorio e dev'essere uno di quelli sotto.
- La lunghezza descrive quanto spazio in caratteri riservare al campo. Attributi con dimensione statico (GROUP, PEX, SIZE, USER) non supportano la lunghezza.
- L'extra serve a definire degli attributi in più. Solo alcuni lo supportano.
These are the keys supported by the formatter:
- `ATIME`: Last access time (con sintassi di default `%b %d %Y %H:%M`); Extra definisce il formato data (e.g. `{ATIME:8:%H:%M}`)
- `CTIME`: Creation time (con sintassi di default `%b %d %Y %H:%M`); Extra definisce il formato data (e.g. `{CTIME:8:%H:%M}`)
- `GROUP`: Owner group
- `MTIME`: Last change time (con sintassi di default `%b %d %Y %H:%M`); Extra definisce il formato data (e.g. `{MTIME:8:%H:%M}`)
- `NAME`: Nome file (Elided if longer than LENGTH)
- `PEX`: Permessi utente (formato UNIX)
- `SIZE`: Dimensione file (omesso per le directory)
- `SYMLINK`: Link simbolico (se presente `-> {FILE_PATH}`)
- `USER`: Owner user
Se lasciata vuota, la sintassi di default sarà utilizzata: `{NAME:24} {PEX} {USER} {SIZE} {MTIME:17:%b %d %Y %H:%M}`
---
## Temi 🎨
termscp fornisce anche una funzionalità strafiga: la possibilità di impostare i colori per tutta l'interfaccia.
Se vuoi impostare i colori, ci sono due modi per farlo:
- dal **menù di configurazione**
- importando un **tema** da file
Per personalizzare i colori dovrai andare nella configurazione temi, partendo dal menù di autenticazione, premendo `<CTRL+C>` e premendo due volte `<TAB>`. Dovresti essere quindi in configurazione nel tab `themes`.
Da qui puoi spostarti con le frecce per cambiare lo stile che vuoi, come mostrato nella GIF qua sotto:
![Themes](https://github.com/veeso/termscp/blob/main/assets/images/themes.gif?raw=true)
termscp supporta diverse sintassi per i colori, sia il formato hex (`#rrggbb`) che rgb `rgb(r, g, b)`, ma anche i **[colori CSS](https://www.w3schools.com/cssref/css_colors.asp)** (tipo `crimson`) 😉. C'è anche una chiave speciale `Default`. Default significa che per il colore verrà usato il default in base al tipo di elemento (foreground per i testi e linee, background per gli sfondi e i riempimenti).
Come detto già in precedenza, puoi anche importare i temi da file. Volendo puoi anche creare un tema prendendo ispirazione da quelli situati nella cartella `themes/` del repository ed importarli su termscp con `termscp -t <theme_file>`. Se l'operazione va a buon fine dovrebbe dirti che l'ha importato con successo.
### Il tema non carica 😱
Probabilmente è dovuto ad un aggiornamento che ha rotto il tema. Se viene aggiunta una nuova chiave nel tema (ma questo accade molto raramente), il tema non verrà più caricato. Ci sono diverse soluzioni veloci per questo problema.
1. Ricarica il tema: se stai usando un tema "ufficiale" fornito nel repository, basterà ricaricarlo, perché li aggiorno sempre quando modifico i temi:
```sh
termscp -t <theme.toml>
```
2. Sistema il tuo tema a mano: puoi modificare il tuo tema con un editor di testo tipo `vim` e aggiungere la chiave mancante. Il il tema si trova in `$CONFIG_DIR/termscp/theme.toml` dove `$CONFIG_DIR` è:
- FreeBSD/GNU-Linux: `$HOME/.config/`
- MacOs: `$HOME/Library/Application Support`
- Windows: `%appdata%`
❗ Le chiavi mancanti vengono riportate nel CHANGELOG sotto `BREAKING CHANGES` per la versione installata.
### Stili 💈
Puoi trovare qui sotto la definizione per ogni chiave.
Attenzione che gli stili **non coinvolgono la pagina di configurazione**, per renderla sempre accessibile nel caso gli stili siano inutilizzabili.
#### Pagina autenticazione
| Key | Description |
|----------------|------------------------------------|
| auth_address | Colore del campo indirizzo IP |
| auth_bookmarks | Colore del pannello segnalibri |
| auth_password | Colore del campo password |
| auth_port | Colore del campo numero porta |
| auth_protocol | Colore del selettore di protocollo |
| auth_recents | Colore del pannello recenti |
| auth_username | Colore del campo nome utente |
#### Pagina explorer e trasferimento
| Key | Description |
|--------------------------------------|---------------------------------------------------------------------------|
| transfer_local_explorer_background | Sfondo explorer locale |
| transfer_local_explorer_foreground | Foreground explorer locale |
| transfer_local_explorer_highlighted | Colore bordo e file selezionato explorer locale |
| transfer_remote_explorer_background | Sfondo explorer remoto |
| transfer_remote_explorer_foreground | Foreground explorer remoto |
| transfer_remote_explorer_highlighted | Colore bordo e file selezionato explorer remoto |
| transfer_log_background | Sfondo pannello di log |
| transfer_log_window | Colore bordi e testo log |
| transfer_progress_bar_partial | Colore barra progresso parziale |
| transfer_progress_bar_total | Colore barra progresso totale |
| transfer_status_hidden | Colore status bar file nascosti |
| transfer_status_sorting | Colore status bar ordinamento file; si applica anche al popup ordinamento |
| transfer_status_sync_browsing | Colore status bar per sync browsing |
#### Misc
Questi stili si applicano a varie componenti dell'applicazione.
| Key | Description |
|-------------------|---------------------------------------------|
| misc_error_dialog | Colore dialoghi errore |
| misc_info_dialog | Colore per dialoghi informazioni |
| misc_input_dialog | Colore per dialoghi input (tipo copia file) |
| misc_keys | Colore per abbinamento tasti |
| misc_quit_dialog | Colore per dialogo quit |
| misc_save_dialog | Colore per dialogo salva |
| misc_warn_dialog | Colore per dialoghi avvertimento |
---
## Editor di testo ✏
Con termscp puoi anche modificare i file di testo direttamente da terminale, utilizzando il tuo editor preferito.
Non importa se il file si trova in locale od in remoto, termscp ti consente di modificare e sincronizzare le modifiche per entrambi.
Nel caso il file si trovi su host remoto, il file verrà prima scaricato temporaneamente in locale, modificato e poi nel caso ci siano state modifiche, reinviato in remoto.
> ❗ Ricorda: **puoi modificare solo i file testuali**; non puoi modificare i file binari.
---
## Logging 🩺
termscp scrive un file di log per ogni sessione, nel percorso seguente:
- `$HOME/.config/termscp/termscp.log` su Linux/BSD
- `$HOME/Library/Application Support/termscp/termscp.log` su MacOs
- `FOLDERID_RoamingAppData\termscp\termscp.log` su Windows
Il log non viene ruotato, ma viene troncato ad ogni lancio di termscp, quindi se devi riportare un issue, non avviare termscp fino a che non avrai salvato il file di log.
I log sono sempre riportati a livello di *trace*, quindi sono piuttosto parlanti.
Ho scritto questo FAQ sui log, visto che potresti avere qualche dubbio:
> Si può ridurre la verbosità?
No. Il motivo è piuttosto semplice: quando c'è un problema, devi sapere cosa lo sta causando e l'unico modo per farlo e avere il log alla massima verbosità per avere la massima precisione sul controllo del flusso.
> Se trace è il livello di verbosità, non si raggiungono dimensioni enormi?
Probabilmente no, a meno che tu tenga sempre termscp acceso. Una lunga sessione potrebbe raggiungere i 10MB di log, ma una sessione media all'incirca 2MB.
> Non voglio il logging, posso disabilitarlo?
Sì, puoi. Basta lanciare termscp con `-q or --quiet` come opzione. Puoi mantenerlo persistente salvandolo come alias nella tua shell. Ricorda che i log vengono usati per diagnosticare problemi e considerando che questo è un progetto open-source è anche un modo per contribuire al progetto 😉. Non voglio far sentire in colpa nessuno, ma tanto per dire.
> Il logging è sicuro?
Se ti chiedi se il log espone dati sensibili, il log non espone nessuna password o dato sensibile.
## Notifiche 📫
termscp invierà notifiche destkop per i seguenti eventi:
- a **Transferimento completato**: La notifica verrà inviata a seguito di un trasferimento completato.
- ❗ La notifica verrà mostrata solo se la dimensione totale del trasferimento è uguale o maggiore al parametro `Notifications: minimum transfer size` definito in configurazione.
- a **Transferimento fallito**: La notifica verrà inviata a seguito di un trasferimento fallito.
- ❗ La notifica verrà mostrata solo se la dimensione totale del trasferimento è uguale o maggiore al parametro `Notifications: minimum transfer size` definito in configurazione.
- ad **Aggiornamento disponibile**: Ogni volta che una nuova versione di termscp è disponibile, verrà mostrata una notifica.
- ad **Aggiornamento installato**: Al termine dell'installazione di un aggiornamento, verrà mostrata una notifica.
- ad **Aggiornamento fallito**: Al fallimento dell'installazione di un aggiornamento, verrà mostrata una notifica.
❗ Se vuoi disabilitare le notifiche, è sufficiente andare in configurazione ed impostare `Enable notifications?` a `No` 😉.
❗ Se vuoi modificare la soglia minima per le notifiche dei trasferimenti, puoi impostare il valore di `Notifications: minimum transfer size` in configurazione 🙂.

486
docs/man.md Normal file
View file

@ -0,0 +1,486 @@
# User manual 🎓
- [User manual 🎓](#user-manual-)
- [Usage ❓](#usage-)
- [Address argument 🌎](#address-argument-)
- [AWS S3 address argument](#aws-s3-address-argument)
- [How Password can be provided 🔐](#how-password-can-be-provided-)
- [Aws S3 credentials 🦊](#aws-s3-credentials-)
- [File explorer 📂](#file-explorer-)
- [Keybindings ⌨](#keybindings-)
- [Work on multiple files 🥷](#work-on-multiple-files-)
- [Synchronized browsing ⏲️](#synchronized-browsing-)
- [Open and Open With 🚪](#open-and-open-with-)
- [Bookmarks ⭐](#bookmarks-)
- [Are my passwords Safe 😈](#are-my-passwords-safe-)
- [Linux Keyring](#linux-keyring)
- [KeepassXC setup for termscp](#keepassxc-setup-for-termscp)
- [Configuration ⚙️](#configuration-)
- [SSH Key Storage 🔐](#ssh-key-storage-)
- [File Explorer Format](#file-explorer-format)
- [Themes 🎨](#themes-)
- [My theme won't load 😱](#my-theme-wont-load-)
- [Styles 💈](#styles-)
- [Authentication page](#authentication-page)
- [Transfer page](#transfer-page)
- [Misc](#misc)
- [Text Editor ✏](#text-editor-)
- [Logging 🩺](#logging-)
- [Notifications 📫](#notifications-)
## Usage ❓
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
- `-c, --config` Open termscp starting from the configuration page
- `-q, --quiet` Disable logging
- `-t, --theme <path>` Import specified theme
- `-u, --update` Update termscp to latest version
- `-v, --version` Print version info
- `-h, --help` Print help page
termscp can be started in two different mode, if no extra arguments is provided, termscp will show the authentication form, where the user will be able to provide the parameters required to connect to the remote peer.
Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server.
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][: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 (*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 (*defined in configuration*) to 192.168.1.31; username is `root`
```sh
termscp root@192.168.1.31
```
- Connect using scp to 192.168.1.31, port is 4022; username is `omar`
```sh
termscp scp://omar@192.168.1.31:4022
```
- 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
```
#### AWS S3 address argument
Aws S3 has a different syntax for CLI address argument, for obvious reasons, but I managed to keep it the more similar as possible to the generic address argument:
```txt
s3://<bucket-name>@<region>[:profile][:/wrkdir]
```
e.g.
```txt
s3://buckethead@eu-central-1:default:/assets
```
#### How Password can be provided 🔐
You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
Password can be basically provided through 3 ways when address argument is provided:
- `-P, --password` option: just use this CLI option providing the password. I strongly unrecommend this method, since it's very insecure (since you might keep the password in the shell history)
- Via `sshpass`: you can provide password via `sshpass`, e.g. `sshpass -f ~/.ssh/topsecret.key termscp cvisintin@192.168.1.31`
- You will be prompted for it: if you don't use any of the previous methods, you will be prompted for the password, as happens with the more classics tools such as `scp`, `ssh`, etc.
---
## Aws S3 credentials 🦊
In order to connect to an Aws S3 bucket you must obviously provide some credentials.
There are basically two ways to achieve this, and as you've probably already noticed you **can't** do that via the authentication form.
So these are the ways you can provide the credentials for s3:
1. Use your credentials file: just configure the AWS cli via `aws configure` and your credentials should already be located at `~/.aws/credentials`. In case you're using a profile different from `default`, just provide it in the profile field in the authentication form.
2. **Environment variables**: you can always provide your credentials as environment variables. Keep in mind that these credentials **will always override** the credentials located in the `credentials` file. See how to configure the environment below:
These should always be mandatory:
- `AWS_ACCESS_KEY_ID`: aws access key ID (usually starts with `AKIA...`)
- `AWS_SECRET_ACCESS_KEY`: the secret access key
In case you've configured a stronger security, you *may* require these too:
- `AWS_SECURITY_TOKEN`: security token
- `AWS_SESSION_TOKEN`: session token
⚠️ Your credentials are safe: termscp won't manipulate these values directly! Your credentials are directly consumed by the **s3** crate.
In case you've got some concern regarding security, please contact the library author on [Github](https://github.com/durch/rust-s3) ⚠️
---
## File explorer 📂
When we refer to file explorers in termscp, we refer to the panels you can see after establishing a connection with the remote.
These panels are basically 3 (yes, three actually):
- Local explorer panel: it is displayed on the left of your screen and shows the current directory entries for localhost
- Remote explorer panel: it is displayed on the right of your screen and shows the current directory entries for the remote host.
- Find results panel: depending on where you're searching for files (local/remote) it will replace the local or the explorer panel. This panel shows the entries matching the search query you performed.
In order to change panel you need to type `<LEFT>` to move the remote explorer panel and `<RIGHT>` to move back to the local explorer panel. Whenever you are in the find results panel, you need to press `<ESC>` to exit panel and go back to the previous panel.
### Keybindings ⌨
| Key | Command | Reminder |
|---------------|-------------------------------------------------------|-------------|
| `<ESC>` | Disconnect from remote; return to authentication page | |
| `<TAB>` | Switch between log tab and explorer | |
| `<BACKSPACE>` | Go to previous directory in stack | |
| `<RIGHT>` | Move to remote explorer tab | |
| `<LEFT>` | Move to local explorer tab | |
| `<UP>` | Move up in selected list | |
| `<DOWN>` | Move down in selected list | |
| `<PGUP>` | Move up in selected list by 8 rows | |
| `<PGDOWN>` | Move down in selected list by 8 rows | |
| `<ENTER>` | Enter directory | |
| `<SPACE>` | Upload / download selected file | |
| `<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 |
| `<F>` | Search for files (wild match is supported) | Find |
| `<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 / Clear selection | List |
| `<M>` | Select a file | Mark |
| `<N>` | Create new file with provided name | New |
| `<O>` | Edit file; see Text editor | Open |
| `<Q>` | Quit termscp | Quit |
| `<R>` | Rename file | Rename |
| `<S>` | Save file as... | Save |
| `<U>` | Go to parent directory | Upper |
| `<V>` | Open file with default program for filetype | View |
| `<W>` | Open file with provided program | With |
| `<X>` | Execute a command | eXecute |
| `<Y>` | Toggle synchronized browsing | sYnc |
| `<DEL>` | Delete file | |
| `<CTRL+A>` | Select all files | |
| `<CTRL+C>` | Abort file transfer process | |
### Work on multiple files 🥷
You can opt to work on multiple files, selecting them pressing `<M>`, in order to select the current file, or pressing `<CTRL+A>`, which will select all the files in the working directory.
Once a file is marked for selection, it will be displayed with a `*` on the left.
When working on selection, only selected file will be processed for actions, while the current highlighted item will be ignored.
It is possible to work on multiple files also when in the find result panel.
All the actions are available when working with multiple files, but be aware that some actions work in a slightly different way. Let's dive in:
- *Copy*: whenever you copy a file, you'll be prompted to insert the destination name. When working with multiple file, this name refers to the destination directory where all these files will be copied.
- *Rename*: same as copy, but will move files there.
- *Save as*: same as copy, but will write them there.
### Synchronized browsing ⏲️
When enabled, synchronized browsing, will allow you to synchronize the navigation between the two panels.
This means that whenever you'll change the working directory on one panel, the same action will be reproduced on the other panel. If you want to enable synchronized browsing just press `<Y>`; press twice to disable. While enabled, the synchronized browsing state will be reported on the status bar on `ON`.
> ❗ at the moment, whenever you try to access an unexisting directory, you won't be prompted to create it. This might change in a future update.
### Open and Open With 🚪
Open and open with commands are powered by [open-rs](https://docs.rs/crate/open/1.7.0).
When opening files with View command (`<V>`), the system default application for the file type will be used. To do so, the default operting system service will be used, so be sure to have at least one of these installed on your system:
- **Windows** users: you don't have to worry about it, since the crate will use the `start` command.
- **MacOS** users: you don't have to worry either, since the crate will use `open`, which is already installed on your system.
- **Linux** users: one of these should be installed
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- **WSL** users: *wslview* is required, you must install [wslu](https://github.com/wslutilities/wslu).
> Q: Can I edit remote files using the view command?
> A: No, at least not directly from the "remote panel". You have to download it to a local directory first, that's due to the fact that when you open a remote file, the file is downloaded into a temporary directory, but there's no way to create a watcher for the file to check when the program you used to open it was closed, so termscp is not able to know when you're done editing the file.
---
## 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.
If you're concerned about the security of the password saved for your bookmarks, please read the [chapter below 👀](#are-my-passwords-safe-).
In order to create a new bookmark, just follow these steps:
1. Type in the authentication form the parameters to connect to your remote server
2. Press `<CTRL+S>`
3. Type in the name you want to give to the bookmark
4. Choose whether to remind the password or not
5. Press `<ENTER>` to submit
whenever you want to use the previously saved connection, just press `<TAB>` to navigate to the bookmarks list and load the bookmark parameters into the form pressing `<ENTER>`.
![Bookmarks](https://github.com/veeso/termscp/blob/main/assets/images/bookmarks.gif?raw=true)
### Are my passwords Safe 😈
Sure 😉.
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? Absolutely! (except for BSD and WSL users 😢)
On **Windows**, **Linux** and **MacOS** the key used to encrypt passwords is stored, if possible (but should be), respectively in the *Windows Vault*, in the *system keyring* and into the *Keychain*. This is actually super-safe and is directly managed by your operating system.
❗ Please, notice that if you're a Linux user, you'd better to read the [chapter below 👀](#linux-keyring), because the keyring might not be enabled or supported on your system!
On *BSD* and *WSL*, 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 😉.
#### Linux Keyring
We all love Linux thanks to the freedom it gives to the users. You can basically do anything you want as a Linux user, but this has also some cons, such as the fact that often there is no standard applications across different distributions. And this involves keyring too.
This means that on Linux there might be no keyring installed on your system. Unfortunately the library we use to work with the key storage requires a service which exposes `org.freedesktop.secrets` on D-BUS and the worst fact is that there only two services exposing it.
- ❗ If you use GNOME as desktop environment (e.g. ubuntu users), you should already be fine, since keyring is already provided by `gnome-keyring` and everything should already be working.
- ❗ For other desktop environment users there is a nice program you can use to get a keyring which is [KeepassXC](https://keepassxc.org/), which I use on my Manjaro installation (with KDE) and works fine. The only problem is that you have to setup it to be used along with termscp (but it's quite simple). To get started with KeepassXC read more [here](#keepassxc-setup-for-termscp).
- ❗ What about you don't want to install any of these services? Well, there's no problem! **termscp will keep working as usual**, but it will save the key in a file, as it usually does for BSD and WSL.
##### KeepassXC setup for termscp
Follow these steps in order to setup keepassXC for termscp:
1. Install KeepassXC
2. Go to "tools" > "settings" in toolbar
3. Select "Secret service integration" and toggle "Enable KeepassXC freedesktop.org secret service integration"
4. Create a database, if you don't have one yet: from toolbar "Database" > "New database"
5. From toolbar: "Database" > "Database settings"
6. Select "Secret service integration" and toggle "Expose entries under this group"
7. Select the group in the list where you want the termscp secret to be kept. Remember that this group might be used by any other application to store secrets via DBUS.
---
## 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 manually, 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:
- **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.
- **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.
- **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.
- **Prompt when replacing existing files?**: If set to `yes`, termscp will prompt for confirmation you whenever a file transfer would cause an existing file on target host to be replaced.
- **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.
- **Remote File formatter syntax**: syntax to display file info for each file in the remote explorer. See [File explorer format](#file-explorer-format)
- **Local File formatter syntax**: syntax to display file info for each file in the local explorer. See [File explorer format](#file-explorer-format)
- **Enable notifications?**: If set to `Yes`, notifications will be displayed.
- **Notifications: minimum transfer size**: if transfer size is greater or equal than the specified value, notifications for transfer will be displayed. The accepted values are in format `{UNSIGNED} B/KB/MB/GB/TB/PB`
### 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 is possible both for local and remote host, so you can have two different syntax in use. These fields, with name `File formatter syntax (local)` and `File formatter syntax (remote)` 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 LENGTH)
- `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}`
---
## Themes 🎨
Termscp provides you with an awesome feature: the possibility to set the colors for several components in the application.
If you want to customize termscp there are two available ways to do so:
- From the **configuration menu**
- Importing a **theme file**
In order to create your own customization from termscp, all you have to do so is to enter the configuration from the auth activity, pressing `<CTRL+C>` and then `<TAB>` twice. You should have now moved to the `themes` panel.
Here you can move with `<UP>` and `<DOWN>` to change the style you want to change, as shown in the gif below:
![Themes](https://github.com/veeso/termscp/blob/main/assets/images/themes.gif?raw=true)
termscp supports both the traditional explicit hex (`#rrggbb`) and rgb `rgb(r, g, b)` syntax to provide colors, but also **[css colors](https://www.w3schools.com/cssref/css_colors.asp)** (such as `crimson`) are accepted 😉. There is also a special keywork which is `Default`. Default means that the color used will be the default foreground or background color based on the situation (foreground for texts and lines, background for well, guess what).
As said before, you can also import theme files. You can take inspiration from or directly use one of the themes provided along with termscp, located in the `themes/` directory of this repository and import them running termscp as `termscp -t <theme_file>`. If everything was fine, it should tell you the theme has successfully been imported.
### My theme won't load 😱
This is probably due to a recent update which has broken the theme. Whenever I add a new key to themes, the saved theme won't load. To fix this issues there are two really quick-fix solutions:
1. Reload theme: whenever I release an update I will also patch the "official" themes, so you just have to download it from the repository again and re-import the theme via `-t` option
```sh
termscp -t <theme.toml>
```
2. Fix your theme: If you're using a custom theme, then you can edit via `vim` and add the missing key. The theme is located at `$CONFIG_DIR/termscp/theme.toml` where `$CONFIG_DIR` is:
- FreeBSD/GNU-Linux: `$HOME/.config/`
- MacOs: `$HOME/Library/Application Support`
- Windows: `%appdata%`
❗ Missing keys are reported in the CHANGELOG under `BREAKING CHANGES` for the version you've just installed.
### Styles 💈
You can find in the table below, the description for each style field.
Please, notice that **styles won't apply to configuration page**, in order to make it always accessible in case you mess everything up
#### Authentication page
| Key | Description |
|----------------|------------------------------------------|
| auth_address | Color of the input field for IP address |
| auth_bookmarks | Color of the bookmarks panel |
| auth_password | Color of the input field for password |
| auth_port | Color of the input field for port number |
| auth_protocol | Color of the radio group for protocol |
| auth_recents | Color of the recents panel |
| auth_username | Color of the input field for username |
#### Transfer page
| Key | Description |
|--------------------------------------|---------------------------------------------------------------------------|
| transfer_local_explorer_background | Background color of localhost explorer |
| transfer_local_explorer_foreground | Foreground color of localhost explorer |
| transfer_local_explorer_highlighted | Border and highlighted color for localhost explorer |
| transfer_remote_explorer_background | Background color of remote explorer |
| transfer_remote_explorer_foreground | Foreground color of remote explorer |
| transfer_remote_explorer_highlighted | Border and highlighted color for remote explorer |
| transfer_log_background | Background color for log panel |
| transfer_log_window | Window color for log panel |
| transfer_progress_bar_partial | Partial progress bar color |
| transfer_progress_bar_total | Total progress bar color |
| transfer_status_hidden | Color for status bar "hidden" label |
| transfer_status_sorting | Color for status bar "sorting" label; applies also to file sorting dialog |
| transfer_status_sync_browsing | Color for status bar "sync browsing" label |
#### Misc
These styles applie to different part of the application.
| Key | Description |
|-------------------|---------------------------------------------|
| misc_error_dialog | Color for error messages |
| misc_info_dialog | Color for info dialogs |
| misc_input_dialog | Color for input dialogs (such as copy file) |
| misc_keys | Color of text for key strokes |
| misc_quit_dialog | Color for quit dialogs |
| misc_save_dialog | Color for save dialogs |
| misc_warn_dialog | Color for warn dialogs |
---
## 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.
---
## Logging 🩺
termscp writes a log file for each session, which is written at
- `$HOME/.config/termscp/termscp.log` on Linux/BSD
- `$HOME/Library/Application Support/termscp/termscp.log` on MacOs
- `FOLDERID_RoamingAppData\termscp\termscp.log` on Windows
the log won't be rotated, but will just be truncated after each launch of termscp, so if you want to report an issue and you want to attach your log file, keep in mind to save the log file in a safe place before using termscp again.
The log file always reports in *trace* level, so it is kinda verbose.
I know you might have some questions regarding log files, so I made a kind of a Q/A:
> Is it possible to reduce verbosity?
No. The reason is quite simple: when an issue happens, you must be able to know what's causing it and the only way to do that, is to have the log file with the maximum verbosity level set.
> If trace level is set for logging, is the file going to reach a huge size?
Probably not, unless you never quit termscp, but I think that's unlikely to happen. A long session may produce up to 10MB of log files (I said a long session), but I think a normal session won't exceed 2MB.
> I don't want logging, can I turn it off?
Yes, you can. Just start termscp with `-q or --quiet` option. You can alias termscp to make it persistent. Remember that logging is used to diagnose issues, so since behind every open source project, there should always be this kind of mutual help, keeping log files might be your way to support the project 😉. I don't want you to feel guilty, but just to say.
> Is logging safe?
If you're concerned about security, the log file doesn't contain any plain password, so don't worry and exposes the same information the sibling file `bookmarks` reports.
## Notifications 📫
Termscp will send Desktop notifications for these kind of events:
- on **Transfer completed**: The notification will be sent once a transfer has been successfully completed.
- ❗ The notification will be displayed only if the transfer total size is at least the specified `Notifications: minimum transfer size` in the configuration.
- on **Transfer failed**: The notification will be sent once a transfer has failed due to an error.
- ❗ The notification will be displayed only if the transfer total size is at least the specified `Notifications: minimum transfer size` in the configuration.
- on **Update available**: Whenever a new version of termscp is available, a notification will be displayed.
- on **Update installed**: Whenever a new version of termscp has been installed, a notification will be displayed.
- on **Update failed**: Whenever the installation of the update fails, a notification will be displayed.
❗ If you prefer to keep notifications turned off, you can just enter setup and set `Enable notifications?` to `No` 😉.
❗ If you want to change the minimum transfer size to display notifications, you can change the value in the configuration with key `Notifications: minimum transfer size` and set it to whatever suits better for you 🙂.

29
docs/misc/README.deb.txt Normal file
View file

@ -0,0 +1,29 @@
Termscp is a feature rich terminal file transfer and explorer, with support for SCP/SFTP/FTP/S3.
Basically is a terminal utility with an TUI to connect to a remote server to retrieve and upload files and
to interact with the local file system.
It is Linux, MacOS, FreeBSD and Windows compatible.
Features:
- 📁 Different communication protocols
- SFTP
- SCP
- FTP and FTPS
- Aws S3
- 🖥 Explore and operate on the remote and on the local machine file system with a handy UI
- Create, remove, rename, search, view and edit files
- ⭐ Connect to your favourite hosts through built-in bookmarks and recent connections
- 📝 View and edit files with your favourite applications
- 💁 SFTP/SCP authentication with SSH keys and username/password
- 🐧 Compatible with Windows, Linux, FreeBSD and MacOS
- 🎨 Make it yours!
- Themes
- Custom file explorer format
- Customizable text editor
- Customizable file sorting
- and many other parameters...
- 📫 Get notified via Desktop Notifications when a large file has been transferred
- 🔐 Save your password in your operating system key vault
- 🦀 Rust-powered
- 👀 Developed keeping an eye on performance
- 🦄 Frequent awesome updates

303
docs/zh-CN/README.md Normal file
View file

@ -0,0 +1,303 @@
# termscp
<p align="center">
<img src="/assets/images/termscp.svg" width="256" height="256" />
</p>
<p align="center">~ 功能丰富的终端文件传输 ~</p>
<p align="center">
<a href="https://veeso.github.io/termscp/" target="_blank">网站</a>
·
<a href="https://veeso.github.io/termscp/#get-started" target="_blank">安装</a>
·
<a href="https://veeso.github.io/termscp/#user-manual" target="_blank">用户手册</a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp"
><img
height="20"
src="/assets/images/flags/us.png"
alt="English"
/></a>
&nbsp;
<a
href="https://github.com/veeso/termscp/blob/main/docs/de/README.md"
><img
height="20"
src="/assets/images/flags/de.png"
alt="Deutsch"
/></a>
&nbsp;
<a
href="https://github.com/veeso/termscp/blob/main/docs/es/README.md"
><img
height="20"
src="/assets/images/flags/es.png"
alt="Español"
/></a>
&nbsp;
<a
href="https://github.com/veeso/termscp/blob/main/docs/fr/README.md"
><img
height="20"
src="/assets/images/flags/fr.png"
alt="Français"
/></a>
&nbsp;
<a
href="https://github.com/veeso/termscp/blob/main/docs/it/README.md"
><img
height="20"
src="/assets/images/flags/it.png"
alt="Italiano"
/></a>
&nbsp;
<a
href="https://github.com/veeso/termscp/blob/main/docs/zh-CN/README.md"
><img
height="20"
src="/assets/images/flags/cn.png"
alt="简体中文"
/></a>
</p>
<p align="center"><a href="https://veeso.github.io/" target="_blank">@veeso</a> 开发</p>
<p align="center">当前版本: 0.7.0 (12/10/2021)</p>
<p align="center">
<a href="https://opensource.org/licenses/MIT"
><img
src="https://img.shields.io/badge/License-MIT-teal.svg"
alt="License-MIT"
/></a>
<a href="https://github.com/veeso/termscp/stargazers"
><img
src="https://img.shields.io/github/stars/veeso/termscp.svg"
alt="Repo stars"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/d/termscp.svg"
alt="Downloads counter"
/></a>
<a href="https://crates.io/crates/termscp"
><img
src="https://img.shields.io/crates/v/termscp.svg"
alt="Latest version"
/></a>
<a href="https://www.buymeacoffee.com/veeso"
><img
src="https://img.shields.io/badge/Donate-BuyMeACoffee-yellow.svg"
alt="Buy me a coffee"
/></a>
</p>
<p align="center">
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Linux/badge.svg"
alt="Linux CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/MacOS/badge.svg"
alt="MacOS CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/Windows/badge.svg"
alt="Windows CI"
/></a>
<a href="https://github.com/veeso/termscp/actions"
><img
src="https://github.com/veeso/termscp/workflows/FreeBSD/badge.svg"
alt="FreeBSD CI"
/></a>
<a href="https://coveralls.io/github/veeso/termscp"
><img
src="https://coveralls.io/repos/github/veeso/termscp/badge.svg"
alt="Coveralls"
/></a>
<a href="https://docs.rs/termscp"
><img
src="https://docs.rs/termscp/badge.svg"
alt="Docs"
/></a>
</p>
---
## 关于 termscp 🖥
termscp 是一个功能丰富的终端文件传输和浏览器,支持 SCP/SFTP/FTP/S3。 所以基本上是一个带有 TUI 的终端实用程序,用于连接到远程服务器以检索和上传文件并与本地文件系统进行交互。
它与 **Linux**、**MacOS**、**FreeBSD** 和 **Windows** 兼容。
![Explorer](/assets/images/explorer.gif)
---
## 特征 🎁
- 📁 不同的通讯协议
- **SFTP**
- **SCP**
- **FTP** and **FTPS**
- **Aws S3**
- 🖥 使用方便的 UI 在远程和本地机器文件系统上探索和操作
- 创建、删除、重命名、搜索、查看和编辑文件
- ⭐ 通过内置书签和最近的连接连接到您最喜欢的主机
- 📝 使用您喜欢的应用程序查看和编辑文件
- 💁 使用 SSH 密钥和用户名/密码进行 SFTP/SCP 身份验证
- 🐧 与 Windows、Linux、FreeBSD 和 MacOS 兼容
- 🎨 让它成为你的!
- 主题
- 自定义文件浏览器格式
- 可定制的文本编辑器
- 可定制的文件排序
- 和许多其他参数...
- 📫 传输大文件时通过桌面通知获得通知
- 🔐 将密码保存在操作系统密钥保管库中
- 🦀 Rust 动力
- 👀 开发时注意性能
- 🦄 频繁的精彩更新
---
## 开始 🚀
如果您正在考虑安装termscp我要感谢您💜 我希望你会喜欢termscp
如果您想为此项目做出贡献,请不要忘记查看我们的贡献指南。 [阅读更多](../../CONTRIBUTING.md)
如果您是 Linux、FreeBSD 或 MacOS 用户,这个简单的 shell 脚本将使用单个命令在您的系统上安装 termscp
```sh
curl --proto '=https' --tlsv1.2 -sSLf "https://git.io/JBhDb" | sh
```
如果您是 Windows 用户,则可以使用 [Chocolatey](https://chocolatey.org/) 安装 termscp
```sh
choco install termscp
```
如需更多信息或其他平台,请访问 [veeso.github.io](https://veeso.github.io/termscp/#get-started) 查看所有安装方法。
⚠️ 如果您正在寻找如何更新 termscp 只需从 CLI 运行 termscp `(sudo) termscp --update` ⚠️
### 要求 ❗
- **Linux** 用户:
- libssh
- libdbus-1
- pkg-config
- **FreeBSD** 用户:
- libssh
- dbus
- pkgconf
### 可选要求 ✔️
这些要求不是运行 termscp 的强制要求,而是要享受它的所有功能
- **Linux/FreeBSD** 用户:
- 用 `V` **打开** 文件(至少其中之一)
- *xdg-open*
- *gio*
- *gnome-open*
- *kde-open*
- **Linux** 用户:
- keyring manager: [在用户手册中阅读更多内容](man.md#linux-keyring)
- **WSL** 用户
- 用 `V` **打开** 文件(至少其中之一)
- [wslu](https://github.com/wslutilities/wslu)
---
## 支持我 ☕
如果您喜欢 termscp 并且希望看到该项目不断发展和改进,请考虑在 **Buy me a coffee** 上捐款以支持我🥳
[![给我买一杯咖啡](https://img.buymeacoffee.com/button-api/?text=%E7%BB%99%E6%88%91%E4%B9%B0%E4%B8%80%E6%9D%AF%E5%92%96%E5%95%A1&emoji=&slug=veeso&button_colour=404040&font_colour=ffffff&font_family=Comic&outline_colour=ffffff&coffee_colour=FFDD00)](https://www.buymeacoffee.com/veeso)
或者,如果您愿意,您也可以在 PayPal 上捐款:
[![Donate](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://www.paypal.me/chrisintin)
---
## 用户手册和文档 📚
用户手册可以在[termscp的网站](https://veeso.github.io/termscp/#user-manual)上找到 或者在[Github](man.md)上。
开发者文档可以在 <https://docs.rs/termscp> 的 Rust Docs 上找到。
---
## 已知的问题 🧻
- `NoSuchFileOrDirectory` 连接时 (WSL1): 我知道这个问题,我猜这是 WSL 的一个小故障。 别担心,只需将 termscp 可执行文件移动到另一个 PATH 位置,例如`/usr/bin`,或者通过适当的包格式(例如 deb安装它。
---
## 贡献和问题 🤝🏻
欢迎贡献、错误报告、新功能和问题! 😉
如果您有任何问题或疑虑或者您想建议新功能或者您只想改进termscp请随时打开问题或 PR。
请遵循 [我们的贡献指南](../../CONTRIBUTING.md)
---
## 变更日志 ⏳
查看termscp的更新日志 [这里](../../CHANGELOG.md)
---
## 供电 💪
termscp 由这些很棒的项目提供支持:
- [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)
- [open-rs](https://github.com/Byron/open-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-s3](https://github.com/durch/rust-s3)
- [self_update](https://github.com/jaemk/self_update)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
- [suppaftp](https://github.com/veeso/suppaftp)
- [textwrap](https://github.com/mgeisler/textwrap)
- [tui-rs](https://github.com/fdehau/tui-rs)
- [tui-realm](https://github.com/veeso/tui-realm)
- [whoami](https://github.com/libcala/whoami)
- [wildmatch](https://github.com/becheran/wildmatch)
---
## 画廊 🎬
> 家
![Auth](/assets/images/auth.gif)
> 书签
![Bookmarks](/assets/images/bookmarks.gif)
> 设置
![Setup](/assets/images/config.gif)
> 文本编辑器
![TextEditor](/assets/images/text-editor.gif)
---
## 执照 📃
“termscp”在 MIT 许可下获得许可。
您可以阅读整个许可证 [这里](../../LICENSE)

View file

@ -3,7 +3,9 @@
- [操作指南 🎓](#操作指南-)
- [用法](#用法)
- [地址参数](#地址参数)
- [AWS S3 地址参数](#aws-s3-地址参数)
- [如何输入密码](#如何输入密码)
- [Aws S3 凭证](#aws-s3-凭证)
- [文件浏览](#文件浏览)
- [快捷键](#快捷键)
- [处理多个文件](#处理多个文件)
@ -12,18 +14,19 @@
- [书签](#书签)
- [我的密码安全吗?](#我的密码安全吗)
- [Linux Keyring](#linux-keyring)
- [KeepassXC setup for termscp](#keepassxc-setup-for-termscp)
- [用于 termscp 的 KeepassXC 设置](#用于-termscp-的-keepassxc-设置)
- [配置](#配置)
- [SSH Key Storage](#ssh-key-storage)
- [资源管理器格式](#资源管理器格式)
- [主题](#主题)
- [样式](#样式)
- [我的主题无法加载](#我的主题无法加载)
- [登录页](#登录页)
- [文件传输页](#文件传输页)
- [Misc](#misc)
- [文本编辑器](#文本编辑器)
- [如何配置文本编辑器?](#如何配置文本编辑器)
- [日志](#日志)
- [通知](#通知)
## 用法
@ -78,6 +81,20 @@ termscp有两种不同的启动模式不带参数时termscp将显示登录
termscp scp://omar@192.168.1.31:4022:/tmp
```
#### AWS S3 地址参数
出于显而易见的原因Aws S3 对 CLI 地址参数有不同的语法,但我设法使其与通用地址参数尽可能相似:
```txt
s3://<bucket-name>@<region>[:profile][:/wrkdir]
```
例如
```txt
s3://buckethead@eu-central-1:default:/assets
```
#### 如何输入密码
你可能已经注意到url参数中没有办法直接附加密码你可以通过以下三种方式提供密码
@ -88,15 +105,38 @@ termscp有两种不同的启动模式不带参数时termscp将显示登录
---
## 文件浏览
## Aws S3 凭证
为了连接到 Aws S3 存储桶,您显然必须提供一些凭据。
基本上有两种方法可以实现这一点,而且您可能已经注意到您**不能**通过身份验证表单来做到这一点。
因此,您可以通过以下方式为 s3 提供凭据:
1. 使用您的凭证文件:只需通过`aws configure` 配置AWS cli您的凭证应该已经位于`~/.aws/credentials`。 如果您使用的配置文件不同于“默认”,只需在身份验证表单的配置文件字段中提供它。
2. **环境变量**: 您始终可以将您的凭据作为环境变量提供。 请记住,这些凭据**将始终覆盖**位于 `credentials` 文件中的凭据。 下面看看如何配置环境:
这些应该始终是强制性的:
- `AWS_ACCESS_KEY_ID`: aws 访问密钥 ID通常以 `AKIA...` 开头)
- `AWS_SECRET_ACCESS_KEY`: 秘密访问密钥
如果您配置了更强的安全性,您*可能*也需要这些:
- `AWS_SECURITY_TOKEN`: 安全令牌
- `AWS_SESSION_TOKEN`: 会话令牌
⚠️ 您的凭据是安全的termscp 不会直接操作这些值! 您的凭据直接由 **s3** crate 使用。
如果您对安全有一些担忧,请联系 [Github](https://github.com/durch/rust-s3) 上的库作者 ⚠️
---
## 文件浏览
termscp中的文件资源管理器是指你与远程建立连接后可以看到的面板。
面板由3个部分组成是的就这三个
- 本地资源管理器面板它显示在你的屏幕左侧显示localhost的当前目录文件列表。
- 远程资源管理器面板:它显示在你屏幕的右边,显示远程主机的当前目录文件列表。
- Find results panel: depending on where you're searching for files (local/remote) it will replace the local or the explorer panel. This panel shows the entries matching the search query you performed.查找结果面板:根据你搜索文件的位置(本地/远程),它将取代对应资源管理器面板。这个面板显示与你执行的搜索查询相匹配的条目。
- 查找结果面板:根据你搜索文件的位置(本地/远程),它将取代对应资源管理器面板。这个面板显示与你执行的搜索查询相匹配的条目。
为了切换面板,你需要输入 `<LEFT>` 来移动远程资源管理器面板,`<RIGHT>` 来移动回本地资源管理器面板。当在查找结果面板时,你需要按`<ESC>`来退出面板,回到前一个面板。
@ -153,11 +193,11 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
启用时,同步浏览将允许你在两个面板之间同步导航操作。这意味着,每当你在一个面板上改变工作目录时,同样的动作会在另一个面板上重现。如果你想启用同步浏览,只需按下`<Y>`;按两次就可以禁用。当启用时,同步浏览的状态将在状态栏上显示为`ON`。
*警告*目前,每当你试图访问一个不存在的目录,你不会被提示创建它。这点可能会在未来的更新中改进。
> ❗ 目前,每当你试图访问一个不存在的目录,你不会被提示创建它。这点可能会在未来的更新中改进。
### 打开/打开方式
打开和打开方式的功能是由[open-rs]https://docs.rs/crate/open/1.7.0提供的。
打开和打开方式的功能是由 [open-rs](https://docs.rs/crate/open/2.1.0)提供的。
执行视图命令(`<V>`)时,关联该文件类型的系统默认应用程序会被调用以打开当前文件。这依赖于操作系统默认的服务,所以要确保你的系统中至少安装了一个这样的服务:
- **Windows** 用户: 无需额外操作,程序内部会调用 `start` 命令。
@ -186,13 +226,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
对于书签(不包括服务器连接历史记录)而言,也可以保存用于验证的密码。注意默认情况下不保存密码,必须在保存新书签时通过提示指定密码。
> 我对在termscp中存储密码的做法非常持保留意见。原因是什么在你的电脑上保存密码可能会让黑客进入你所注册的任何服务器。但我必须承认对许多机器来说每次都输入密码真的很无聊而且很多时候我必须与局域网内的机器一起工作这对攻击者来说没有任何好处所以我想出了一个折衷办法来处理密码。
我强烈建议你遵循这些原则,以决定你是否应该本地保存密码:
- **绝对不要** 在连接公网的机器上本地保存密码,只可以在局域网机器上这么做
- 确保你的机器有网络保护措施。可以的话,对你的磁盘进行加密,并且在你离开时锁定你的电脑。
- 最好是在确保目标机器不易受影响的情况下才保存密码。
如果您担心为您的书签保存的密码的安全性,请阅读[以下章节](#我的密码安全吗?)👀
请按照以下步骤新建书签:
@ -226,7 +260,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
- ❗对于其他桌面环境的用户,有一个很好的程序,你可以用它来获得钥匙串,这就是[KeepassXC](https://keepassxc.org/)我在我的Manjaro中使用它带KDE一切都很正常。唯一的问题是你必须设置它与termscp一起使用但这很简单。要开始使用KeepassXC请阅读更多[这里]#keepassxc-setup-for-termscp
- ❗如果你不想安装任何这些服务呢?好吧,这没有问题! **termscp依然能正常工作**但它会将密钥保存在一个文件中就像它通常为BSD和WSL做的那样。
##### KeepassXC setup for termscp
##### 用于 termscp 的 KeepassXC 设置
参照以下步骤为termscp配置keepassXC
@ -259,9 +293,12 @@ termscp和书签一样只需要保证这些路径是可访问的
- **Default Protocol**默认协议是termscp中默认使用的文件传输协议。这适用于登录页和地址中的CLI参数。
- **Show Hidden Files**:选择是否应默认显示隐藏文件。你可以在运行时按 `A` 来切换是否显示隐藏的文件。
- **Check for updates**:如果设置为 `yes`termscp将通过Github API检查是否有新版本的termscp。
- **Prompt when replacing existing files?**: 如果设置为 `yes`则在文件传输会导致目标主机上的现有文件被替换时termscp 将提示您确认。
- **Group Dirs**:选择在文件浏览器中是否对文件夹进行分组。如果选择 `Display first`,目录将根据设置的方法排序,但仍显示在文件之前;如果选择 `Display last`,则正好相反。
- **Remote File formatter syntax**:在远程资源管理器中为每个文件显示文件信息的语法。参见[资源管理器格式](#资源管理器格式)
- **Local File formatter syntax**:在本地资源管理器中显示每个文件的文件信息的语法。参见[资源管理器格式](#资源管理器格式)
- **Enable notifications?**: 如果设置为 `Yes`,则会显示通知。
- **Notifications: minimum transfer size**: 如果传输大小大于或等于指定值,将显示传输通知。 接受的值格式为 `{UNSIGNED} B/KB/MB/GB/TB/PB`
### SSH Key Storage
@ -269,11 +306,11 @@ termscp和书签一样只需要保证这些路径是可访问的
你可以从配置中切换到到 `SSH Keys` tab页来访问SSH密钥存储在那里你可以
- **Add a new key**:只需按下`<CTRL+N>`,你将被提示创建一个新的密钥。提供主机名/ip地址和与该钥匙关联的用户名最后会打开一个文本编辑器将**PRIVATE** SSH key粘贴到文本编辑器中保存并退出。
- **Remove an existing key**:只要在你想删除的密钥上按下`<DEL>`或`<CTRL+E>`,就可以从 termscp 中永久删除该密钥。
- **Edit an existing key**:只需在你想编辑的密钥上按下`<ENTER>`,就可以修改私钥。
- **添加新密钥**:只需按下`<CTRL+N>`,你将被提示创建一个新的密钥。提供主机名/ip地址和与该钥匙关联的用户名最后会打开一个文本编辑器将**PRIVATE** SSH key粘贴到文本编辑器中保存并退出。
- **删除现有密钥**:只要在你想删除的密钥上按下`<DEL>`或`<CTRL+E>`,就可以从 termscp 中永久删除该密钥。
- **编辑现有密钥**:只需在你想编辑的密钥上按下`<ENTER>`,就可以修改私钥。
> 问:等等,我的私钥受密码保护,也是可以用的吗?
> 问:等等,我的私钥受密码保护,也是可以用的吗?
> 答当然可以。termscp中提供的认证密码对用户名/密码认证和RSA密钥认证都有效。
### 资源管理器格式
@ -292,7 +329,7 @@ termscp和书签一样只需要保证这些路径是可访问的
- `CTIME`: 创建时间(语法为`%b %d %Y %H:%M`Extra参数可以指定时间显示语法例如`{CTIME:8:%H:%M}`
- `GROUP`: 所属组
- `MTIME`: 最后修改时间(语法为`%b %d %Y %H:%M`Extra参数可以指定时间显示语法例如`{MTIME:8:%H:%M}`
- `NAME`: 文件名(超过24个字符的部分会被省略)
- `NAME`: 文件名(超过 LENGTH 个字符的部分会被省略)
- `PEX`: 文件权限UNIX格式
- `SIZE`: 文件大小(目录不显示)
- `SYMLINK`: 超链接(如果存在的话`-> {FILE_PATH}`)。
@ -314,7 +351,7 @@ Termscp为你提供了一个很棒的功能可以为应用程序中的几个
在这里你可以用`<UP>`和`<DOWN>`移动来选择你想改变的样式,如下图所示:
![Themes](../assets/images/themes.gif)
![Themes](https://github.com/veeso/termscp/blob/main/assets/images/themes.gif?raw=true)
termscp支持传统的十六进制`#rrggbb`和RGB`rgb(r, g, b)`语法来表示颜色,但也接受 **[css颜色](https://www.w3schools.com/cssref/css_colors.asp)**(如`crimson`)😉。还有一个特殊的关键词是`Default`,意味着使用的颜色将是基于情景的默认前景或背景颜色(文本和线条的前景色,以及容器的背景色,你猜是什么)。
@ -325,6 +362,24 @@ termscp支持传统的十六进制`#rrggbb`和RGB`rgb(r, g, b)`语法来
你可以在下面的表格中找到每个样式字段的描述。
请注意,**样式在配置页面不起作用**,以保证它总是可以访问,以防你把一切都弄乱了。
### 我的主题无法加载
这可能是由于最近的更新破坏了主题。 每当我向主题添加新密钥时,保存的主题都不会加载。 要解决此问题,有两个真正的快速修复解决方案:
1. 重新加载主题:每当我发布更新时,我也会修补“官方”主题,因此您只需再次从存储库下载它并通过 `-t` 选项重新导入主题
```sh
termscp -t <theme.toml>
```
2. 修复您的主题:如果您使用自定义主题,那么您可以通过 `vim` 进行编辑并添加缺少的键。 主题位于 `$CONFIG_DIR/termscp/theme.toml`,其中 `$CONFIG_DIR` 是:
- FreeBSD/GNU-Linux: `$HOME/.config/`
- MacOs: `$HOME/Library/Application Support`
- Windows: `%appdata%`
❗ 对于您刚刚安装的版本,在 `BREAKING CHANGES` 下的 `CHANGELOG` 中报告了丢失的键。
#### 登录页
| 字段 | 描述 |
@ -362,6 +417,7 @@ termscp支持传统的十六进制`#rrggbb`和RGB`rgb(r, g, b)`语法来
| 字段 | 描述 |
|-------------------|---------------------------------------------|
| misc_error_dialog | 报错信息的颜色 |
| misc_info_dialog | 信息对话框的颜色 |
| misc_input_dialog | 输入对话框的颜色(比如拷贝文件时) |
| misc_keys | 键盘输入文字的颜色 |
| misc_quit_dialog | 退出窗口的颜色 |
@ -375,11 +431,7 @@ termscp支持传统的十六进制`#rrggbb`和RGB`rgb(r, g, b)`语法来
Termscp有很多功能你可能已经注意到了其中之一就是可以查看和编辑文本文件。不管文件是在本地主机还是在远程主机上termscp都提供了在你喜欢的文本编辑器中打开文件的功能。
如果文件位于远程主机上,该文件将首先被下载到你的临时文件目录中,然后,**只有**在对该文件进行了修改的情况下,才会重新上传至远程主机上。
多说一句,**你只能编辑文本文件**;二进制文件是不可以的。
### 如何配置文本编辑器?
文本编辑器是通过[awesome crate](https://github.com/milkey-mouse/edit)自动查找的如果你想改变默认的文本编辑器请在termscp配置中改变它。[阅读更多](#配置)
> ❗ 多说一句,**你只能编辑文本文件**;二进制文件是不可以的。
---
@ -408,3 +460,18 @@ termscp会为每个会话创建一个日志文件该文件在
> 日志是安全的吗?
如果你担心安全问题,日志文件不包含任何普通的密码,所以不用担心,它暴露的信息与同级文件 `书签` 报告的信息相同。
## 通知
termscp 将针对这些类型的事件发送桌面通知:
- **传输完成** 传输成功完成后将发送通知。
- ❗ 仅当传输总大小至少为配置中指定的 `Notifications: minimum transfer size` 时才会显示通知。
- **传输失败**:一旦传输因错误而失败,将发送通知。
- ❗ 仅当传输总大小至少为配置中指定的 `Notifications: minimum transfer size` 时才会显示通知。
- **更新可用**每当有新版本的termscp 可用时,都会显示通知。
- **更新已安装**每当安装了新版本的termscp 时,都会显示通知。
- **更新失败**:每当更新安装失败时,都会显示通知。
❗ 如果您希望保持关闭通知,您只需进入设置并将 `Enable notifications?` 设置为 `No`😉。
❗ 如果您想更改最小传输大小以显示通知,您可以使用键 `Notifications: minimum transfer size` 更改配置中的值,并将其设置为更适合您的任何值🙂。

View file

@ -8,7 +8,7 @@
# -f, -y, --force, --yes
# Skip the confirmation prompt during installation
TERMSCP_VERSION="0.6.1"
TERMSCP_VERSION="0.7.0"
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
DEB_URL="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
RPM_URL="${GITHUB_URL}/termscp-${TERMSCP_VERSION}-1.x86_64.rpm"
@ -278,7 +278,7 @@ install_bsd_cargo_deps() {
set -e
confirm "${YELLOW}libssh, gcc${NO_COLOR} are required to install ${GREEN}termscp${NO_COLOR}; would you like to proceed?"
sudo="$(elevate_priv_ex /usr/local/bin)"
$sudo pkg install -y curl wget libssh gcc dbus
$sudo pkg install -y curl wget libssh gcc dbus pkgconf
info "Dependencies installed successfully"
}

View file

@ -25,31 +25,57 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
use std::collections::HashMap;
use std::str::FromStr;
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserHosts
///
/// UserHosts contains all the hosts saved by the user in the data storage
/// It contains both `Bookmark`
#[derive(Deserialize, Serialize, Debug)]
pub struct UserHosts {
pub bookmarks: HashMap<String, Bookmark>,
pub recents: HashMap<String, Bookmark>,
}
#[derive(Deserialize, Serialize, std::fmt::Debug, PartialEq)]
/// ## Bookmark
///
/// Bookmark describes a single bookmark entry in the user hosts storage
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq)]
pub struct Bookmark {
pub address: String,
pub port: u16,
pub protocol: String,
pub username: String,
pub password: Option<String>, // Password is optional; base64, aes-128 encrypted password
#[serde(
deserialize_with = "deserialize_protocol",
serialize_with = "serialize_protocol"
)]
pub protocol: FileTransferProtocol,
/// Address for generic parameters
pub address: Option<String>,
/// Port number for generic parameters
pub port: Option<u16>,
/// Username for generic parameters
pub username: Option<String>,
/// Password is optional; base64, aes-128 encrypted password
pub password: Option<String>,
/// S3 params; optional. When used other fields are empty for sure
pub s3: Option<S3Params>,
}
/// ## S3Params
///
/// Connection parameters for Aws s3 protocol
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Default)]
pub struct S3Params {
pub bucket: String,
pub region: String,
pub profile: Option<String>,
}
// -- impls
impl Default for UserHosts {
fn default() -> Self {
Self {
@ -59,6 +85,87 @@ impl Default for UserHosts {
}
}
impl From<FileTransferParams> for Bookmark {
fn from(params: FileTransferParams) -> Self {
let protocol: FileTransferProtocol = params.protocol;
// Create generic or others
match params.params {
ProtocolParams::Generic(params) => Self {
protocol,
address: Some(params.address),
port: Some(params.port),
username: params.username,
password: params.password,
s3: None,
},
ProtocolParams::AwsS3(params) => Self {
protocol,
address: None,
port: None,
username: None,
password: None,
s3: Some(S3Params::from(params)),
},
}
}
}
impl From<Bookmark> for FileTransferParams {
fn from(bookmark: Bookmark) -> Self {
// Create generic or others based on protocol
match bookmark.protocol {
FileTransferProtocol::AwsS3 => {
let params = bookmark.s3.unwrap_or_default();
let params = AwsS3Params::from(params);
Self::new(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params))
}
protocol => {
let params = GenericProtocolParams::default()
.address(bookmark.address.unwrap_or_default())
.port(bookmark.port.unwrap_or(22))
.username(bookmark.username)
.password(bookmark.password);
Self::new(protocol, ProtocolParams::Generic(params))
}
}
}
}
impl From<AwsS3Params> for S3Params {
fn from(params: AwsS3Params) -> Self {
S3Params {
bucket: params.bucket_name,
region: params.region,
profile: params.profile,
}
}
}
impl From<S3Params> for AwsS3Params {
fn from(params: S3Params) -> Self {
AwsS3Params::new(params.bucket, params.region, params.profile)
}
}
fn deserialize_protocol<'de, D>(deserializer: D) -> Result<FileTransferProtocol, D::Error>
where
D: Deserializer<'de>,
{
let s: &str = Deserialize::deserialize(deserializer)?;
// Parse color
match FileTransferProtocol::from_str(s) {
Err(err) => Err(DeError::custom(err)),
Ok(protocol) => Ok(protocol),
}
}
fn serialize_protocol<S>(protocol: &FileTransferProtocol, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(protocol.to_string().as_str())
}
// Tests
#[cfg(test)]
@ -77,48 +184,117 @@ mod tests {
#[test]
fn test_bookmarks_bookmark_new() {
let bookmark: Bookmark = Bookmark {
address: String::from("192.168.1.1"),
port: 22,
protocol: String::from("SFTP"),
username: String::from("root"),
address: Some(String::from("192.168.1.1")),
port: Some(22),
protocol: FileTransferProtocol::Sftp,
username: Some(String::from("root")),
password: Some(String::from("password")),
s3: None,
};
let recent: Bookmark = Bookmark {
address: String::from("192.168.1.2"),
port: 22,
protocol: String::from("SCP"),
username: String::from("admin"),
address: Some(String::from("192.168.1.2")),
port: Some(22),
protocol: FileTransferProtocol::Scp,
username: Some(String::from("admin")),
password: Some(String::from("password")),
s3: None,
};
let mut bookmarks: HashMap<String, Bookmark> = HashMap::with_capacity(1);
bookmarks.insert(String::from("test"), bookmark);
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
recents.insert(String::from("ISO20201218T181432"), recent);
let hosts: UserHosts = UserHosts {
bookmarks: bookmarks,
recents: recents,
};
let hosts: UserHosts = UserHosts { bookmarks, recents };
// Verify
let bookmark: &Bookmark = hosts.bookmarks.get(&String::from("test")).unwrap();
assert_eq!(bookmark.address, String::from("192.168.1.1"));
assert_eq!(bookmark.port, 22);
assert_eq!(bookmark.protocol, String::from("SFTP"));
assert_eq!(bookmark.username, String::from("root"));
assert_eq!(
*bookmark.password.as_ref().unwrap(),
String::from("password")
);
assert_eq!(bookmark.address.as_deref().unwrap(), "192.168.1.1");
assert_eq!(bookmark.port.unwrap(), 22);
assert_eq!(bookmark.protocol, FileTransferProtocol::Sftp);
assert_eq!(bookmark.username.as_deref().unwrap(), "root");
assert_eq!(bookmark.password.as_deref().unwrap(), "password");
let bookmark: &Bookmark = hosts
.recents
.get(&String::from("ISO20201218T181432"))
.unwrap();
assert_eq!(bookmark.address, String::from("192.168.1.2"));
assert_eq!(bookmark.port, 22);
assert_eq!(bookmark.protocol, String::from("SCP"));
assert_eq!(bookmark.username, String::from("admin"));
assert_eq!(
*bookmark.password.as_ref().unwrap(),
String::from("password")
);
assert_eq!(bookmark.address.as_deref().unwrap(), "192.168.1.2");
assert_eq!(bookmark.port.unwrap(), 22);
assert_eq!(bookmark.protocol, FileTransferProtocol::Scp);
assert_eq!(bookmark.username.as_deref().unwrap(), "admin");
assert_eq!(bookmark.password.as_deref().unwrap(), "password");
}
#[test]
fn bookmark_from_generic_ftparams() {
let params = ProtocolParams::Generic(GenericProtocolParams {
address: "127.0.0.1".to_string(),
port: 10222,
username: Some(String::from("root")),
password: Some(String::from("omar")),
});
let params: FileTransferParams = FileTransferParams::new(FileTransferProtocol::Scp, params);
let bookmark = Bookmark::from(params);
assert_eq!(bookmark.protocol, FileTransferProtocol::Scp);
assert_eq!(bookmark.address.as_deref().unwrap(), "127.0.0.1");
assert_eq!(bookmark.port.unwrap(), 10222);
assert_eq!(bookmark.username.as_deref().unwrap(), "root");
assert_eq!(bookmark.password.as_deref().unwrap(), "omar");
assert!(bookmark.s3.is_none());
}
#[test]
fn bookmark_from_s3_ftparams() {
let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test")));
let params: FileTransferParams =
FileTransferParams::new(FileTransferProtocol::AwsS3, params);
let bookmark = Bookmark::from(params);
assert_eq!(bookmark.protocol, FileTransferProtocol::AwsS3);
assert!(bookmark.address.is_none());
assert!(bookmark.port.is_none());
assert!(bookmark.username.is_none());
assert!(bookmark.password.is_none());
let s3: &S3Params = bookmark.s3.as_ref().unwrap();
assert_eq!(s3.bucket.as_str(), "omar");
assert_eq!(s3.region.as_str(), "eu-west-1");
assert_eq!(s3.profile.as_deref().unwrap(), "test");
}
#[test]
fn ftparams_from_generic_bookmark() {
let bookmark: Bookmark = Bookmark {
address: Some(String::from("192.168.1.1")),
port: Some(22),
protocol: FileTransferProtocol::Sftp,
username: Some(String::from("root")),
password: Some(String::from("password")),
s3: None,
};
let params = FileTransferParams::from(bookmark);
assert_eq!(params.protocol, FileTransferProtocol::Sftp);
let gparams = params.params.generic_params().unwrap();
assert_eq!(gparams.address.as_str(), "192.168.1.1");
assert_eq!(gparams.port, 22);
assert_eq!(gparams.username.as_deref().unwrap(), "root");
assert_eq!(gparams.password.as_deref().unwrap(), "password");
}
#[test]
fn ftparams_from_s3_bookmark() {
let bookmark: Bookmark = Bookmark {
protocol: FileTransferProtocol::AwsS3,
address: None,
port: None,
username: None,
password: None,
s3: Some(S3Params {
bucket: String::from("veeso"),
region: String::from("eu-west-1"),
profile: None,
}),
};
let params = FileTransferParams::from(bookmark);
assert_eq!(params.protocol, FileTransferProtocol::AwsS3);
let gparams = params.params.s3_params().unwrap();
assert_eq!(gparams.bucket_name.as_str(), "veeso");
assert_eq!(gparams.region.as_str(), "eu-west-1");
assert_eq!(gparams.profile, None);
}
}

View file

@ -33,6 +33,8 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
pub const DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD: u64 = 536870912; // 512MB
#[derive(Deserialize, Serialize, std::fmt::Debug)]
/// ## UserConfig
///
@ -51,10 +53,13 @@ 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 check_for_updates: Option<bool>, // @! Since 0.3.3
pub prompt_on_file_replace: Option<bool>, // @! Since 0.7.0; Default True
pub group_dirs: Option<String>,
pub file_fmt: Option<String>, // Refers to local host (for backward compatibility)
pub remote_file_fmt: Option<String>, // @! Since 0.5.0
pub notifications: Option<bool>, // @! Since 0.7.0; Default true
pub notification_threshold: Option<u64>, // @! Since 0.7.0; Default 512MB
}
#[derive(Deserialize, Serialize, std::fmt::Debug)]
@ -84,9 +89,12 @@ impl Default for UserInterfaceConfig {
default_protocol: FileTransferProtocol::Sftp.to_string(),
show_hidden_files: false,
check_for_updates: Some(true),
prompt_on_file_replace: Some(true),
group_dirs: None,
file_fmt: None,
remote_file_fmt: None,
notifications: Some(true),
notification_threshold: Some(DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD),
}
}
}
@ -120,14 +128,18 @@ mod tests {
text_editor: PathBuf::from("nano"),
show_hidden_files: true,
check_for_updates: Some(true),
prompt_on_file_replace: Some(true),
group_dirs: Some(String::from("first")),
file_fmt: Some(String::from("{NAME}")),
remote_file_fmt: Some(String::from("{USER}")),
notifications: Some(true),
notification_threshold: Some(DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD),
};
assert_eq!(ui.default_protocol, String::from("SFTP"));
assert_eq!(ui.text_editor, PathBuf::from("nano"));
assert_eq!(ui.show_hidden_files, true);
assert_eq!(ui.check_for_updates, Some(true));
assert_eq!(ui.prompt_on_file_replace, Some(true));
assert_eq!(ui.group_dirs, Some(String::from("first")));
assert_eq!(ui.file_fmt, Some(String::from("{NAME}")));
let cfg: UserConfig = UserConfig {
@ -145,11 +157,17 @@ mod tests {
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.prompt_on_file_replace, Some(true));
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
assert_eq!(
cfg.user_interface.remote_file_fmt,
Some(String::from("{USER}"))
);
assert_eq!(cfg.user_interface.notifications, Some(true));
assert_eq!(
cfg.user_interface.notification_threshold,
Some(DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD)
);
}
}

View file

@ -141,17 +141,19 @@ where
mod tests {
use super::*;
use crate::config::bookmarks::{Bookmark, S3Params, UserHosts};
use crate::config::params::UserConfig;
use crate::config::themes::Theme;
use crate::filetransfer::FileTransferProtocol;
use crate::utils::test_helpers::create_file_ioers;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
use std::io::{Seek, SeekFrom};
use std::path::PathBuf;
use tuirealm::tui::style::Color;
use crate::config::bookmarks::{Bookmark, UserHosts};
use crate::config::params::UserConfig;
use crate::config::themes::Theme;
use crate::utils::test_helpers::create_file_ioers;
#[test]
fn test_config_serialization_errors() {
let error: SerializerError = SerializerError::new(SerializerErrorKind::Syntax);
@ -199,6 +201,9 @@ mod tests {
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.prompt_on_file_replace.unwrap(), false);
assert_eq!(cfg.user_interface.notifications.unwrap(), false);
assert_eq!(cfg.user_interface.notification_threshold.unwrap(), 1024);
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
assert_eq!(
cfg.user_interface.file_fmt,
@ -242,8 +247,11 @@ mod tests {
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!(cfg.user_interface.prompt_on_file_replace.is_none());
assert!(cfg.user_interface.file_fmt.is_none());
assert!(cfg.user_interface.remote_file_fmt.is_none());
assert!(cfg.user_interface.notifications.is_none());
assert!(cfg.user_interface.notification_threshold.is_none());
// Verify keys
assert_eq!(
*cfg.remote
@ -315,9 +323,12 @@ mod tests {
text_editor = "vim"
show_hidden_files = true
check_for_updates = true
prompt_on_file_replace = false
group_dirs = "last"
file_fmt = "{NAME} {PEX}"
remote_file_fmt = "{NAME} {USER}"
notifications = false
notification_threshold = 1024
[remote.ssh_keys]
"192.168.1.31" = "/home/omar/.ssh/raspberry.key"
@ -373,31 +384,42 @@ mod tests {
// Verify recents
assert_eq!(hosts.recents.len(), 1);
let host: &Bookmark = hosts.recents.get("ISO20201215T094000Z").unwrap();
assert_eq!(host.address, String::from("172.16.104.10"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SCP"));
assert_eq!(host.username, String::from("root"));
assert_eq!(host.address.as_deref().unwrap(), "172.16.104.10");
assert_eq!(host.port.unwrap(), 22);
assert_eq!(host.protocol, FileTransferProtocol::Scp);
assert_eq!(host.username.as_deref().unwrap(), "root");
assert_eq!(host.password, None);
// Verify bookmarks
assert_eq!(hosts.bookmarks.len(), 3);
assert_eq!(hosts.bookmarks.len(), 4);
let host: &Bookmark = hosts.bookmarks.get("raspberrypi2").unwrap();
assert_eq!(host.address, String::from("192.168.1.31"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SFTP"));
assert_eq!(host.username, String::from("root"));
assert_eq!(*host.password.as_ref().unwrap(), String::from("mypassword"));
assert_eq!(host.address.as_deref().unwrap(), "192.168.1.31");
assert_eq!(host.port.unwrap(), 22);
assert_eq!(host.protocol, FileTransferProtocol::Sftp);
assert_eq!(host.username.as_deref().unwrap(), "root");
assert_eq!(host.password.as_deref().unwrap(), "mypassword");
let host: &Bookmark = hosts.bookmarks.get("msi-estrem").unwrap();
assert_eq!(host.address, String::from("192.168.1.30"));
assert_eq!(host.port, 22);
assert_eq!(host.protocol, String::from("SFTP"));
assert_eq!(host.username, String::from("cvisintin"));
assert_eq!(*host.password.as_ref().unwrap(), String::from("mysecret"));
assert_eq!(host.address.as_deref().unwrap(), "192.168.1.30");
assert_eq!(host.port.unwrap(), 22);
assert_eq!(host.protocol, FileTransferProtocol::Sftp);
assert_eq!(host.username.as_deref().unwrap(), "cvisintin");
assert_eq!(host.password.as_deref().unwrap(), "mysecret");
let host: &Bookmark = hosts.bookmarks.get("aws-server-prod1").unwrap();
assert_eq!(host.address, String::from("51.23.67.12"));
assert_eq!(host.port, 21);
assert_eq!(host.protocol, String::from("FTPS"));
assert_eq!(host.username, String::from("aws001"));
assert_eq!(host.address.as_deref().unwrap(), "51.23.67.12");
assert_eq!(host.port.unwrap(), 21);
assert_eq!(host.protocol, FileTransferProtocol::Ftp(true));
assert_eq!(host.username.as_deref().unwrap(), "aws001");
assert_eq!(host.password, None);
// Aws s3 bucket
let host: &Bookmark = hosts.bookmarks.get("my-bucket").unwrap();
assert_eq!(host.address, None);
assert_eq!(host.port, None);
assert_eq!(host.username, None);
assert_eq!(host.password, None);
assert_eq!(host.protocol, FileTransferProtocol::AwsS3);
let s3 = host.s3.as_ref().unwrap();
assert_eq!(s3.bucket.as_str(), "veeso");
assert_eq!(s3.region.as_str(), "eu-west-1");
assert_eq!(s3.profile.as_deref().unwrap(), "default");
}
#[test]
@ -416,32 +438,50 @@ mod tests {
bookmarks.insert(
String::from("raspberrypi2"),
Bookmark {
address: String::from("192.168.1.31"),
port: 22,
protocol: String::from("SFTP"),
username: String::from("root"),
address: Some(String::from("192.168.1.31")),
port: Some(22),
protocol: FileTransferProtocol::Sftp,
username: Some(String::from("root")),
password: None,
s3: None,
},
);
bookmarks.insert(
String::from("msi-estrem"),
Bookmark {
address: String::from("192.168.1.30"),
port: 4022,
protocol: String::from("SFTP"),
username: String::from("cvisintin"),
address: Some(String::from("192.168.1.30")),
port: Some(4022),
protocol: FileTransferProtocol::Sftp,
username: Some(String::from("cvisintin")),
password: Some(String::from("password")),
s3: None,
},
);
bookmarks.insert(
String::from("my-bucket"),
Bookmark {
address: None,
port: None,
protocol: FileTransferProtocol::AwsS3,
username: None,
password: None,
s3: Some(S3Params {
bucket: "veeso".to_string(),
region: "eu-west-1".to_string(),
profile: None,
}),
},
);
let mut recents: HashMap<String, Bookmark> = HashMap::with_capacity(1);
recents.insert(
String::from("ISO20201215T094000Z"),
Bookmark {
address: String::from("192.168.1.254"),
port: 3022,
protocol: String::from("SCP"),
username: String::from("omar"),
address: Some(String::from("192.168.1.254")),
port: Some(3022),
protocol: FileTransferProtocol::Scp,
username: Some(String::from("omar")),
password: Some(String::from("aaa")),
s3: None,
},
);
let tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
@ -482,6 +522,14 @@ mod tests {
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root", password = "mypassword" }
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP", username = "cvisintin", password = "mysecret" }
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[bookmarks.my-bucket]
protocol = "S3"
[bookmarks.my-bucket.s3]
bucket = "veeso"
region = "eu-west-1"
profile = "default"
[recents]
ISO20201215T094000Z = { address = "172.16.104.10", port = 22, protocol = "SCP", username = "root" }
@ -497,7 +545,7 @@ mod tests {
let file_content: &str = r#"
[bookmarks]
raspberrypi2 = { address = "192.168.1.31", port = 22, protocol = "SFTP", username = "root"}
msi-estrem = { address = "192.168.1.30", port = 22, protocol = "SFTP" }
msi-estrem = { address = "192.168.1.30", port = 22 }
aws-server-prod1 = { address = "51.23.67.12", port = 21, protocol = "FTPS", username = "aws001" }
[recents]
@ -517,6 +565,7 @@ mod tests {
auth_recents = "LightBlue"
auth_username = "LightMagenta"
misc_error_dialog = "Red"
misc_info_dialog = "LightYellow"
misc_input_dialog = "240,240,240"
misc_keys = "Cyan"
misc_quit_dialog = "Yellow"

View file

@ -83,6 +83,11 @@ pub struct Theme {
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_info_dialog: Color,
#[serde(
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_input_dialog: Color,
#[serde(
deserialize_with = "deserialize_color",
@ -183,6 +188,7 @@ impl Default for Theme {
auth_recents: Color::LightBlue,
auth_username: Color::LightMagenta,
misc_error_dialog: Color::Red,
misc_info_dialog: Color::LightYellow,
misc_input_dialog: Color::Reset,
misc_keys: Color::Cyan,
misc_quit_dialog: Color::Yellow,
@ -245,6 +251,7 @@ mod test {
assert_eq!(theme.auth_recents, Color::LightBlue);
assert_eq!(theme.auth_username, Color::LightMagenta);
assert_eq!(theme.misc_error_dialog, Color::Red);
assert_eq!(theme.misc_info_dialog, Color::LightYellow);
assert_eq!(theme.misc_input_dialog, Color::Reset);
assert_eq!(theme.misc_keys, Color::Cyan);
assert_eq!(theme.misc_quit_dialog, Color::Yellow);

View file

@ -28,27 +28,29 @@
// locals
use crate::fs::{FsEntry, FsFile};
// ext
use std::io::{Read, Write};
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use thiserror::Error;
use wildmatch::WildMatch;
// exports
pub mod ftp_transfer;
pub mod params;
pub mod scp_transfer;
pub mod sftp_transfer;
mod transfer;
pub use params::FileTransferParams;
// -- export types
pub use params::{FileTransferParams, ProtocolParams};
pub use transfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
/// ## FileTransferProtocol
///
/// This enum defines the different transfer protocol available in termscp
#[derive(PartialEq, Debug, std::clone::Clone, Copy)]
#[derive(PartialEq, Debug, Clone, Copy)]
pub enum FileTransferProtocol {
Sftp,
Scp,
Ftp(bool), // Bool is for secure (true => ftps)
AwsS3,
}
/// ## FileTransferError
@ -60,15 +62,6 @@ pub struct FileTransferError {
msg: Option<String>,
}
impl FileTransferError {
/// ### kind
///
/// Returns the error kind
pub fn kind(&self) -> FileTransferErrorType {
self.code
}
}
/// ## FileTransferErrorType
///
/// FileTransferErrorType defines the possible errors available for a file transfer
@ -116,6 +109,13 @@ impl FileTransferError {
err.msg = Some(msg);
err
}
/// ### kind
///
/// Returns the error kind
pub fn kind(&self) -> FileTransferErrorType {
self.code
}
}
impl std::fmt::Display for FileTransferError {
@ -127,29 +127,25 @@ impl std::fmt::Display for FileTransferError {
}
}
/// ## FileTransferResult
///
/// Result type returned by a `FileTransfer` implementation
pub type FileTransferResult<T> = Result<T, FileTransferError>;
/// ## FileTransfer
///
/// File transfer trait must be implemented by all the file transfers and defines the method used by a generic file transfer
pub trait FileTransfer {
/// ### connect
///
/// Connect to the remote server
/// Can return banner / welcome message on success
fn connect(
&mut self,
address: String,
port: u16,
username: Option<String>,
password: Option<String>,
) -> Result<Option<String>, FileTransferError>;
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>>;
/// ### disconnect
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> Result<(), FileTransferError>;
fn disconnect(&mut self) -> FileTransferResult<()>;
/// ### is_connected
///
@ -160,68 +156,78 @@ pub trait FileTransfer {
///
/// Print working directory
fn pwd(&mut self) -> Result<PathBuf, FileTransferError>;
fn pwd(&mut self) -> FileTransferResult<PathBuf>;
/// ### change_dir
///
/// Change working directory
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError>;
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf>;
/// ### copy
///
/// Copy file to destination
fn copy(&mut self, src: &FsEntry, dst: &Path) -> Result<(), FileTransferError>;
fn copy(&mut self, src: &FsEntry, dst: &Path) -> FileTransferResult<()>;
/// ### list_dir
///
/// List directory entries
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError>;
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>>;
/// ### mkdir
///
/// Make directory
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError>;
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()>;
/// ### remove
///
/// Remove a file or a directory
fn remove(&mut self, file: &FsEntry) -> Result<(), FileTransferError>;
fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()>;
/// ### rename
///
/// Rename file or a directory
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError>;
fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()>;
/// ### stat
///
/// Stat file and return FsEntry
fn stat(&mut self, path: &Path) -> Result<FsEntry, FileTransferError>;
fn stat(&mut self, path: &Path) -> FileTransferResult<FsEntry>;
/// ### exec
///
/// Execute a command on remote host
fn exec(&mut self, cmd: &str) -> Result<String, FileTransferError>;
fn exec(&mut self, cmd: &str) -> FileTransferResult<String>;
/// ### send_file
///
/// Send file to remote
/// File name is referred to the name of the file as it will be saved
/// Data contains the file data
/// Returns file and its size
/// Returns file and its size.
/// By default returns unsupported feature
fn send_file(
&mut self,
local: &FsFile,
file_name: &Path,
) -> Result<Box<dyn Write>, FileTransferError>;
_local: &FsFile,
_file_name: &Path,
) -> FileTransferResult<Box<dyn Write>> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### recv_file
///
/// Receive file from remote with provided name
/// Returns file and its size
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError>;
/// By default returns unsupported feature
fn recv_file(&mut self, _file: &FsFile) -> FileTransferResult<Box<dyn Read>> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### on_sent
///
@ -230,7 +236,10 @@ pub trait FileTransfer {
/// The purpose of this method is to finalize the connection with the peer when writing data.
/// This is necessary for some protocols such as FTP.
/// You must call this method each time you want to finalize the write of the remote file.
fn on_sent(&mut self, writable: Box<dyn Write>) -> Result<(), FileTransferError>;
/// By default this function returns already `Ok(())`
fn on_sent(&mut self, _writable: Box<dyn Write>) -> FileTransferResult<()> {
Ok(())
}
/// ### on_recv
///
@ -239,13 +248,77 @@ pub trait FileTransfer {
/// The purpose of this method is to finalize the connection with the peer when reading data.
/// This mighe be necessary for some protocols.
/// You must call this method each time you want to finalize the read of the remote file.
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError>;
/// By default this function returns already `Ok(())`
fn on_recv(&mut self, _readable: Box<dyn Read>) -> FileTransferResult<()> {
Ok(())
}
/// ### send_file_wno_stream
///
/// Send a file to remote WITHOUT using streams.
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
/// By default this function uses the streams function to copy content from reader to writer
fn send_file_wno_stream(
&mut self,
src: &FsFile,
dest: &Path,
mut reader: Box<dyn Read>,
) -> FileTransferResult<()> {
match self.is_connected() {
true => {
let mut stream = self.send_file(src, dest)?;
io::copy(&mut reader, &mut stream).map_err(|e| {
FileTransferError::new_ex(FileTransferErrorType::ProtocolError, e.to_string())
})?;
self.on_sent(stream)
}
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### recv_file_wno_stream
///
/// Receive a file from remote WITHOUT using streams.
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
/// For safety reasons this function doesn't accept the `Write` trait, but the destination path.
/// By default this function uses the streams function to copy content from reader to writer
fn recv_file_wno_stream(&mut self, src: &FsFile, dest: &Path) -> FileTransferResult<()> {
match self.is_connected() {
true => {
let mut writer = File::create(dest).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("Could not open local file: {}", e),
)
})?;
let mut stream = self.recv_file(src)?;
io::copy(&mut stream, &mut writer)
.map(|_| ())
.map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
e.to_string(),
)
})?;
self.on_recv(stream)
}
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### find
///
/// Find files from current directory (in all subdirectories) whose name matches the provided search
/// Search supports wildcards ('?', '*')
fn find(&mut self, search: &str) -> Result<Vec<FsEntry>, FileTransferError> {
fn find(&mut self, search: &str) -> FileTransferResult<Vec<FsEntry>> {
match self.is_connected() {
true => {
// Starting from current directory, iter dir
@ -265,11 +338,7 @@ pub trait FileTransfer {
/// Search recursively in `dir` for file matching the wildcard.
/// NOTE: DON'T RE-IMPLEMENT THIS FUNCTION, unless the file transfer provides a faster way to do so
/// NOTE: don't call this method from outside; consider it as private
fn iter_search(
&mut self,
dir: &Path,
filter: &WildMatch,
) -> Result<Vec<FsEntry>, FileTransferError> {
fn iter_search(&mut self, dir: &Path, filter: &WildMatch) -> FileTransferResult<Vec<FsEntry>> {
let mut drained: Vec<FsEntry> = Vec::new();
// Scan directory
match self.list_dir(dir) {
@ -314,6 +383,7 @@ impl std::string::ToString for FileTransferProtocol {
},
FileTransferProtocol::Scp => "SCP",
FileTransferProtocol::Sftp => "SFTP",
FileTransferProtocol::AwsS3 => "S3",
})
}
}
@ -326,6 +396,7 @@ impl std::str::FromStr for FileTransferProtocol {
"FTPS" => Ok(FileTransferProtocol::Ftp(true)),
"SCP" => Ok(FileTransferProtocol::Scp),
"SFTP" => Ok(FileTransferProtocol::Sftp),
"S3" => Ok(FileTransferProtocol::AwsS3),
_ => Err(s.to_string()),
}
}
@ -385,6 +456,14 @@ mod tests {
FileTransferProtocol::from_str("scp").ok().unwrap(),
FileTransferProtocol::Scp
);
assert_eq!(
FileTransferProtocol::from_str("S3").ok().unwrap(),
FileTransferProtocol::AwsS3
);
assert_eq!(
FileTransferProtocol::from_str("s3").ok().unwrap(),
FileTransferProtocol::AwsS3
);
// Error
assert!(FileTransferProtocol::from_str("dummy").is_err());
// To String
@ -398,6 +477,7 @@ mod tests {
);
assert_eq!(FileTransferProtocol::Scp.to_string(), String::from("SCP"));
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
}
#[test]

View file

@ -32,44 +32,132 @@ use std::path::{Path, PathBuf};
/// ### FileTransferParams
///
/// Holds connection parameters for file transfers
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct FileTransferParams {
pub protocol: FileTransferProtocol,
pub params: ProtocolParams,
pub entry_directory: Option<PathBuf>,
}
/// ## ProtocolParams
///
/// Container for protocol params
#[derive(Debug, Clone)]
pub enum ProtocolParams {
Generic(GenericProtocolParams),
AwsS3(AwsS3Params),
}
/// ## GenericProtocolParams
///
/// Protocol params used by most common protocols
#[derive(Debug, Clone)]
pub struct GenericProtocolParams {
pub address: String,
pub port: u16,
pub protocol: FileTransferProtocol,
pub username: Option<String>,
pub password: Option<String>,
pub entry_directory: Option<PathBuf>,
}
/// ## AwsS3Params
///
/// Connection parameters for AWS S3 protocol
#[derive(Debug, Clone)]
pub struct AwsS3Params {
pub bucket_name: String,
pub region: String,
pub profile: Option<String>,
}
impl FileTransferParams {
/// ### new
///
/// Instantiates a new `FileTransferParams`
pub fn new<S: AsRef<str>>(address: S) -> Self {
pub fn new(protocol: FileTransferProtocol, params: ProtocolParams) -> Self {
Self {
address: address.as_ref().to_string(),
port: 22,
protocol: FileTransferProtocol::Sftp,
username: None,
password: None,
protocol,
params,
entry_directory: None,
}
}
/// ### port
/// ### entry_directory
///
/// Set port for params
pub fn port(mut self, port: u16) -> Self {
self.port = port;
/// Set entry directory
pub fn entry_directory<P: AsRef<Path>>(mut self, dir: Option<P>) -> Self {
self.entry_directory = dir.map(|x| x.as_ref().to_path_buf());
self
}
}
impl Default for FileTransferParams {
fn default() -> Self {
Self::new(FileTransferProtocol::Sftp, ProtocolParams::default())
}
}
impl Default for ProtocolParams {
fn default() -> Self {
Self::Generic(GenericProtocolParams::default())
}
}
impl ProtocolParams {
/// ### generic_params
///
/// Retrieve generic parameters from protocol params if any
pub fn generic_params(&self) -> Option<&GenericProtocolParams> {
match self {
ProtocolParams::Generic(params) => Some(params),
_ => None,
}
}
pub fn mut_generic_params(&mut self) -> Option<&mut GenericProtocolParams> {
match self {
ProtocolParams::Generic(params) => Some(params),
_ => None,
}
}
/// ### s3_params
///
/// Retrieve AWS S3 parameters if any
pub fn s3_params(&self) -> Option<&AwsS3Params> {
match self {
ProtocolParams::AwsS3(params) => Some(params),
_ => None,
}
}
}
// -- Generic protocol params
impl Default for GenericProtocolParams {
fn default() -> Self {
Self {
address: "localhost".to_string(),
port: 22,
username: None,
password: None,
}
}
}
impl GenericProtocolParams {
/// ### address
///
/// Set address to params
pub fn address<S: AsRef<str>>(mut self, address: S) -> Self {
self.address = address.as_ref().to_string();
self
}
/// ### protocol
/// ### port
///
/// Set protocol for params
pub fn protocol(mut self, protocol: FileTransferProtocol) -> Self {
self.protocol = protocol;
/// Set port to params
pub fn port(mut self, port: u16) -> Self {
self.port = port;
self
}
@ -88,19 +176,20 @@ impl FileTransferParams {
self.password = password.map(|x| x.as_ref().to_string());
self
}
/// ### entry_directory
///
/// Set entry directory
pub fn entry_directory<P: AsRef<Path>>(mut self, dir: Option<P>) -> Self {
self.entry_directory = dir.map(|x| x.as_ref().to_path_buf());
self
}
}
impl Default for FileTransferParams {
fn default() -> Self {
Self::new("localhost")
// -- S3 params
impl AwsS3Params {
/// ### new
///
/// Instantiates a new `AwsS3Params` struct
pub fn new<S: AsRef<str>>(bucket: S, region: S, profile: Option<S>) -> Self {
Self {
bucket_name: bucket.as_ref().to_string(),
region: region.as_ref().to_string(),
profile: profile.map(|x| x.as_ref().to_string()),
}
}
}
@ -112,26 +201,49 @@ mod test {
#[test]
fn test_filetransfer_params() {
let params: FileTransferParams = FileTransferParams::new("test.rebex.net")
.port(2222)
.protocol(FileTransferProtocol::Scp)
.username(Some("omar"))
.password(Some("foobar"))
.entry_directory(Some(&Path::new("/tmp")));
assert_eq!(params.address.as_str(), "test.rebex.net");
assert_eq!(params.port, 2222);
let params: FileTransferParams =
FileTransferParams::new(FileTransferProtocol::Scp, ProtocolParams::default())
.entry_directory(Some(&Path::new("/tmp")));
assert_eq!(
params.params.generic_params().unwrap().address.as_str(),
"localhost"
);
assert_eq!(params.protocol, FileTransferProtocol::Scp);
assert_eq!(params.username.as_ref().unwrap(), "omar");
assert_eq!(params.password.as_ref().unwrap(), "foobar");
assert_eq!(
params.entry_directory.as_deref().unwrap(),
Path::new("/tmp")
);
}
#[test]
fn test_filetransfer_params_default() {
let params: FileTransferParams = FileTransferParams::default();
fn params_default() {
let params: GenericProtocolParams = ProtocolParams::default()
.generic_params()
.unwrap()
.to_owned();
assert_eq!(params.address.as_str(), "localhost");
assert_eq!(params.port, 22);
assert_eq!(params.protocol, FileTransferProtocol::Sftp);
assert!(params.username.is_none());
assert!(params.password.is_none());
}
#[test]
fn params_aws_s3() {
let params: AwsS3Params = AwsS3Params::new("omar", "eu-west-1", Some("test"));
assert_eq!(params.bucket_name.as_str(), "omar");
assert_eq!(params.region.as_str(), "eu-west-1");
assert_eq!(params.profile.as_deref().unwrap(), "test");
}
#[test]
fn references() {
let mut params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test")));
assert!(params.s3_params().is_some());
assert!(params.generic_params().is_none());
assert!(params.mut_generic_params().is_none());
let mut params = ProtocolParams::default();
assert!(params.s3_params().is_none());
assert!(params.generic_params().is_some());
assert!(params.mut_generic_params().is_some());
}
}

View file

@ -1,4 +1,4 @@
//! ## Ftp_transfer
//! ## FTP transfer
//!
//! `ftp_transfer` is the module which provides the implementation for the FTP/FTPS file transfer
@ -25,7 +25,9 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use super::{
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
};
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use crate::utils::fmt::shadow_password;
use crate::utils::path;
@ -178,25 +180,24 @@ impl FileTransfer for FtpFileTransfer {
///
/// Connect to the remote server
fn connect(
&mut self,
address: String,
port: u16,
username: Option<String>,
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
// Get stream
info!("Connecting to {}:{}", address, port);
let mut stream: FtpStream = match FtpStream::connect(format!("{}:{}", address, port)) {
Ok(stream) => stream,
Err(err) => {
error!("Failed to connect: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
));
}
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>> {
let params = match params.generic_params() {
Some(params) => params,
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
};
// Get stream
info!("Connecting to {}:{}", params.address, params.port);
let mut stream: FtpStream =
match FtpStream::connect(format!("{}:{}", params.address, params.port)) {
Ok(stream) => stream,
Err(err) => {
error!("Failed to connect: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
));
}
};
// If SSL, open secure session
if self.ftps {
info!("Setting up TLS stream...");
@ -214,7 +215,7 @@ impl FileTransfer for FtpFileTransfer {
));
}
};
stream = match stream.into_secure(ctx, address.as_str()) {
stream = match stream.into_secure(ctx, params.address.as_str()) {
Ok(s) => s,
Err(err) => {
error!("Failed to setup TLS stream: {}", err);
@ -226,12 +227,12 @@ impl FileTransfer for FtpFileTransfer {
};
}
// Login (use anonymous if credentials are unspecified)
let username: String = match username {
Some(u) => u,
let username: String = match &params.username {
Some(u) => u.to_string(),
None => String::from("anonymous"),
};
let password: String = match password {
Some(pwd) => pwd,
let password: String = match &params.password {
Some(pwd) => pwd.to_string(),
None => String::new(),
};
info!(
@ -271,7 +272,7 @@ impl FileTransfer for FtpFileTransfer {
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> Result<(), FileTransferError> {
fn disconnect(&mut self) -> FileTransferResult<()> {
info!("Disconnecting from FTP server...");
match &mut self.stream {
Some(stream) => match stream.quit() {
@ -301,7 +302,7 @@ impl FileTransfer for FtpFileTransfer {
///
/// Print working directory
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
fn pwd(&mut self) -> FileTransferResult<PathBuf> {
info!("PWD");
match &mut self.stream {
Some(stream) => match stream.pwd() {
@ -321,7 +322,7 @@ impl FileTransfer for FtpFileTransfer {
///
/// Change working directory
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError> {
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf> {
let dir: PathBuf = Self::resolve(dir);
info!("Changing directory to {}", dir.display());
match &mut self.stream {
@ -341,7 +342,7 @@ impl FileTransfer for FtpFileTransfer {
/// ### copy
///
/// Copy file to destination
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> Result<(), FileTransferError> {
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> FileTransferResult<()> {
// FTP doesn't support file copy
debug!("COPY issues (will fail, since unsupported)");
Err(FileTransferError::new(
@ -353,7 +354,7 @@ impl FileTransfer for FtpFileTransfer {
///
/// List directory entries
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>> {
let dir: PathBuf = Self::resolve(path);
info!("LIST dir {}", dir.display());
match &mut self.stream {
@ -377,7 +378,7 @@ impl FileTransfer for FtpFileTransfer {
/// ### mkdir
///
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> {
let dir: PathBuf = Self::resolve(dir);
info!("MKDIR {}", dir.display());
match &mut self.stream {
@ -407,7 +408,7 @@ impl FileTransfer for FtpFileTransfer {
/// ### remove
///
/// Remove a file or a directory
fn remove(&mut self, fsentry: &FsEntry) -> Result<(), FileTransferError> {
fn remove(&mut self, fsentry: &FsEntry) -> FileTransferResult<()> {
if self.stream.is_none() {
return Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
@ -494,7 +495,7 @@ impl FileTransfer for FtpFileTransfer {
/// ### rename
///
/// Rename file or a directory
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()> {
let dst: PathBuf = Self::resolve(dst);
info!(
"Renaming {} to {}",
@ -526,7 +527,7 @@ impl FileTransfer for FtpFileTransfer {
/// ### stat
///
/// Stat file and return FsEntry
fn stat(&mut self, _path: &Path) -> Result<FsEntry, FileTransferError> {
fn stat(&mut self, _path: &Path) -> FileTransferResult<FsEntry> {
match &mut self.stream {
Some(_) => Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
@ -540,7 +541,7 @@ impl FileTransfer for FtpFileTransfer {
/// ### exec
///
/// Execute a command on remote host
fn exec(&mut self, _cmd: &str) -> Result<String, FileTransferError> {
fn exec(&mut self, _cmd: &str) -> FileTransferResult<String> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
@ -556,7 +557,7 @@ impl FileTransfer for FtpFileTransfer {
&mut self,
_local: &FsFile,
file_name: &Path,
) -> Result<Box<dyn Write>, FileTransferError> {
) -> FileTransferResult<Box<dyn Write>> {
let file_name: PathBuf = Self::resolve(file_name);
info!("Sending file {}", file_name.display());
match &mut self.stream {
@ -577,7 +578,7 @@ impl FileTransfer for FtpFileTransfer {
///
/// Receive file from remote with provided name
/// Returns file and its size
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
fn recv_file(&mut self, file: &FsFile) -> FileTransferResult<Box<dyn Read>> {
info!("Receiving file {}", file.abs_path.display());
match &mut self.stream {
Some(stream) => match stream.retr_as_stream(&file.abs_path.as_path().to_string_lossy())
@ -601,7 +602,7 @@ impl FileTransfer for FtpFileTransfer {
/// The purpose of this method is to finalize the connection with the peer when writing data.
/// This is necessary for some protocols such as FTP.
/// You must call this method each time you want to finalize the write of the remote file.
fn on_sent(&mut self, writable: Box<dyn Write>) -> Result<(), FileTransferError> {
fn on_sent(&mut self, writable: Box<dyn Write>) -> FileTransferResult<()> {
info!("Finalizing put stream");
match &mut self.stream {
Some(stream) => match stream.finalize_put_stream(writable) {
@ -624,7 +625,7 @@ impl FileTransfer for FtpFileTransfer {
/// The purpose of this method is to finalize the connection with the peer when reading data.
/// This mighe be necessary for some protocols.
/// You must call this method each time you want to finalize the read of the remote file.
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError> {
fn on_recv(&mut self, readable: Box<dyn Read>) -> FileTransferResult<()> {
info!("Finalizing get");
match &mut self.stream {
Some(stream) => match stream.finalize_retr_stream(readable) {
@ -645,6 +646,7 @@ impl FileTransfer for FtpFileTransfer {
mod tests {
use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::utils::file::open_file;
#[cfg(feature = "with-containers")]
use crate::utils::test_helpers::write_file;
@ -672,17 +674,15 @@ mod tests {
// Sample file
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect
#[cfg(not(feature = "github-actions"))]
let hostname: String = String::from("127.0.0.1");
#[cfg(feature = "github-actions")]
let hostname: String = String::from("127.0.0.1");
assert!(ftp
.connect(
hostname,
10021,
Some(String::from("test")),
Some(String::from("test")),
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address(hostname)
.port(10021)
.username(Some("test"))
.password(Some("test"))
))
.is_ok());
assert_eq!(ftp.is_connected(), true);
// Get pwd
@ -810,12 +810,13 @@ mod tests {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(
String::from("127.0.0.1"),
10021,
Some(String::from("omar")),
Some(String::from("ommlar")),
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10021)
.username(Some("omar"))
.password(Some("ommlar"))
))
.is_err());
}
@ -824,7 +825,13 @@ mod tests {
fn test_filetransfer_ftp_no_credentials() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
assert!(ftp
.connect(String::from("127.0.0.1"), 10021, None, None)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10021)
.username::<&str>(None)
.password::<&str>(None)
))
.is_err());
}
@ -833,12 +840,13 @@ mod tests {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(
String::from("mybadserver.veryverybad.awful"),
21,
Some(String::from("omar")),
Some(String::from("ommlar")),
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("mybad.veribad.server")
.port(21)
.username::<&str>(None)
.password::<&str>(None)
))
.is_err());
}
@ -890,12 +898,13 @@ mod tests {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(
String::from("test.rebex.net"),
21,
Some(String::from("demo")),
Some(String::from("password"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("test.rebex.net")
.port(21)
.username(Some("demo"))
.password(Some("password"))
))
.is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));

View file

@ -0,0 +1,20 @@
//! # transfer
//!
//! This module exposes all the file transfers supported by termscp
// -- import
use super::{
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
};
// -- modules
mod ftp;
mod s3;
mod scp;
mod sftp;
// -- export
pub use self::s3::S3FileTransfer;
pub use ftp::FtpFileTransfer;
pub use scp::ScpFileTransfer;
pub use sftp::SftpFileTransfer;

View file

@ -0,0 +1,699 @@
//! ## S3 transfer
//!
//! S3 file transfer module
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// -- mod
mod object;
// Locals
use super::{
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
};
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::utils::path;
use object::S3Object;
// ext
use s3::creds::Credentials;
use s3::serde_types::Object;
use s3::{Bucket, Region};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str::FromStr;
/// ## S3FileTransfer
///
/// Aws s3 file transfer
pub struct S3FileTransfer {
bucket: Option<Bucket>,
wrkdir: PathBuf,
}
impl Default for S3FileTransfer {
fn default() -> Self {
Self {
bucket: None,
wrkdir: PathBuf::from("/"),
}
}
}
impl S3FileTransfer {
/// ### list_objects
///
/// List objects contained in `p` path
fn list_objects(&self, p: &Path, list_dir: bool) -> FileTransferResult<Vec<S3Object>> {
// Make path relative
let key: String = Self::fmt_path(p, list_dir);
debug!("Query list directory {}; key: {}", p.display(), key);
self.query_objects(key, true)
}
/// ### stat_object
///
/// Stat an s3 object
fn stat_object(&self, p: &Path) -> FileTransferResult<S3Object> {
let key: String = Self::fmt_path(p, false);
debug!("Query stat object {}; key: {}", p.display(), key);
let objects = self.query_objects(key, false)?;
// Absolutize path
let absol: PathBuf = path::absolutize(Path::new("/"), p);
// Find associated object
match objects
.into_iter()
.find(|x| x.path.as_path() == absol.as_path())
{
Some(obj) => Ok(obj),
None => Err(FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
format!("{}: No such file or directory", p.display()),
)),
}
}
/// ### query_objects
///
/// Query objects at key
fn query_objects(
&self,
key: String,
only_direct_children: bool,
) -> FileTransferResult<Vec<S3Object>> {
let results = self.bucket.as_ref().unwrap().list(key.clone(), None);
match results {
Ok(entries) => {
let mut objects: Vec<S3Object> = Vec::new();
entries.iter().for_each(|x| {
x.contents
.iter()
.filter(|x| {
if only_direct_children {
Self::list_object_should_be_kept(x, key.as_str())
} else {
true
}
})
.for_each(|x| objects.push(S3Object::from(x)))
});
debug!("Found objects: {:?}", objects);
Ok(objects)
}
Err(e) => Err(FileTransferError::new_ex(
FileTransferErrorType::DirStatFailed,
e.to_string(),
)),
}
}
/// ### list_object_should_be_kept
///
/// Returns whether object should be kept after list command.
/// The object won't be kept if:
///
/// 1. is not a direct child of provided dir
fn list_object_should_be_kept(obj: &Object, dir: &str) -> bool {
Self::is_direct_child(obj.key.as_str(), dir)
}
/// ### is_direct_child
///
/// Checks whether Object's key is direct child of `parent` path.
fn is_direct_child(key: &str, parent: &str) -> bool {
key == format!("{}{}", parent, S3Object::object_name(key))
|| key == format!("{}{}/", parent, S3Object::object_name(key))
}
/// ### resolve
///
/// Make s3 absolute path from a given path
fn resolve(&self, p: &Path) -> PathBuf {
path::diff_paths(path::absolutize(self.wrkdir.as_path(), p), &Path::new("/"))
.unwrap_or_default()
}
/// ### fmt_fs_entry_path
///
/// fmt path for fsentry according to format expected by s3
fn fmt_fs_file_path(f: &FsFile) -> String {
Self::fmt_path(f.abs_path.as_path(), false)
}
/// ### fmt_path
///
/// fmt path for fsentry according to format expected by s3
fn fmt_path(p: &Path, is_dir: bool) -> String {
// prevent root as slash
if p == Path::new("/") {
return "".to_string();
}
// Remove root only if absolute
#[cfg(target_family = "unix")]
let is_absolute: bool = p.is_absolute();
// NOTE: don't use is_absolute: on windows won't work
#[cfg(target_family = "windows")]
let is_absolute: bool = p.display().to_string().starts_with('/');
let p: PathBuf = match is_absolute {
true => path::diff_paths(p, &Path::new("/")).unwrap_or_default(),
false => p.to_path_buf(),
};
// NOTE: windows only: resolve paths
#[cfg(target_family = "windows")]
let p: PathBuf = PathBuf::from(path_slash::PathExt::to_slash_lossy(p.as_path()).as_str());
// Fmt
match is_dir {
true => {
let mut p: String = p.display().to_string();
if !p.ends_with('/') {
p.push('/');
}
p
}
false => p.to_string_lossy().to_string(),
}
}
}
impl FileTransfer for S3FileTransfer {
/// ### connect
///
/// Connect to the remote server
/// Can return banner / welcome message on success
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>> {
// Verify parameters are S3
let params = match params.s3_params() {
Some(params) => params,
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
};
// Load credentials
debug!("Loading credentials... (profile {:?})", params.profile);
let credentials: Credentials =
Credentials::new(None, None, None, None, params.profile.as_deref()).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("Could not load s3 credentials: {}", e),
)
})?;
// Parse region
debug!("Parsing region {}", params.region);
let region: Region = Region::from_str(params.region.as_str()).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("Could not parse s3 region: {}", e),
)
})?;
debug!(
"Credentials loaded! Connecting to bucket {}...",
params.bucket_name
);
self.bucket = Some(
Bucket::new(params.bucket_name.as_str(), region, credentials).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("Could not connect to bucket {}: {}", params.bucket_name, e),
)
})?,
);
info!("Connection successfully established");
Ok(None)
}
/// ### disconnect
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> FileTransferResult<()> {
info!("Disconnecting from S3 bucket...");
match self.bucket.take() {
Some(bucket) => {
drop(bucket);
Ok(())
}
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### is_connected
///
/// Indicates whether the client is connected to remote
fn is_connected(&self) -> bool {
self.bucket.is_some()
}
/// ### pwd
///
/// Print working directory
fn pwd(&mut self) -> FileTransferResult<PathBuf> {
info!("PWD");
match self.is_connected() {
true => Ok(self.wrkdir.clone()),
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### change_dir
///
/// Change working directory
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf> {
match &self.bucket.is_some() {
true => {
// Always allow entering root
if dir == Path::new("/") {
self.wrkdir = dir.to_path_buf();
info!("New working directory: {}", self.wrkdir.display());
return Ok(self.wrkdir.clone());
}
// Check if directory exists
debug!("Entering directory {}...", dir.display());
let dir_p: PathBuf = self.resolve(dir);
let dir_s: String = Self::fmt_path(dir_p.as_path(), true);
debug!("Searching for key {} (path: {})...", dir_s, dir_p.display());
// Check if directory already exists
if self
.stat_object(PathBuf::from(dir_s.as_str()).as_path())
.is_ok()
{
self.wrkdir = path::absolutize(Path::new("/"), dir_p.as_path());
info!("New working directory: {}", self.wrkdir.display());
Ok(self.wrkdir.clone())
} else {
Err(FileTransferError::new(
FileTransferErrorType::NoSuchFileOrDirectory,
))
}
}
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### copy
///
/// Copy file to destination
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> FileTransferResult<()> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### list_dir
///
/// List directory entries
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>> {
match self.is_connected() {
true => self
.list_objects(path, true)
.map(|x| x.into_iter().map(|x| x.into()).collect()),
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### mkdir
///
/// Make directory
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> {
match &self.bucket {
Some(bucket) => {
let dir: String = Self::fmt_path(self.resolve(dir).as_path(), true);
debug!("Making directory {}...", dir);
// Check if directory already exists
if self
.stat_object(PathBuf::from(dir.as_str()).as_path())
.is_ok()
{
error!("Directory {} already exists", dir);
return Err(FileTransferError::new(
FileTransferErrorType::DirectoryAlreadyExists,
));
}
bucket
.put_object(dir.as_str(), &[])
.map(|_| ())
.map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("Could not make directory: {}", e),
)
})
}
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### remove
///
/// Remove a file or a directory
fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()> {
let path = Self::fmt_path(
path::diff_paths(file.get_abs_path(), &Path::new("/"))
.unwrap_or_default()
.as_path(),
file.is_dir(),
);
info!("Removing object {}...", path);
match &self.bucket {
Some(bucket) => bucket.delete_object(path).map(|_| ()).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("Could not remove file: {}", e),
)
}),
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### rename
///
/// Rename file or a directory
fn rename(&mut self, _file: &FsEntry, _dst: &Path) -> FileTransferResult<()> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### stat
///
/// Stat file and return FsEntry
fn stat(&mut self, p: &Path) -> FileTransferResult<FsEntry> {
match self.is_connected() {
true => {
// First try as a "file"
let path: PathBuf = self.resolve(p);
if let Ok(obj) = self.stat_object(path.as_path()) {
return Ok(obj.into());
}
// Try as a "directory"
debug!("Failed to stat object as file; trying as a directory...");
let path: PathBuf = PathBuf::from(Self::fmt_path(path.as_path(), true));
self.stat_object(path.as_path()).map(|x| x.into())
}
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### exec
///
/// Execute a command on remote host
fn exec(&mut self, _cmd: &str) -> FileTransferResult<String> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### send_file_wno_stream
///
/// Send a file to remote WITHOUT using streams.
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
/// By default this function uses the streams function to copy content from reader to writer
fn send_file_wno_stream(
&mut self,
_src: &FsFile,
dest: &Path,
mut reader: Box<dyn Read>,
) -> FileTransferResult<()> {
match &mut self.bucket {
Some(bucket) => {
let key = Self::fmt_path(dest, false);
info!("Query PUT for key '{}'", key);
bucket
.put_object_stream(&mut reader, key.as_str())
.map(|_| ())
.map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("Could not put file: {}", e),
)
})
}
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### recv_file_wno_stream
///
/// Receive a file from remote WITHOUT using streams.
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
/// By default this function uses the streams function to copy content from reader to writer
fn recv_file_wno_stream(&mut self, src: &FsFile, dest: &Path) -> FileTransferResult<()> {
match &mut self.bucket {
Some(bucket) => {
let mut writer = File::create(dest).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("Could not open local file: {}", e),
)
})?;
let key = Self::fmt_fs_file_path(src);
info!("Query GET for key '{}'", key);
bucket
.get_object_stream(key.as_str(), &mut writer)
.map(|_| ())
.map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("Could not get file: {}", e),
)
})
}
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[cfg(feature = "with-s3-ci")]
use crate::filetransfer::params::AwsS3Params;
#[cfg(feature = "with-s3-ci")]
use crate::utils::random;
use crate::utils::test_helpers;
use pretty_assertions::assert_eq;
#[cfg(feature = "with-s3-ci")]
use std::env;
#[cfg(feature = "with-s3-ci")]
use tempfile::NamedTempFile;
#[test]
fn s3_new() {
let s3: S3FileTransfer = S3FileTransfer::default();
assert_eq!(s3.wrkdir.as_path(), Path::new("/"));
assert!(s3.bucket.is_none());
}
#[test]
fn s3_is_direct_child() {
assert_eq!(S3FileTransfer::is_direct_child("pippo/", ""), true);
assert_eq!(
S3FileTransfer::is_direct_child("pippo/sottocartella/", ""),
false
);
assert_eq!(
S3FileTransfer::is_direct_child("pippo/sottocartella/", "pippo/"),
true
);
assert_eq!(
S3FileTransfer::is_direct_child("pippo/sottocartella/", "pippo"), // This case must be handled indeed
false
);
assert_eq!(
S3FileTransfer::is_direct_child(
"pippo/sottocartella/readme.md",
"pippo/sottocartella/"
),
true
);
assert_eq!(
S3FileTransfer::is_direct_child(
"pippo/sottocartella/readme.md",
"pippo/sottocartella/"
),
true
);
}
#[test]
fn s3_resolve() {
let mut s3: S3FileTransfer = S3FileTransfer::default();
s3.wrkdir = PathBuf::from("/tmp");
// Absolute
assert_eq!(
s3.resolve(&Path::new("/tmp/sottocartella/")).as_path(),
Path::new("tmp/sottocartella")
);
// Relative
assert_eq!(
s3.resolve(&Path::new("subfolder/")).as_path(),
Path::new("tmp/subfolder")
);
}
#[test]
fn s3_fmt_fs_file_path() {
let f: FsFile =
test_helpers::make_fsentry(&Path::new("/tmp/omar.txt"), false).unwrap_file();
assert_eq!(
S3FileTransfer::fmt_fs_file_path(&f).as_str(),
"tmp/omar.txt"
);
}
#[test]
fn s3_fmt_path() {
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("/tmp/omar.txt"), false).as_str(),
"tmp/omar.txt"
);
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("omar.txt"), false).as_str(),
"omar.txt"
);
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("/tmp/subfolder"), true).as_str(),
"tmp/subfolder/"
);
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("tmp/subfolder"), true).as_str(),
"tmp/subfolder/"
);
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("tmp"), true).as_str(),
"tmp/"
);
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("tmp/"), true).as_str(),
"tmp/"
);
assert_eq!(S3FileTransfer::fmt_path(&Path::new("/"), true).as_str(), "");
}
// -- test transfer
#[cfg(feature = "with-s3-ci")]
#[test]
fn s3_filetransfer() {
// Gather s3 environment args
let bucket: String = env::var("AWS_S3_BUCKET").ok().unwrap();
let region: String = env::var("AWS_S3_REGION").ok().unwrap();
let params = get_ftparams(bucket, region);
// Get transfer
let mut s3 = S3FileTransfer::default();
// Connect
assert!(s3.connect(&params).is_ok());
// Check is connected
assert_eq!(s3.is_connected(), true);
// Pwd
assert_eq!(s3.pwd().ok().unwrap(), PathBuf::from("/"));
// Go to github-ci directory
assert!(s3.change_dir(&Path::new("/github-ci")).is_ok());
assert_eq!(s3.pwd().ok().unwrap(), PathBuf::from("/github-ci"));
// Find
assert_eq!(s3.find("*.jpg").ok().unwrap().len(), 1);
// List directory (3 entries)
assert_eq!(s3.list_dir(&Path::new("/github-ci")).ok().unwrap().len(), 3);
// Go to playground
assert!(s3.change_dir(&Path::new("/github-ci/playground")).is_ok());
assert_eq!(
s3.pwd().ok().unwrap(),
PathBuf::from("/github-ci/playground")
);
// Create directory
let dir_name: String = format!("{}/", random::random_alphanumeric_with_len(8));
let mut dir_path: PathBuf = PathBuf::from("/github-ci/playground");
dir_path.push(dir_name.as_str());
let dir_entry = test_helpers::make_fsentry(dir_path.as_path(), true);
assert!(s3.mkdir(dir_path.as_path()).is_ok());
assert!(s3.change_dir(dir_path.as_path()).is_ok());
// Copy/rename file is unsupported
assert!(s3.copy(&dir_entry, &Path::new("/copia")).is_err());
assert!(s3.rename(&dir_entry, &Path::new("/copia")).is_err());
// Exec is unsupported
assert!(s3.exec("omar!").is_err());
// Stat file
let entry = s3
.stat(&Path::new("/github-ci/avril_lavigne.jpg"))
.ok()
.unwrap()
.unwrap_file();
assert_eq!(entry.name.as_str(), "avril_lavigne.jpg");
assert_eq!(
entry.abs_path.as_path(),
Path::new("/github-ci/avril_lavigne.jpg")
);
assert_eq!(entry.ftype.as_deref().unwrap(), "jpg");
assert_eq!(entry.size, 101738);
assert_eq!(entry.user, None);
assert_eq!(entry.group, None);
assert_eq!(entry.unix_pex, None);
// Download file
let (local_file_entry, local_file): (FsFile, NamedTempFile) =
test_helpers::create_sample_file_entry();
let remote_entry =
test_helpers::make_fsentry(&Path::new("/github-ci/avril_lavigne.jpg"), false)
.unwrap_file();
assert!(s3
.recv_file_wno_stream(&remote_entry, local_file.path())
.is_ok());
// Upload file
let mut dest_path = dir_path.clone();
dest_path.push("aurellia_lavagna.jpg");
let reader = Box::new(File::open(local_file.path()).ok().unwrap());
assert!(s3
.send_file_wno_stream(&local_file_entry, dest_path.as_path(), reader)
.is_ok());
// Remove temp dir
assert!(s3.remove(&dir_entry).is_ok());
// Disconnect
assert!(s3.disconnect().is_ok());
}
#[cfg(feature = "with-s3-ci")]
fn get_ftparams(bucket: String, region: String) -> ProtocolParams {
ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, None))
}
}

View file

@ -0,0 +1,247 @@
//! ## S3 object
//!
//! This module exposes the S3Object structure, which is an intermediate structure to work with
//! S3 objects. Easy to be converted into a FsEntry.
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use super::{FsDirectory, FsEntry, FsFile, Object};
use crate::utils::parser::parse_datetime;
use crate::utils::path;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
/// ## S3Object
///
/// An intermediate struct to work with s3 `Object`.
/// Really easy to be converted into a `FsEntry`
#[derive(Debug)]
pub struct S3Object {
pub name: String,
pub path: PathBuf,
pub size: usize,
pub last_modified: SystemTime,
/// Whether or not represents a directory. I already know directories don't exist in s3!
pub is_dir: bool,
}
impl From<&Object> for S3Object {
fn from(obj: &Object) -> Self {
let is_dir: bool = obj.key.ends_with('/');
let abs_path: PathBuf = path::absolutize(
PathBuf::from("/").as_path(),
PathBuf::from(obj.key.as_str()).as_path(),
);
let last_modified: SystemTime =
match parse_datetime(obj.last_modified.as_str(), "%Y-%m-%dT%H:%M:%S%Z") {
Ok(dt) => dt,
Err(_) => UNIX_EPOCH,
};
Self {
name: Self::object_name(obj.key.as_str()),
path: abs_path,
size: obj.size as usize,
last_modified,
is_dir,
}
}
}
impl From<S3Object> for FsEntry {
fn from(obj: S3Object) -> Self {
let abs_path: PathBuf = path::absolutize(Path::new("/"), obj.path.as_path());
match obj.is_dir {
true => FsEntry::Directory(FsDirectory {
name: obj.name,
abs_path,
last_change_time: obj.last_modified,
last_access_time: obj.last_modified,
creation_time: obj.last_modified,
symlink: None,
user: None,
group: None,
unix_pex: None,
}),
false => FsEntry::File(FsFile {
name: obj.name,
ftype: obj
.path
.extension()
.map(|x| x.to_string_lossy().to_string()),
abs_path,
size: obj.size,
last_change_time: obj.last_modified,
last_access_time: obj.last_modified,
creation_time: obj.last_modified,
symlink: None,
user: None,
group: None,
unix_pex: None,
}),
}
}
}
impl S3Object {
/// ### object_name
///
/// Get object name from key
pub fn object_name(key: &str) -> String {
let mut tokens = key.split('/');
let count = tokens.clone().count();
let demi_last: String = match count > 1 {
true => tokens.nth(count - 2).unwrap().to_string(),
false => String::new(),
};
if let Some(last) = tokens.last() {
// If last is not empty, return last one
if !last.is_empty() {
return last.to_string();
}
}
// Return demi last
demi_last
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use std::time::Duration;
#[test]
fn object_to_s3object_file() {
let obj: Object = Object {
key: String::from("pippo/sottocartella/chiedo.gif"),
e_tag: String::default(),
size: 1516966,
owner: None,
storage_class: String::default(),
last_modified: String::from("2021-08-28T10:20:37.000Z"),
};
let s3_obj: S3Object = S3Object::from(&obj);
assert_eq!(s3_obj.name.as_str(), "chiedo.gif");
assert_eq!(
s3_obj.path.as_path(),
Path::new("/pippo/sottocartella/chiedo.gif")
);
assert_eq!(s3_obj.size, 1516966);
assert_eq!(s3_obj.is_dir, false);
assert_eq!(
s3_obj
.last_modified
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1630146037)
);
}
#[test]
fn object_to_s3object_dir() {
let obj: Object = Object {
key: String::from("temp/"),
e_tag: String::default(),
size: 0,
owner: None,
storage_class: String::default(),
last_modified: String::from("2021-08-28T10:20:37.000Z"),
};
let s3_obj: S3Object = S3Object::from(&obj);
assert_eq!(s3_obj.name.as_str(), "temp");
assert_eq!(s3_obj.path.as_path(), Path::new("/temp"));
assert_eq!(s3_obj.size, 0);
assert_eq!(s3_obj.is_dir, true);
assert_eq!(
s3_obj
.last_modified
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1630146037)
);
}
#[test]
fn fsentry_from_s3obj_file() {
let obj: S3Object = S3Object {
name: String::from("chiedo.gif"),
path: PathBuf::from("/pippo/sottocartella/chiedo.gif"),
size: 1516966,
is_dir: false,
last_modified: UNIX_EPOCH,
};
let entry: FsFile = FsEntry::from(obj).unwrap_file();
assert_eq!(entry.name.as_str(), "chiedo.gif");
assert_eq!(
entry.abs_path.as_path(),
Path::new("/pippo/sottocartella/chiedo.gif")
);
assert_eq!(entry.creation_time, UNIX_EPOCH);
assert_eq!(entry.last_change_time, UNIX_EPOCH);
assert_eq!(entry.last_access_time, UNIX_EPOCH);
assert_eq!(entry.size, 1516966);
assert_eq!(entry.ftype.unwrap().as_str(), "gif");
assert_eq!(entry.user, None);
assert_eq!(entry.group, None);
assert_eq!(entry.unix_pex, None);
}
#[test]
fn fsentry_from_s3obj_directory() {
let obj: S3Object = S3Object {
name: String::from("temp"),
path: PathBuf::from("/temp"),
size: 0,
is_dir: true,
last_modified: UNIX_EPOCH,
};
let entry: FsDirectory = FsEntry::from(obj).unwrap_dir();
assert_eq!(entry.name.as_str(), "temp");
assert_eq!(entry.abs_path.as_path(), Path::new("/temp"));
assert_eq!(entry.creation_time, UNIX_EPOCH);
assert_eq!(entry.last_change_time, UNIX_EPOCH);
assert_eq!(entry.last_access_time, UNIX_EPOCH);
assert_eq!(entry.user, None);
assert_eq!(entry.group, None);
assert_eq!(entry.unix_pex, None);
}
#[test]
fn object_name() {
assert_eq!(
S3Object::object_name("pippo/sottocartella/chiedo.gif").as_str(),
"chiedo.gif"
);
assert_eq!(
S3Object::object_name("pippo/sottocartella/").as_str(),
"sottocartella"
);
assert_eq!(S3Object::object_name("pippo/").as_str(), "pippo");
}
}

View file

@ -1,4 +1,4 @@
//! ## SCP_Transfer
//! ## SCP transfer
//!
//! `scps_transfer` is the module which provides the implementation for the SCP file transfer
@ -26,7 +26,9 @@
* SOFTWARE.
*/
// Locals
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use super::{
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
};
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::{fmt_time, shadow_password};
@ -278,7 +280,7 @@ impl ScpFileTransfer {
&mut self,
path: &Path,
cmd: &str,
) -> Result<String, FileTransferError> {
) -> FileTransferResult<String> {
self.perform_shell_cmd(format!("cd \"{}\"; {}", path.display(), cmd).as_str())
}
@ -286,7 +288,7 @@ impl ScpFileTransfer {
///
/// Perform a shell command and read the output from shell
/// This operation is, obviously, blocking.
fn perform_shell_cmd(&mut self, cmd: &str) -> Result<String, FileTransferError> {
fn perform_shell_cmd(&mut self, cmd: &str) -> FileTransferResult<String> {
match self.session.as_mut() {
Some(session) => {
debug!("Running command: {}", cmd);
@ -333,17 +335,15 @@ impl FileTransfer for ScpFileTransfer {
/// ### connect
///
/// Connect to the remote server
fn connect(
&mut self,
address: String,
port: u16,
username: Option<String>,
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>> {
let params = match params.generic_params() {
Some(params) => params,
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
};
// Setup tcp stream
info!("Connecting to {}:{}", address, port);
info!("Connecting to {}:{}", params.address, params.port);
let socket_addresses: Vec<SocketAddr> =
match format!("{}:{}", address, port).to_socket_addrs() {
match format!("{}:{}", params.address, params.port).to_socket_addrs() {
Ok(s) => s.collect(),
Err(err) => {
return Err(FileTransferError::new_ex(
@ -398,14 +398,14 @@ impl FileTransfer for ScpFileTransfer {
err.to_string(),
));
}
let username: String = match username {
Some(u) => u,
let username: String = match &params.username {
Some(u) => u.to_string(),
None => String::from(""),
};
// Check if it is possible to authenticate using a RSA key
match self
.key_storage
.resolve(address.as_str(), username.as_str())
.resolve(params.address.as_str(), username.as_str())
{
Some(rsa_key) => {
debug!(
@ -418,7 +418,7 @@ impl FileTransfer for ScpFileTransfer {
username.as_str(),
None,
rsa_key.as_path(),
password.as_deref(),
params.password.as_deref(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
@ -432,11 +432,16 @@ impl FileTransfer for ScpFileTransfer {
debug!(
"Authenticating with username {} and password {}",
username,
shadow_password(password.as_deref().unwrap_or(""))
shadow_password(params.password.as_deref().unwrap_or(""))
);
if let Err(err) = session.userauth_password(
username.as_str(),
password.unwrap_or_else(|| String::from("")).as_str(),
params
.password
.as_ref()
.cloned()
.unwrap_or_else(|| String::from(""))
.as_str(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
@ -469,7 +474,7 @@ impl FileTransfer for ScpFileTransfer {
/// ### disconnect
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> Result<(), FileTransferError> {
fn disconnect(&mut self) -> FileTransferResult<()> {
info!("Disconnecting from remote...");
match self.session.as_ref() {
Some(session) => {
@ -503,7 +508,7 @@ impl FileTransfer for ScpFileTransfer {
///
/// Print working directory
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
fn pwd(&mut self) -> FileTransferResult<PathBuf> {
info!("PWD: {}", self.wrkdir.display());
match self.is_connected() {
true => Ok(self.wrkdir.clone()),
@ -517,7 +522,7 @@ impl FileTransfer for ScpFileTransfer {
///
/// Change working directory
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError> {
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf> {
match self.is_connected() {
true => {
let p: PathBuf = self.wrkdir.clone();
@ -561,7 +566,7 @@ impl FileTransfer for ScpFileTransfer {
/// ### copy
///
/// Copy file to destination
fn copy(&mut self, src: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
fn copy(&mut self, src: &FsEntry, dst: &Path) -> FileTransferResult<()> {
match self.is_connected() {
true => {
let dst: PathBuf = Self::resolve(dst);
@ -609,7 +614,7 @@ impl FileTransfer for ScpFileTransfer {
///
/// List directory entries
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>> {
match self.is_connected() {
true => {
// Send ls -l to path
@ -654,7 +659,7 @@ impl FileTransfer for ScpFileTransfer {
///
/// Make directory
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> {
match self.is_connected() {
true => {
let dir: PathBuf = Self::resolve(dir);
@ -700,7 +705,7 @@ impl FileTransfer for ScpFileTransfer {
/// ### remove
///
/// Remove a file or a directory
fn remove(&mut self, file: &FsEntry) -> Result<(), FileTransferError> {
fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()> {
// Yay, we have rm -rf here :D
match self.is_connected() {
true => {
@ -738,7 +743,7 @@ impl FileTransfer for ScpFileTransfer {
/// ### rename
///
/// Rename file or a directory
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()> {
match self.is_connected() {
true => {
// Get path
@ -781,7 +786,7 @@ impl FileTransfer for ScpFileTransfer {
/// ### stat
///
/// Stat file and return FsEntry
fn stat(&mut self, path: &Path) -> Result<FsEntry, FileTransferError> {
fn stat(&mut self, path: &Path) -> FileTransferResult<FsEntry> {
let path: PathBuf = Self::absolutize(self.wrkdir.as_path(), path);
match self.is_connected() {
true => {
@ -826,7 +831,7 @@ impl FileTransfer for ScpFileTransfer {
/// ### exec
///
/// Execute a command on remote host
fn exec(&mut self, cmd: &str) -> Result<String, FileTransferError> {
fn exec(&mut self, cmd: &str) -> FileTransferResult<String> {
match self.is_connected() {
true => {
let p: PathBuf = self.wrkdir.clone();
@ -855,7 +860,7 @@ impl FileTransfer for ScpFileTransfer {
&mut self,
local: &FsFile,
file_name: &Path,
) -> Result<Box<dyn Write>, FileTransferError> {
) -> FileTransferResult<Box<dyn Write>> {
match self.session.as_ref() {
Some(session) => {
let file_name: PathBuf = Self::absolutize(self.wrkdir.as_path(), file_name);
@ -922,7 +927,7 @@ impl FileTransfer for ScpFileTransfer {
///
/// Receive file from remote with provided name
/// Returns file and its size
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
fn recv_file(&mut self, file: &FsFile) -> FileTransferResult<Box<dyn Read>> {
match self.session.as_ref() {
Some(session) => {
info!("Receiving file {}", file.abs_path.display());
@ -942,36 +947,13 @@ impl FileTransfer for ScpFileTransfer {
)),
}
}
/// ### on_sent
///
/// Finalize send method.
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
/// The purpose of this method is to finalize the connection with the peer when writing data.
/// This is necessary for some protocols such as FTP.
/// You must call this method each time you want to finalize the write of the remote file.
fn on_sent(&mut self, _writable: Box<dyn Write>) -> Result<(), FileTransferError> {
// Nothing to do
Ok(())
}
/// ### on_recv
///
/// Finalize recv method.
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
/// The purpose of this method is to finalize the connection with the peer when reading data.
/// This mighe be necessary for some protocols.
/// You must call this method each time you want to finalize the read of the remote file.
fn on_recv(&mut self, _readable: Box<dyn Read>) -> Result<(), FileTransferError> {
// Nothing to do
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::utils::test_helpers::make_fsentry;
use pretty_assertions::assert_eq;
@ -993,12 +975,13 @@ mod tests {
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10222,
Some(String::from("sftp")),
Some(String::from("password"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10222)
.username(Some("sftp"))
.password(Some("password"))
))
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
@ -1180,12 +1163,13 @@ mod tests {
let mut client: ScpFileTransfer = ScpFileTransfer::new(storage);
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10222,
Some(String::from("sftp")),
None,
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10222)
.username(Some("sftp"))
.password::<&str>(None)
))
.is_ok());
assert_eq!(client.is_connected(), true);
assert!(client.disconnect().is_ok());
@ -1195,12 +1179,13 @@ mod tests {
fn test_filetransfer_scp_bad_auth() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("127.0.0.1"),
10222,
Some(String::from("demo")),
Some(String::from("badpassword"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10222)
.username(Some("sftp"))
.password(Some("badpassword"))
))
.is_err());
}
@ -1209,7 +1194,13 @@ mod tests {
fn test_filetransfer_scp_no_credentials() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(String::from("127.0.0.1"), 10222, None, None)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10222)
.username::<&str>(None)
.password::<&str>(None)
))
.is_err());
}
@ -1217,12 +1208,13 @@ mod tests {
fn test_filetransfer_scp_bad_server() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("mybadserver.veryverybad.awful"),
22,
None,
None
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("mybad.verybad.server")
.port(10222)
.username(Some("sftp"))
.password(Some("password"))
))
.is_err());
}

View file

@ -1,4 +1,4 @@
//! ## SFTP_Transfer
//! ## SFTP transfer
//!
//! `sftp_transfer` is the module which provides the implementation for the SFTP file transfer
@ -26,7 +26,9 @@
* SOFTWARE.
*/
// Locals
use super::{FileTransfer, FileTransferError, FileTransferErrorType};
use super::{
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
};
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::{fmt_time, shadow_password};
@ -64,7 +66,7 @@ impl SftpFileTransfer {
/// ### get_abs_path
///
/// Get absolute path from path argument and check if it exists
fn get_remote_path(&self, p: &Path) -> Result<PathBuf, FileTransferError> {
fn get_remote_path(&self, p: &Path) -> FileTransferResult<PathBuf> {
match p.is_relative() {
true => {
let mut root: PathBuf = self.wrkdir.clone();
@ -202,7 +204,7 @@ impl SftpFileTransfer {
/// ### perform_shell_cmd_with
///
/// Perform a shell command, but change directory to specified path first
fn perform_shell_cmd_with_path(&mut self, cmd: &str) -> Result<String, FileTransferError> {
fn perform_shell_cmd_with_path(&mut self, cmd: &str) -> FileTransferResult<String> {
self.perform_shell_cmd(format!("cd \"{}\"; {}", self.wrkdir.display(), cmd).as_str())
}
@ -210,7 +212,7 @@ impl SftpFileTransfer {
///
/// Perform a shell command and read the output from shell
/// This operation is, obviously, blocking.
fn perform_shell_cmd(&mut self, cmd: &str) -> Result<String, FileTransferError> {
fn perform_shell_cmd(&mut self, cmd: &str) -> FileTransferResult<String> {
match self.session.as_mut() {
Some(session) => {
// Create channel
@ -257,17 +259,15 @@ impl FileTransfer for SftpFileTransfer {
/// ### connect
///
/// Connect to the remote server
fn connect(
&mut self,
address: String,
port: u16,
username: Option<String>,
password: Option<String>,
) -> Result<Option<String>, FileTransferError> {
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>> {
let params = match params.generic_params() {
Some(params) => params,
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
};
// Setup tcp stream
info!("Connecting to {}:{}", address, port);
info!("Connecting to {}:{}", params.address, params.port);
let socket_addresses: Vec<SocketAddr> =
match format!("{}:{}", address, port).to_socket_addrs() {
match format!("{}:{}", params.address, params.port).to_socket_addrs() {
Ok(s) => s.collect(),
Err(err) => {
return Err(FileTransferError::new_ex(
@ -321,14 +321,14 @@ impl FileTransfer for SftpFileTransfer {
err.to_string(),
));
}
let username: String = match username {
Some(u) => u,
let username: String = match &params.username {
Some(u) => u.to_string(),
None => String::from(""),
};
// Check if it is possible to authenticate using a RSA key
match self
.key_storage
.resolve(address.as_str(), username.as_str())
.resolve(params.address.as_str(), username.as_str())
{
Some(rsa_key) => {
debug!(
@ -341,7 +341,7 @@ impl FileTransfer for SftpFileTransfer {
username.as_str(),
None,
rsa_key.as_path(),
password.as_deref(),
params.password.as_deref(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
@ -355,11 +355,16 @@ impl FileTransfer for SftpFileTransfer {
debug!(
"Authenticating with username {} and password {}",
username,
shadow_password(password.as_deref().unwrap_or(""))
shadow_password(params.password.as_deref().unwrap_or(""))
);
if let Err(err) = session.userauth_password(
username.as_str(),
password.unwrap_or_else(|| String::from("")).as_str(),
params
.password
.as_ref()
.cloned()
.unwrap_or_else(|| String::from(""))
.as_str(),
) {
error!("Authentication failed: {}", err);
return Err(FileTransferError::new_ex(
@ -410,7 +415,7 @@ impl FileTransfer for SftpFileTransfer {
/// ### disconnect
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> Result<(), FileTransferError> {
fn disconnect(&mut self) -> FileTransferResult<()> {
info!("Disconnecting from remote...");
match self.session.as_ref() {
Some(session) => {
@ -444,7 +449,7 @@ impl FileTransfer for SftpFileTransfer {
/// ### pwd
///
/// Print working directory
fn pwd(&mut self) -> Result<PathBuf, FileTransferError> {
fn pwd(&mut self) -> FileTransferResult<PathBuf> {
info!("PWD: {}", self.wrkdir.display());
match self.sftp {
Some(_) => Ok(self.wrkdir.clone()),
@ -457,7 +462,7 @@ impl FileTransfer for SftpFileTransfer {
/// ### change_dir
///
/// Change working directory
fn change_dir(&mut self, dir: &Path) -> Result<PathBuf, FileTransferError> {
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf> {
match self.sftp.as_ref() {
Some(_) => {
// Change working directory
@ -474,7 +479,7 @@ impl FileTransfer for SftpFileTransfer {
/// ### copy
///
/// Copy file to destination
fn copy(&mut self, src: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
fn copy(&mut self, src: &FsEntry, dst: &Path) -> FileTransferResult<()> {
// NOTE: use SCP command to perform copy (UNSAFE)
match self.is_connected() {
true => {
@ -520,7 +525,7 @@ impl FileTransfer for SftpFileTransfer {
/// ### list_dir
///
/// List directory entries
fn list_dir(&mut self, path: &Path) -> Result<Vec<FsEntry>, FileTransferError> {
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>> {
match self.sftp.as_ref() {
Some(sftp) => {
// Get path
@ -553,7 +558,7 @@ impl FileTransfer for SftpFileTransfer {
///
/// Make directory
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
fn mkdir(&mut self, dir: &Path) -> Result<(), FileTransferError> {
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> {
match self.sftp.as_ref() {
Some(sftp) => {
// Make directory
@ -583,7 +588,7 @@ impl FileTransfer for SftpFileTransfer {
/// ### remove
///
/// Remove a file or a directory
fn remove(&mut self, file: &FsEntry) -> Result<(), FileTransferError> {
fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()> {
if self.sftp.is_none() {
return Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
@ -627,7 +632,7 @@ impl FileTransfer for SftpFileTransfer {
/// ### rename
///
/// Rename file or a directory
fn rename(&mut self, file: &FsEntry, dst: &Path) -> Result<(), FileTransferError> {
fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()> {
match self.sftp.as_ref() {
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
@ -656,7 +661,7 @@ impl FileTransfer for SftpFileTransfer {
/// ### stat
///
/// Stat file and return FsEntry
fn stat(&mut self, path: &Path) -> Result<FsEntry, FileTransferError> {
fn stat(&mut self, path: &Path) -> FileTransferResult<FsEntry> {
match self.sftp.as_ref() {
Some(sftp) => {
// Get path
@ -680,7 +685,7 @@ impl FileTransfer for SftpFileTransfer {
/// ### exec
///
/// Execute a command on remote host
fn exec(&mut self, cmd: &str) -> Result<String, FileTransferError> {
fn exec(&mut self, cmd: &str) -> FileTransferResult<String> {
info!("Executing command {}", cmd);
match self.is_connected() {
true => match self.perform_shell_cmd_with_path(cmd) {
@ -705,7 +710,7 @@ impl FileTransfer for SftpFileTransfer {
&mut self,
local: &FsFile,
file_name: &Path,
) -> Result<Box<dyn Write>, FileTransferError> {
) -> FileTransferResult<Box<dyn Write>> {
match self.sftp.as_ref() {
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
@ -746,7 +751,7 @@ impl FileTransfer for SftpFileTransfer {
/// ### recv_file
///
/// Receive file from remote with provided name
fn recv_file(&mut self, file: &FsFile) -> Result<Box<dyn Read>, FileTransferError> {
fn recv_file(&mut self, file: &FsFile) -> FileTransferResult<Box<dyn Read>> {
match self.sftp.as_ref() {
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
@ -766,36 +771,21 @@ impl FileTransfer for SftpFileTransfer {
}
}
}
/// ### on_sent
///
/// Finalize send method. This method must be implemented only if necessary.
/// The purpose of this method is to finalize the connection with the peer when writing data.
/// This is necessary for some protocols such as FTP.
/// You must call this method each time you want to finalize the write of the remote file.
fn on_sent(&mut self, _writable: Box<dyn Write>) -> Result<(), FileTransferError> {
Ok(())
}
/// ### on_recv
///
/// Finalize recv method. This method must be implemented only if necessary.
/// The purpose of this method is to finalize the connection with the peer when reading data.
/// This mighe be necessary for some protocols.
/// You must call this method each time you want to finalize the read of the remote file.
fn on_recv(&mut self, _readable: Box<dyn Read>) -> Result<(), FileTransferError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::utils::test_helpers::make_fsentry;
#[cfg(feature = "with-containers")]
use crate::utils::test_helpers::{create_sample_file_entry, write_file, write_ssh_key};
use crate::utils::test_helpers::{
create_sample_file, create_sample_file_entry, write_file, write_ssh_key,
};
use pretty_assertions::assert_eq;
#[cfg(feature = "with-containers")]
use std::fs::File;
#[test]
fn test_filetransfer_sftp_new() {
@ -814,12 +804,13 @@ mod tests {
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10022,
Some(String::from("sftp")),
Some(String::from("password"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10022)
.username(Some("sftp"))
.password(Some("password"))
))
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
@ -889,6 +880,11 @@ mod tests {
.unwrap();
write_file(&file, &mut writable);
assert!(client.on_sent(writable).is_ok());
// Upload file without stream
let reader = Box::new(File::open(entry.abs_path.as_path()).ok().unwrap());
assert!(client
.send_file_wno_stream(&entry, PathBuf::from("README2.md").as_path(), reader)
.is_ok());
// Upload file (err)
assert!(client
.send_file(&entry, PathBuf::from("/ommlar/omarone").as_path())
@ -898,10 +894,10 @@ mod tests {
.list_dir(PathBuf::from("/tmp/omar").as_path())
.ok()
.unwrap();
assert_eq!(list.len(), 2);
assert_eq!(list.len(), 3);
// Find
assert_eq!(client.find("*.txt").ok().unwrap().len(), 1);
assert_eq!(client.find("*.md").ok().unwrap().len(), 1);
assert_eq!(client.find("*.md").ok().unwrap().len(), 2);
assert_eq!(client.find("*.jpeg").ok().unwrap().len(), 0);
// Rename
assert!(client
@ -955,6 +951,9 @@ mod tests {
let mut data: Vec<u8> = vec![0; 1024];
assert!(readable.read(&mut data).is_ok());
assert!(client.on_recv(readable).is_ok());
let dest_file = create_sample_file();
// Receive file wno stream
assert!(client.recv_file_wno_stream(&file, dest_file.path()).is_ok());
// Receive file (err)
assert!(client.recv_file(&entry).is_err());
// Cleanup
@ -979,12 +978,13 @@ mod tests {
let mut client: SftpFileTransfer = SftpFileTransfer::new(storage);
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10022,
Some(String::from("sftp")),
None,
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10022)
.username(Some("sftp"))
.password::<&str>(None)
))
.is_ok());
assert_eq!(client.is_connected(), true);
assert!(client.disconnect().is_ok());
@ -994,12 +994,13 @@ mod tests {
fn test_filetransfer_sftp_bad_auth() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("127.0.0.1"),
10022,
Some(String::from("demo")),
Some(String::from("badpassword"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10022)
.username(Some("sftp"))
.password(Some("badpassword"))
))
.is_err());
}
@ -1008,7 +1009,13 @@ mod tests {
fn test_filetransfer_sftp_no_credentials() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(String::from("127.0.0.1"), 10022, None, None)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10022)
.username::<&str>(None)
.password::<&str>(None)
))
.is_err());
}
@ -1018,12 +1025,13 @@ mod tests {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10022,
Some(String::from("sftp")),
Some(String::from("password"))
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10022)
.username(Some("sftp"))
.password(Some("password"))
))
.is_ok());
// get realpath
assert!(client
@ -1054,12 +1062,13 @@ mod tests {
fn test_filetransfer_sftp_bad_server() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("mybadserver.veryverybad.awful"),
22,
None,
None
)
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("myverybad.verybad.server")
.port(10022)
.username(Some("sftp"))
.password(Some("password"))
))
.is_err());
}

View file

@ -38,7 +38,9 @@ use std::fs::set_permissions;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
// Locals
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
#[cfg(target_family = "unix")]
use crate::fs::UnixPex;
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::utils::path;
/// ## HostErrorType

View file

@ -47,18 +47,20 @@ extern crate lazy_static;
extern crate log;
#[macro_use]
extern crate magic_crypt;
extern crate notify_rust;
extern crate open;
#[cfg(target_os = "windows")]
extern crate path_slash;
extern crate rand;
extern crate regex;
extern crate s3;
extern crate self_update;
extern crate ssh2;
extern crate suppaftp;
extern crate tempfile;
extern crate textwrap;
extern crate tui_realm_stdlib;
extern crate tuirealm;
extern crate ureq;
#[cfg(target_family = "unix")]
extern crate users;
extern crate whoami;

View file

@ -62,11 +62,17 @@ use system::logging;
enum Task {
Activity(NextActivity),
ImportTheme(PathBuf),
InstallUpdate,
}
#[derive(FromArgs)]
#[argh(description = "
where positional can be: [protocol://user@address:port:wrkdir] [local-wrkdir]
where positional can be: [address] [local-wrkdir]
Address syntax can be:
- `protocol://user@address:port:wrkdir` for protocols such as Sftp, Scp, Ftp
- `s3://bucket-name@region:profile:/wrkdir` for Aws S3 protocol
Please, report issues to <https://github.com/veeso/termscp>
Please, consider supporting the author <https://www.buymeacoffee.com/veeso>")]
@ -79,6 +85,12 @@ struct Args {
quiet: bool,
#[argh(option, short = 't', description = "import specified theme")]
theme: Option<String>,
#[argh(
switch,
short = 'u',
description = "update termscp to the latest version"
)]
update: bool,
#[argh(
option,
short = 'T',
@ -172,6 +184,9 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
if let Some(theme) = args.theme {
run_opts.task = Task::ImportTheme(PathBuf::from(theme));
}
if args.update {
run_opts.task = Task::InstallUpdate;
}
// @! Ordinary mode
// Remote argument
if let Some(remote) = args.positional.get(0) {
@ -180,7 +195,9 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
Ok(mut remote) => {
// If password is provided, set password
if let Some(passwd) = args.password {
remote = remote.password(Some(passwd));
if let Some(mut params) = remote.params.mut_generic_params() {
params.password = Some(passwd);
}
}
// Set params
run_opts.remote = Some(remote);
@ -209,25 +226,26 @@ fn parse_args(args: Args) -> Result<RunOpts, String> {
fn read_password(run_opts: &mut RunOpts) -> Result<(), String> {
// Initialize client if necessary
if let Some(remote) = run_opts.remote.as_mut() {
debug!("User has specified remote options: address: {:?}, port: {:?}, protocol: {:?}, user: {:?}, password: {}", remote.address, remote.port, remote.protocol, remote.username, utils::fmt::shadow_password(remote.password.as_deref().unwrap_or("")));
if remote.password.is_none() {
// Ask password if unspecified
remote.password = match rpassword::read_password_from_tty(Some("Password: ")) {
Ok(p) => {
if p.is_empty() {
None
} else {
debug!(
"Read password from tty: {}",
utils::fmt::shadow_password(p.as_str())
);
Some(p)
if let Some(mut params) = remote.params.mut_generic_params() {
if params.password.is_none() {
// Ask password if unspecified
params.password = match rpassword::read_password_from_tty(Some("Password: ")) {
Ok(p) => {
if p.is_empty() {
None
} else {
debug!(
"Read password from tty: {}",
utils::fmt::shadow_password(p.as_str())
);
Some(p)
}
}
}
Err(_) => {
return Err("Could not read password from prompt".to_string());
}
};
Err(_) => {
return Err("Could not read password from prompt".to_string());
}
};
}
}
}
Ok(())
@ -248,6 +266,16 @@ fn run(mut run_opts: RunOpts) -> i32 {
1
}
},
Task::InstallUpdate => match support::install_update() {
Ok(msg) => {
println!("{}", msg);
0
}
Err(err) => {
eprintln!("Could not install update: {}", err);
1
}
},
Task::Activity(activity) => {
// Get working directory
let wrkdir: PathBuf = match env::current_dir() {

View file

@ -26,7 +26,13 @@
* SOFTWARE.
*/
// mod
use crate::system::{environment, theme_provider::ThemeProvider};
use crate::system::{
auto_update::{Update, UpdateStatus},
config_client::ConfigClient,
environment,
notifications::Notification,
theme_provider::ThemeProvider,
};
use std::fs;
use std::path::{Path, PathBuf};
@ -51,6 +57,37 @@ pub fn import_theme(p: &Path) -> Result<(), String> {
.map_err(|e| format!("Could not import theme: {}", e))
}
/// ### install_update
///
/// Install latest version of termscp if an update is available
pub fn install_update() -> Result<String, String> {
match Update::default()
.show_progress(true)
.ask_confirm(true)
.upgrade()
{
Ok(UpdateStatus::AlreadyUptodate) => Ok("termscp is already up to date".to_string()),
Ok(UpdateStatus::UpdateInstalled(v)) => {
if get_config_client()
.map(|x| x.get_notifications())
.unwrap_or(true)
{
Notification::update_installed(v.as_str());
}
Ok(format!("termscp has been updated to version {}", v))
}
Err(err) => {
if get_config_client()
.map(|x| x.get_notifications())
.unwrap_or(true)
{
Notification::update_failed(err.to_string());
}
Err(err.to_string())
}
}
}
/// ### get_config_dir
///
/// Get configuration directory
@ -66,3 +103,19 @@ fn get_config_dir() -> Result<PathBuf, String> {
)),
}
}
/// ### get_config_client
///
/// Get configuration client
fn get_config_client() -> Option<ConfigClient> {
match get_config_dir() {
Err(_) => None,
Ok(dir) => {
let (cfg_path, ssh_key_dir) = environment::get_config_paths(dir.as_path());
match ConfigClient::new(cfg_path.as_path(), ssh_key_dir.as_path()) {
Err(_) => None,
Ok(c) => Some(c),
}
}
}
}

235
src/system/auto_update.rs Normal file
View file

@ -0,0 +1,235 @@
//! ## Auto update
//!
//! Automatic update module. This module is used to upgrade the current version of termscp to the latest available on Github
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use crate::utils::parser::parse_semver;
pub use self_update::errors::Error as UpdateError;
use self_update::{
backends::github::Update as GithubUpdater, cargo_crate_version, update::Release as UpdRelease,
Status,
};
/// ### UpdateStatus
///
/// The status of the update in case of success
#[derive(Debug, Eq, PartialEq)]
pub enum UpdateStatus {
/// Termscp is already up to date
AlreadyUptodate,
/// The update has been correctly installed
UpdateInstalled(String),
}
/// ## Release
///
/// Info related to a github release
#[derive(Debug)]
pub struct Release {
pub version: String,
pub body: String,
}
/// ## Update
///
/// The update structure defines the options used to install the update.
/// Once you're fine with the options, just call the `upgrade()` method to upgrade termscp.
#[derive(Debug)]
pub struct Update {
ask_confirm: bool,
progress: bool,
}
impl Update {
/// ### show_progress
///
/// Set whether to show or not the progress bar
pub fn show_progress(mut self, opt: bool) -> Self {
self.progress = opt;
self
}
/// ### ask_confirm
///
/// Set whether to ask for confirm when updating
pub fn ask_confirm(mut self, opt: bool) -> Self {
self.ask_confirm = opt;
self
}
pub fn upgrade(self) -> Result<UpdateStatus, UpdateError> {
info!("Updating termscp...");
GithubUpdater::configure()
// Set default options
.repo_owner("veeso")
.repo_name("termscp")
.bin_name("termscp")
.current_version(cargo_crate_version!())
.no_confirm(!self.ask_confirm)
.show_download_progress(self.progress)
.show_output(self.progress)
.build()?
.update()
.map(UpdateStatus::from)
}
/// ### is_new_version_available
///
/// Returns whether a new version of termscp is available
/// In case of success returns Ok(Option<Release>), 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 is_new_version_available() -> Result<Option<Release>, UpdateError> {
info!("Checking whether a new version is available...");
GithubUpdater::configure()
// Set default options
.repo_owner("veeso")
.repo_name("termscp")
.bin_name("termscp")
.current_version(cargo_crate_version!())
.no_confirm(true)
.show_download_progress(false)
.show_output(false)
.build()?
.get_latest_release()
.map(Release::from)
.map(Self::check_version)
}
/// ### check_version
///
/// In case received version is newer than current one, version as Some is returned; otherwise None
fn check_version(r: Release) -> Option<Release> {
match parse_semver(r.version.as_str()) {
Some(new_version) => {
// Check if version is different
debug!(
"New version: {}; current version: {}",
new_version,
cargo_crate_version!()
);
if new_version.as_str() > cargo_crate_version!() {
Some(r) // New version is available
} else {
None // No new version
}
}
None => None,
}
}
}
impl Default for Update {
fn default() -> Self {
Self {
progress: false,
ask_confirm: false,
}
}
}
impl From<Status> for UpdateStatus {
fn from(s: Status) -> Self {
match s {
Status::UpToDate(_) => Self::AlreadyUptodate,
Status::Updated(v) => Self::UpdateInstalled(v),
}
}
}
impl From<UpdRelease> for Release {
fn from(r: UpdRelease) -> Self {
Self {
version: r.version,
body: r.body.unwrap_or_default(),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn auto_update_default() {
let upd: Update = Update::default();
assert_eq!(upd.ask_confirm, false);
assert_eq!(upd.progress, false);
let upd = upd.ask_confirm(true).show_progress(true);
assert_eq!(upd.ask_confirm, true);
assert_eq!(upd.progress, true);
}
#[test]
#[cfg(not(all(target_os = "macos", feature = "github-actions")))]
fn auto_update() {
// Wno version
assert_eq!(
Update::default()
.show_progress(true)
.upgrade()
.ok()
.unwrap(),
UpdateStatus::AlreadyUptodate,
);
}
#[test]
#[cfg(not(all(target_os = "macos", feature = "github-actions")))]
fn check_for_updates() {
println!("{:?}", Update::is_new_version_available());
assert!(Update::is_new_version_available().is_ok());
}
#[test]
fn update_status() {
assert_eq!(
UpdateStatus::from(Status::Updated(String::from("0.6.0"))),
UpdateStatus::UpdateInstalled(String::from("0.6.0"))
);
assert_eq!(
UpdateStatus::from(Status::UpToDate(String::from("0.6.0"))),
UpdateStatus::AlreadyUptodate
);
}
#[test]
fn release() {
let release: UpdRelease = UpdRelease {
name: String::from("termscp 0.7.0"),
version: String::from("0.7.0"),
date: String::from("2021-09-12T00:00:00Z"),
body: Some(String::from("fixed everything")),
assets: vec![],
};
let release: Release = Release::from(release);
assert_eq!(release.body.as_str(), "fixed everything");
assert_eq!(release.version.as_str(), "0.7.0");
}
}

View file

@ -34,14 +34,13 @@ use crate::config::{
bookmarks::{Bookmark, UserHosts},
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
};
use crate::filetransfer::FileTransferProtocol;
use crate::filetransfer::FileTransferParams;
use crate::utils::crypto;
use crate::utils::fmt::fmt_time;
use crate::utils::random::random_alphanumeric_with_len;
// Ext
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
use std::time::SystemTime;
@ -166,59 +165,45 @@ impl BookmarksClient {
/// ### get_bookmark
///
/// Get bookmark associated to key
pub fn get_bookmark(
&self,
key: &str,
) -> Option<(String, u16, FileTransferProtocol, String, Option<String>)> {
let entry: &Bookmark = self.hosts.bookmarks.get(key)?;
pub fn get_bookmark(&self, key: &str) -> Option<FileTransferParams> {
debug!("Getting bookmark {}", key);
Some((
entry.address.clone(),
entry.port,
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
Ok(proto) => proto,
Err(err) => {
error!(
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
err
);
FileTransferProtocol::Sftp // Default
let mut entry: Bookmark = self.hosts.bookmarks.get(key).cloned()?;
// Decrypt password first
if let Some(pwd) = entry.password.as_mut() {
match self.decrypt_str(pwd.as_str()) {
Ok(decrypted_pwd) => {
*pwd = decrypted_pwd;
}
},
entry.username.clone(),
match &entry.password {
// Decrypted password if Some; if decryption fails return None
Some(pwd) => match self.decrypt_str(pwd.as_str()) {
Ok(decrypted_pwd) => Some(decrypted_pwd),
Err(err) => {
error!("Failed to decrypt password for bookmark: {}", err);
None
}
},
None => None,
},
))
Err(err) => {
error!("Failed to decrypt password for bookmark: {}", err);
}
}
}
// Then convert into
Some(FileTransferParams::from(entry))
}
/// ### add_recent
///
/// Add a new recent to bookmarks
pub fn add_bookmark(
pub fn add_bookmark<S: AsRef<str>>(
&mut self,
name: String,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
name: S,
params: FileTransferParams,
save_password: bool,
) {
let name: String = name.as_ref().to_string();
if name.is_empty() {
error!("Fatal error; bookmark name is empty");
panic!("Bookmark name can't be empty");
}
// Make bookmark
info!("Added bookmark {} with address {}", name, addr);
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, password);
info!("Added bookmark {}", name);
let mut host: Bookmark = self.make_bookmark(params);
// If not save_password, set password to `None`
if !save_password {
host.password = None;
}
self.hosts.bookmarks.insert(name, host);
}
@ -239,43 +224,25 @@ impl BookmarksClient {
/// ### get_recent
///
/// Get recent associated to key
pub fn get_recent(&self, key: &str) -> Option<(String, u16, FileTransferProtocol, String)> {
pub fn get_recent(&self, key: &str) -> Option<FileTransferParams> {
// NOTE: password is not decrypted; recents will never have password
info!("Getting bookmark {}", key);
let entry: &Bookmark = self.hosts.recents.get(key)?;
Some((
entry.address.clone(),
entry.port,
match FileTransferProtocol::from_str(entry.protocol.as_str()) {
Ok(proto) => proto,
Err(err) => {
error!(
"Found invalid protocol in bookmarks: {}; defaulting to SFTP",
err
);
FileTransferProtocol::Sftp // Default
}
},
entry.username.clone(),
))
let entry: Bookmark = self.hosts.recents.get(key).cloned()?;
Some(FileTransferParams::from(entry))
}
/// ### add_recent
///
/// Add a new recent to bookmarks
pub fn add_recent(
&mut self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
) {
pub fn add_recent(&mut self, params: FileTransferParams) {
// Make bookmark
let host: Bookmark = self.make_bookmark(addr, port, protocol, username, None);
let mut host: Bookmark = self.make_bookmark(params);
// Null password for recents
host.password = None;
// Check if duplicated
for recent_host in self.hosts.recents.values() {
if *recent_host == host {
debug!("Discarding recent since duplicated ({})", host.address);
for (key, value) in &self.hosts.recents {
if *value == host {
debug!("Discarding recent since duplicated ({})", key);
// Don't save duplicates
return;
}
@ -300,7 +267,7 @@ impl BookmarksClient {
}
}
let name: String = fmt_time(SystemTime::now(), "ISO%Y%m%dT%H%M%S");
info!("Saved recent host {} ({})", name, host.address);
info!("Saved recent host {}", name);
self.hosts.recents.insert(name, host);
}
@ -376,21 +343,13 @@ impl BookmarksClient {
/// ### make_bookmark
///
/// Make bookmark from credentials
fn make_bookmark(
&self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
) -> Bookmark {
Bookmark {
address: addr,
port,
username,
protocol: protocol.to_string(),
password: password.map(|p| self.encrypt_str(p.as_str())),
fn make_bookmark(&self, params: FileTransferParams) -> Bookmark {
let mut bookmark: Bookmark = Bookmark::from(params);
// Encrypt password
if let Some(pwd) = bookmark.password {
bookmark.password = Some(self.encrypt_str(pwd.as_str()));
}
bookmark
}
/// ### encrypt_str
@ -419,6 +378,8 @@ impl BookmarksClient {
mod tests {
use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::filetransfer::{FileTransferProtocol, ProtocolParams};
use pretty_assertions::assert_eq;
use std::thread::sleep;
@ -473,19 +434,23 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add some bookmarks
client.add_bookmark(
String::from("raspberry"),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
"raspberry",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword"),
),
true,
);
client.add_recent(
String::from("192.168.1.31"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.31",
22,
"pi",
Some("mypassword"),
));
let recent_key: String = String::from(client.iter_recents().next().unwrap());
assert!(client.write_bookmarks().is_ok());
let key: String = client.key.clone();
@ -494,19 +459,18 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Verify it loaded parameters correctly
assert_eq!(client.key, key);
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) =
client.get_bookmark(&String::from("raspberry")).unwrap();
let bookmark = ftparams_to_tup(client.get_bookmark("raspberry").unwrap());
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(*bookmark.4.as_ref().unwrap(), String::from("mypassword"));
let bookmark: (String, u16, FileTransferProtocol, String) =
client.get_recent(&recent_key).unwrap();
let bookmark = ftparams_to_tup(client.get_recent(&recent_key).unwrap());
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(bookmark.4, None);
}
#[test]
@ -519,26 +483,31 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
String::from("raspberry"),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
"raspberry",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword"),
),
true,
);
client.add_bookmark(
String::from("raspberry2"),
String::from("192.168.1.32"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword2")),
"raspberry2",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword2"),
),
true,
);
// Iter
assert_eq!(client.iter_bookmarks().count(), 2);
// Get bookmark
let bookmark: (String, u16, FileTransferProtocol, String, Option<String>) =
client.get_bookmark(&String::from("raspberry")).unwrap();
let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
@ -565,15 +534,45 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
String::from(""),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
"",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword"),
),
true,
);
}
#[test]
fn save_bookmark_wno_password() {
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
"raspberry",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword"),
),
false,
);
let bookmark = ftparams_to_tup(client.get_bookmark(&String::from("raspberry")).unwrap());
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(bookmark.4, None);
}
#[test]
fn test_system_bookmarks_manipulate_recents() {
@ -583,22 +582,23 @@ mod tests {
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_recent(
String::from("192.168.1.31"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.31",
22,
"pi",
Some("mypassword"),
));
// Iter
assert_eq!(client.iter_recents().count(), 1);
let key: String = String::from(client.iter_recents().next().unwrap());
// Get bookmark
let bookmark: (String, u16, FileTransferProtocol, String) =
client.get_recent(&key).unwrap();
let bookmark = ftparams_to_tup(client.get_recent(&key).unwrap());
assert_eq!(bookmark.0, String::from("192.168.1.31"));
assert_eq!(bookmark.1, 22);
assert_eq!(bookmark.2, FileTransferProtocol::Sftp);
assert_eq!(bookmark.3, String::from("pi"));
assert_eq!(bookmark.4, None);
// Write bookmarks
assert!(client.write_bookmarks().is_ok());
// Delete bookmark
@ -618,18 +618,20 @@ mod tests {
let mut client: BookmarksClient =
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_recent(
String::from("192.168.1.31"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
client.add_recent(
String::from("192.168.1.31"),
"192.168.1.31",
22,
"pi",
Some("mypassword"),
));
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.31",
22,
"pi",
Some("mypassword"),
));
// There should be only one recent
assert_eq!(client.iter_recents().count(), 1);
}
@ -644,39 +646,60 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 2).unwrap();
// Add recent, wait 1 second for each one (cause the name depends on time)
// 1
client.add_recent(
String::from("192.168.1.1"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.1",
22,
"pi",
Some("mypassword"),
));
sleep(Duration::from_secs(1));
// 2
client.add_recent(
String::from("192.168.1.2"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.2",
22,
"pi",
Some("mypassword"),
));
sleep(Duration::from_secs(1));
// 3
client.add_recent(
String::from("192.168.1.3"),
22,
client.add_recent(make_generic_ftparams(
FileTransferProtocol::Sftp,
String::from("pi"),
);
"192.168.1.3",
22,
"pi",
Some("mypassword"),
));
// Limit is 2
assert_eq!(client.iter_recents().count(), 2);
// Check that 192.168.1.1 has been removed
let key: String = client.iter_recents().nth(0).unwrap().to_string();
assert!(matches!(
client.hosts.recents.get(&key).unwrap().address.as_str(),
client
.hosts
.recents
.get(&key)
.unwrap()
.address
.as_ref()
.cloned()
.unwrap_or_default()
.as_str(),
"192.168.1.2" | "192.168.1.3"
));
let key: String = client.iter_recents().nth(1).unwrap().to_string();
assert!(matches!(
client.hosts.recents.get(&key).unwrap().address.as_str(),
client
.hosts
.recents
.get(&key)
.unwrap()
.address
.as_ref()
.cloned()
.unwrap_or_default()
.as_str(),
"192.168.1.2" | "192.168.1.3"
));
}
@ -691,12 +714,15 @@ mod tests {
BookmarksClient::new(cfg_path.as_path(), key_path.as_path(), 16).unwrap();
// Add bookmark
client.add_bookmark(
String::from(""),
String::from("192.168.1.31"),
22,
FileTransferProtocol::Sftp,
String::from("pi"),
Some(String::from("mypassword")),
"",
make_generic_ftparams(
FileTransferProtocol::Sftp,
"192.168.1.31",
22,
"pi",
Some("mypassword"),
),
true,
);
}
@ -724,4 +750,35 @@ mod tests {
c.push("bookmarks.toml");
(c, k)
}
fn make_generic_ftparams(
protocol: FileTransferProtocol,
address: &str,
port: u16,
username: &str,
password: Option<&str>,
) -> FileTransferParams {
let params = ProtocolParams::Generic(
GenericProtocolParams::default()
.address(address)
.port(port)
.username(Some(username))
.password(password),
);
FileTransferParams::new(protocol, params)
}
fn ftparams_to_tup(
params: FileTransferParams,
) -> (String, u16, FileTransferProtocol, String, Option<String>) {
let protocol = params.protocol;
let p = params.params.generic_params().unwrap();
(
p.address.to_string(),
p.port,
protocol,
p.username.as_ref().cloned().unwrap_or_default(),
p.password.as_ref().cloned(),
)
}
}

View file

@ -27,7 +27,7 @@
*/
// Locals
use crate::config::{
params::UserConfig,
params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD},
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
};
use crate::filetransfer::FileTransferProtocol;
@ -181,6 +181,23 @@ impl ConfigClient {
self.config.user_interface.check_for_updates = Some(value);
}
/// ### get_prompt_on_file_replace
///
/// Get value of `prompt_on_file_replace`
pub fn get_prompt_on_file_replace(&self) -> bool {
self.config
.user_interface
.prompt_on_file_replace
.unwrap_or(true)
}
/// ### set_prompt_on_file_replace
///
/// Set new value for `prompt_on_file_replace`
pub fn set_prompt_on_file_replace(&mut self, value: bool) {
self.config.user_interface.prompt_on_file_replace = Some(value);
}
/// ### get_group_dirs
///
/// Get GroupDirs value from configuration (will be converted from string)
@ -237,6 +254,37 @@ impl ConfigClient {
};
}
/// ### get_notifications
///
/// Get value of `notifications`
pub fn get_notifications(&self) -> bool {
self.config.user_interface.notifications.unwrap_or(true)
}
/// ### set_notifications
///
/// Set new value for `notifications`
pub fn set_notifications(&mut self, value: bool) {
self.config.user_interface.notifications = Some(value);
}
/// ### get_notification_threshold
///
/// Get value of `notification_threshold`
pub fn get_notification_threshold(&self) -> u64 {
self.config
.user_interface
.notification_threshold
.unwrap_or(DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD)
}
/// ### set_notification_threshold
///
/// Set new value for `notification_threshold`
pub fn set_notification_threshold(&mut self, value: u64) {
self.config.user_interface.notification_threshold = Some(value);
}
// SSH Keys
/// ### save_ssh_key
@ -580,6 +628,20 @@ mod tests {
assert_eq!(client.get_check_for_updates(), false);
}
#[test]
fn test_system_config_prompt_on_file_replace() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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_prompt_on_file_replace(), true); // Null ?
client.set_prompt_on_file_replace(true);
assert_eq!(client.get_prompt_on_file_replace(), true);
client.set_prompt_on_file_replace(false);
assert_eq!(client.get_prompt_on_file_replace(), false);
}
#[test]
fn test_system_config_group_dirs() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
@ -626,6 +688,37 @@ mod tests {
assert_eq!(client.get_remote_file_fmt(), None);
}
#[test]
fn test_system_config_notifications() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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_notifications(), true); // Null ?
client.set_notifications(true);
assert_eq!(client.get_notifications(), true);
client.set_notifications(false);
assert_eq!(client.get_notifications(), false);
}
#[test]
fn test_system_config_remote_notification_threshold() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
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_notification_threshold(),
DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD
); // Null ?
client.set_notification_threshold(1024);
assert_eq!(client.get_notification_threshold(), 1024);
client.set_notification_threshold(64);
assert_eq!(client.get_notification_threshold(), 64);
}
#[test]
fn test_system_config_ssh_keys() {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();

View file

@ -26,10 +26,12 @@
* SOFTWARE.
*/
// modules
pub mod auto_update;
pub mod bookmarks_client;
pub mod config_client;
pub mod environment;
pub(self) mod keys;
pub mod logging;
pub mod notifications;
pub mod sshkey_storage;
pub mod theme_provider;

View file

@ -0,0 +1,82 @@
//! # Notifications
//!
//! This module exposes the function to send notifications to the guest OS
#[cfg(all(unix, not(target_os = "macos")))]
use notify_rust::Hint;
use notify_rust::{Notification as OsNotification, Timeout};
/// ## Notification
///
/// A notification helper which provides all the functions to send the available notifications for termscp
pub struct Notification;
impl Notification {
/// ### transfer_completed
///
/// Notify a transfer has been completed with success
pub fn transfer_completed<S: AsRef<str>>(body: S) {
Self::notify(
"Transfer completed ✅",
body.as_ref(),
Some("transfer.complete"),
);
}
/// ### transfer_error
///
/// Notify a transfer has failed
pub fn transfer_error<S: AsRef<str>>(body: S) {
Self::notify("Transfer failed ❌", body.as_ref(), Some("transfer.error"));
}
/// ### update_available
///
/// Notify a new version of termscp is available for download
pub fn update_available<S: AsRef<str>>(version: S) {
Self::notify(
"New version available ⬇️",
format!("termscp {} is now available for download", version.as_ref()).as_str(),
None,
);
}
/// ### update_installed
///
/// Notify the update has been correctly installed
pub fn update_installed<S: AsRef<str>>(version: S) {
Self::notify(
"Update installed 🎉",
format!("termscp {} has been installed! Restart termscp to enjoy the latest version of termscp 🙂", version.as_ref()).as_str(),
None,
);
}
/// ### update_failed
///
/// Notify the update installation has failed
pub fn update_failed<S: AsRef<str>>(err: S) {
Self::notify("Update installation failed ❌", err.as_ref(), None);
}
/// ### notify
///
/// Notify guest OS with provided Summary, body and optional category
/// e.g. Category is supported on FreeBSD/Linux only
#[allow(unused_variables)]
fn notify(summary: &str, body: &str, category: Option<&str>) {
let mut notification = OsNotification::new();
// Set common params
notification
.appname(env!("CARGO_PKG_NAME"))
.summary(summary)
.body(body)
.timeout(Timeout::Milliseconds(10000));
// Set category if any
#[cfg(all(unix, not(target_os = "macos")))]
if let Some(category) = category {
notification.hint(Hint::Category(category.to_string()));
}
let _ = notification.show();
}
}

View file

@ -26,14 +26,15 @@
* SOFTWARE.
*/
// Locals
use super::{AuthActivity, FileTransferProtocol};
use super::{AuthActivity, FileTransferParams};
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::environment;
// Ext
use std::path::PathBuf;
use tui_realm_stdlib::{input::InputPropsBuilder, radio::RadioPropsBuilder};
use tuirealm::{Payload, PropsBuilder, Value};
use tui_realm_stdlib::{InputPropsBuilder, RadioPropsBuilder};
use tuirealm::PropsBuilder;
impl AuthActivity {
/// ### del_bookmark
@ -62,9 +63,7 @@ impl AuthActivity {
if let Some(key) = self.bookmarks_list.get(idx) {
if let Some(bookmark) = bookmarks_cli.get_bookmark(key) {
// Load parameters into components
self.load_bookmark_into_gui(
bookmark.0, bookmark.1, bookmark.2, bookmark.3, bookmark.4,
);
self.load_bookmark_into_gui(bookmark);
}
}
}
@ -74,20 +73,15 @@ impl AuthActivity {
///
/// Save current input fields as a bookmark
pub(super) fn save_bookmark(&mut self, name: String, save_password: bool) {
let (address, port, protocol, username, password) = self.get_input();
let params = match self.collect_host_params() {
Ok(p) => p,
Err(e) => {
self.mount_error(e);
return;
}
};
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
// Check if password must be saved
let password: Option<String> = match save_password {
true => match self
.view
.get_state(super::COMPONENT_RADIO_BOOKMARK_SAVE_PWD)
{
Some(Payload::One(Value::Usize(0))) => Some(password), // Yes
_ => None, // No such component / No
},
false => None,
};
bookmarks_cli.add_bookmark(name.clone(), address, port, protocol, username, password);
bookmarks_cli.add_bookmark(name.clone(), params, save_password);
// Save bookmarks
self.write_bookmarks();
// Remove `name` from bookmarks if exists
@ -122,9 +116,7 @@ impl AuthActivity {
if let Some(key) = self.recents_list.get(idx) {
if let Some(bookmark) = client.get_recent(key) {
// Load parameters
self.load_bookmark_into_gui(
bookmark.0, bookmark.1, bookmark.2, bookmark.3, None,
);
self.load_bookmark_into_gui(bookmark);
}
}
}
@ -134,9 +126,15 @@ impl AuthActivity {
///
/// Save current input fields as a "recent"
pub(super) fn save_recent(&mut self) {
let (address, port, protocol, username, _password) = self.get_input();
let params = match self.collect_host_params() {
Ok(p) => p,
Err(e) => {
self.mount_error(e);
return;
}
};
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
bookmarks_cli.add_recent(address, port, protocol, username);
bookmarks_cli.add_recent(params);
// Save bookmarks
self.write_bookmarks();
}
@ -234,40 +232,66 @@ impl AuthActivity {
/// ### load_bookmark_into_gui
///
/// Load bookmark data into the gui components
fn load_bookmark_into_gui(
&mut self,
addr: String,
port: u16,
protocol: FileTransferProtocol,
username: String,
password: Option<String>,
) {
fn load_bookmark_into_gui(&mut self, bookmark: FileTransferParams) {
// Load parameters into components
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = RadioPropsBuilder::from(props)
.with_value(Self::protocol_enum_to_opt(bookmark.protocol))
.build();
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
}
match bookmark.params {
ProtocolParams::AwsS3(params) => self.load_bookmark_s3_into_gui(params),
ProtocolParams::Generic(params) => self.load_bookmark_generic_into_gui(params),
}
}
fn load_bookmark_generic_into_gui(&mut self, params: GenericProtocolParams) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_ADDR) {
let props = InputPropsBuilder::from(props).with_value(addr).build();
let props = InputPropsBuilder::from(props)
.with_value(params.address.clone())
.build();
self.view.update(super::COMPONENT_INPUT_ADDR, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PORT) {
let props = InputPropsBuilder::from(props)
.with_value(port.to_string())
.with_value(params.port.to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PORT, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_PROTOCOL) {
let props = RadioPropsBuilder::from(props)
.with_value(Self::protocol_enum_to_opt(protocol))
.build();
self.view.update(super::COMPONENT_RADIO_PROTOCOL, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_USERNAME) {
let props = InputPropsBuilder::from(props).with_value(username).build();
let props = InputPropsBuilder::from(props)
.with_value(params.username.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_USERNAME, props);
}
if let Some(password) = password {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = InputPropsBuilder::from(props).with_value(password).build();
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_PASSWORD) {
let props = InputPropsBuilder::from(props)
.with_value(params.password.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_PASSWORD, props);
}
}
fn load_bookmark_s3_into_gui(&mut self, params: AwsS3Params) {
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_BUCKET) {
let props = InputPropsBuilder::from(props)
.with_value(params.bucket_name.clone())
.build();
self.view.update(super::COMPONENT_INPUT_S3_BUCKET, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_REGION) {
let props = InputPropsBuilder::from(props)
.with_value(params.region.clone())
.build();
self.view.update(super::COMPONENT_INPUT_S3_REGION, props);
}
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_S3_PROFILE) {
let props = InputPropsBuilder::from(props)
.with_value(params.profile.as_deref().unwrap_or_default().to_string())
.build();
self.view.update(super::COMPONENT_INPUT_S3_PROFILE, props);
}
}
}

View file

@ -26,6 +26,9 @@
* SOFTWARE.
*/
use super::{AuthActivity, FileTransferParams, FileTransferProtocol};
use crate::filetransfer::params::{AwsS3Params, GenericProtocolParams, ProtocolParams};
use crate::system::auto_update::{Release, Update, UpdateStatus};
use crate::system::notifications::Notification;
impl AuthActivity {
/// ### protocol_opt_to_enum
@ -36,6 +39,7 @@ impl AuthActivity {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp,
}
}
@ -49,6 +53,7 @@ impl AuthActivity {
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
}
}
@ -59,6 +64,7 @@ impl AuthActivity {
match protocol {
FileTransferProtocol::Sftp | FileTransferProtocol::Scp => 22,
FileTransferProtocol::Ftp(_) => 21,
FileTransferProtocol::AwsS3 => 22, // Doesn't matter, since not used
}
}
@ -83,15 +89,24 @@ impl AuthActivity {
/// ### collect_host_params
///
/// Get input values from fields or return an error if fields are invalid
/// Collect host params as `FileTransferParams`
pub(super) fn collect_host_params(&self) -> Result<FileTransferParams, &'static str> {
let (address, port, protocol, username, password): (
String,
u16,
FileTransferProtocol,
String,
String,
) = self.get_input();
let protocol: FileTransferProtocol = self.get_protocol();
match protocol {
FileTransferProtocol::AwsS3 => self.collect_s3_host_params(protocol),
protocol => self.collect_generic_host_params(protocol),
}
}
/// ### collect_generic_host_params
///
/// Get input values from fields or return an error if fields are invalid to work as generic
pub(super) fn collect_generic_host_params(
&self,
protocol: FileTransferProtocol,
) -> Result<FileTransferParams, &'static str> {
let (address, port, username, password): (String, u16, String, String) =
self.get_generic_params_input();
if address.is_empty() {
return Err("Invalid host");
}
@ -99,18 +114,122 @@ impl AuthActivity {
return Err("Invalid port");
}
Ok(FileTransferParams {
address,
port,
protocol,
username: match username.is_empty() {
true => None,
false => Some(username),
},
password: match password.is_empty() {
true => None,
false => Some(password),
},
params: ProtocolParams::Generic(
GenericProtocolParams::default()
.address(address)
.port(port)
.username(match username.is_empty() {
true => None,
false => Some(username),
})
.password(match password.is_empty() {
true => None,
false => Some(password),
}),
),
entry_directory: None,
})
}
/// ### collect_s3_host_params
///
/// Get input values from fields or return an error if fields are invalid to work as aws s3
pub(super) fn collect_s3_host_params(
&self,
protocol: FileTransferProtocol,
) -> Result<FileTransferParams, &'static str> {
let (bucket, region, profile): (String, String, Option<String>) =
self.get_s3_params_input();
if bucket.is_empty() {
return Err("Invalid bucket");
}
if region.is_empty() {
return Err("Invalid region");
}
Ok(FileTransferParams {
protocol,
params: ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, profile)),
entry_directory: None,
})
}
// -- update install
/// ### check_for_updates
///
/// If enabled in configuration, check for updates from Github
pub(super) fn check_for_updates(&mut self) {
debug!("Check for updates...");
// Check version only if unset in the store
let ctx = self.context_mut();
if !ctx.store().isset(super::STORE_KEY_LATEST_VERSION) {
debug!("Version is not set in storage");
if ctx.config().get_check_for_updates() {
debug!("Check for updates is enabled");
// Send request
match Update::is_new_version_available() {
Ok(Some(Release { version, body })) => {
// If some, store version and release notes
info!("Latest version is: {}", version);
if ctx.config().get_notifications() {
// Notify new version available
Notification::update_available(version.as_str());
}
// Store info
ctx.store_mut()
.set_string(super::STORE_KEY_LATEST_VERSION, version);
ctx.store_mut()
.set_string(super::STORE_KEY_RELEASE_NOTES, body);
}
Ok(None) => {
info!("Latest version is: {} (current)", env!("CARGO_PKG_VERSION"));
// Just set flag as check
ctx.store_mut().set(super::STORE_KEY_LATEST_VERSION);
}
Err(err) => {
// Report error
error!("Failed to get latest version: {}", err);
self.mount_error(
format!("Could not check for new updates: {}", err).as_str(),
);
}
}
} else {
info!("Check for updates is disabled");
}
}
}
/// ### install_update
///
/// Install latest termscp version via GUI
pub(super) fn install_update(&mut self) {
// Umount release notes
self.umount_release_notes();
// Mount wait box
self.mount_wait("Installing update. Please wait…");
// Refresh UI
self.view();
// Install update
let result = Update::default().show_progress(false).upgrade();
// Umount wait
self.umount_wait();
// Show outcome
match result {
Ok(UpdateStatus::AlreadyUptodate) => self.mount_info("termscp is already up to date!"),
Ok(UpdateStatus::UpdateInstalled(ver)) => {
if self.config().get_notifications() {
Notification::update_installed(ver.as_str());
}
self.mount_info(format!("termscp has been updated to version {}!", ver))
}
Err(err) => {
if self.config().get_notifications() {
Notification::update_failed(err.to_string());
}
self.mount_error(format!("Could not install update: {}", err))
}
}
}
}

View file

@ -36,7 +36,7 @@ use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
use crate::system::bookmarks_client::BookmarksClient;
use crate::utils::git;
use crate::system::config_client::ConfigClient;
// Includes
use crossterm::event::Event;
@ -51,17 +51,23 @@ const COMPONENT_TEXT_NEW_VERSION_NOTES: &str = "TEXTAREA_NEW_VERSION";
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
const COMPONENT_TEXT_INFO: &str = "TEXT_INFO";
const COMPONENT_TEXT_WAIT: &str = "TEXT_WAIT";
const COMPONENT_TEXT_SIZE_ERR: &str = "TEXT_SIZE_ERR";
const COMPONENT_INPUT_ADDR: &str = "INPUT_ADDRESS";
const COMPONENT_INPUT_PORT: &str = "INPUT_PORT";
const COMPONENT_INPUT_USERNAME: &str = "INPUT_USERNAME";
const COMPONENT_INPUT_PASSWORD: &str = "INPUT_PASSWORD";
const COMPONENT_INPUT_BOOKMARK_NAME: &str = "INPUT_BOOKMARK_NAME";
const COMPONENT_INPUT_S3_BUCKET: &str = "INPUT_S3_BUCKET";
const COMPONENT_INPUT_S3_REGION: &str = "INPUT_S3_REGION";
const COMPONENT_INPUT_S3_PROFILE: &str = "INPUT_S3_PROFILE";
const COMPONENT_RADIO_PROTOCOL: &str = "RADIO_PROTOCOL";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK: &str = "RADIO_DELETE_BOOKMARK";
const COMPONENT_RADIO_BOOKMARK_DEL_RECENT: &str = "RADIO_DELETE_RECENT";
const COMPONENT_RADIO_BOOKMARK_SAVE_PWD: &str = "RADIO_SAVE_PASSWORD";
const COMPONENT_RADIO_INSTALL_UPDATE: &str = "RADIO_INSTALL_UPDATE";
const COMPONENT_BOOKMARKS_LIST: &str = "BOOKMARKS_LIST";
const COMPONENT_RECENTS_LIST: &str = "RECENTS_LIST";
@ -104,45 +110,6 @@ impl AuthActivity {
}
}
/// ### on_create
///
/// If enabled in configuration, check for updates from Github
fn check_for_updates(&mut self) {
debug!("Check for updates...");
// Check version only if unset in the store
let ctx: &mut Context = self.context_mut();
if !ctx.store().isset(STORE_KEY_LATEST_VERSION) {
debug!("Version is not set in storage");
if ctx.config().get_check_for_updates() {
debug!("Check for updates is enabled");
// Send request
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
Ok(Some(git::GithubTag { tag_name, body })) => {
// If some, store version and release notes
info!("Latest version is: {}", tag_name);
ctx.store_mut()
.set_string(STORE_KEY_LATEST_VERSION, tag_name);
ctx.store_mut().set_string(STORE_KEY_RELEASE_NOTES, body);
}
Ok(None) => {
info!("Latest version is: {} (current)", env!("CARGO_PKG_VERSION"));
// Just set flag as check
ctx.store_mut().set(STORE_KEY_LATEST_VERSION);
}
Err(err) => {
// Report error
error!("Failed to get latest version: {}", err);
self.mount_error(
format!("Could not check for new updates: {}", err).as_str(),
);
}
}
} else {
info!("Check for updates is disabled");
}
}
}
/// ### context
///
/// Returns a reference to context
@ -157,12 +124,29 @@ impl AuthActivity {
self.context.as_mut().unwrap()
}
/// ### config
///
/// Returns config client reference
fn config(&self) -> &ConfigClient {
self.context().config()
}
/// ### theme
///
/// Returns a reference to theme
fn theme(&self) -> &Theme {
self.context().theme_provider().theme()
}
/// ### input_mask
///
/// Get current input mask to show
fn input_mask(&self) -> InputMask {
match self.get_protocol() {
FileTransferProtocol::AwsS3 => InputMask::AwsS3,
_ => InputMask::Generic,
}
}
}
impl Activity for AuthActivity {
@ -261,3 +245,12 @@ impl Activity for AuthActivity {
}
}
}
/// ## InputMask
///
/// Auth form input mask
#[derive(Eq, PartialEq)]
enum InputMask {
Generic,
AwsS3,
}

View file

@ -27,12 +27,14 @@
*/
// locals
use super::{
AuthActivity, FileTransferProtocol, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
AuthActivity, FileTransferProtocol, InputMask, COMPONENT_BOOKMARKS_LIST, COMPONENT_INPUT_ADDR,
COMPONENT_INPUT_BOOKMARK_NAME, COMPONENT_INPUT_PASSWORD, COMPONENT_INPUT_PORT,
COMPONENT_INPUT_S3_BUCKET, COMPONENT_INPUT_S3_PROFILE, COMPONENT_INPUT_S3_REGION,
COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR,
COMPONENT_TEXT_HELP, COMPONENT_TEXT_NEW_VERSION_NOTES, COMPONENT_TEXT_SIZE_ERR,
COMPONENT_RADIO_INSTALL_UPDATE, COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT,
COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR, COMPONENT_TEXT_HELP, COMPONENT_TEXT_INFO,
COMPONENT_TEXT_NEW_VERSION_NOTES, COMPONENT_TEXT_SIZE_ERR, COMPONENT_TEXT_WAIT,
};
use crate::ui::keymap::*;
use tui_realm_stdlib::InputPropsBuilder;
@ -53,54 +55,80 @@ impl Update for AuthActivity {
Some(msg) => match msg {
// Focus ( DOWN )
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_ADDR);
// Give focus based on current mask
match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_ADDR),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_BUCKET),
};
None
}
// -- generic mask (DOWN)
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PASSWORD);
None
}
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_DOWN => {
// Give focus to port
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// -- s3 mask (DOWN)
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_S3_REGION);
None
}
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_S3_PROFILE);
None
}
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Focus ( UP )
// -- generic (UP)
(COMPONENT_INPUT_PASSWORD, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_USERNAME);
None
}
(COMPONENT_INPUT_USERNAME, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PORT);
None
}
(COMPONENT_INPUT_PORT, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_ADDR);
None
}
(COMPONENT_INPUT_ADDR, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// -- s3 (UP)
(COMPONENT_INPUT_S3_BUCKET, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
(COMPONENT_INPUT_S3_REGION, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_S3_BUCKET);
None
}
(COMPONENT_INPUT_S3_PROFILE, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_S3_REGION);
None
}
(COMPONENT_RADIO_PROTOCOL, key) if key == &MSG_KEY_UP => {
// Give focus to port
self.view.active(COMPONENT_INPUT_PASSWORD);
// Give focus based on current mask
match self.input_mask() {
InputMask::Generic => self.view.active(COMPONENT_INPUT_PASSWORD),
InputMask::AwsS3 => self.view.active(COMPONENT_INPUT_S3_PROFILE),
};
None
}
// Protocol - On Change
@ -144,14 +172,20 @@ impl Update for AuthActivity {
// Enter
(COMPONENT_BOOKMARKS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_bookmark(*idx);
// Give focus to input password
self.view.active(COMPONENT_INPUT_PASSWORD);
// Give focus to input password (or to protocol if not generic)
self.view.active(match self.input_mask() {
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
});
None
}
(COMPONENT_RECENTS_LIST, Msg::OnSubmit(Payload::One(Value::Usize(idx)))) => {
self.load_recent(*idx);
// Give focus to input password
self.view.active(COMPONENT_INPUT_PASSWORD);
self.view.active(match self.input_mask() {
InputMask::Generic => COMPONENT_INPUT_PASSWORD,
InputMask::AwsS3 => COMPONENT_INPUT_S3_BUCKET,
});
None
}
// Bookmark radio
@ -219,15 +253,44 @@ impl Update for AuthActivity {
self.umount_error();
None
}
(COMPONENT_TEXT_ERROR, _) => None,
(COMPONENT_TEXT_NEW_VERSION_NOTES, key)
if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER =>
{
// -- Text info
(COMPONENT_TEXT_INFO, key) if key == &MSG_KEY_ESC || key == &MSG_KEY_ENTER => {
// Umount text info
self.umount_info();
None
}
(COMPONENT_TEXT_ERROR, _) | (COMPONENT_TEXT_INFO, _) => None,
// -- Text wait
(COMPONENT_TEXT_WAIT, _) => None,
// -- Release notes
(COMPONENT_TEXT_NEW_VERSION_NOTES, key) if key == &MSG_KEY_ESC => {
// Umount release notes
self.umount_release_notes();
None
}
(COMPONENT_TEXT_NEW_VERSION_NOTES, key) if key == &MSG_KEY_TAB => {
// Focus to radio update
self.view.active(COMPONENT_RADIO_INSTALL_UPDATE);
None
}
(COMPONENT_TEXT_NEW_VERSION_NOTES, _) => None,
// -- Install update radio
(COMPONENT_RADIO_INSTALL_UPDATE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Install update
self.install_update();
None
}
(COMPONENT_RADIO_INSTALL_UPDATE, Msg::OnSubmit(Payload::One(Value::Usize(1)))) => {
// Umount
self.umount_release_notes();
None
}
(COMPONENT_RADIO_INSTALL_UPDATE, key) if key == &MSG_KEY_TAB => {
// Focus to changelog
self.view.active(COMPONENT_TEXT_NEW_VERSION_NOTES);
None
}
(COMPONENT_RADIO_INSTALL_UPDATE, _) => None,
// Help
(_, key) if key == &MSG_KEY_CTRL_H => {
// Show help
@ -320,7 +383,7 @@ impl Update for AuthActivity {
if key == &MSG_KEY_TAB =>
{
// Give focus to address
self.view.active(COMPONENT_INPUT_ADDR);
self.view.active(COMPONENT_RADIO_PROTOCOL);
None
}
// Any <TAB>, go to bookmarks

View file

@ -26,18 +26,16 @@
* SOFTWARE.
*/
// Locals
use super::{AuthActivity, Context, FileTransferProtocol};
use super::{AuthActivity, Context, FileTransferProtocol, InputMask};
use crate::filetransfer::params::ProtocolParams;
use crate::filetransfer::FileTransferParams;
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
use crate::utils::ui::draw_area_in;
// Ext
use tui_realm_stdlib::{
input::{Input, InputPropsBuilder},
label::{Label, LabelPropsBuilder},
list::{List, ListPropsBuilder},
paragraph::{Paragraph, ParagraphPropsBuilder},
radio::{Radio, RadioPropsBuilder},
span::{Span, SpanPropsBuilder},
textarea::{Textarea, TextareaPropsBuilder},
Input, InputPropsBuilder, Label, LabelPropsBuilder, List, ListPropsBuilder, Paragraph,
ParagraphPropsBuilder, Radio, RadioPropsBuilder, Span, SpanPropsBuilder, Textarea,
TextareaPropsBuilder,
};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
@ -101,67 +99,63 @@ impl AuthActivity {
// Get default protocol
let default_protocol: FileTransferProtocol = self.context().config().get_default_protocol();
// Protocol
self.view.mount(
self.mount_radio(
super::COMPONENT_RADIO_PROTOCOL,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(protocol_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, protocol_color)
.with_title("Protocol", Alignment::Left)
.with_options(&["SFTP", "SCP", "FTP", "FTPS"])
.with_value(Self::protocol_enum_to_opt(default_protocol))
.rewind(true)
.build(),
)),
"Protocol",
&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"],
Self::protocol_enum_to_opt(default_protocol),
protocol_color,
);
// Address
self.view.mount(
self.mount_input(
super::COMPONENT_INPUT_ADDR,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(addr_color)
.with_borders(Borders::ALL, BorderType::Rounded, addr_color)
.with_label("Remote host", Alignment::Left)
.build(),
)),
"Remote host",
addr_color,
InputType::Text,
);
// Port
self.view.mount(
self.mount_input_ex(
super::COMPONENT_INPUT_PORT,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(port_color)
.with_borders(Borders::ALL, BorderType::Rounded, port_color)
.with_label("Port number", Alignment::Left)
.with_input(InputType::Number)
.with_input_len(5)
.with_value(Self::get_default_port_for_protocol(default_protocol).to_string())
.build(),
)),
"Port number",
port_color,
InputType::Number,
Some(5),
Some(Self::get_default_port_for_protocol(default_protocol).to_string()),
);
// Username
self.view.mount(
self.mount_input(
super::COMPONENT_INPUT_USERNAME,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(username_color)
.with_borders(Borders::ALL, BorderType::Rounded, username_color)
.with_label("Username", Alignment::Left)
.build(),
)),
"Username",
username_color,
InputType::Text,
);
// Password
self.view.mount(
self.mount_input(
super::COMPONENT_INPUT_PASSWORD,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(password_color)
.with_borders(Borders::ALL, BorderType::Rounded, password_color)
.with_label("Password", Alignment::Left)
.with_input(InputType::Password)
.build(),
)),
"Password",
password_color,
InputType::Password,
);
// Bucket
self.mount_input(
super::COMPONENT_INPUT_S3_BUCKET,
"Bucket name",
addr_color,
InputType::Text,
);
// Region
self.mount_input(
super::COMPONENT_INPUT_S3_REGION,
"Region",
port_color,
InputType::Text,
);
// Profile
self.mount_input(
super::COMPONENT_INPUT_S3_PROFILE,
"Profile",
username_color,
InputType::Text,
);
// Version notice
if let Some(version) = self
@ -178,7 +172,7 @@ impl AuthActivity {
.with_spans(vec![
TextSpan::from("termscp "),
TextSpan::new(version.as_str()).underlined().bold(),
TextSpan::from(" is NOW available! Get it from <https://veeso.github.io/termscp/>; view release notes with <CTRL+R>"),
TextSpan::from(" is NOW available! Install update and view release notes with <CTRL+R>"),
])
.build(),
)),
@ -240,20 +234,43 @@ impl AuthActivity {
let auth_chunks = Layout::default()
.constraints(
[
Constraint::Length(1), // h1
Constraint::Length(1), // h2
Constraint::Length(1), // Version
Constraint::Length(3), // protocol
Constraint::Length(3), // host
Constraint::Length(3), // port
Constraint::Length(3), // username
Constraint::Length(3), // password
Constraint::Length(3), // footer
Constraint::Length(1), // h1
Constraint::Length(1), // h2
Constraint::Length(1), // Version
Constraint::Length(3), // protocol
Constraint::Length(self.input_mask_size()), // Input mask
Constraint::Length(3), // footer
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(chunks[0]);
// Input mask chunks
let input_mask = match self.input_mask() {
InputMask::AwsS3 => Layout::default()
.constraints(
[
Constraint::Length(3), // bucket
Constraint::Length(3), // region
Constraint::Length(3), // profile
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
InputMask::Generic => Layout::default()
.constraints(
[
Constraint::Length(3), // host
Constraint::Length(3), // port
Constraint::Length(3), // username
Constraint::Length(3), // password
]
.as_ref(),
)
.direction(Direction::Vertical)
.split(auth_chunks[4]),
};
// Create bookmark chunks
let bookmark_chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
@ -269,16 +286,29 @@ impl AuthActivity {
.render(super::COMPONENT_TEXT_NEW_VERSION, f, auth_chunks[2]);
self.view
.render(super::COMPONENT_RADIO_PROTOCOL, f, auth_chunks[3]);
// Render input mask
match self.input_mask() {
InputMask::AwsS3 => {
self.view
.render(super::COMPONENT_INPUT_S3_BUCKET, f, input_mask[0]);
self.view
.render(super::COMPONENT_INPUT_S3_REGION, f, input_mask[1]);
self.view
.render(super::COMPONENT_INPUT_S3_PROFILE, f, input_mask[2]);
}
InputMask::Generic => {
self.view
.render(super::COMPONENT_INPUT_ADDR, f, input_mask[0]);
self.view
.render(super::COMPONENT_INPUT_PORT, f, input_mask[1]);
self.view
.render(super::COMPONENT_INPUT_USERNAME, f, input_mask[2]);
self.view
.render(super::COMPONENT_INPUT_PASSWORD, f, input_mask[3]);
}
}
self.view
.render(super::COMPONENT_INPUT_ADDR, f, auth_chunks[4]);
self.view
.render(super::COMPONENT_INPUT_PORT, f, auth_chunks[5]);
self.view
.render(super::COMPONENT_INPUT_USERNAME, f, auth_chunks[6]);
self.view
.render(super::COMPONENT_INPUT_PASSWORD, f, auth_chunks[7]);
self.view
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[8]);
.render(super::COMPONENT_TEXT_FOOTER, f, auth_chunks[5]);
// Bookmark chunks
self.view
.render(super::COMPONENT_BOOKMARKS_LIST, f, bookmark_chunks[0]);
@ -293,6 +323,22 @@ impl AuthActivity {
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_INFO) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_INFO, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_WAIT) {
if props.visible {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_WAIT, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_SIZE_ERR) {
if props.visible {
let popup = draw_area_in(f.size(), 80, 20);
@ -336,10 +382,22 @@ impl AuthActivity {
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_NEW_VERSION_NOTES) {
if props.visible {
// make popup
let popup = draw_area_in(f.size(), 90, 90);
let popup = draw_area_in(f.size(), 90, 85);
f.render_widget(Clear, popup);
let popup_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(90), // Notes
Constraint::Length(3), // Install radio
]
.as_ref(),
)
.split(popup);
self.view
.render(super::COMPONENT_TEXT_NEW_VERSION_NOTES, f, popup);
.render(super::COMPONENT_TEXT_NEW_VERSION_NOTES, f, popup_chunks[0]);
self.view
.render(super::COMPONENT_RADIO_INSTALL_UPDATE, f, popup_chunks[1]);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_HELP) {
@ -388,19 +446,13 @@ impl AuthActivity {
.bookmarks_list
.iter()
.map(|x| {
let entry: (String, u16, FileTransferProtocol, String, _) = self
.bookmarks_client
.as_ref()
.unwrap()
.get_bookmark(x)
.unwrap();
format!(
"{} ({}://{}@{}:{})",
Self::fmt_bookmark(
x,
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
self.bookmarks_client
.as_ref()
.unwrap()
.get_bookmark(x)
.unwrap(),
)
})
.collect();
@ -426,19 +478,12 @@ impl AuthActivity {
.recents_list
.iter()
.map(|x| {
let entry: (String, u16, FileTransferProtocol, String) = self
.bookmarks_client
.as_ref()
.unwrap()
.get_recent(x)
.unwrap();
format!(
"{}://{}@{}:{}",
entry.2.to_string().to_lowercase(),
entry.3,
entry.0,
entry.1
Self::fmt_recent(
self.bookmarks_client
.as_ref()
.unwrap()
.get_recent(x)
.unwrap(),
)
})
.collect();
@ -461,23 +506,9 @@ impl AuthActivity {
/// ### mount_error
///
/// Mount error box
pub(super) fn mount_error(&mut self, text: &str) {
// Mount
pub(super) fn mount_error<S: AsRef<str>>(&mut self, text: S) {
let err_color = self.theme().misc_error_dialog;
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_foreground(err_color)
.with_borders(Borders::ALL, BorderType::Thick, err_color)
.bold()
.with_text_alignment(Alignment::Center)
.with_texts(vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(super::COMPONENT_TEXT_ERROR);
self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text.as_ref(), err_color);
}
/// ### umount_error
@ -487,28 +518,47 @@ impl AuthActivity {
self.view.umount(super::COMPONENT_TEXT_ERROR);
}
/// ### mount_info
///
/// Mount info box
pub(super) fn mount_info<S: AsRef<str>>(&mut self, text: S) {
let color = self.theme().misc_info_dialog;
self.mount_text_dialog(super::COMPONENT_TEXT_INFO, text.as_ref(), color);
}
/// ### umount_info
///
/// Umount info message
pub(super) fn umount_info(&mut self) {
self.view.umount(super::COMPONENT_TEXT_INFO);
}
/// ### mount_error
///
/// Mount wait box
pub(super) fn mount_wait(&mut self, text: &str) {
let wait_color = self.theme().misc_info_dialog;
self.mount_text_dialog(super::COMPONENT_TEXT_WAIT, text, wait_color);
}
/// ### umount_wait
///
/// Umount wait message
pub(super) fn umount_wait(&mut self) {
self.view.umount(super::COMPONENT_TEXT_WAIT);
}
/// ### mount_size_err
///
/// Mount size error
pub(super) fn mount_size_err(&mut self) {
// Mount
let err_color = self.theme().misc_error_dialog;
self.view.mount(
self.mount_text_dialog(
super::COMPONENT_TEXT_SIZE_ERR,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_foreground(err_color)
.with_borders(Borders::ALL, BorderType::Thick, err_color)
.bold()
.with_texts(vec![TextSpan::from(
"termscp requires at least 24 lines of height to run",
)])
.with_text_alignment(Alignment::Center)
.build(),
)),
"termscp requires at least 24 lines of height to run",
err_color,
);
// Give focus to error
self.view.active(super::COMPONENT_TEXT_SIZE_ERR);
}
/// ### umount_size_err
@ -524,20 +574,13 @@ impl AuthActivity {
pub(super) fn mount_quit(&mut self) {
// Protocol
let quit_color = self.theme().misc_quit_dialog;
self.view.mount(
self.mount_radio_dialog(
super::COMPONENT_RADIO_QUIT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(quit_color)
.with_borders(Borders::ALL, BorderType::Rounded, quit_color)
.with_inverted_color(Color::Black)
.with_title("Quit termscp?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.rewind(true)
.build(),
)),
"Quit termscp?",
&["Yes", "No"],
0,
quit_color,
);
self.view.active(super::COMPONENT_RADIO_QUIT);
}
/// ### umount_quit
@ -552,23 +595,13 @@ impl AuthActivity {
/// Mount bookmark delete dialog
pub(super) fn mount_bookmark_del_dialog(&mut self) {
let warn_color = self.theme().misc_warn_dialog;
self.view.mount(
self.mount_radio_dialog(
super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(warn_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, warn_color)
.with_title("Delete bookmark?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.with_value(1)
.rewind(true)
.build(),
)),
"Delete bookmark?",
&["Yes", "No"],
1,
warn_color,
);
// Active
self.view
.active(super::COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK);
}
/// ### umount_bookmark_del_dialog
@ -584,22 +617,13 @@ impl AuthActivity {
/// Mount recent delete dialog
pub(super) fn mount_recent_del_dialog(&mut self) {
let warn_color = self.theme().misc_warn_dialog;
self.view.mount(
self.mount_radio_dialog(
super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(warn_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, warn_color)
.with_title("Delete bookmark?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.with_value(1)
.rewind(true)
.build(),
)),
"Delete bookmark?",
&["Yes", "No"],
1,
warn_color,
);
// Active
self.view.active(super::COMPONENT_RADIO_BOOKMARK_DEL_RECENT);
}
/// ### umount_recent_del_dialog
@ -721,17 +745,25 @@ impl AuthActivity {
if let Some(release_notes) = ctx.store().get_string(super::STORE_KEY_RELEASE_NOTES) {
// make spans
let spans: Vec<TextSpan> = release_notes.lines().map(TextSpan::from).collect();
let info_color = self.theme().misc_info_dialog;
self.view.mount(
super::COMPONENT_TEXT_NEW_VERSION_NOTES,
Box::new(Textarea::new(
TextareaPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_borders(Borders::ALL, BorderType::Rounded, info_color)
.with_title("Release notes", Alignment::Center)
.with_texts(spans)
.build(),
)),
);
self.view.active(super::COMPONENT_TEXT_NEW_VERSION_NOTES);
// Mount install popup
self.mount_radio_dialog(
super::COMPONENT_RADIO_INSTALL_UPDATE,
"Install new version?",
&["Yes", "No"],
0,
info_color,
);
}
}
}
@ -741,18 +773,35 @@ impl AuthActivity {
/// Umount release notes text area
pub(super) fn umount_release_notes(&mut self) {
self.view.umount(super::COMPONENT_TEXT_NEW_VERSION_NOTES);
self.view.umount(super::COMPONENT_RADIO_INSTALL_UPDATE);
}
/// ### get_input
/// ### get_protocol
///
/// Get protocol from view
pub(super) fn get_protocol(&self) -> FileTransferProtocol {
self.get_input_protocol()
}
/// ### get_generic_params
///
/// Collect input values from view
pub(super) fn get_input(&self) -> (String, u16, FileTransferProtocol, String, String) {
pub(super) fn get_generic_params_input(&self) -> (String, u16, String, String) {
let addr: String = self.get_input_addr();
let port: u16 = self.get_input_port();
let protocol: FileTransferProtocol = self.get_input_protocol();
let username: String = self.get_input_username();
let password: String = self.get_input_password();
(addr, port, protocol, username, password)
(addr, port, username, password)
}
/// ### get_s3_params_input
///
/// Collect s3 input values from view
pub(super) fn get_s3_params_input(&self) -> (String, String, Option<String>) {
let bucket: String = self.get_input_s3_bucket();
let region: String = self.get_input_s3_region();
let profile: Option<String> = self.get_input_s3_profile();
(bucket, region, profile)
}
pub(super) fn get_input_addr(&self) -> String {
@ -792,4 +841,166 @@ impl AuthActivity {
_ => String::new(),
}
}
pub(super) fn get_input_s3_bucket(&self) -> String {
match self.view.get_state(super::COMPONENT_INPUT_S3_BUCKET) {
Some(Payload::One(Value::Str(x))) => x,
_ => String::new(),
}
}
pub(super) fn get_input_s3_region(&self) -> String {
match self.view.get_state(super::COMPONENT_INPUT_S3_REGION) {
Some(Payload::One(Value::Str(x))) => x,
_ => String::new(),
}
}
pub(super) fn get_input_s3_profile(&self) -> Option<String> {
match self.view.get_state(super::COMPONENT_INPUT_S3_PROFILE) {
Some(Payload::One(Value::Str(x))) => match x.is_empty() {
true => None,
false => Some(x),
},
_ => None,
}
}
/// ### input_mask_size
///
/// Returns the input mask size based on current input mask
pub(super) fn input_mask_size(&self) -> u16 {
match self.input_mask() {
InputMask::AwsS3 => 9,
InputMask::Generic => 12,
}
}
/// ### fmt_bookmark
///
/// Format bookmark to display on ui
fn fmt_bookmark(name: &str, b: FileTransferParams) -> String {
let addr: String = Self::fmt_recent(b);
format!("{} ({})", name, addr)
}
/// ### fmt_recent
///
/// Format recent connection to display on ui
fn fmt_recent(b: FileTransferParams) -> String {
let protocol: String = b.protocol.to_string().to_lowercase();
match b.params {
ProtocolParams::AwsS3(s3) => {
let profile: String = match s3.profile {
Some(p) => format!("[{}]", p),
None => String::default(),
};
format!(
"{}://{} ({}) {}",
protocol, s3.bucket_name, s3.region, profile
)
}
ProtocolParams::Generic(params) => {
let username: String = match params.username {
None => String::default(),
Some(u) => format!("{}@", u),
};
format!(
"{}://{}{}:{}",
protocol, username, params.address, params.port
)
}
}
}
// -- mount helpers
fn mount_text_dialog(&mut self, id: &str, text: &str, color: Color) {
// Mount
self.view.mount(
id,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Thick, color)
.with_foreground(color)
.bold()
.with_text_alignment(Alignment::Center)
.with_texts(vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(id);
}
fn mount_radio_dialog(
&mut self,
id: &str,
text: &str,
opts: &[&str],
default: usize,
color: Color,
) {
self.view.mount(
id,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, color)
.with_title(text, Alignment::Center)
.with_options(opts)
.with_value(default)
.rewind(true)
.build(),
)),
);
// Active
self.view.active(id);
}
fn mount_radio(&mut self, id: &str, text: &str, opts: &[&str], default: usize, color: Color) {
self.view.mount(
id,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, color)
.with_title(text, Alignment::Left)
.with_options(opts)
.with_value(default)
.rewind(true)
.build(),
)),
);
}
fn mount_input(&mut self, id: &str, label: &str, fg: Color, typ: InputType) {
self.mount_input_ex(id, label, fg, typ, None, None);
}
fn mount_input_ex(
&mut self,
id: &str,
label: &str,
fg: Color,
typ: InputType,
len: Option<usize>,
value: Option<String>,
) {
let mut props = InputPropsBuilder::default();
props
.with_foreground(fg)
.with_borders(Borders::ALL, BorderType::Rounded, fg)
.with_label(label, Alignment::Left)
.with_input(typ);
if let Some(len) = len {
props.with_input_len(len);
}
if let Some(value) = value {
props.with_value(value);
}
self.view.mount(id, Box::new(Input::new(props.build())));
}
}

View file

@ -125,7 +125,7 @@ impl FileTransferActivity {
Err(err) => match err.kind() {
FileTransferErrorType::UnsupportedFeature => {
// If copy is not supported, perform the tricky copy
self.tricky_copy(entry, dest);
let _ = self.tricky_copy(entry, dest);
}
_ => self.log_and_alert(
LogLevel::Error,
@ -143,7 +143,7 @@ impl FileTransferActivity {
/// ### tricky_copy
///
/// Tricky copy will be used whenever copy command is not available on remote host
fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) {
pub(super) fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) -> Result<(), String> {
// NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen
self.umount_wait();
// match entry
@ -157,7 +157,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: could not create temporary file: {}", err),
);
return;
return Err(String::from("Could not create temporary file"));
}
};
// Download file
@ -170,7 +170,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: could not download to temporary file: {}", err),
);
return;
return Err(err);
}
// Get local fs entry
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) {
@ -184,7 +184,7 @@ impl FileTransferActivity {
err
),
);
return;
return Err(err.to_string());
}
};
// Upload file to destination
@ -202,8 +202,9 @@ impl FileTransferActivity {
err
),
);
return;
return Err(err);
}
Ok(())
}
FsEntry::Directory(_) => {
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
@ -213,7 +214,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: could not create temporary directory: {}", err),
);
return;
return Err(err.to_string());
}
};
// Get path of dest
@ -227,7 +228,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: failed to download file: {}", err),
);
return;
return Err(err);
}
// Stat dir
let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) {
@ -241,7 +242,7 @@ impl FileTransferActivity {
err
),
);
return;
return Err(err.to_string());
}
};
// Upload to destination
@ -255,8 +256,9 @@ impl FileTransferActivity {
LogLevel::Error,
format!("Copy failed: failed to send file: {}", err),
);
return;
return Err(err);
}
Ok(())
}
}
}

View file

@ -113,7 +113,6 @@ impl FileTransferActivity {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
#[cfg(not(target_os = "windows"))]
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
}
@ -128,7 +127,6 @@ impl FileTransferActivity {
),
Err(err) => return Err(format!("Could not open editor: {}", err)),
}
#[cfg(not(target_os = "windows"))]
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();

View file

@ -27,7 +27,9 @@
*/
// locals
use super::super::browser::FileExplorerTab;
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use super::{
FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferOpts, TransferPayload,
};
use std::path::PathBuf;
@ -69,7 +71,7 @@ impl FileTransferActivity {
}
}
pub(crate) fn action_find_transfer(&mut self, save_as: Option<String>) {
pub(crate) fn action_find_transfer(&mut self, opts: TransferOpts) {
let wrkdir: PathBuf = match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => self.remote().wrkdir.clone(),
FileExplorerTab::FindRemote | FileExplorerTab::Remote => self.local().wrkdir.clone(),
@ -77,10 +79,19 @@ impl FileTransferActivity {
match self.get_found_selected_entries() {
SelectedEntry::One(entry) => match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
if let Err(err) = self.filetransfer_send(
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if opts.check_replace
&& self.config().get_prompt_on_file_replace()
&& self.remote_file_exists(file_to_check.as_path())
{
// Save pending transfer
self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
);
} else if let Err(err) = self.filetransfer_send(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
opts.save_as,
) {
self.log_and_alert(
LogLevel::Error,
@ -90,10 +101,19 @@ impl FileTransferActivity {
}
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
if let Err(err) = self.filetransfer_recv(
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if opts.check_replace
&& self.config().get_prompt_on_file_replace()
&& self.local_file_exists(file_to_check.as_path())
{
// Save pending transfer
self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
);
} else if let Err(err) = self.filetransfer_recv(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
opts.save_as,
) {
self.log_and_alert(
LogLevel::Error,
@ -106,13 +126,32 @@ impl FileTransferActivity {
SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input
let mut dest_path: PathBuf = wrkdir;
if let Some(save_as) = save_as {
if let Some(save_as) = opts.save_as {
dest_path.push(save_as);
}
// Iter files
let entries = entries.iter().map(|x| x.get_realfile()).collect();
let entries: Vec<FsEntry> = entries.iter().map(|x| x.get_realfile()).collect();
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
if opts.check_replace && self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries
.iter()
.filter(|x| {
self.remote_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
)
})
.collect();
// Save pending transfer
if !existing_files.is_empty() {
self.set_pending_transfer_many(
existing_files,
&dest_path.to_string_lossy().to_owned(),
);
return;
}
}
if let Err(err) = self.filetransfer_send(
TransferPayload::Many(entries),
dest_path.as_path(),
@ -128,6 +167,26 @@ impl FileTransferActivity {
}
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
if opts.check_replace && self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries
.iter()
.filter(|x| {
self.local_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
)
})
.collect();
// Save pending transfer
// Save pending transfer
if !existing_files.is_empty() {
self.set_pending_transfer_many(
existing_files,
&dest_path.to_string_lossy().to_owned(),
);
return;
}
}
if let Err(err) = self.filetransfer_recv(
TransferPayload::Many(entries),
dest_path.as_path(),

View file

@ -25,7 +25,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
pub(self) use super::{FileTransferActivity, FsEntry, LogLevel, TransferPayload};
pub(self) use super::{
browser::FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, TransferOpts,
TransferPayload,
};
use tuirealm::{Payload, Value};
// actions

View file

@ -27,6 +27,7 @@
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel};
use std::fs::File;
use std::path::PathBuf;
impl FileTransferActivity {
@ -99,24 +100,29 @@ impl FileTransferActivity {
};
if let FsEntry::File(local_file) = local_file {
// Create file
match self.client.send_file(&local_file, file_path.as_path()) {
let reader = Box::new(match File::open(tfile.path()) {
Ok(f) => f,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Could not open tempfile: {}", err),
);
return;
}
});
match self
.client
.send_file_wno_stream(&local_file, file_path.as_path(), reader)
{
Err(err) => self.log_and_alert(
LogLevel::Error,
format!("Could not create file \"{}\": {}", file_path.display(), err),
),
Ok(writer) => {
// Finalize write
if let Err(err) = self.client.on_sent(writer) {
self.log_and_alert(
LogLevel::Warn,
format!("Could not finalize file: {}", err),
);
} else {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
}
Ok(_) => {
self.log(
LogLevel::Info,
format!("Created file \"{}\"", file_path.display()),
);
// Reload files
self.reload_remote_dir();
}

View file

@ -27,6 +27,7 @@
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use crate::filetransfer::FileTransferErrorType;
use std::path::{Path, PathBuf};
impl FileTransferActivity {
@ -114,6 +115,9 @@ impl FileTransferActivity {
),
);
}
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
self.tricky_move(entry, dest);
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
@ -125,4 +129,41 @@ impl FileTransferActivity {
),
}
}
/// ### tricky_move
///
/// Tricky move will be used whenever copy command is not available on remote host.
/// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`)
fn tricky_move(&mut self, entry: &FsEntry, dest: &Path) {
debug!(
"Using tricky-move to move entry {} to {}",
entry.get_abs_path().display(),
dest.display()
);
if self.tricky_copy(entry.clone(), dest).is_ok() {
// Delete remote existing entry
debug!("Tricky-copy worked; removing existing remote entry");
match self.client.remove(entry) {
Ok(_) => self.log(
LogLevel::Info,
format!(
"Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(),
dest.display()
),
),
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Copied \"{}\" to \"{}\"; but failed to remove src: {}",
entry.get_abs_path().display(),
dest.display(),
err
),
),
}
} else {
error!("Tricky move aborted due to tricky-copy failure");
}
}
}

View file

@ -26,34 +26,87 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
use std::path::PathBuf;
use super::{
super::STORAGE_PENDING_TRANSFER, FileExplorerTab, FileTransferActivity, FsEntry, LogLevel,
SelectedEntry, TransferOpts, TransferPayload,
};
use std::path::{Path, PathBuf};
impl FileTransferActivity {
pub(crate) fn action_local_saveas(&mut self, input: String) {
self.action_local_send_file(Some(input));
self.local_send_file(TransferOpts::default().save_as(Some(input)));
}
pub(crate) fn action_remote_saveas(&mut self, input: String) {
self.action_remote_recv_file(Some(input));
self.remote_recv_file(TransferOpts::default().save_as(Some(input)));
}
pub(crate) fn action_local_send(&mut self) {
self.action_local_send_file(None);
self.local_send_file(TransferOpts::default());
}
pub(crate) fn action_remote_recv(&mut self) {
self.action_remote_recv_file(None);
self.remote_recv_file(TransferOpts::default());
}
fn action_local_send_file(&mut self, save_as: Option<String>) {
/// ### action_finalize_pending_transfer
///
/// Finalize "pending" transfer.
/// The pending transfer is created after a transfer which required a user action to be completed first.
/// The name of the file to transfer, is contained in the storage at `STORAGE_PENDING_TRANSFER`.
/// NOTE: Panics if `STORAGE_PENDING_TRANSFER` is undefined
pub(crate) fn action_finalize_pending_transfer(&mut self) {
// Retrieve pending transfer
let file_name = self
.context_mut()
.store_mut()
.take_string(STORAGE_PENDING_TRANSFER);
// Send file
match self.browser.tab() {
FileExplorerTab::Local => self.local_send_file(
TransferOpts::default()
.save_as(file_name)
.check_replace(false),
),
FileExplorerTab::Remote => self.remote_recv_file(
TransferOpts::default()
.save_as(file_name)
.check_replace(false),
),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => self.action_find_transfer(
TransferOpts::default()
.save_as(file_name)
.check_replace(false),
),
}
// Reload browsers
match self.browser.tab() {
FileExplorerTab::Local | FileExplorerTab::FindLocal => {
self.update_remote_filelist();
}
FileExplorerTab::Remote | FileExplorerTab::FindRemote => {
self.update_local_filelist();
}
}
}
fn local_send_file(&mut self, opts: TransferOpts) {
let wrkdir: PathBuf = self.remote().wrkdir.clone();
match self.get_local_selected_entries() {
SelectedEntry::One(entry) => {
if let Err(err) = self.filetransfer_send(
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if opts.check_replace
&& self.config().get_prompt_on_file_replace()
&& self.remote_file_exists(file_to_check.as_path())
{
// Save pending transfer
self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
);
} else if let Err(err) = self.filetransfer_send(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
opts.save_as,
) {
{
self.log_and_alert(
@ -67,11 +120,30 @@ impl FileTransferActivity {
SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input
let mut dest_path: PathBuf = wrkdir;
if let Some(save_as) = save_as {
if let Some(save_as) = opts.save_as {
dest_path.push(save_as);
}
// Iter files
let entries = entries.iter().map(|x| x.get_realfile()).collect();
let entries: Vec<FsEntry> = entries.iter().map(|x| x.get_realfile()).collect();
if opts.check_replace && self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries
.iter()
.filter(|x| {
self.remote_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
)
})
.collect();
// Save pending transfer
if !existing_files.is_empty() {
self.set_pending_transfer_many(
existing_files,
&dest_path.to_string_lossy().to_owned(),
);
return;
}
}
if let Err(err) = self.filetransfer_send(
TransferPayload::Many(entries),
dest_path.as_path(),
@ -90,14 +162,23 @@ impl FileTransferActivity {
}
}
fn action_remote_recv_file(&mut self, save_as: Option<String>) {
fn remote_recv_file(&mut self, opts: TransferOpts) {
let wrkdir: PathBuf = self.local().wrkdir.clone();
match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => {
if let Err(err) = self.filetransfer_recv(
let file_to_check = Self::file_to_check(&entry, opts.save_as.as_ref());
if opts.check_replace
&& self.config().get_prompt_on_file_replace()
&& self.local_file_exists(file_to_check.as_path())
{
// Save pending transfer
self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
);
} else if let Err(err) = self.filetransfer_recv(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
opts.save_as,
) {
{
self.log_and_alert(
@ -111,11 +192,30 @@ impl FileTransferActivity {
SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input
let mut dest_path: PathBuf = wrkdir;
if let Some(save_as) = save_as {
if let Some(save_as) = opts.save_as {
dest_path.push(save_as);
}
// Iter files
let entries = entries.iter().map(|x| x.get_realfile()).collect();
let entries: Vec<FsEntry> = entries.iter().map(|x| x.get_realfile()).collect();
if opts.check_replace && self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries
.iter()
.filter(|x| {
self.local_file_exists(
Self::file_to_check_many(x, dest_path.as_path()).as_path(),
)
})
.collect();
// Save pending transfer
if !existing_files.is_empty() {
self.set_pending_transfer_many(
existing_files,
&dest_path.to_string_lossy().to_owned(),
);
return;
}
}
if let Err(err) = self.filetransfer_recv(
TransferPayload::Many(entries),
dest_path.as_path(),
@ -133,4 +233,42 @@ impl FileTransferActivity {
SelectedEntry::None => {}
}
}
/// ### set_pending_transfer
///
/// Set pending transfer into storage
pub(crate) fn set_pending_transfer(&mut self, file_name: &str) {
self.mount_radio_replace(file_name);
// Put pending transfer in store
self.context_mut()
.store_mut()
.set_string(STORAGE_PENDING_TRANSFER, file_name.to_string());
}
/// ### set_pending_transfer_many
///
/// Set pending transfer for many files into storage and mount radio
pub(crate) fn set_pending_transfer_many(&mut self, files: Vec<&FsEntry>, dest_path: &str) {
let file_names: Vec<&str> = files.iter().map(|x| x.get_name()).collect();
self.mount_radio_replace_many(file_names.as_slice());
self.context_mut()
.store_mut()
.set_string(STORAGE_PENDING_TRANSFER, dest_path.to_string());
}
/// ### file_to_check
///
/// Get file to check for path
pub(crate) fn file_to_check(e: &FsEntry, alt: Option<&String>) -> PathBuf {
match alt {
Some(s) => PathBuf::from(s),
None => PathBuf::from(e.get_name()),
}
}
pub(crate) fn file_to_check_many(e: &FsEntry, wrkdir: &Path) -> PathBuf {
let mut p = wrkdir.to_path_buf();
p.push(e.get_name());
p
}
}

View file

@ -29,6 +29,8 @@ use bytesize::ByteSize;
use std::fmt;
use std::time::Instant;
// -- States and progress
/// ### TransferStates
///
/// TransferStates contains the states related to the transfer process
@ -85,6 +87,13 @@ impl TransferStates {
pub fn aborted(&self) -> bool {
self.aborted
}
/// ### full_size
///
/// Returns the size of the entire transfer
pub fn full_size(&self) -> usize {
self.full.total
}
}
impl Default for ProgressStates {
@ -140,6 +149,10 @@ impl ProgressStates {
///
/// Calculate progress in a range between 0.0 to 1.0
pub fn calc_progress(&self) -> f64 {
// Prevent dividing by 0
if self.total == 0 {
return 0.0;
}
let prog: f64 = (self.written as f64) / (self.total as f64);
match prog > 1.0 {
true => 1.0,
@ -191,6 +204,45 @@ impl ProgressStates {
}
}
// -- Options
/// ## TransferOpts
///
/// Defines the transfer options for transfer actions
pub struct TransferOpts {
/// Save file as
pub save_as: Option<String>,
/// Whether to check if file is being replaced
pub check_replace: bool,
}
impl Default for TransferOpts {
fn default() -> Self {
Self {
save_as: None,
check_replace: true,
}
}
}
impl TransferOpts {
/// ### save_as
///
/// Define the name of the file to be saved
pub fn save_as<S: AsRef<str>>(mut self, n: Option<S>) -> Self {
self.save_as = n.map(|x| x.as_ref().to_string());
self
}
/// ### check_replace
///
/// Set whether to check if the file being transferred will "replace" an existing one
pub fn check_replace(mut self, opt: bool) -> Self {
self.check_replace = opt;
self
}
}
#[cfg(test)]
mod test {
@ -238,6 +290,11 @@ mod test {
// Check if terminated at started
states.started = Instant::now();
assert_eq!(states.calc_bytes_per_second(), 1024);
// Divide by zero
let states: ProgressStates = ProgressStates::default();
assert_eq!(states.total, 0);
assert_eq!(states.written, 0);
assert_eq!(states.calc_progress(), 0.0);
}
#[test]
@ -255,5 +312,19 @@ mod test {
assert_eq!(states.aborted(), true);
states.reset();
assert_eq!(states.aborted(), false);
states.full.total = 1024;
assert_eq!(states.full_size(), 1024);
}
#[test]
fn transfer_opts() {
let opts = TransferOpts::default();
assert_eq!(opts.check_replace, true);
assert!(opts.save_as.is_none());
let opts = TransferOpts::default()
.check_replace(false)
.save_as(Some("omar.txt"));
assert_eq!(opts.save_as.as_deref().unwrap(), "omar.txt");
assert_eq!(opts.check_replace, false);
}
}

View file

@ -22,11 +22,15 @@
* SOFTWARE.
*/
// Locals
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord};
use super::{ConfigClient, FileTransferActivity, LogLevel, LogRecord, TransferPayload};
use crate::filetransfer::ProtocolParams;
use crate::system::environment;
use crate::system::notifications::Notification;
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::fmt_millis;
use crate::utils::path;
// Ext
use bytesize::ByteSize;
use std::env;
use std::path::{Path, PathBuf};
use tuirealm::Update;
@ -134,4 +138,97 @@ impl FileTransferActivity {
pub(super) fn remote_to_abs_path(&self, path: &Path) -> PathBuf {
path::absolutize(self.remote().wrkdir.as_path(), path)
}
/// ### get_remote_hostname
///
/// Get remote hostname
pub(super) fn get_remote_hostname(&self) -> String {
let ft_params = self.context().ft_params().unwrap();
match &ft_params.params {
ProtocolParams::Generic(params) => params.address.clone(),
ProtocolParams::AwsS3(params) => params.bucket_name.clone(),
}
}
/// ### get_connection_msg
///
/// Get connection message to show to client
pub(super) fn get_connection_msg(params: &ProtocolParams) -> String {
match params {
ProtocolParams::Generic(params) => {
info!(
"Client is not connected to remote; connecting to {}:{}",
params.address, params.port
);
format!("Connecting to {}:{}", params.address, params.port)
}
ProtocolParams::AwsS3(params) => {
info!(
"Client is not connected to remote; connecting to {} ({})",
params.bucket_name, params.region
);
format!("Connecting to {}", params.bucket_name)
}
}
}
/// ### notify_transfer_completed
///
/// Send notification regarding transfer completed
/// The notification is sent only when these conditions are satisfied:
///
/// - notifications are enabled
/// - transfer size is greater or equal than notification threshold
pub(super) fn notify_transfer_completed(&self, payload: &TransferPayload) {
if self.config().get_notifications()
&& self.config().get_notification_threshold() as usize <= self.transfer.full_size()
{
Notification::transfer_completed(self.transfer_completed_msg(payload));
}
}
/// ### notify_transfer_error
///
/// Send notification regarding transfer error
/// The notification is sent only when these conditions are satisfied:
///
/// - notifications are enabled
/// - transfer size is greater or equal than notification threshold
pub(super) fn notify_transfer_error(&self, msg: &str) {
if self.config().get_notifications()
&& self.config().get_notification_threshold() as usize <= self.transfer.full_size()
{
Notification::transfer_error(msg);
}
}
fn transfer_completed_msg(&self, payload: &TransferPayload) -> String {
let transfer_stats = format!(
"took {} seconds; at {}/s",
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
);
match payload {
TransferPayload::File(file) => {
format!(
"File \"{}\" has been successfully transferred ({})",
file.name, transfer_stats
)
}
TransferPayload::Any(entry) => {
format!(
"\"{}\" has been successfully transferred ({})",
entry.get_name(),
transfer_stats
)
}
TransferPayload::Many(entries) => {
format!(
"{} files has been successfully transferred ({})",
entries.len(),
transfer_stats
)
}
}
}
}

View file

@ -36,17 +36,15 @@ pub(self) mod view;
// locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::ftp_transfer::FtpFileTransfer;
use crate::filetransfer::scp_transfer::ScpFileTransfer;
use crate::filetransfer::sftp_transfer::SftpFileTransfer;
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
use crate::fs::explorer::FileExplorer;
use crate::fs::FsEntry;
use crate::host::Localhost;
use crate::system::config_client::ConfigClient;
pub(self) use lib::browser;
use lib::browser::Browser;
use lib::transfer::TransferStates;
use lib::transfer::{TransferOpts, TransferStates};
pub(self) use session::TransferPayload;
// Includes
@ -59,6 +57,7 @@ use tuirealm::View;
// -- Storage keys
const STORAGE_EXPLORER_WIDTH: &str = "FILETRANSFER_EXPLORER_WIDTH";
const STORAGE_PENDING_TRANSFER: &str = "FILETRANSFER_PENDING_TRANSFER";
// -- components
@ -82,12 +81,14 @@ const COMPONENT_INPUT_OPEN_WITH: &str = "INPUT_OPEN_WITH";
const COMPONENT_INPUT_RENAME: &str = "INPUT_RENAME";
const COMPONENT_INPUT_SAVEAS: &str = "INPUT_SAVEAS";
const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
const COMPONENT_RADIO_REPLACE: &str = "RADIO_REPLACE"; // NOTE: used for file transfers, to choose whether to replace files
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";
const COMPONENT_SPAN_STATUS_BAR_LOCAL: &str = "STATUS_BAR_LOCAL";
const COMPONENT_SPAN_STATUS_BAR_REMOTE: &str = "STATUS_BAR_REMOTE";
const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO";
const COMPONENT_LIST_REPLACING_FILES: &str = "LIST_REPLACING_FILES"; // NOTE: used for file transfers, to list files which are going to be replaced
/// ## LogLevel
///
@ -155,6 +156,7 @@ impl FileTransferActivity {
FileTransferProtocol::Scp => {
Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client)))
}
FileTransferProtocol::AwsS3 => Box::new(S3FileTransfer::default()),
},
browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
@ -290,12 +292,9 @@ impl Activity for FileTransferActivity {
}
// Check if connected (popup must be None, otherwise would try reconnecting in loop in case of error)
if !self.client.is_connected() && self.view.get_props(COMPONENT_TEXT_FATAL).is_none() {
let params = self.context().ft_params().unwrap();
info!(
"Client is not connected to remote; connecting to {}:{}",
params.address, params.port
);
let msg: String = format!("Connecting to {}:{}", params.address, params.port);
let ftparams = self.context().ft_params().unwrap();
// print params
let msg: String = Self::get_connection_msg(&ftparams.params);
// Set init state to connecting popup
self.mount_wait(msg.as_str());
// Force ui draw

View file

@ -34,6 +34,7 @@ use crate::utils::fmt::fmt_millis;
// Ext
use bytesize::ByteSize;
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use std::time::Instant;
@ -76,22 +77,20 @@ impl FileTransferActivity {
///
/// Connect to remote
pub(super) fn connect(&mut self) {
let params = self.context().ft_params().unwrap().clone();
let addr: String = params.address.clone();
let entry_dir: Option<PathBuf> = params.entry_directory.clone();
let ft_params = self.context().ft_params().unwrap().clone();
let entry_dir: Option<PathBuf> = ft_params.entry_directory.clone();
// Connect to remote
match self.client.connect(
params.address,
params.port,
params.username,
params.password,
) {
match self.client.connect(&ft_params.params) {
Ok(welcome) => {
if let Some(banner) = welcome {
// Log welcome
self.log(
LogLevel::Info,
format!("Established connection with '{}': \"{}\"", addr, banner),
format!(
"Established connection with '{}': \"{}\"",
self.get_remote_hostname(),
banner
),
);
}
// Try to change directory to entry directory
@ -121,8 +120,7 @@ impl FileTransferActivity {
///
/// disconnect from remote
pub(super) fn disconnect(&mut self) {
let params = self.context().ft_params().unwrap();
let msg: String = format!("Disconnecting from {}", params.address);
let msg: String = format!("Disconnecting from {}", self.get_remote_hostname());
// Show popup disconnecting
self.mount_wait(msg.as_str());
// Disconnect
@ -208,17 +206,27 @@ impl FileTransferActivity {
dst_name: Option<String>,
) -> Result<(), String> {
// Use different method based on payload
match payload {
TransferPayload::Any(entry) => {
self.filetransfer_send_any(&entry, curr_remote_path, dst_name)
let result = match payload {
TransferPayload::Any(ref entry) => {
self.filetransfer_send_any(entry, curr_remote_path, dst_name)
}
TransferPayload::File(file) => {
self.filetransfer_send_file(&file, curr_remote_path, dst_name)
TransferPayload::File(ref file) => {
self.filetransfer_send_file(file, curr_remote_path, dst_name)
}
TransferPayload::Many(entries) => {
TransferPayload::Many(ref entries) => {
self.filetransfer_send_many(entries, curr_remote_path)
}
};
// Notify
match &result {
Ok(_) => {
self.notify_transfer_completed(&payload);
}
Err(e) => {
self.notify_transfer_error(e.as_str());
}
}
result
}
/// ### filetransfer_send_file
@ -270,10 +278,10 @@ impl FileTransferActivity {
// Mount progress bar
self.mount_progress_bar(format!("Uploading {}", entry.get_abs_path().display()));
// Send recurse
self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
// Umount progress bar
self.umount_progress_bar();
Ok(())
result
}
/// ### filetransfer_send_many
@ -281,7 +289,7 @@ impl FileTransferActivity {
/// Send many entries to remote
fn filetransfer_send_many(
&mut self,
entries: Vec<FsEntry>,
entries: &[FsEntry],
curr_remote_path: &Path,
) -> Result<(), String> {
// Reset states
@ -295,12 +303,14 @@ impl FileTransferActivity {
// Mount progress bar
self.mount_progress_bar(format!("Uploading {} entries…", entries.len()));
// Send recurse
entries
let result = entries
.iter()
.for_each(|x| self.filetransfer_send_recurse(x, curr_remote_path, None));
.map(|x| self.filetransfer_send_recurse(x, curr_remote_path, None))
.find(|x| x.is_err())
.unwrap_or(Ok(()));
// Umount progress bar
self.umount_progress_bar();
Ok(())
result
}
fn filetransfer_send_recurse(
@ -308,7 +318,7 @@ impl FileTransferActivity {
entry: &FsEntry,
curr_remote_path: &Path,
dst_name: Option<String>,
) {
) -> Result<(), String> {
// Write popup
let file_name: String = match entry {
FsEntry::Directory(dir) => dir.name.clone(),
@ -322,44 +332,42 @@ impl FileTransferActivity {
};
remote_path.push(remote_file_name);
// Match entry
match entry {
let result: Result<(), String> = match entry {
FsEntry::File(file) => {
if let Err(err) = self.filetransfer_send_one(file, remote_path.as_path(), file_name)
{
// Log error
self.log_and_alert(
LogLevel::Error,
format!("Failed to upload file {}: {}", file.name, err),
);
// If transfer was abrupted or there was an IO error on remote, remove file
if matches!(
err,
TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_)
) {
// Stat file on remote and remove it if exists
match self.client.stat(remote_path.as_path()) {
Err(err) => self.log(
LogLevel::Error,
format!(
"Could not remove created file {}: {}",
remote_path.display(),
err
match self.filetransfer_send_one(file, remote_path.as_path(), file_name) {
Err(err) => {
// If transfer was abrupted or there was an IO error on remote, remove file
if matches!(
err,
TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_)
) {
// Stat file on remote and remove it if exists
match self.client.stat(remote_path.as_path()) {
Err(err) => self.log(
LogLevel::Error,
format!(
"Could not remove created file {}: {}",
remote_path.display(),
err
),
),
),
Ok(entry) => {
if let Err(err) = self.client.remove(&entry) {
self.log(
LogLevel::Error,
format!(
"Could not remove created file {}: {}",
remote_path.display(),
err
),
);
Ok(entry) => {
if let Err(err) = self.client.remove(&entry) {
self.log(
LogLevel::Error,
format!(
"Could not remove created file {}: {}",
remote_path.display(),
err
),
);
}
}
}
}
Err(err.to_string())
}
Ok(_) => Ok(()),
}
}
FsEntry::Directory(dir) => {
@ -389,7 +397,7 @@ impl FileTransferActivity {
err
),
);
return;
return Err(err.to_string());
}
}
// Get files in dir
@ -402,8 +410,13 @@ impl FileTransferActivity {
break;
}
// Send entry; name is always None after first call
self.filetransfer_send_recurse(entry, remote_path.as_path(), None);
if let Err(err) =
self.filetransfer_send_recurse(entry, remote_path.as_path(), None)
{
return Err(err);
}
}
Ok(())
}
Err(err) => {
self.log_and_alert(
@ -414,10 +427,11 @@ impl FileTransferActivity {
err
),
);
Err(err.to_string())
}
}
}
}
};
// Scan dir on remote
self.reload_remote_dir();
// If aborted; show popup
@ -428,6 +442,7 @@ impl FileTransferActivity {
format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()),
);
}
result
}
/// ### filetransfer_send_file
@ -442,103 +457,165 @@ impl FileTransferActivity {
// Upload file
// Try to open local file
match self.host.open_file_read(local.abs_path.as_path()) {
Ok(mut fhnd) => match self.client.send_file(local, remote) {
Ok(mut rhnd) => {
// Write file
let file_size: usize =
fhnd.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer
self.transfer.partial.init(file_size);
// rewind
if let Err(err) = fhnd.seek(std::io::SeekFrom::Start(0)) {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Write remote file
let mut total_bytes_written: usize = 0;
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely written,
// Or filetransfer has been aborted
while total_bytes_written < file_size && !self.transfer.aborted() {
// Handle input events (each 500ms) or if never fetched before
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match fhnd.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match rhnd.write(&buffer[delta..bytes_read]) {
Ok(bytes) => {
delta += bytes;
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(
err,
));
}
}
}
delta
Ok(fhnd) => match self.client.send_file(local, remote) {
Ok(rhnd) => {
self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd)
}
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd)
}
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
},
Err(err) => Err(TransferErrorReason::HostError(err)),
}
}
/// ### filetransfer_send_one_with_stream
///
/// Send file to remote using stream
fn filetransfer_send_one_with_stream(
&mut self,
local: &FsFile,
remote: &Path,
file_name: String,
mut reader: File,
mut writer: Box<dyn Write>,
) -> Result<(), TransferErrorReason> {
// Write file
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer
self.transfer.partial.init(file_size);
// rewind
if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Write remote file
let mut total_bytes_written: usize = 0;
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely written,
// Or filetransfer has been aborted
while total_bytes_written < file_size && !self.transfer.aborted() {
// Handle input events (each 500ms) or if never fetched before
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match reader.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match writer.write(&buffer[delta..bytes_read]) {
Ok(bytes) => {
delta += bytes;
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
}
Err(err) => {
return Err(TransferErrorReason::LocalIoError(err));
}
};
// Increase progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
delta
}
// Finalize stream
if let Err(err) = self.client.on_sent(rhnd) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// if upload was abrupted, return error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
}
Err(err) => return Err(TransferErrorReason::FileTransferError(err)),
},
Err(err) => return Err(TransferErrorReason::HostError(err)),
Err(err) => {
return Err(TransferErrorReason::LocalIoError(err));
}
};
// Increase progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
}
// Finalize stream
if let Err(err) = self.client.on_sent(writer) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// if upload was abrupted, return error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}
/// ### filetransfer_send_one_wno_stream
///
/// Send an `FsFile` to remote without using streams.
fn filetransfer_send_one_wno_stream(
&mut self,
local: &FsFile,
remote: &Path,
file_name: String,
mut reader: File,
) -> Result<(), TransferErrorReason> {
// Write file
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer
self.transfer.partial.init(file_size);
// rewind
if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
return Err(TransferErrorReason::CouldNotRewind(err));
}
// Draw before
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
// Send file
if let Err(err) = self
.client
.send_file_wno_stream(local, remote, Box::new(reader))
{
return Err(TransferErrorReason::FileTransferError(err));
}
// Set transfer size ok
self.transfer.partial.update_progress(file_size);
self.transfer.full.update_progress(file_size);
// Draw again after
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
// log and return Ok
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}
@ -553,11 +630,23 @@ impl FileTransferActivity {
local_path: &Path,
dst_name: Option<String>,
) -> Result<(), String> {
match payload {
TransferPayload::Any(entry) => self.filetransfer_recv_any(&entry, local_path, dst_name),
TransferPayload::File(file) => self.filetransfer_recv_file(&file, local_path),
TransferPayload::Many(entries) => self.filetransfer_recv_many(entries, local_path),
let result = match payload {
TransferPayload::Any(ref entry) => {
self.filetransfer_recv_any(entry, local_path, dst_name)
}
TransferPayload::File(ref file) => self.filetransfer_recv_file(file, local_path),
TransferPayload::Many(ref entries) => self.filetransfer_recv_many(entries, local_path),
};
// Notify
match &result {
Ok(_) => {
self.notify_transfer_completed(&payload);
}
Err(e) => {
self.notify_transfer_error(e.as_str());
}
}
result
}
/// ### filetransfer_recv_any
@ -579,10 +668,10 @@ impl FileTransferActivity {
// Mount progress bar
self.mount_progress_bar(format!("Downloading {}", entry.get_abs_path().display()));
// Receive
self.filetransfer_recv_recurse(entry, local_path, dst_name);
let result = self.filetransfer_recv_recurse(entry, local_path, dst_name);
// Umount progress bar
self.umount_progress_bar();
Ok(())
result
}
/// ### filetransfer_recv_file
@ -609,7 +698,7 @@ impl FileTransferActivity {
/// Send many entries to remote
fn filetransfer_recv_many(
&mut self,
entries: Vec<FsEntry>,
entries: &[FsEntry],
curr_remote_path: &Path,
) -> Result<(), String> {
// Reset states
@ -623,12 +712,14 @@ impl FileTransferActivity {
// Mount progress bar
self.mount_progress_bar(format!("Downloading {} entries…", entries.len()));
// Send recurse
entries
let result = entries
.iter()
.for_each(|x| self.filetransfer_recv_recurse(x, curr_remote_path, None));
.map(|x| self.filetransfer_recv_recurse(x, curr_remote_path, None))
.find(|x| x.is_err())
.unwrap_or(Ok(()));
// Umount progress bar
self.umount_progress_bar();
Ok(())
result
}
fn filetransfer_recv_recurse(
@ -636,14 +727,14 @@ impl FileTransferActivity {
entry: &FsEntry,
local_path: &Path,
dst_name: Option<String>,
) {
) -> Result<(), String> {
// Write popup
let file_name: String = match entry {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
// Match entry
match entry {
let result: Result<(), String> = match entry {
FsEntry::File(file) => {
// Get local file
let mut local_file_path: PathBuf = PathBuf::from(local_path);
@ -656,10 +747,6 @@ impl FileTransferActivity {
if let Err(err) =
self.filetransfer_recv_one(local_file_path.as_path(), file, file_name)
{
self.log_and_alert(
LogLevel::Error,
format!("Could not download file {}: {}", file.name, err),
);
// If transfer was abrupted or there was an IO error on remote, remove file
if matches!(
err,
@ -689,6 +776,9 @@ impl FileTransferActivity {
}
}
}
Err(err.to_string())
} else {
Ok(())
}
}
FsEntry::Directory(dir) => {
@ -738,12 +828,15 @@ impl FileTransferActivity {
}
// Receive entry; name is always None after first call
// Local path becomes local_dir_path
self.filetransfer_recv_recurse(
if let Err(err) = self.filetransfer_recv_recurse(
entry,
local_dir_path.as_path(),
None,
);
) {
return Err(err);
}
}
Ok(())
}
Err(err) => {
self.log_and_alert(
@ -754,6 +847,7 @@ impl FileTransferActivity {
err
),
);
Err(err.to_string())
}
}
}
@ -766,10 +860,11 @@ impl FileTransferActivity {
err
),
);
Err(err.to_string())
}
}
}
}
};
// Reload directory on local
self.reload_local_dir();
// if aborted; show alert
@ -783,6 +878,7 @@ impl FileTransferActivity {
),
);
}
result
}
/// ### filetransfer_recv_one
@ -796,120 +892,187 @@ impl FileTransferActivity {
) -> Result<(), TransferErrorReason> {
// Try to open local file
match self.host.open_file_write(local) {
Ok(mut local_file) => {
Ok(local_file) => {
// Download file from remote
match self.client.recv_file(remote) {
Ok(mut rhnd) => {
let mut total_bytes_written: usize = 0;
// Init transfer
self.transfer.partial.init(remote.size);
// Write local file
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely read,
// Or filetransfer has been aborted
while total_bytes_written < remote.size && !self.transfer.aborted() {
// Handle input events (each 500 ms) or is None
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match rhnd.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match local_file.write(&buffer[delta..bytes_read]) {
Ok(bytes) => delta += bytes,
Err(err) => {
return Err(TransferErrorReason::LocalIoError(
err,
));
}
}
}
delta
}
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
};
// Set progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
}
// Finalize stream
if let Err(err) = self.client.on_recv(rhnd) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// If download was abrupted, return Error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
// Apply file mode to file
#[cfg(any(
target_family = "unix",
target_os = "macos",
target_os = "linux"
))]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
local.display(),
err
),
);
}
}
// Log
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(rhnd) => self.filetransfer_recv_one_with_stream(
local, remote, file_name, rhnd, local_file,
),
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
self.filetransfer_recv_one_wno_stream(local, remote, file_name)
}
Err(err) => return Err(TransferErrorReason::FileTransferError(err)),
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
}
}
Err(err) => return Err(TransferErrorReason::HostError(err)),
Err(err) => Err(TransferErrorReason::HostError(err)),
}
}
/// ### filetransfer_recv_one_with_stream
///
/// Receive an `FsEntry` from remote using stream
fn filetransfer_recv_one_with_stream(
&mut self,
local: &Path,
remote: &FsFile,
file_name: String,
mut reader: Box<dyn Read>,
mut writer: File,
) -> Result<(), TransferErrorReason> {
let mut total_bytes_written: usize = 0;
// Init transfer
self.transfer.partial.init(remote.size);
// Write local file
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely read,
// Or filetransfer has been aborted
while total_bytes_written < remote.size && !self.transfer.aborted() {
// Handle input events (each 500 ms) or is None
if last_input_event_fetch.is_none()
|| last_input_event_fetch
.unwrap_or_else(Instant::now)
.elapsed()
.as_millis()
>= 500
{
// Read events
self.read_input_event();
// Reset instant
last_input_event_fetch = Some(Instant::now());
}
// Read till you can
let mut buffer: [u8; 65536] = [0; 65536];
let delta: usize = match reader.read(&mut buffer) {
Ok(bytes_read) => {
total_bytes_written += bytes_read;
if bytes_read == 0 {
continue;
} else {
let mut delta: usize = 0;
while delta < bytes_read {
// Write bytes
match writer.write(&buffer[delta..bytes_read]) {
Ok(bytes) => delta += bytes,
Err(err) => {
return Err(TransferErrorReason::LocalIoError(err));
}
}
}
delta
}
}
Err(err) => {
return Err(TransferErrorReason::RemoteIoError(err));
}
};
// Set progress
self.transfer.partial.update_progress(delta);
self.transfer.full.update_progress(delta);
// Draw only if a significant progress has been made (performance improvement)
if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
// Draw
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
last_progress_val = self.transfer.partial.calc_progress();
}
}
// Finalize stream
if let Err(err) = self.client.on_recv(reader) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
);
}
// If download was abrupted, return Error
if self.transfer.aborted() {
return Err(TransferErrorReason::Abrupted);
}
// Apply file mode to file
#[cfg(target_family = "unix")]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
local.display(),
err
),
);
}
}
// Log
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}
/// ### filetransfer_recv_one_with_stream
///
/// Receive an `FsEntry` from remote without using stream
fn filetransfer_recv_one_wno_stream(
&mut self,
local: &Path,
remote: &FsFile,
file_name: String,
) -> Result<(), TransferErrorReason> {
// Init transfer
self.transfer.partial.init(remote.size);
// Draw before transfer
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
// recv wno stream
if let Err(err) = self.client.recv_file_wno_stream(remote, local) {
return Err(TransferErrorReason::FileTransferError(err));
}
// Update progress at the end
self.transfer.partial.update_progress(remote.size);
self.transfer.full.update_progress(remote.size);
// Draw after transfer
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
// Apply file mode to file
#[cfg(target_family = "unix")]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
local.display(),
err
),
);
}
}
// Log
self.log(
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
),
);
Ok(())
}
@ -1060,4 +1223,14 @@ impl FileTransferActivity {
}
}
}
// -- file exist
pub(crate) fn local_file_exists(&mut self, p: &Path) -> bool {
self.host.file_exists(p)
}
pub(crate) fn remote_file_exists(&mut self, p: &Path) -> bool {
self.client.stat(p).is_ok()
}
}

View file

@ -27,14 +27,15 @@
*/
// locals
use super::{
actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel,
actions::SelectedEntry, browser::FileExplorerTab, FileTransferActivity, LogLevel, TransferOpts,
COMPONENT_EXPLORER_FIND, COMPONENT_EXPLORER_LOCAL, COMPONENT_EXPLORER_REMOTE,
COMPONENT_INPUT_COPY, COMPONENT_INPUT_EXEC, COMPONENT_INPUT_FIND, COMPONENT_INPUT_GOTO,
COMPONENT_INPUT_MKDIR, COMPONENT_INPUT_NEWFILE, COMPONENT_INPUT_OPEN_WITH,
COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO, COMPONENT_LOG_BOX,
COMPONENT_PROGRESS_BAR_FULL, COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE,
COMPONENT_RADIO_DISCONNECT, COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SORTING,
COMPONENT_TEXT_ERROR, COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
COMPONENT_INPUT_RENAME, COMPONENT_INPUT_SAVEAS, COMPONENT_LIST_FILEINFO,
COMPONENT_LIST_REPLACING_FILES, COMPONENT_LOG_BOX, COMPONENT_PROGRESS_BAR_FULL,
COMPONENT_PROGRESS_BAR_PARTIAL, COMPONENT_RADIO_DELETE, COMPONENT_RADIO_DISCONNECT,
COMPONENT_RADIO_QUIT, COMPONENT_RADIO_REPLACE, COMPONENT_RADIO_SORTING, COMPONENT_TEXT_ERROR,
COMPONENT_TEXT_FATAL, COMPONENT_TEXT_HELP,
};
use crate::fs::explorer::FileSorting;
use crate::fs::FsEntry;
@ -42,7 +43,7 @@ use crate::ui::components::{file_list::FileListPropsBuilder, logbox::LogboxProps
use crate::ui::keymap::*;
use crate::utils::fmt::fmt_path_elide_ex;
// externals
use tui_realm_stdlib::progress_bar::ProgressBarPropsBuilder;
use tui_realm_stdlib::ProgressBarPropsBuilder;
use tuirealm::{
props::{Alignment, PropsBuilder, TableBuilder, TextSpan},
tui::style::Color,
@ -358,7 +359,7 @@ impl Update for FileTransferActivity {
}
(COMPONENT_EXPLORER_FIND, key) if key == &MSG_KEY_SPACE => {
// Get entry
self.action_find_transfer(None);
self.action_find_transfer(TransferOpts::default());
// Reload files
match self.browser.tab() {
// NOTE: swapped by purpose
@ -583,7 +584,7 @@ impl Update for FileTransferActivity {
FileExplorerTab::Remote => self.action_remote_saveas(input.to_string()),
FileExplorerTab::FindLocal | FileExplorerTab::FindRemote => {
// Get entry
self.action_find_transfer(Some(input.to_string()));
self.action_find_transfer(TransferOpts::default().save_as(Some(input)));
}
}
self.umount_saveas();
@ -653,6 +654,36 @@ impl Update for FileTransferActivity {
}
}
(COMPONENT_RADIO_DELETE, _) => None,
// -- replace
(COMPONENT_RADIO_REPLACE, key)
if key == &MSG_KEY_ESC
|| key == &Msg::OnSubmit(Payload::One(Value::Usize(1))) =>
{
self.umount_radio_replace();
None
}
(COMPONENT_RADIO_REPLACE, key) if key == &MSG_KEY_TAB => {
if self.is_radio_replace_extended() {
self.view.active(COMPONENT_LIST_REPLACING_FILES);
}
None
}
(COMPONENT_RADIO_REPLACE, Msg::OnSubmit(Payload::One(Value::Usize(0)))) => {
// Choice is 'YES'
self.umount_radio_replace();
self.action_finalize_pending_transfer();
None
}
(COMPONENT_RADIO_REPLACE, _) => None,
(COMPONENT_LIST_REPLACING_FILES, key) if key == &MSG_KEY_TAB => {
self.view.active(COMPONENT_RADIO_REPLACE);
None
}
(COMPONENT_LIST_REPLACING_FILES, key) if key == &MSG_KEY_ESC => {
self.umount_radio_replace();
None
}
(COMPONENT_LIST_REPLACING_FILES, _) => None,
// -- disconnect
(COMPONENT_RADIO_DISCONNECT, key)
if key == &MSG_KEY_ESC
@ -810,14 +841,14 @@ impl FileTransferActivity {
.store()
.get_unsigned(super::STORAGE_EXPLORER_WIDTH)
.unwrap_or(256);
let params = self.context().ft_params().unwrap();
let hostname = self.get_remote_hostname();
let hostname: String = format!(
"{}:{} ",
params.address,
hostname,
fmt_path_elide_ex(
self.remote().wrkdir.as_path(),
width,
params.address.len() + 3 // 3 because of '/…/'
hostname.len() + 3 // 3 because of '/…/'
)
);
let files: Vec<String> = self

View file

@ -40,13 +40,9 @@ use crate::utils::ui::draw_area_in;
use bytesize::ByteSize;
use std::path::PathBuf;
use tui_realm_stdlib::{
input::{Input, InputPropsBuilder},
list::{List, ListPropsBuilder},
paragraph::{Paragraph, ParagraphPropsBuilder},
progress_bar::{ProgressBar, ProgressBarPropsBuilder},
radio::{Radio, RadioPropsBuilder},
span::{Span, SpanPropsBuilder},
table::{Table, TablePropsBuilder},
Input, InputPropsBuilder, List, ListPropsBuilder, Paragraph, ParagraphPropsBuilder,
ProgressBar, ProgressBarPropsBuilder, Radio, RadioPropsBuilder, Span, SpanPropsBuilder, Table,
TablePropsBuilder,
};
use tuirealm::props::{Alignment, PropsBuilder, TableBuilder, TextSpan};
use tuirealm::tui::{
@ -312,6 +308,34 @@ impl FileTransferActivity {
self.view.render(super::COMPONENT_RADIO_DELETE, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_REPLACE) {
if props.visible {
// NOTE: handle extended / normal modes
if self.is_radio_replace_extended() {
let popup = draw_area_in(f.size(), 50, 50);
f.render_widget(Clear, popup);
let popup_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Percentage(85), // List
Constraint::Percentage(15), // Radio
]
.as_ref(),
)
.split(popup);
self.view
.render(super::COMPONENT_LIST_REPLACING_FILES, f, popup_chunks[0]);
self.view
.render(super::COMPONENT_RADIO_REPLACE, f, popup_chunks[1]);
} else {
let popup = draw_area_in(f.size(), 50, 10);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_RADIO_REPLACE, f, popup);
}
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_DISCONNECT) {
if props.visible {
let popup = draw_area_in(f.size(), 30, 10);
@ -382,20 +406,7 @@ impl FileTransferActivity {
pub(super) fn mount_error(&mut self, text: &str) {
// Mount
let error_color = self.theme().misc_error_dialog;
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_foreground(error_color)
.with_borders(Borders::ALL, BorderType::Rounded, error_color)
.bold()
.with_text_alignment(Alignment::Center)
.with_texts(vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(super::COMPONENT_TEXT_ERROR);
self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, error_color);
}
/// ### umount_error
@ -408,46 +419,21 @@ impl FileTransferActivity {
pub(super) fn mount_fatal(&mut self, text: &str) {
// Mount
let error_color = self.theme().misc_error_dialog;
self.view.mount(
super::COMPONENT_TEXT_FATAL,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_foreground(error_color)
.with_borders(Borders::ALL, BorderType::Rounded, error_color)
.bold()
.with_text_alignment(Alignment::Center)
.with_texts(vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(super::COMPONENT_TEXT_FATAL);
self.mount_text_dialog(super::COMPONENT_TEXT_FATAL, text, error_color);
}
pub(super) fn mount_wait(&mut self, text: &str) {
self.mount_wait_ex(text, Color::Reset);
self.mount_wait_ex(text);
}
pub(super) fn mount_blocking_wait(&mut self, text: &str) {
self.mount_wait_ex(text, Color::Reset);
self.mount_wait_ex(text);
self.view();
}
fn mount_wait_ex(&mut self, text: &str, color: Color) {
// Mount
let mut builder: ParagraphPropsBuilder = ParagraphPropsBuilder::default();
builder
.with_foreground(color)
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.bold()
.with_text_alignment(Alignment::Center)
.with_texts(vec![TextSpan::from(text)]);
self.view.mount(
super::COMPONENT_TEXT_WAIT,
Box::new(Paragraph::new(builder.build())),
);
// Give focus to info
self.view.active(super::COMPONENT_TEXT_WAIT);
fn mount_wait_ex(&mut self, text: &str) {
let color = self.theme().misc_info_dialog;
self.mount_text_dialog(super::COMPONENT_TEXT_WAIT, text, color);
}
pub(super) fn umount_wait(&mut self) {
@ -460,20 +446,13 @@ impl FileTransferActivity {
pub(super) fn mount_quit(&mut self) {
// Protocol
let quit_color = self.theme().misc_quit_dialog;
self.view.mount(
self.mount_radio_dialog(
super::COMPONENT_RADIO_QUIT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(quit_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, quit_color)
.with_title("Are you sure you want to quit?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.rewind(true)
.build(),
)),
"Are you sure you want to quit?",
&["Yes", "No"],
0,
quit_color,
);
self.view.active(super::COMPONENT_RADIO_QUIT);
}
/// ### umount_quit
@ -489,20 +468,13 @@ impl FileTransferActivity {
pub(super) fn mount_disconnect(&mut self) {
// Protocol
let quit_color = self.theme().misc_quit_dialog;
self.view.mount(
self.mount_radio_dialog(
super::COMPONENT_RADIO_DISCONNECT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(quit_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, quit_color)
.with_title("Are you sure you want to disconnect?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.rewind(true)
.build(),
)),
"Are you sure you want to disconnect?",
&["Yes", "No"],
0,
quit_color,
);
self.view.active(super::COMPONENT_RADIO_DISCONNECT);
}
/// ### umount_disconnect
@ -514,17 +486,12 @@ impl FileTransferActivity {
pub(super) fn mount_copy(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
self.mount_input_dialog(
super::COMPONENT_INPUT_COPY,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label("Copy file(s) to…", Alignment::Center)
.build(),
)),
"Copy file(s) to…",
"",
input_color,
);
self.view.active(super::COMPONENT_INPUT_COPY);
}
pub(super) fn umount_copy(&mut self) {
@ -533,17 +500,12 @@ impl FileTransferActivity {
pub(super) fn mount_exec(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
self.mount_input_dialog(
super::COMPONENT_INPUT_EXEC,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label("Execute command", Alignment::Center)
.build(),
)),
"Execute command",
"",
input_color,
);
self.view.active(super::COMPONENT_INPUT_EXEC);
}
pub(super) fn umount_exec(&mut self) {
@ -590,18 +552,12 @@ impl FileTransferActivity {
pub(super) fn mount_find_input(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
self.mount_input_dialog(
super::COMPONENT_INPUT_FIND,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label("Search files by name", Alignment::Center)
.build(),
)),
"Search files by name",
"",
input_color,
);
// Give focus to input find
self.view.active(super::COMPONENT_INPUT_FIND);
}
pub(super) fn umount_find_input(&mut self) {
@ -611,17 +567,12 @@ impl FileTransferActivity {
pub(super) fn mount_goto(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
self.mount_input_dialog(
super::COMPONENT_INPUT_GOTO,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label("Change working directory", Alignment::Center)
.build(),
)),
"Change working directory",
"",
input_color,
);
self.view.active(super::COMPONENT_INPUT_GOTO);
}
pub(super) fn umount_goto(&mut self) {
@ -630,17 +581,12 @@ impl FileTransferActivity {
pub(super) fn mount_mkdir(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
self.mount_input_dialog(
super::COMPONENT_INPUT_MKDIR,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label("Insert directory name", Alignment::Center)
.build(),
)),
"Insert directory name",
"",
input_color,
);
self.view.active(super::COMPONENT_INPUT_MKDIR);
}
pub(super) fn umount_mkdir(&mut self) {
@ -649,17 +595,12 @@ impl FileTransferActivity {
pub(super) fn mount_newfile(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
self.mount_input_dialog(
super::COMPONENT_INPUT_NEWFILE,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label("New file name", Alignment::Center)
.build(),
)),
"New file name",
"",
input_color,
);
self.view.active(super::COMPONENT_INPUT_NEWFILE);
}
pub(super) fn umount_newfile(&mut self) {
@ -668,17 +609,12 @@ impl FileTransferActivity {
pub(super) fn mount_openwith(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
self.mount_input_dialog(
super::COMPONENT_INPUT_OPEN_WITH,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label("Open file with…", Alignment::Center)
.build(),
)),
"Open file with…",
"",
input_color,
);
self.view.active(super::COMPONENT_INPUT_OPEN_WITH);
}
pub(super) fn umount_openwith(&mut self) {
@ -687,17 +623,12 @@ impl FileTransferActivity {
pub(super) fn mount_rename(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
self.mount_input_dialog(
super::COMPONENT_INPUT_RENAME,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label("Move file(s) to…", Alignment::Center)
.build(),
)),
"Move file(s) to…",
"",
input_color,
);
self.view.active(super::COMPONENT_INPUT_RENAME);
}
pub(super) fn umount_rename(&mut self) {
@ -706,17 +637,7 @@ impl FileTransferActivity {
pub(super) fn mount_saveas(&mut self) {
let input_color = self.theme().misc_input_dialog;
self.view.mount(
super::COMPONENT_INPUT_SAVEAS,
Box::new(Input::new(
InputPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, input_color)
.with_foreground(input_color)
.with_label("Save as…", Alignment::Center)
.build(),
)),
);
self.view.active(super::COMPONENT_INPUT_SAVEAS);
self.mount_input_dialog(super::COMPONENT_INPUT_SAVEAS, "Save as…", "", input_color);
}
pub(super) fn umount_saveas(&mut self) {
@ -777,25 +698,13 @@ impl FileTransferActivity {
FileSorting::Name => 0,
FileSorting::Size => 3,
};
self.view.mount(
self.mount_radio_dialog(
super::COMPONENT_RADIO_SORTING,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(sorting_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, sorting_color)
.with_title("Sort files by", Alignment::Center)
.with_options(&[
String::from("Name"),
String::from("Modify time"),
String::from("Creation time"),
String::from("Size"),
])
.with_value(index)
.build(),
)),
"Sort files by",
&["Name", "Modify time", "Creation time", "Size"],
index,
sorting_color,
);
self.view.active(super::COMPONENT_RADIO_SORTING);
}
pub(super) fn umount_file_sorting(&mut self) {
@ -804,27 +713,74 @@ impl FileTransferActivity {
pub(super) fn mount_radio_delete(&mut self) {
let warn_color = self.theme().misc_warn_dialog;
self.view.mount(
self.mount_radio_dialog(
super::COMPONENT_RADIO_DELETE,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(warn_color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Plain, warn_color)
.with_title("Delete file", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.with_value(1)
.rewind(true)
.build(),
)),
"Delete file",
&["Yes", "No"],
1,
warn_color,
);
self.view.active(super::COMPONENT_RADIO_DELETE);
}
pub(super) fn umount_radio_delete(&mut self) {
self.view.umount(super::COMPONENT_RADIO_DELETE);
}
pub(super) fn mount_radio_replace(&mut self, file_name: &str) {
let warn_color = self.theme().misc_warn_dialog;
self.mount_radio_dialog(
super::COMPONENT_RADIO_REPLACE,
format!("File '{}' already exists. Overwrite file?", file_name),
&["Yes", "No"],
0,
warn_color,
);
}
pub(super) fn mount_radio_replace_many(&mut self, files: &[&str]) {
let warn_color = self.theme().misc_warn_dialog;
// Make rows
let rows = files.iter().map(|x| vec![TextSpan::new(x)]).collect();
self.view.mount(
super::COMPONENT_LIST_REPLACING_FILES,
Box::new(List::new(
ListPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, warn_color)
.scrollable(true)
.with_highlighted_color(warn_color)
.with_highlighted_str(Some("> "))
.with_title(
"The following files are going to be replaced",
Alignment::Center,
)
.with_foreground(warn_color)
.with_rows(rows)
.build(),
)),
);
self.mount_radio_dialog(
super::COMPONENT_RADIO_REPLACE,
"Overwrite files?",
&["Yes", "No"],
0,
warn_color,
);
}
/// ### is_radio_replace_extended
///
/// Returns whether radio replace is in "extended" mode (for many files)
pub(super) fn is_radio_replace_extended(&self) -> bool {
self.view
.get_state(super::COMPONENT_LIST_REPLACING_FILES)
.is_some()
}
pub(super) fn umount_radio_replace(&mut self) {
self.view.umount(super::COMPONENT_RADIO_REPLACE);
self.view.umount(super::COMPONENT_LIST_REPLACING_FILES); // NOTE: replace anyway
}
pub(super) fn mount_file_info(&mut self, file: &FsEntry) {
let mut texts: TableBuilder = TableBuilder::default();
// Abs path
@ -1113,4 +1069,65 @@ impl FileTransferActivity {
false => "Hide",
}
}
// -- Mount helpers
fn mount_text_dialog(&mut self, id: &str, text: &str, color: Color) {
// Mount
self.view.mount(
id,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Thick, color)
.with_foreground(color)
.bold()
.with_text_alignment(Alignment::Center)
.with_texts(vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(id);
}
fn mount_input_dialog(&mut self, id: &str, text: &str, val: &str, color: Color) {
self.view.mount(
id,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(color)
.with_label(text, Alignment::Center)
.with_borders(Borders::ALL, BorderType::Rounded, color)
.with_value(val.to_string())
.build(),
)),
);
self.view.active(id);
}
fn mount_radio_dialog<S: AsRef<str>>(
&mut self,
id: &str,
text: S,
opts: &[&str],
default: usize,
color: Color,
) {
self.view.mount(
id,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, color)
.with_title(text.as_ref(), Alignment::Center)
.with_options(opts)
.with_value(default)
.rewind(true)
.build(),
)),
);
// Active
self.view.active(id);
}
}

View file

@ -182,7 +182,6 @@ impl SetupActivity {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
#[cfg(not(target_os = "windows"))]
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
}
@ -215,7 +214,6 @@ impl SetupActivity {
}
}
// Restore terminal
#[cfg(not(target_os = "windows"))]
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();
@ -254,6 +252,9 @@ impl SetupActivity {
super::COMPONENT_COLOR_MISC_ERROR => {
theme.misc_error_dialog = color;
}
super::COMPONENT_COLOR_MISC_INFO => {
theme.misc_info_dialog = color;
}
super::COMPONENT_COLOR_MISC_INPUT => {
theme.misc_input_dialog = color;
}

View file

@ -98,7 +98,6 @@ impl SetupActivity {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
#[cfg(not(target_os = "windows"))]
ctx.leave_alternate_screen();
// Get result
let result: Result<(), String> = match ctx.config().iter_ssh_keys().nth(idx) {
@ -123,7 +122,6 @@ impl SetupActivity {
// Clear screen
ctx.clear_screen();
// Enter alternate mode
#[cfg(not(target_os = "windows"))]
ctx.enter_alternate_screen();
// Re-enable raw mode
if let Err(err) = enable_raw_mode() {

View file

@ -54,9 +54,12 @@ const COMPONENT_INPUT_TEXT_EDITOR: &str = "INPUT_TEXT_EDITOR";
const COMPONENT_RADIO_DEFAULT_PROTOCOL: &str = "RADIO_DEFAULT_PROTOCOL";
const COMPONENT_RADIO_HIDDEN_FILES: &str = "RADIO_HIDDEN_FILES";
const COMPONENT_RADIO_UPDATES: &str = "RADIO_CHECK_UPDATES";
const COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE: &str = "RADIO_PROMPT_ON_FILE_REPLACE";
const COMPONENT_RADIO_GROUP_DIRS: &str = "RADIO_GROUP_DIRS";
const COMPONENT_INPUT_LOCAL_FILE_FMT: &str = "INPUT_LOCAL_FILE_FMT";
const COMPONENT_INPUT_REMOTE_FILE_FMT: &str = "INPUT_REMOTE_FILE_FMT";
const COMPONENT_RADIO_NOTIFICATIONS_ENABLED: &str = "RADIO_NOTIFICATIONS_ENABLED";
const COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD: &str = "INPUT_NOTIFICATIONS_THRESHOLD";
// -- ssh keys
const COMPONENT_LIST_SSH_KEYS: &str = "LIST_SSH_KEYS";
const COMPONENT_INPUT_SSH_HOST: &str = "INPUT_SSH_HOST";
@ -75,6 +78,7 @@ const COMPONENT_COLOR_AUTH_PROTOCOL: &str = "COMPONENT_COLOR_AUTH_PROTOCOL";
const COMPONENT_COLOR_AUTH_RECENTS: &str = "COMPONENT_COLOR_AUTH_RECENTS";
const COMPONENT_COLOR_AUTH_USERNAME: &str = "COMPONENT_COLOR_AUTH_USERNAME";
const COMPONENT_COLOR_MISC_ERROR: &str = "COMPONENT_COLOR_MISC_ERROR";
const COMPONENT_COLOR_MISC_INFO: &str = "COMPONENT_COLOR_MISC_INFO";
const COMPONENT_COLOR_MISC_INPUT: &str = "COMPONENT_COLOR_MISC_INPUT";
const COMPONENT_COLOR_MISC_KEYS: &str = "COMPONENT_COLOR_MISC_KEYS";
const COMPONENT_COLOR_MISC_QUIT: &str = "COMPONENT_COLOR_MISC_QUIT";

View file

@ -31,8 +31,8 @@ use super::{
SetupActivity, ViewLayout, COMPONENT_COLOR_AUTH_ADDR, COMPONENT_COLOR_AUTH_BOOKMARKS,
COMPONENT_COLOR_AUTH_PASSWORD, COMPONENT_COLOR_AUTH_PORT, COMPONENT_COLOR_AUTH_PROTOCOL,
COMPONENT_COLOR_AUTH_RECENTS, COMPONENT_COLOR_AUTH_USERNAME, COMPONENT_COLOR_MISC_ERROR,
COMPONENT_COLOR_MISC_INPUT, COMPONENT_COLOR_MISC_KEYS, COMPONENT_COLOR_MISC_QUIT,
COMPONENT_COLOR_MISC_SAVE, COMPONENT_COLOR_MISC_WARN,
COMPONENT_COLOR_MISC_INFO, COMPONENT_COLOR_MISC_INPUT, COMPONENT_COLOR_MISC_KEYS,
COMPONENT_COLOR_MISC_QUIT, COMPONENT_COLOR_MISC_SAVE, COMPONENT_COLOR_MISC_WARN,
COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_BG, COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_FG,
COMPONENT_COLOR_TRANSFER_EXPLORER_LOCAL_HG, COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_BG,
COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_FG, COMPONENT_COLOR_TRANSFER_EXPLORER_REMOTE_HG,
@ -40,9 +40,11 @@ use super::{
COMPONENT_COLOR_TRANSFER_PROG_BAR_FULL, COMPONENT_COLOR_TRANSFER_PROG_BAR_PARTIAL,
COMPONENT_COLOR_TRANSFER_STATUS_HIDDEN, COMPONENT_COLOR_TRANSFER_STATUS_SORTING,
COMPONENT_COLOR_TRANSFER_STATUS_SYNC, COMPONENT_INPUT_LOCAL_FILE_FMT,
COMPONENT_INPUT_REMOTE_FILE_FMT, COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME,
COMPONENT_INPUT_TEXT_EDITOR, COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL,
COMPONENT_RADIO_DEL_SSH_KEY, COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES,
COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, COMPONENT_INPUT_REMOTE_FILE_FMT,
COMPONENT_INPUT_SSH_HOST, COMPONENT_INPUT_SSH_USERNAME, COMPONENT_INPUT_TEXT_EDITOR,
COMPONENT_LIST_SSH_KEYS, COMPONENT_RADIO_DEFAULT_PROTOCOL, COMPONENT_RADIO_DEL_SSH_KEY,
COMPONENT_RADIO_GROUP_DIRS, COMPONENT_RADIO_HIDDEN_FILES,
COMPONENT_RADIO_NOTIFICATIONS_ENABLED, COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
COMPONENT_RADIO_QUIT, COMPONENT_RADIO_SAVE, COMPONENT_RADIO_UPDATES, COMPONENT_TEXT_ERROR,
COMPONENT_TEXT_HELP,
};
@ -87,6 +89,10 @@ impl SetupActivity {
None
}
(COMPONENT_RADIO_UPDATES, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE);
None
}
(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_GROUP_DIRS);
None
}
@ -99,10 +105,26 @@ impl SetupActivity {
None
}
(COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_RADIO_NOTIFICATIONS_ENABLED);
None
}
(COMPONENT_RADIO_NOTIFICATIONS_ENABLED, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD);
None
}
(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_INPUT_TEXT_EDITOR);
None
}
// Input field <UP>
(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_NOTIFICATIONS_ENABLED);
None
}
(COMPONENT_RADIO_NOTIFICATIONS_ENABLED, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT);
None
}
(COMPONENT_INPUT_REMOTE_FILE_FMT, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_LOCAL_FILE_FMT);
None
@ -112,6 +134,10 @@ impl SetupActivity {
None
}
(COMPONENT_RADIO_GROUP_DIRS, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE);
None
}
(COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_RADIO_UPDATES);
None
}
@ -128,7 +154,7 @@ impl SetupActivity {
None
}
(COMPONENT_INPUT_TEXT_EDITOR, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_INPUT_REMOTE_FILE_FMT);
self.view.active(COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD);
None
}
// Error <ENTER> or <ESC>
@ -441,6 +467,10 @@ impl SetupActivity {
None
}
(COMPONENT_COLOR_MISC_ERROR, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_COLOR_MISC_INFO);
None
}
(COMPONENT_COLOR_MISC_INFO, key) if key == &MSG_KEY_DOWN => {
self.view.active(COMPONENT_COLOR_MISC_INPUT);
None
}
@ -551,10 +581,14 @@ impl SetupActivity {
self.view.active(COMPONENT_COLOR_AUTH_RECENTS);
None
}
(COMPONENT_COLOR_MISC_INPUT, key) if key == &MSG_KEY_UP => {
(COMPONENT_COLOR_MISC_INFO, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_COLOR_MISC_ERROR);
None
}
(COMPONENT_COLOR_MISC_INPUT, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_COLOR_MISC_INFO);
None
}
(COMPONENT_COLOR_MISC_KEYS, key) if key == &MSG_KEY_UP => {
self.view.active(COMPONENT_COLOR_MISC_INPUT);
None

View file

@ -36,12 +36,10 @@ pub use ssh_keys::*;
pub use theme::*;
// Ext
use tui_realm_stdlib::{
list::{List, ListPropsBuilder},
paragraph::{Paragraph, ParagraphPropsBuilder},
radio::{Radio, RadioPropsBuilder},
span::{Span, SpanPropsBuilder},
Input, InputPropsBuilder, List, ListPropsBuilder, Paragraph, ParagraphPropsBuilder, Radio,
RadioPropsBuilder, Span, SpanPropsBuilder,
};
use tuirealm::props::{Alignment, PropsBuilder, TableBuilder, TextSpan};
use tuirealm::props::{Alignment, InputType, PropsBuilder, TableBuilder, TextSpan};
use tuirealm::tui::{
style::Color,
widgets::{BorderType, Borders},
@ -76,21 +74,7 @@ impl SetupActivity {
///
/// Mount error box
pub(super) fn mount_error(&mut self, text: &str) {
// Mount
self.view.mount(
super::COMPONENT_TEXT_ERROR,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_foreground(Color::Red)
.bold()
.with_borders(Borders::ALL, BorderType::Rounded, Color::Red)
.with_texts(vec![TextSpan::from(text)])
.with_text_alignment(Alignment::Center)
.build(),
)),
);
// Give focus to error
self.view.active(super::COMPONENT_TEXT_ERROR);
self.mount_text_dialog(super::COMPONENT_TEXT_ERROR, text, Color::Red);
}
/// ### umount_error
@ -104,28 +88,13 @@ impl SetupActivity {
///
/// Mount quit popup
pub(super) fn mount_quit(&mut self) {
self.view.mount(
self.mount_radio_dialog(
super::COMPONENT_RADIO_QUIT,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_title(
"There are unsaved changes! Save changes before leaving?",
Alignment::Center,
)
.with_options(&[
String::from("Save"),
String::from("Don't save"),
String::from("Cancel"),
])
.rewind(true)
.build(),
)),
"There are unsaved changes! Save changes before leaving?",
&["Save", "Don't save", "Cancel"],
0,
Color::LightRed,
);
// Active
self.view.active(super::COMPONENT_RADIO_QUIT);
}
/// ### umount_quit
@ -139,21 +108,13 @@ impl SetupActivity {
///
/// Mount save popup
pub(super) fn mount_save_popup(&mut self) {
self.view.mount(
self.mount_radio_dialog(
super::COMPONENT_RADIO_SAVE,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_title("Save changes?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.rewind(true)
.build(),
)),
"Save changes?",
&["Yes", "No"],
0,
Color::LightYellow,
);
// Active
self.view.active(super::COMPONENT_RADIO_SAVE);
}
/// ### umount_quit
@ -255,4 +216,95 @@ impl SetupActivity {
pub(super) fn umount_help(&mut self) {
self.view.umount(super::COMPONENT_TEXT_HELP);
}
// -- mount helpers
fn mount_text_dialog(&mut self, id: &str, text: &str, color: Color) {
// Mount
self.view.mount(
id,
Box::new(Paragraph::new(
ParagraphPropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Thick, color)
.with_foreground(color)
.bold()
.with_text_alignment(Alignment::Center)
.with_texts(vec![TextSpan::from(text)])
.build(),
)),
);
// Give focus to error
self.view.active(id);
}
fn mount_radio_dialog(
&mut self,
id: &str,
text: &str,
opts: &[&str],
default: usize,
color: Color,
) {
self.view.mount(
id,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, color)
.with_title(text, Alignment::Center)
.with_options(opts)
.with_value(default)
.rewind(true)
.build(),
)),
);
// Active
self.view.active(id);
}
fn mount_radio(&mut self, id: &str, text: &str, opts: &[&str], default: usize, color: Color) {
self.view.mount(
id,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(color)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, color)
.with_title(text, Alignment::Left)
.with_options(opts)
.with_value(default)
.rewind(true)
.build(),
)),
);
}
fn mount_input(&mut self, id: &str, label: &str, fg: Color, typ: InputType) {
self.mount_input_ex(id, label, fg, typ, None, None);
}
fn mount_input_ex(
&mut self,
id: &str,
label: &str,
fg: Color,
typ: InputType,
len: Option<usize>,
value: Option<String>,
) {
let mut props = InputPropsBuilder::default();
props
.with_foreground(fg)
.with_borders(Borders::ALL, BorderType::Rounded, fg)
.with_label(label, Alignment::Left)
.with_input(typ);
if let Some(len) = len {
props.with_input_len(len);
}
if let Some(value) = value {
props.with_value(value);
}
self.view.mount(id, Box::new(Input::new(props.build())));
}
}

View file

@ -27,16 +27,14 @@
* SOFTWARE.
*/
// Locals
use super::{Context, SetupActivity};
use super::{Context, InputType, SetupActivity};
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
use crate::ui::components::bytes::{Bytes, BytesPropsBuilder};
use crate::utils::ui::draw_area_in;
// Ext
use std::path::PathBuf;
use tui_realm_stdlib::{
input::{Input, InputPropsBuilder},
radio::{Radio, RadioPropsBuilder},
};
use tui_realm_stdlib::{InputPropsBuilder, RadioPropsBuilder};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
@ -62,95 +60,74 @@ impl SetupActivity {
// Footer
self.mount_footer();
// Input fields
self.view.mount(
self.mount_input(
super::COMPONENT_INPUT_TEXT_EDITOR,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightGreen)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
.with_label("Text editor", Alignment::Left)
.build(),
)),
"Text editor",
Color::LightGreen,
InputType::Text,
);
self.view.active(super::COMPONENT_INPUT_TEXT_EDITOR); // <-- Focus
self.view.mount(
self.mount_radio(
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightCyan)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightCyan)
.with_title("Default file transfer protocol", Alignment::Left)
.with_options(&[
String::from("SFTP"),
String::from("SCP"),
String::from("FTP"),
String::from("FTPS"),
])
.rewind(true)
.build(),
)),
"Default protocol",
&["SFTP", "SCP", "FTP", "FTPS", "AWS S3"],
0,
Color::LightCyan,
);
self.view.mount(
self.mount_radio(
super::COMPONENT_RADIO_HIDDEN_FILES,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_title("Show hidden files (by default)?", Alignment::Left)
.with_options(&[String::from("Yes"), String::from("No")])
.rewind(true)
.build(),
)),
"Show hidden files (by default)?",
&["Yes", "No"],
0,
Color::LightRed,
);
self.view.mount(
self.mount_radio(
super::COMPONENT_RADIO_UPDATES,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightYellow)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_title("Check for updates?", Alignment::Left)
.with_options(&[String::from("Yes"), String::from("No")])
.rewind(true)
.build(),
)),
"Check for updates?",
&["Yes", "No"],
0,
Color::LightYellow,
);
self.view.mount(
self.mount_radio(
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
"Prompt when replacing existing files?",
&["Yes", "No"],
0,
Color::LightCyan,
);
self.mount_radio(
super::COMPONENT_RADIO_GROUP_DIRS,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightMagenta)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightMagenta)
.with_title("Group directories", Alignment::Left)
.with_options(&[
String::from("Display first"),
String::from("Display Last"),
String::from("No"),
])
.rewind(true)
.build(),
)),
"Group directories",
&["Display first", "Display last", "No"],
0,
Color::LightMagenta,
);
self.view.mount(
self.mount_input(
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightBlue)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightBlue)
.with_label("File formatter syntax (local)", Alignment::Left)
.build(),
)),
"File formatter syntax (local)",
Color::LightGreen,
InputType::Text,
);
self.mount_input(
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
"File formatter syntax (remote)",
Color::LightCyan,
InputType::Text,
);
self.mount_radio(
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
"Enable notifications?",
&["Yes", "No"],
0,
Color::LightRed,
);
self.view.mount(
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
Box::new(Input::new(
InputPropsBuilder::default()
.with_foreground(Color::LightGreen)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightGreen)
.with_label("File formatter syntax (remote)", Alignment::Left)
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
Box::new(Bytes::new(
BytesPropsBuilder::default()
.with_foreground(Color::LightYellow)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightYellow)
.with_label("Notifications: minimum transfer size", Alignment::Left)
.build(),
)),
);
@ -168,7 +145,7 @@ impl SetupActivity {
.constraints(
[
Constraint::Length(3), // Current tab
Constraint::Length(21), // Main body
Constraint::Length(18), // Main body
Constraint::Length(3), // Help footer
]
.as_ref(),
@ -177,8 +154,13 @@ impl SetupActivity {
// Render common widget
self.view.render(super::COMPONENT_RADIO_TAB, f, chunks[0]);
self.view.render(super::COMPONENT_TEXT_FOOTER, f, chunks[2]);
// Make chunks
// Make chunks (two columns)
let ui_cfg_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]);
// Column 1
let ui_cfg_chunks_col1 = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
@ -186,27 +168,67 @@ impl SetupActivity {
Constraint::Length(3), // Protocol tab
Constraint::Length(3), // Hidden files
Constraint::Length(3), // Updates tab
Constraint::Length(3), // Prompt file replace
Constraint::Length(3), // Group dirs
Constraint::Length(3), // Local Format input
Constraint::Length(3), // Remote Format input
]
.as_ref(),
)
.split(chunks[1]);
.split(ui_cfg_chunks[0]);
self.view
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks[0]);
.render(super::COMPONENT_INPUT_TEXT_EDITOR, f, ui_cfg_chunks_col1[0]);
self.view.render(
super::COMPONENT_RADIO_DEFAULT_PROTOCOL,
f,
ui_cfg_chunks_col1[1],
);
self.view.render(
super::COMPONENT_RADIO_HIDDEN_FILES,
f,
ui_cfg_chunks_col1[2],
);
self.view
.render(super::COMPONENT_RADIO_DEFAULT_PROTOCOL, f, ui_cfg_chunks[1]);
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks_col1[3]);
self.view.render(
super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE,
f,
ui_cfg_chunks_col1[4],
);
self.view
.render(super::COMPONENT_RADIO_HIDDEN_FILES, f, ui_cfg_chunks[2]);
self.view
.render(super::COMPONENT_RADIO_UPDATES, f, ui_cfg_chunks[3]);
self.view
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks[4]);
self.view
.render(super::COMPONENT_INPUT_LOCAL_FILE_FMT, f, ui_cfg_chunks[5]);
self.view
.render(super::COMPONENT_INPUT_REMOTE_FILE_FMT, f, ui_cfg_chunks[6]);
.render(super::COMPONENT_RADIO_GROUP_DIRS, f, ui_cfg_chunks_col1[5]);
// Column 2
let ui_cfg_chunks_col2 = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length(3), // Local Format input
Constraint::Length(3), // Remote Format input
Constraint::Length(3), // Notifications enabled
Constraint::Length(3), // Notifications threshold
Constraint::Length(1), // Filler
]
.as_ref(),
)
.split(ui_cfg_chunks[1]);
self.view.render(
super::COMPONENT_INPUT_LOCAL_FILE_FMT,
f,
ui_cfg_chunks_col2[0],
);
self.view.render(
super::COMPONENT_INPUT_REMOTE_FILE_FMT,
f,
ui_cfg_chunks_col2[1],
);
self.view.render(
super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED,
f,
ui_cfg_chunks_col2[2],
);
self.view.render(
super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD,
f,
ui_cfg_chunks_col2[3],
);
// Popups
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_ERROR) {
if props.visible {
@ -265,6 +287,7 @@ impl SetupActivity {
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(false) => 2,
FileTransferProtocol::Ftp(true) => 3,
FileTransferProtocol::AwsS3 => 4,
};
let props = RadioPropsBuilder::from(props).with_value(protocol).build();
let _ = self
@ -289,6 +312,20 @@ impl SetupActivity {
let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self.view.update(super::COMPONENT_RADIO_UPDATES, props);
}
// File replace
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE)
{
let updates: usize = match self.config().get_prompt_on_file_replace() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(updates).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE, props);
}
// Group dirs
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_GROUP_DIRS) {
let dirs: usize = match self.config().get_group_dirs() {
@ -315,6 +352,31 @@ impl SetupActivity {
.view
.update(super::COMPONENT_INPUT_REMOTE_FILE_FMT, props);
}
// Notifications enabled
if let Some(props) = self
.view
.get_props(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
{
let enabled: usize = match self.config().get_notifications() {
true => 0,
false => 1,
};
let props = RadioPropsBuilder::from(props).with_value(enabled).build();
let _ = self
.view
.update(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED, props);
}
// Notifications threshold
if let Some(props) = self
.view
.get_props(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
{
let value: u64 = self.config().get_notification_threshold();
let props = BytesPropsBuilder::from(props).with_value(value).build();
let _ = self
.view
.update(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD, props);
}
}
/// ### collect_input_values
@ -334,6 +396,7 @@ impl SetupActivity {
1 => FileTransferProtocol::Scp,
2 => FileTransferProtocol::Ftp(false),
3 => FileTransferProtocol::Ftp(true),
4 => FileTransferProtocol::AwsS3,
_ => FileTransferProtocol::Sftp,
};
self.config_mut().set_default_protocol(protocol);
@ -350,6 +413,13 @@ impl SetupActivity {
let check: bool = matches!(opt, 0);
self.config_mut().set_check_for_updates(check);
}
if let Some(Payload::One(Value::Usize(opt))) = self
.view
.get_state(super::COMPONENT_RADIO_PROMPT_ON_FILE_REPLACE)
{
let check: bool = matches!(opt, 0);
self.config_mut().set_prompt_on_file_replace(check);
}
if let Some(Payload::One(Value::Str(fmt))) =
self.view.get_state(super::COMPONENT_INPUT_LOCAL_FILE_FMT)
{
@ -370,5 +440,17 @@ impl SetupActivity {
};
self.config_mut().set_group_dirs(dirs);
}
if let Some(Payload::One(Value::Usize(opt))) = self
.view
.get_state(super::COMPONENT_RADIO_NOTIFICATIONS_ENABLED)
{
self.config_mut().set_notifications(opt == 0);
}
if let Some(Payload::One(Value::U64(bytes))) = self
.view
.get_state(super::COMPONENT_INPUT_NOTIFICATIONS_THRESHOLD)
{
self.config_mut().set_notification_threshold(bytes);
}
}
}

View file

@ -31,10 +31,7 @@ use super::{Context, SetupActivity};
use crate::ui::components::bookmark_list::{BookmarkList, BookmarkListPropsBuilder};
use crate::utils::ui::draw_area_in;
// Ext
use tui_realm_stdlib::{
input::{Input, InputPropsBuilder},
radio::{Radio, RadioPropsBuilder},
};
use tui_realm_stdlib::{Input, InputPropsBuilder};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
@ -172,22 +169,13 @@ impl SetupActivity {
///
/// Mount delete ssh key component
pub(crate) fn mount_del_ssh_key(&mut self) {
self.view.mount(
self.mount_radio_dialog(
super::COMPONENT_RADIO_DEL_SSH_KEY,
Box::new(Radio::new(
RadioPropsBuilder::default()
.with_color(Color::LightRed)
.with_inverted_color(Color::Black)
.with_borders(Borders::ALL, BorderType::Rounded, Color::LightRed)
.with_title("Delete key?", Alignment::Center)
.with_options(&[String::from("Yes"), String::from("No")])
.with_value(1) // Default: No
.rewind(true)
.build(),
)),
"Delete key?",
&["Yes", "No"],
1,
Color::LightRed,
);
// Active
self.view.active(super::COMPONENT_RADIO_DEL_SSH_KEY);
}
/// ### umount_del_ssh_key

View file

@ -33,7 +33,7 @@ use crate::ui::components::color_picker::{ColorPicker, ColorPickerPropsBuilder};
use crate::utils::parser::parse_color;
use crate::utils::ui::draw_area_in;
// Ext
use tui_realm_stdlib::label::{Label, LabelPropsBuilder};
use tui_realm_stdlib::{Label, LabelPropsBuilder};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
style::Color,
@ -70,6 +70,7 @@ impl SetupActivity {
// Misc
self.mount_title(super::COMPONENT_COLOR_MISC_TITLE, "Misc styles");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_ERROR, "Error");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_INFO, "Info dialogs");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_INPUT, "Input fields");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_KEYS, "Key strokes");
self.mount_color_picker(super::COMPONENT_COLOR_MISC_QUIT, "Quit dialogs");
@ -222,12 +223,12 @@ impl SetupActivity {
[
Constraint::Length(1), // Title
Constraint::Length(3), // Error
Constraint::Length(3), // Info
Constraint::Length(3), // Input
Constraint::Length(3), // Keys
Constraint::Length(3), // Quit
Constraint::Length(3), // Save
Constraint::Length(3), // Warn
Constraint::Length(3), // Empty
]
.as_ref(),
)
@ -237,15 +238,17 @@ impl SetupActivity {
self.view
.render(super::COMPONENT_COLOR_MISC_ERROR, f, misc_colors_layout[1]);
self.view
.render(super::COMPONENT_COLOR_MISC_INPUT, f, misc_colors_layout[2]);
.render(super::COMPONENT_COLOR_MISC_INFO, f, misc_colors_layout[2]);
self.view
.render(super::COMPONENT_COLOR_MISC_KEYS, f, misc_colors_layout[3]);
.render(super::COMPONENT_COLOR_MISC_INPUT, f, misc_colors_layout[3]);
self.view
.render(super::COMPONENT_COLOR_MISC_QUIT, f, misc_colors_layout[4]);
.render(super::COMPONENT_COLOR_MISC_KEYS, f, misc_colors_layout[4]);
self.view
.render(super::COMPONENT_COLOR_MISC_SAVE, f, misc_colors_layout[5]);
.render(super::COMPONENT_COLOR_MISC_QUIT, f, misc_colors_layout[5]);
self.view
.render(super::COMPONENT_COLOR_MISC_WARN, f, misc_colors_layout[6]);
.render(super::COMPONENT_COLOR_MISC_SAVE, f, misc_colors_layout[6]);
self.view
.render(super::COMPONENT_COLOR_MISC_WARN, f, misc_colors_layout[7]);
let transfer_colors_layout_col1 = Layout::default()
.direction(Direction::Vertical)
@ -405,6 +408,7 @@ impl SetupActivity {
self.update_color(super::COMPONENT_COLOR_AUTH_RECENTS, theme.auth_recents);
self.update_color(super::COMPONENT_COLOR_AUTH_USERNAME, theme.auth_username);
self.update_color(super::COMPONENT_COLOR_MISC_ERROR, theme.misc_error_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_INFO, theme.misc_info_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_INPUT, theme.misc_input_dialog);
self.update_color(super::COMPONENT_COLOR_MISC_KEYS, theme.misc_keys);
self.update_color(super::COMPONENT_COLOR_MISC_QUIT, theme.misc_quit_dialog);
@ -495,6 +499,9 @@ impl SetupActivity {
let misc_error_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_ERROR)
.map_err(|_| super::COMPONENT_COLOR_MISC_ERROR)?;
let misc_info_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_INFO)
.map_err(|_| super::COMPONENT_COLOR_MISC_INFO)?;
let misc_input_dialog: Color = self
.get_color(super::COMPONENT_COLOR_MISC_INPUT)
.map_err(|_| super::COMPONENT_COLOR_MISC_INPUT)?;
@ -560,6 +567,7 @@ impl SetupActivity {
theme.auth_recents = auth_recents;
theme.auth_username = auth_username;
theme.misc_error_dialog = misc_error_dialog;
theme.misc_info_dialog = misc_info_dialog;
theme.misc_input_dialog = misc_input_dialog;
theme.misc_keys = misc_keys;
theme.misc_quit_dialog = misc_quit_dialog;

310
src/ui/components/bytes.rs Normal file
View file

@ -0,0 +1,310 @@
//! ## Bytes
//!
//! `Bytes` component extends an `Input` component in order to provide an input type for byte size.
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
use crate::utils::fmt::fmt_bytes;
use crate::utils::parser::parse_bytesize;
// ext
use tui_realm_stdlib::{Input, InputPropsBuilder};
use tuirealm::event::Event;
use tuirealm::props::{Alignment, Props, PropsBuilder};
use tuirealm::tui::{
layout::Rect,
style::Color,
widgets::{BorderType, Borders},
};
use tuirealm::{Component, Frame, Msg, Payload, Value};
// -- props
/// ## BytesPropsBuilder
///
/// A wrapper around an `InputPropsBuilder`
pub struct BytesPropsBuilder {
puppet: InputPropsBuilder,
}
impl Default for BytesPropsBuilder {
fn default() -> Self {
Self {
puppet: InputPropsBuilder::default(),
}
}
}
impl PropsBuilder for BytesPropsBuilder {
fn build(&mut self) -> Props {
self.puppet.build()
}
fn hidden(&mut self) -> &mut Self {
self.puppet.hidden();
self
}
fn visible(&mut self) -> &mut Self {
self.puppet.visible();
self
}
}
impl From<Props> for BytesPropsBuilder {
fn from(props: Props) -> Self {
BytesPropsBuilder {
puppet: InputPropsBuilder::from(props),
}
}
}
impl BytesPropsBuilder {
/// ### with_borders
///
/// Set component borders style
pub fn with_borders(
&mut self,
borders: Borders,
variant: BorderType,
color: Color,
) -> &mut Self {
self.puppet.with_borders(borders, variant, color);
self
}
/// ### with_label
///
/// Set input label
pub fn with_label<S: AsRef<str>>(&mut self, label: S, alignment: Alignment) -> &mut Self {
self.puppet.with_label(label, alignment);
self
}
/// ### with_color
///
/// Set initial value for component
pub fn with_foreground(&mut self, color: Color) -> &mut Self {
self.puppet.with_foreground(color);
self
}
/// ### with_color
///
/// Set initial value for component
pub fn with_value(&mut self, val: u64) -> &mut Self {
self.puppet.with_value(fmt_bytes(val));
self
}
}
// -- component
/// ## Bytes
///
/// a wrapper component of `Input` which adds a superset of rules to behave as a color picker
pub struct Bytes {
input: Input,
native_color: Color,
}
impl Bytes {
/// ### new
///
/// Instantiate a new `Bytes`
pub fn new(props: Props) -> Self {
// Instantiate a new color picker using input
Self {
native_color: props.foreground,
input: Input::new(props),
}
}
/// ### update_colors
///
/// Update colors to match selected color, with provided one
fn update_colors(&mut self, color: Color) {
let mut props = self.get_props();
props.foreground = color;
props.borders.color = color;
let _ = self.input.update(props);
}
}
impl Component for Bytes {
/// ### render
///
/// Based on the current properties and states, renders a widget using the provided render engine in the provided Area
/// If focused, cursor is also set (if supported by widget)
#[cfg(not(tarpaulin_include))]
fn render(&self, render: &mut Frame, area: Rect) {
self.input.render(render, area);
}
/// ### update
///
/// Update component properties
/// Properties should first be retrieved through `get_props` which creates a builder from
/// existing properties and then edited before calling update.
/// Returns a Msg to the view
fn update(&mut self, props: Props) -> Msg {
let msg: Msg = self.input.update(props);
match msg {
Msg::OnChange(Payload::One(Value::Str(input))) => {
match parse_bytesize(input.as_str()) {
Some(bytes) => {
// return OK
self.update_colors(self.native_color);
Msg::OnChange(Payload::One(Value::U64(bytes.as_u64())))
}
None => {
// Invalid color
self.update_colors(Color::Red);
Msg::None
}
}
}
msg => msg,
}
}
/// ### get_props
///
/// Returns a props builder starting from component properties.
/// This returns a prop builder in order to make easier to create
/// new properties for the element.
fn get_props(&self) -> Props {
self.input.get_props()
}
/// ### on
///
/// Handle input event and update internal states.
/// Returns a Msg to the view
fn on(&mut self, ev: Event) -> Msg {
// Capture message from input
match self.input.on(ev) {
Msg::OnChange(Payload::One(Value::Str(input))) => {
// Capture color and validate
match parse_bytesize(input.as_str()) {
Some(bytes) => {
// Update color and return OK
self.update_colors(self.native_color);
Msg::OnChange(Payload::One(Value::U64(bytes.as_u64())))
}
None => {
// Invalid color
self.update_colors(Color::Red);
Msg::None
}
}
}
Msg::OnSubmit(_) => Msg::None,
msg => msg,
}
}
/// ### get_state
///
/// Get current state from component
/// For this component returns Unsigned if the input type is a number, otherwise a text
/// The value is always the current input.
fn get_state(&self) -> Payload {
match self.input.get_state() {
Payload::One(Value::Str(bytes)) => match parse_bytesize(bytes.as_str()) {
None => Payload::None,
Some(bytes) => Payload::One(Value::U64(bytes.as_u64())),
},
_ => Payload::None,
}
}
// -- events
/// ### blur
///
/// Blur component; basically remove focus
fn blur(&mut self) {
self.input.blur();
}
/// ### active
///
/// Active component; basically give focus
fn active(&mut self) {
self.input.active();
}
}
#[cfg(test)]
mod test {
use super::*;
use crossterm::event::{KeyCode, KeyEvent};
use pretty_assertions::assert_eq;
#[test]
fn bytes_input() {
let mut component: Bytes = Bytes::new(
BytesPropsBuilder::default()
.visible()
.with_value(1024)
.with_borders(Borders::ALL, BorderType::Double, Color::Rgb(204, 170, 0))
.with_label("omar", Alignment::Left)
.with_foreground(Color::Red)
.build(),
);
// Focus
component.blur();
component.active();
// Get value
assert_eq!(component.get_state(), Payload::One(Value::U64(1024)));
// Set an invalid color
let props = InputPropsBuilder::from(component.get_props())
.with_value(String::from("#pippo1"))
.hidden()
.build();
assert_eq!(component.update(props), Msg::None);
assert_eq!(component.get_state(), Payload::None);
// Reset color
let props = BytesPropsBuilder::from(component.get_props())
.with_value(111)
.hidden()
.build();
assert_eq!(
component.update(props),
Msg::OnChange(Payload::One(Value::U64(111)))
);
// Backspace (invalid)
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Backspace))),
Msg::None
);
// Press '1'
assert_eq!(
component.on(Event::Key(KeyEvent::from(KeyCode::Char('B')))),
Msg::OnChange(Payload::One(Value::U64(111)))
);
}
}

View file

@ -30,7 +30,7 @@
use crate::utils::fmt::fmt_color;
use crate::utils::parser::parse_color;
// ext
use tui_realm_stdlib::input::{Input, InputPropsBuilder};
use tui_realm_stdlib::{Input, InputPropsBuilder};
use tuirealm::event::Event;
use tuirealm::props::{Alignment, Props, PropsBuilder};
use tuirealm::tui::{

View file

@ -188,21 +188,27 @@ impl OwnStates {
/// ### incr_list_index
///
/// Incremenet list index
pub fn incr_list_index(&mut self) {
/// Incremenet list index.
/// If `can_rewind` is `true` the index rewinds when boundary is reached
pub fn incr_list_index(&mut self, can_rewind: bool) {
// Check if index is at last element
if self.list_index + 1 < self.list_len() {
self.list_index += 1;
} else if can_rewind {
self.list_index = 0;
}
}
/// ### decr_list_index
///
/// Decrement list index
pub fn decr_list_index(&mut self) {
/// If `can_rewind` is `true` the index rewinds when boundary is reached
pub fn decr_list_index(&mut self, can_rewind: bool) {
// Check if index is bigger than 0
if self.list_index > 0 {
self.list_index -= 1;
} else if self.list_len() > 0 && can_rewind {
self.list_index = self.list_len() - 1;
}
}
@ -388,25 +394,25 @@ impl Component for FileList {
match key.code {
KeyCode::Down => {
// Update states
self.states.incr_list_index();
self.states.incr_list_index(true);
Msg::None
}
KeyCode::Up => {
// Update states
self.states.decr_list_index();
self.states.decr_list_index(true);
Msg::None
}
KeyCode::PageDown => {
// Update states
for _ in 0..8 {
self.states.incr_list_index();
self.states.incr_list_index(false);
}
Msg::None
}
KeyCode::PageUp => {
// Update states
for _ in 0..8 {
self.states.decr_list_index();
self.states.decr_list_index(false);
}
Msg::None
}
@ -525,14 +531,21 @@ mod tests {
assert_eq!(states.selected[0], 4);
// Index
states.init_list_states(2);
states.incr_list_index();
// Incr
states.incr_list_index(false);
assert_eq!(states.list_index(), 1);
states.incr_list_index();
states.incr_list_index(false);
assert_eq!(states.list_index(), 1);
states.decr_list_index();
states.incr_list_index(true);
assert_eq!(states.list_index(), 0);
states.decr_list_index();
// Decr
states.list_index = 1;
states.decr_list_index(false);
assert_eq!(states.list_index(), 0);
states.decr_list_index(false);
assert_eq!(states.list_index(), 0);
states.decr_list_index(true);
assert_eq!(states.list_index(), 1);
// Try fixing index
states.init_list_states(5);
states.list_index = 4;

View file

@ -27,6 +27,7 @@
*/
// exports
pub mod bookmark_list;
pub mod bytes;
pub mod color_picker;
pub mod file_list;
pub mod logbox;

View file

@ -33,9 +33,12 @@ use crate::system::config_client::ConfigClient;
use crate::system::theme_provider::ThemeProvider;
// Includes
use crossterm::event::DisableMouseCapture;
use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
#[cfg(target_family = "unix")]
use crossterm::{
event::DisableMouseCapture,
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
};
use std::io::{stdout, Stdout};
use tuirealm::tui::backend::CrosstermBackend;
use tuirealm::tui::Terminal;
@ -64,15 +67,12 @@ impl Context {
theme_provider: ThemeProvider,
error: Option<String>,
) -> Context {
// Create terminal
let mut stdout = stdout();
assert!(execute!(stdout, EnterAlternateScreen).is_ok());
Context {
ft_params: None,
config_client,
store: Store::init(),
input_hnd: InputHandler::new(),
terminal: Terminal::new(CrosstermBackend::new(stdout)).unwrap(),
terminal: Terminal::new(CrosstermBackend::new(Self::stdout())).unwrap(),
theme_provider,
error,
}
@ -141,7 +141,7 @@ impl Context {
/// ### enter_alternate_screen
///
/// Enter alternate screen (gui window)
#[cfg(not(target_os = "windows"))]
#[cfg(target_family = "unix")]
pub fn enter_alternate_screen(&mut self) {
match execute!(
self.terminal.backend_mut(),
@ -153,9 +153,16 @@ impl Context {
}
}
/// ### enter_alternate_screen
///
/// Enter alternate screen (gui window)
#[cfg(target_family = "windows")]
pub fn enter_alternate_screen(&self) {}
/// ### leave_alternate_screen
///
/// Go back to normal screen (gui window)
#[cfg(target_family = "unix")]
pub fn leave_alternate_screen(&mut self) {
match execute!(
self.terminal.backend_mut(),
@ -167,6 +174,12 @@ impl Context {
}
}
/// ### leave_alternate_screen
///
/// Go back to normal screen (gui window)
#[cfg(target_family = "windows")]
pub fn leave_alternate_screen(&self) {}
/// ### clear_screen
///
/// Clear terminal screen
@ -176,6 +189,18 @@ impl Context {
Ok(_) => info!("Cleared screen"),
}
}
#[cfg(target_family = "unix")]
fn stdout() -> Stdout {
let mut stdout = stdout();
assert!(execute!(stdout, EnterAlternateScreen).is_ok());
stdout
}
#[cfg(target_family = "windows")]
fn stdout() -> Stdout {
stdout()
}
}
impl Drop for Context {

View file

@ -67,6 +67,7 @@ impl Store {
}
// -- getters
/// ### get_string
///
/// Get string from store
@ -168,6 +169,58 @@ impl Store {
pub fn set(&mut self, key: &str) {
self.store.insert(key.to_string(), StoreState::Flag);
}
// -- Consumers
/// ### take_string
///
/// Take string from store
pub fn take_string(&mut self, key: &str) -> Option<String> {
match self.store.remove(key) {
Some(StoreState::Str(s)) => Some(s),
_ => None,
}
}
/// ### take_signed
///
/// Take signed from store
pub fn take_signed(&mut self, key: &str) -> Option<isize> {
match self.store.remove(key) {
Some(StoreState::Signed(i)) => Some(i),
_ => None,
}
}
/// ### take_unsigned
///
/// Take unsigned from store
pub fn take_unsigned(&mut self, key: &str) -> Option<usize> {
match self.store.remove(key) {
Some(StoreState::Unsigned(u)) => Some(u),
_ => None,
}
}
/// ### get_float
///
/// Take float from store
pub fn take_float(&mut self, key: &str) -> Option<f64> {
match self.store.remove(key) {
Some(StoreState::Float(f)) => Some(f),
_ => None,
}
}
/// ### get_boolean
///
/// Take boolean from store
pub fn take_boolean(&mut self, key: &str) -> Option<bool> {
match self.store.remove(key) {
Some(StoreState::Boolean(b)) => Some(b),
_ => None,
}
}
}
#[cfg(test)]
@ -184,20 +237,30 @@ mod tests {
// Test string
store.set_string("test", String::from("hello"));
assert_eq!(*store.get_string("test").as_ref().unwrap(), "hello");
assert_eq!(store.take_string("test").unwrap(), "hello".to_string());
assert_eq!(store.take_string("test"), None);
// Test isize
store.set_signed("number", 3005);
assert_eq!(store.get_signed("number").unwrap(), 3005);
assert_eq!(store.take_signed("number").unwrap(), 3005);
assert_eq!(store.take_signed("number"), None);
store.set_signed("number", -123);
assert_eq!(store.get_signed("number").unwrap(), -123);
// Test usize
store.set_unsigned("unumber", 1024);
assert_eq!(store.get_unsigned("unumber").unwrap(), 1024);
assert_eq!(store.take_unsigned("unumber").unwrap(), 1024);
assert_eq!(store.take_unsigned("unumber"), None);
// Test float
store.set_float("float", 3.33);
assert_eq!(store.get_float("float").unwrap(), 3.33);
assert_eq!(store.take_float("float").unwrap(), 3.33);
assert_eq!(store.take_float("float"), None);
// Test boolean
store.set_boolean("bool", true);
assert_eq!(store.get_boolean("bool").unwrap(), true);
assert_eq!(store.take_boolean("bool").unwrap(), true);
assert_eq!(store.take_boolean("bool"), None);
// Test flag
store.set("myflag");
assert_eq!(store.isset("myflag"), true);

View file

@ -287,6 +287,25 @@ pub fn shadow_password(s: &str) -> String {
(0..s.len()).map(|_| '*').collect()
}
/// ### fmt_bytes
///
/// Format bytes
pub fn fmt_bytes(v: u64) -> String {
if v >= 1125899906842624 {
format!("{} PB", v / 1125899906842624)
} else if v >= 1099511627776 {
format!("{} TB", v / 1099511627776)
} else if v >= 1073741824 {
format!("{} GB", v / 1073741824)
} else if v >= 1048576 {
format!("{} MB", v / 1048576)
} else if v >= 1024 {
format!("{} KB", v / 1024)
} else {
format!("{} B", v)
}
}
#[cfg(test)]
mod tests {
@ -599,4 +618,14 @@ mod tests {
fn test_utils_fmt_shadow_password() {
assert_eq!(shadow_password("foobar"), String::from("******"));
}
#[test]
fn format_bytes() {
assert_eq!(fmt_bytes(110).as_str(), "110 B");
assert_eq!(fmt_bytes(2048).as_str(), "2 KB");
assert_eq!(fmt_bytes(2097152).as_str(), "2 MB");
assert_eq!(fmt_bytes(4294967296).as_str(), "4 GB");
assert_eq!(fmt_bytes(3298534883328).as_str(), "3 TB");
assert_eq!(fmt_bytes(3377699720527872).as_str(), "3 PB");
}
}

View file

@ -1,95 +0,0 @@
//! ## git
//!
//! `git` is the module which provides utilities to interact through the GIT API and to perform some stuff at git level
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Locals
use super::parser::parse_semver;
// Others
use serde::Deserialize;
#[derive(Debug, Deserialize)]
/// ## GithubTag
///
/// Info related to a github tag
pub struct GithubTag {
pub tag_name: String,
pub body: 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<GithubTag>), 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<GithubTag>, String> {
// Send request
let github_tag: Result<GithubTag, String> =
match ureq::get("https://api.github.com/repos/veeso/termscp/releases/latest").call() {
Ok(response) => response.into_json::<GithubTag>().map_err(|x| x.to_string()),
Err(err) => Err(err.to_string()),
};
// Check version
match github_tag {
Err(err) => Err(err),
Ok(tag) => {
// Parse version
match parse_semver(tag.tag_name.as_str()) {
Some(new_version) => {
// Check if version is different
if new_version.as_str() > current_version {
Ok(Some(tag)) // 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]
#[cfg(not(all(
any(
target_os = "macos",
target_os = "freebsd",
target_os = "netbsd",
target_os = "netbsd"
),
feature = "github-actions"
)))]
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());
}
}

Some files were not shown because too many files have changed in this diff Show more