27 Commits

Author SHA1 Message Date
veeso
bd99665d1c AUR ok 2021-02-28 21:23:22 +01:00
veeso
93bab299ec debian 8 is too old? 2021-02-28 21:07:51 +01:00
veeso
eb33f93322 rpm name changed 2021-02-28 16:24:24 +01:00
veeso
d165b699f0 Removed archlinux from build (it doesn't work anymore 2021-02-28 16:01:07 +01:00
veeso
7817295d8e The archlinux image is so broken 2021-02-28 15:12:34 +01:00
veeso
a986e531d9 Docker is broken 2021-02-28 15:11:08 +01:00
veeso
8a3b652dcd It seems they broke the archlinux docker image with latest version (gg) 2021-02-28 15:07:44 +01:00
Christian Visintin
efbea63154 Merge pull request #7 from veeso/fetch-new-release
Check for updates through Github API
2021-02-28 14:47:55 +01:00
veeso
61045fa548 Check for updates OK 2021-02-28 13:10:59 +01:00
veeso
da5e1f315d Show new version available in auth activity 2021-02-28 13:01:51 +01:00
veeso
85c57ce027 Handle check for updates in setup activity 2021-02-28 12:47:55 +01:00
veeso
6682c07eb6 Added check_for_updates to config 2021-02-28 12:44:00 +01:00
veeso
4e887c3429 Git: check for new updates (utils) 2021-02-28 12:33:12 +01:00
veeso
6435271be8 Parse semver util 2021-02-28 12:21:28 +01:00
veeso
c9a77fa65d 0.3.3 is ready for release I guess 2021-02-27 20:57:07 +01:00
veeso
cc5399d36e Cargo clippy 2021-02-27 20:49:20 +01:00
veeso
ca1aa5675a Try test threads: 1 2021-02-27 20:30:50 +01:00
ChristianVisintin
e21bfbbd14 Use a regex to parse the remote host args 2021-02-26 16:56:03 +01:00
ChristianVisintin
e948d598b0 Convert to lowercase when sorting bookmarks 2021-02-26 08:13:38 +01:00
Christian Visintin
0173d67a3b Merge pull request #6 from veeso/fmt-props
Format key attributes
2021-02-25 20:08:01 +01:00
veeso
025547a3dc Format key attributes 2021-02-25 17:47:50 +01:00
veeso
af830d603d Now bookmarks and recents are sorted in the UI (bookmarks are sorted by name; recents are sorted by connection datetime) 2021-02-25 16:15:06 +01:00
veeso
669fd23868 Support for older distributions 2021-02-25 14:43:27 +01:00
veeso
4ff7fc079c Added CLI options to set starting working directory on both local and remote hosts 2021-02-25 14:27:34 +01:00
ChristianVisintin
7f24d6db5c Default choice for deleting file set to NO (way too easy to delete files by mistake) 2021-02-16 12:53:28 +01:00
ChristianVisintin
8c8d01c29c working on 0.3.3 2021-02-16 12:50:04 +01:00
ChristianVisintin
d23e6bb60c centos7 dockerfile 2021-01-28 12:03:11 +01:00
33 changed files with 1276 additions and 419 deletions

View File

@@ -14,6 +14,6 @@ jobs:
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
run: cargo test --verbose -- --test-threads 1
- name: Clippy
run: cargo clippy

View File

@@ -14,6 +14,6 @@ jobs:
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
run: cargo test --verbose -- --test-threads 1
- name: Clippy
run: cargo clippy

View File

@@ -1,6 +1,7 @@
# Changelog
- [Changelog](#changelog)
- [0.3.3](#033)
- [0.3.2](#032)
- [0.3.1](#031)
- [0.3.0](#030)
@@ -12,6 +13,23 @@
---
## 0.3.3
Released on 28/02/2021
- **Format key attributes**:
- Added `EXTRA` and `LENGTH` parameters to format keys.
- Now keys are provided with this syntax `{KEY_NAME[:LEN[:EXTRA]}`
- **Check for updates**:
- TermSCP will now check for updates on startup and will show in the main page if there is a new version available
- This feature may be disabled from setup (Check for updates => No)
- Enhancements:
- Default choice for deleting file set to "NO" (way too easy to delete files by mistake)
- Added CLI options to set starting workind directory on both local and remote hosts
- Parse remote host now uses a Regex to gather parts (increased stability).
- Now bookmarks and recents are sorted in the UI (bookmarks are sorted by name; recents are sorted by connection datetime)
- Improved stability
## 0.3.2
Released on 24/01/2021

View File

@@ -9,6 +9,7 @@ Please note we have a [code of conduct](CODE_OF_CONDUCT.md), please follow it in
- [Developer contributions guide](#developer-contributions-guide)
- [How TermSCP works](#how-termscp-works)
- [Activities](#activities)
- [Tests fails due to receivers](#tests-fails-due-to-receivers)
- [Implementing File Transfers](#implementing-file-transfers)
---
@@ -65,6 +66,12 @@ This trait provides only 3 methods:
---
### Tests fails due to receivers
Yes. This happens quite often and is related to the fact that I'm using public SSH/SFTP/FTP server to test file receivers and sometimes this server go down for even a day or more. If your tests don't pass due to this, don't worry, submit the pull request and I'll take care of testing them by myself.
---
### Implementing File Transfers
This chapter describes how to implement a file transfer in TermSCP. A file transfer is a module which implements the `FileTransfer` trait. The file transfer provides different modules to interact with a remote server, which in addition to the most obvious methods, used to download and upload files, provides also methods to list files, delete files, create directories etc.

197
Cargo.lock generated
View File

@@ -193,6 +193,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "chunked_transfer"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e"
[[package]]
name = "cipher"
version = "0.2.5"
@@ -433,6 +439,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "ftp4"
version = "4.0.2"
@@ -535,6 +551,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "idna"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "instant"
version = "0.1.9"
@@ -544,6 +571,12 @@ dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "itoa"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "js-sys"
version = "0.3.46"
@@ -654,6 +687,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]]
name = "matches"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
[[package]]
name = "md-5"
version = "0.9.1"
@@ -891,6 +930,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pkg-config"
version = "0.3.19"
@@ -1055,6 +1100,21 @@ dependencies = [
"winapi",
]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin",
"untrusted",
"web-sys",
"winapi",
]
[[package]]
name = "rpassword"
version = "5.0.1"
@@ -1077,6 +1137,25 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rustls"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "064fd21ff87c6e87ed4506e68beb42459caa4a0e2eb144932e6776768556980b"
dependencies = [
"base64",
"log",
"ring",
"sct",
"webpki",
]
[[package]]
name = "ryu"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
[[package]]
name = "schannel"
version = "0.1.19"
@@ -1093,6 +1172,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "sct"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3042af939fca8c3453b7af0f1c66e533a15a86169e39de2657310ade8f98d3c"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "secret-service"
version = "1.1.3"
@@ -1175,6 +1264,17 @@ dependencies = [
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha2"
version = "0.9.2"
@@ -1231,6 +1331,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "ssh2"
version = "0.9.0"
@@ -1276,7 +1382,7 @@ dependencies = [
[[package]]
name = "termscp"
version = "0.3.2"
version = "0.3.3"
dependencies = [
"bitflags",
"bytesize",
@@ -1301,6 +1407,7 @@ dependencies = [
"toml",
"tui",
"unicode-width",
"ureq",
"users",
"whoami",
]
@@ -1346,6 +1453,21 @@ dependencies = [
"winapi",
]
[[package]]
name = "tinyvec"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "317cca572a0e89c3ce0ca1f1bdc9369547fe318a683418e42ac8f59d14701023"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "toml"
version = "0.5.8"
@@ -1374,6 +1496,24 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
[[package]]
name = "unicode-bidi"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5"
dependencies = [
"matches",
]
[[package]]
name = "unicode-normalization"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.7.1"
@@ -1392,6 +1532,42 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "ureq"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6585dcbf3483242f77b502864478ede62431baf3442b99367d3456ec20c1707b"
dependencies = [
"base64",
"chunked_transfer",
"log",
"once_cell",
"rustls",
"serde",
"serde_json",
"url",
"webpki",
"webpki-roots",
]
[[package]]
name = "url"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "users"
version = "0.11.0"
@@ -1490,6 +1666,25 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "webpki"
version = "0.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea"
dependencies = [
"ring",
"untrusted",
]
[[package]]
name = "webpki-roots"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82015b7e0b8bad8185994674a13a93306bea76cf5a16c5a181382fd3a5ec2376"
dependencies = [
"webpki",
]
[[package]]
name = "which"
version = "3.1.1"

View File

@@ -1,6 +1,6 @@
[package]
name = "termscp"
version = "0.3.2"
version = "0.3.3"
authors = ["Christian Visintin"]
edition = "2018"
license = "GPL-3.0"
@@ -38,6 +38,7 @@ textwrap = "0.13.1"
toml = "0.5.8"
tui = { version = "0.14.0", features = ["crossterm"], default-features = false }
unicode-width = "0.1.7"
ureq = { version = "2.0.2", features = ["json"] }
whoami = "1.1.0"
[target.'cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))'.dependencies]

View File

@@ -1,12 +1,12 @@
# TermSCP
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.3.2-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.3.3-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![Build](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Build](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions) [![codecov](https://codecov.io/gh/veeso/termscp/branch/main/graph/badge.svg?token=au67l7nQah)](https://codecov.io/gh/veeso/termscp)
~ Basically, WinSCP on a terminal ~
Developed by Christian Visintin
Current version: 0.3.2 (24/01/2021)
Current version: 0.3.3 (28/02/2021)
---
@@ -53,16 +53,16 @@ TermSCP is basically a porting of WinSCP to terminal. So basically is a terminal
### Why TermSCP 🤔
It happens quite often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me then to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there is midnight commander too, but actually I don't like it very much tbh (and hasn't a decent support for scp).
It happens quite often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there is midnight commander too, but actually I don't like it very much tbh (and hasn't a decent support for scp).
## Features 🎁
- Different communication protocols
- Different communication protocols support
- SFTP
- SCP
- FTP and FTPS
- Compatible with Windows, Linux, BSD and MacOS
- Practical user interface to explore and operate on the remote and on the local machine file system
- Handy user interface to explore and operate on the remote and on the local machine file system
- Bookmarks and recent connections can be saved to access quickly to your favourite hosts
- Supports text editors to view and edit text files
- Supports both SFTP/SCP authentication through SSH keys and username/password
@@ -98,8 +98,8 @@ Requirements:
### Deb package 📦
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.2_amd64.deb)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.2_amd64.deb`
Get `deb` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.3_amd64.deb)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp_0.3.3_amd64.deb`
then install through dpkg:
@@ -111,8 +111,8 @@ gdebi termscp_*.deb
### RPM package 📦
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.2-1.x86_64.rpm)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.2-1.x86_64.rpm`
Get `rpm` package from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.3-1.x86_64.rpm)
or run `wget https://github.com/veeso/termscp/releases/latest/download/termscp-0.3.3-1.x86_64.rpm`
then install through rpm:
@@ -122,7 +122,7 @@ rpm -U termscp_*.rpm
### AUR Package 🔼
On Arch Linux based distribution, you can install termscp using for example [yay](https://github.com/Jguer/yay), which I recommend to install AUR packages.
On Arch Linux based distribution, you can install termscp using for istance [yay](https://github.com/Jguer/yay), which I recommend to install AUR packages.
```sh
yay -S termscp
@@ -138,7 +138,7 @@ Start PowerShell as administrator and run
choco install termscp
```
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.3.2.nupkg)
Alternatively you can download the ZIP file from [HERE](https://github.com/veeso/termscp/releases/latest/download/termscp.0.3.3.nupkg)
and then with PowerShell started with administrator previleges, run:
@@ -163,6 +163,8 @@ brew install termscp
TermSCP can be started with the following options:
`termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]`
- `-P, --password <password>` if address is provided, password will be this argument
- `-v, --version` Print version info
- `-h, --help` Print help page
@@ -171,23 +173,25 @@ TermSCP can be started in two different mode, if no extra arguments is provided,
Alternatively, the user can provide an address as argument to skip the authentication form and starting directly the connection to the remote server.
If address argument is provided you can also provide the start working directory for local host
### Address argument 🌎
The address argument has the following syntax:
```txt
[protocol]://[username@]<address>[:port]
[protocol://][username@]<address>[:port][:wrkdir]
```
Let's see some example of this particular syntax, since it's very comfortable and you'll probably going to use this instead of the other one...
- Connect using default protocol (*defined in configuration*) to 192.168.1.31, port is default for this protocol (22); username is current user's name
- Connect using default protocol (*defined in configuration*) to 192.168.1.31, port if not provided is default for the selected protocol (in this case depends on your configuration); username is current user's name
```sh
termscp 192.168.1.31
```
- Connect using default protocol (*defined in configuration*) to 192.168.1.31, port is default for this protocol (22); username is `root`
- Connect using default protocol (*defined in configuration*) to 192.168.1.31; username is `root`
```sh
termscp root@192.168.1.31
@@ -199,6 +203,12 @@ Let's see some example of this particular syntax, since it's very comfortable an
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
```
#### How Password can be provided 🔐
You have probably noticed, that, when providing the address as argument, there's no way to provide the password.
@@ -232,7 +242,7 @@ I warmly suggest you to follow these guidelines in order to decide whether you s
- Make sure your machine is protected by attackers. If possible encrypt your disk and don't leave your PC unlocked while you're away.
- Preferably, save passwords only when a compromising of the target machine wouldn't be a problem.
To create a bookmark, just fulfill the authentication form and then input `CTRL+S`; you'll then be asked to give a name to your bookmark, and tadah, the bookmark has been created.
To create a bookmark, just fulfill the authentication form and then input `<CTRL+S>`; you'll then be asked to give a name to your bookmark, and tadah, the bookmark has been created.
If you go to [gallery](#gallery-), there is a GIF showing how bookmarks work 💪.
### Are my passwords Safe 😈
@@ -278,6 +288,7 @@ These parameters can be changed:
- **Default Protocol**: the default protocol is the default value for the file transfer protocol to be used in termscp. This applies for the login page and for the address CLI argument.
- **Text Editor**: the text editor to use. By default termscp will find the default editor for you; with this option you can force an editor to be used (e.g. `vim`). **Also GUI editors are supported**, unless they `nohup` from the parent process so if you ask: yes, you can use `notepad.exe`, and no: **Visual Studio Code doesn't work**.
- **Show Hidden Files**: select whether hidden files shall be displayed by default. You will be able to decide whether to show or not hidden files at runtime pressing `A` anyway.
- **Check for updates**: if set to `yes`, termscp will fetch the Github API to check if there is a new version of termscp available.
- **Group Dirs**: select whether directories should be groupped or not in file explorers. If `Display first` is selected, directories will be sorted using the configured method but displayed before files, viceversa if `Display last` is selected.
### SSH Key Storage 🔐
@@ -295,22 +306,27 @@ You can access the SSH key storage, from configuration moving to the `SSH Keys`
### File Explorer Format
It is possible through configuration to define a custom format for the file explorer. This field, with name `File formatter syntax` will define how the files will be displayed in the file explorer.
The syntax for the formatter is the following `{KEY1}... {KEY2}... {KEYn}...`.
It is possible through configuration to define a custom format for the file explorer. This field, with name `File formatter syntax` will define how the file entries will be displayed in the file explorer.
The syntax for the formatter is the following `{KEY1}... {KEY2:LENGTH}... {KEY3:LENGTH:EXTRA} {KEYn}...`.
Each key in bracket will be replaced with the related attribute, while everything outside brackets will be left unchanged.
- The key name is mandatory and must be one of the keys below
- The length describes the length reserved to display the field. Static attributes doesn't support this (GROUP, PEX, SIZE, USER)
- Extra is supported only by some parameters and is an additional options. See keys to check if extra is supported.
These are the keys supported by the formatter:
- `ATIME`: Last access time (with syntax `%b %d %Y %H:%M`)
- `CTIME`: Creation time (with syntax `%b %d %Y %H:%M`)
- `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`)
- `MTIME`: Last change time (with syntax `%b %d %Y %H:%M`); Extra might be provided as the time syntax (e.g. `{MTIME:8:%H:%M}`)
- `NAME`: File name (Elided if longer than 24)
- `PEX`: File permissions (UNIX format)
- `SIZE`: File size (omitted for directories)
- `SYMLINK`: Symlink (if any `-> {FILE_PATH}`)
- `USER`: Owner user
If left empty, the default formatter syntax will be used: `{NAME} {PEX} {USER} {SIZE} {MTIME}`
If left empty, the default formatter syntax will be used: `{NAME:24} {PEX} {USER} {SIZE} {MTIME:17:%b %d %Y %H:%M}`
---
@@ -362,8 +378,11 @@ The developer documentation can be found on Rust Docs at <https://docs.rs/termsc
## Upcoming Features 🧪
- **Custom explorer format**: possibility to customize the file line in the explorer directly from configuration, with the possibility to choose with information to display.
- **Find command in explorer**: possibility to search for files in explorers.
- **New commands in file explorer** (0.4.0 - March 2021)
- **Find**: search for files through directories, with built-in regex support
- **Execute**: run a command on both local host and remote host in protocols where this is supported
- SCP for sure
- SFTP: might be a challenge, since I should start a SSH session, but I guess it's not impossible
---

45
dist/build/deploy.sh vendored
View File

@@ -7,32 +7,41 @@ fi
VERSION=$1
set -e # Don't fail
# Create pkgs directory
cd ..
PKGS_DIR=$(pwd)/pkgs
cd -
mkdir -p ${PKGS_DIR}/
# Build x86_64
cd x86_64/
docker build --tag termscp-${VERSION}-x86_64 .
# Create container and get deb, rpm
# Build x86_64_deb
cd x86_64_debian9/
docker build --tag termscp-${VERSION}-x86_64_debian9 .
cd -
mkdir -p ${PKGS_DIR}/deb/
mkdir -p ${PKGS_DIR}/rpm/
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64 termscp-${VERSION}-x86_64)
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_debian9 termscp-${VERSION}-x86_64_debian9)
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/debian/termscp_${VERSION}_amd64.deb ${PKGS_DIR}/deb/
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/release/rpmbuild/RPMS/x86_64/termscp-${VERSION}-1.x86_64.rpm ${PKGS_DIR}/rpm/
# Build x86_64_archlinux
cd x86_64_archlinux/
docker build --tag termscp-${VERSION}-x86_64_archlinux .
# Create container and get AUR pkg
# Build x86_64_centos7
cd x86_64_centos7/
docker build --tag termscp-${VERSION}-x86_64_centos7 .
cd -
mkdir -p ${PKGS_DIR}/arch/
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_archlinux termscp-${VERSION}-x86_64_archlinux)
docker cp ${CONTAINER_NAME}:/usr/src/termscp/termscp-${VERSION}-x86_64.tar.gz ${PKGS_DIR}/arch/
docker cp ${CONTAINER_NAME}:/usr/src/termscp/PKGBUILD ${PKGS_DIR}/arch/
docker cp ${CONTAINER_NAME}:/usr/src/termscp/.SRCINFO ${PKGS_DIR}/arch/
# Replace termscp-bin with termscp in PKGBUILD
sed -i 's/termscp-bin/termscp/g' ${PKGS_DIR}/arch/PKGBUILD
mkdir -p ${PKGS_DIR}/rpm/
CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_centos7 termscp-${VERSION}-x86_64_centos7)
docker cp ${CONTAINER_NAME}:/usr/src/termscp/target/release/rpmbuild/RPMS/x86_64/termscp-${VERSION}-1.el7.x86_64.rpm ${PKGS_DIR}/rpm/termscp-${VERSION}-1.x86_64.rpm
# Build x86_64_archlinux
##################### TEMP REMOVED ###################################
# cd x86_64_archlinux/
# docker build --tag termscp-${VERSION}-x86_64_archlinux .
# # Create container and get AUR pkg
# cd -
# mkdir -p ${PKGS_DIR}/arch/
# CONTAINER_NAME=$(docker create termscp-${VERSION}-x86_64_archlinux termscp-${VERSION}-x86_64_archlinux)
# docker cp ${CONTAINER_NAME}:/usr/src/termscp/termscp-${VERSION}-x86_64.tar.gz ${PKGS_DIR}/arch/
# docker cp ${CONTAINER_NAME}:/usr/src/termscp/PKGBUILD ${PKGS_DIR}/arch/
# docker cp ${CONTAINER_NAME}:/usr/src/termscp/.SRCINFO ${PKGS_DIR}/arch/
# # Replace termscp-bin with termscp in PKGBUILD
# sed -i 's/termscp-bin/termscp/g' ${PKGS_DIR}/arch/PKGBUILD
##################### TEMP REMOVED ###################################
exit $?

View File

@@ -1,4 +1,4 @@
FROM archlinux/archlinux:latest as builder
FROM archlinux:base-20210120.0.13969 as builder
WORKDIR /usr/src/
# Install dependencies
@@ -8,15 +8,15 @@ RUN pacman -Syu --noconfirm \
openssl \
pkg-config \
sudo
# Install rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
chmod +x /tmp/rust.sh && \
/tmp/rust.sh -y
# Create build user
RUN useradd build -m && \
passwd -d build && \
mkdir -p termscp && \
chown -R build.build termscp/
# Install rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
chmod +x /tmp/rust.sh && \
/tmp/rust.sh -y
# Clone repository
RUN git clone https://github.com/veeso/termscp.git
# Set workdir to termscp

25
dist/build/x86_64_centos7/Dockerfile vendored Normal file
View File

@@ -0,0 +1,25 @@
FROM centos:centos7 as builder
WORKDIR /usr/src/
# Install dependencies
RUN yum -y install \
git \
gcc \
openssl \
pkgconfig \
openssl-devel
# Install rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
chmod +x /tmp/rust.sh && \
/tmp/rust.sh -y
# Clone repository
RUN git clone https://github.com/veeso/termscp.git
# Set workdir to termscp
WORKDIR /usr/src/termscp/
# Install cargo arxch
RUN source $HOME/.cargo/env && cargo install cargo-rpm
# Build for x86_64
RUN source $HOME/.cargo/env && cargo build --release
# Build pkgs
RUN source $HOME/.cargo/env && yum -y install rpm-build && cargo rpm init && cargo rpm build
CMD ["sh"]

28
dist/build/x86_64_debian8/Dockerfile vendored Normal file
View File

@@ -0,0 +1,28 @@
FROM debian:jessie
WORKDIR /usr/src/
# Install dependencies
RUN apt update && apt install -y \
git \
gcc \
pkg-config \
libssl-dev \
libssh2-1-dev \
curl
# Install rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
chmod +x /tmp/rust.sh && \
/tmp/rust.sh -y
# Clone repository
RUN git clone https://github.com/veeso/termscp.git
# Set workdir to termscp
WORKDIR /usr/src/termscp/
# Install cargo deb
RUN . $HOME/.cargo/env && cargo install cargo-deb
# Build for x86_64
RUN . $HOME/.cargo/env && cargo build --release
# Build pkgs
RUN . $HOME/.cargo/env && cargo deb
CMD ["sh"]

28
dist/build/x86_64_debian9/Dockerfile vendored Normal file
View File

@@ -0,0 +1,28 @@
FROM debian:stretch
WORKDIR /usr/src/
# Install dependencies
RUN apt update && apt install -y \
git \
gcc \
pkg-config \
libssl-dev \
libssh2-1-dev \
curl
# Install rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rust.sh && \
chmod +x /tmp/rust.sh && \
/tmp/rust.sh -y
# Clone repository
RUN git clone https://github.com/veeso/termscp.git
# Set workdir to termscp
WORKDIR /usr/src/termscp/
# Install cargo deb
RUN . $HOME/.cargo/env && cargo install cargo-deb
# Build for x86_64
RUN . $HOME/.cargo/env && cargo build --release
# Build pkgs
RUN . $HOME/.cargo/env && cargo deb
CMD ["sh"]

View File

@@ -1,14 +1,14 @@
pkgbase = termscp-bin
pkgbase = termscp
pkgdesc = TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal.
pkgver = 0.3.2
pkgver = 0.3.3
pkgrel = 1
url = https://github.com/veeso/termscp
arch = x86_64
license = GPL-3.0
provides = termscp
options = strip
source = https://github.com/veeso/termscp/releases/download/v0.3.2/termscp-0.3.2-x86_64.tar.gz
sha256sums = e2700e2e9b741eb273e2633d5cf24ad620365d059bdd4f2b42f3737a7c28a2c7
source = https://github.com/veeso/termscp/releases/download/v0.3.3/termscp-0.3.3-x86_64.tar.gz
sha256sums = 7a8c70add8306a2cb3f2ee1d075a00fef143fc9aad4199797c7462bab1649296
pkgname = termscp-bin
pkgname = termscp

View File

@@ -1,6 +1,6 @@
# Maintainer: Christian Visintin
pkgname=termscp
pkgver=0.3.2
pkgver=0.3.3
pkgrel=1
pkgdesc="TermSCP is a SCP/SFTP/FTPS client for command line with an integrated UI to explore the remote file system. Basically WinSCP on a terminal."
url="https://github.com/veeso/termscp"
@@ -9,7 +9,7 @@ arch=("x86_64")
provides=("termscp")
options=("strip")
source=("https://github.com/veeso/termscp/releases/download/v$pkgver/termscp-$pkgver-x86_64.tar.gz")
sha256sums=("e2700e2e9b741eb273e2633d5cf24ad620365d059bdd4f2b42f3737a7c28a2c7")
sha256sums=("7a8c70add8306a2cb3f2ee1d075a00fef143fc9aad4199797c7462bab1649296")
package() {
install -Dm755 termscp -t "$pkgdir/usr/bin/"

View File

@@ -84,6 +84,7 @@ impl ActivityManager {
protocol: FileTransferProtocol,
username: Option<String>,
password: Option<String>,
entry_directory: Option<PathBuf>,
) {
self.ftparams = Some(FileTransferParams {
address,
@@ -91,6 +92,7 @@ impl ActivityManager {
protocol,
username,
password,
entry_directory,
});
}
@@ -164,6 +166,7 @@ impl ActivityManager {
_ => Some(activity.password.clone()),
},
protocol: activity.protocol,
entry_directory: None, // Has use only when accessing with address
});
break;
}

View File

@@ -55,6 +55,7 @@ pub struct UserInterfaceConfig {
pub text_editor: PathBuf,
pub default_protocol: String,
pub show_hidden_files: bool,
pub check_for_updates: Option<bool>, // @! Since 0.3.3
pub group_dirs: Option<String>,
pub file_fmt: Option<String>,
}
@@ -85,6 +86,7 @@ impl Default for UserInterfaceConfig {
},
default_protocol: FileTransferProtocol::Sftp.to_string(),
show_hidden_files: false,
check_for_updates: Some(true),
group_dirs: None,
file_fmt: None,
}
@@ -172,6 +174,7 @@ mod tests {
default_protocol: String::from("SFTP"),
text_editor: PathBuf::from("nano"),
show_hidden_files: true,
check_for_updates: Some(true),
group_dirs: Some(String::from("first")),
file_fmt: Some(String::from("{NAME}")),
};
@@ -189,6 +192,7 @@ mod tests {
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("nano"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.check_for_updates, Some(true));
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("first")));
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME}")));
}
@@ -201,6 +205,7 @@ mod tests {
let cfg: UserConfig = UserConfig::default();
assert_eq!(cfg.user_interface.default_protocol, String::from("SFTP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
assert_eq!(cfg.remote.ssh_keys.len(), 0);
}

View File

@@ -107,8 +107,12 @@ mod tests {
assert_eq!(cfg.user_interface.default_protocol, String::from("SCP"));
assert_eq!(cfg.user_interface.text_editor, PathBuf::from("vim"));
assert_eq!(cfg.user_interface.show_hidden_files, true);
assert_eq!(cfg.user_interface.check_for_updates.unwrap(), true);
assert_eq!(cfg.user_interface.group_dirs, Some(String::from("last")));
assert_eq!(cfg.user_interface.file_fmt, Some(String::from("{NAME} {PEX}")));
assert_eq!(
cfg.user_interface.file_fmt,
Some(String::from("{NAME} {PEX}"))
);
// Verify keys
assert_eq!(
*cfg.remote
@@ -144,6 +148,7 @@ 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.group_dirs, None);
assert!(cfg.user_interface.check_for_updates.is_none());
assert_eq!(cfg.user_interface.file_fmt, None);
// Verify keys
assert_eq!(
@@ -200,6 +205,7 @@ mod tests {
default_protocol = "SCP"
text_editor = "vim"
show_hidden_files = true
check_for_updates = true
group_dirs = "last"
file_fmt = "{NAME} {PEX}"

View File

@@ -37,23 +37,31 @@ use regex::Regex;
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
use users::{get_group_by_gid, get_user_by_uid};
// Types
type FmtCallback = fn(&Formatter, &FsEntry, &str, &str) -> String;
// FmtCallback: Formatter, fsentry: &FsEntry, cur_str, prefix, length, extra
type FmtCallback = fn(&Formatter, &FsEntry, &str, &str, Option<&usize>, Option<&String>) -> String;
// Keys
const FMT_KEY_ATIME: &str = "{ATIME}";
const FMT_KEY_CTIME: &str = "{CTIME}";
const FMT_KEY_GROUP: &str = "{GROUP}";
const FMT_KEY_MTIME: &str = "{MTIME}";
const FMT_KEY_NAME: &str = "{NAME}";
const FMT_KEY_PEX: &str = "{PEX}";
const FMT_KEY_SIZE: &str = "{SIZE}";
const FMT_KEY_SYMLINK: &str = "{SYMLINK}";
const FMT_KEY_USER: &str = "{USER}";
const FMT_KEY_ATIME: &str = "ATIME";
const FMT_KEY_CTIME: &str = "CTIME";
const FMT_KEY_GROUP: &str = "GROUP";
const FMT_KEY_MTIME: &str = "MTIME";
const FMT_KEY_NAME: &str = "NAME";
const FMT_KEY_PEX: &str = "PEX";
const FMT_KEY_SIZE: &str = "SIZE";
const FMT_KEY_SYMLINK: &str = "SYMLINK";
const FMT_KEY_USER: &str = "USER";
// Default
const FMT_DEFAULT_STX: &str = "{NAME} {PEX} {USER} {SIZE} {MTIME}";
// Regex
lazy_static! {
/**
* Regex matches:
* - group 0: KEY NAME
* - group 1?: LENGTH
* - group 2?: EXTRA
*/
static ref FMT_KEY_REGEX: Regex = Regex::new(r"\{(.*?)\}").ok().unwrap();
static ref FMT_ATTR_REGEX: Regex = Regex::new(r"(?:([A-Z]+))(:?([0-9]+))?(:?(.+))?").ok().unwrap();
}
/// ## CallChainBlock
@@ -65,6 +73,8 @@ lazy_static! {
struct CallChainBlock {
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
next_block: Option<Box<CallChainBlock>>,
}
@@ -72,10 +82,17 @@ impl CallChainBlock {
/// ### new
///
/// Create a new `CallChainBlock`
pub fn new(func: FmtCallback, prefix: String) -> Self {
pub fn new(
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
) -> Self {
CallChainBlock {
func,
prefix,
fmt_len,
fmt_extra,
next_block: None,
}
}
@@ -85,7 +102,14 @@ impl CallChainBlock {
/// Call next callback in the CallChain
pub fn next(&self, fmt: &Formatter, fsentry: &FsEntry, cur_str: &str) -> String {
// Call func
let new_str: String = (self.func)(fmt, fsentry, cur_str, self.prefix.as_str());
let new_str: String = (self.func)(
fmt,
fsentry,
cur_str,
self.prefix.as_str(),
self.fmt_len.as_ref(),
self.fmt_extra.as_ref(),
);
// If next is some, call next, otherwise (END OF CHAIN) return new_str
match &self.next_block {
Some(block) => block.next(fmt, fsentry, new_str.as_str()),
@@ -96,11 +120,21 @@ impl CallChainBlock {
/// ### push
///
/// Push func to the last element in the Call chain
pub fn push(&mut self, func: FmtCallback, prefix: String) {
pub fn push(
&mut self,
func: FmtCallback,
prefix: String,
fmt_len: Option<usize>,
fmt_extra: Option<String>,
) {
// Call recursively until an element with next_block equal to None is found
match &mut self.next_block {
None => self.next_block = Some(Box::new(CallChainBlock::new(func, prefix))),
Some(block) => block.push(func, prefix),
None => {
self.next_block = Some(Box::new(CallChainBlock::new(
func, prefix, fmt_len, fmt_extra,
)))
}
Some(block) => block.push(func, prefix, fmt_len, fmt_extra),
}
}
}
@@ -148,27 +182,72 @@ impl Formatter {
/// ### fmt_atime
///
/// Format last access time
fn fmt_atime(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
// Get date
let datetime: String = fmt_time(fsentry.get_last_access_time(), "%b %d %Y %H:%M");
fn fmt_atime(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
// Get date (use extra args as format or default "%b %d %Y %H:%M")
let datetime: String = fmt_time(
fsentry.get_last_access_time(),
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
// Add to cur str, prefix and the key value
format!("{}{}{:17}", cur_str, prefix, datetime)
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
/// ### fmt_ctime
///
/// Format creation time
fn fmt_ctime(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
fn fmt_ctime(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
// Get date
let datetime: String = fmt_time(fsentry.get_creation_time(), "%b %d %Y %H:%M");
let datetime: String = fmt_time(
fsentry.get_creation_time(),
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
// Add to cur str, prefix and the key value
format!("{}{}{:17}", cur_str, prefix, datetime)
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
/// ### fmt_group
///
/// Format owner group
fn fmt_group(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
fn fmt_group(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get username
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
let group: String = match fsentry.get_group() {
@@ -184,31 +263,67 @@ impl Formatter {
None => 0.to_string(),
};
// Add to cur str, prefix and the key value
format!("{}{}{:12}", cur_str, prefix, group)
format!(
"{}{}{:0width$}",
cur_str,
prefix,
group,
width = fmt_len.unwrap_or(&12)
)
}
/// ### fmt_mtime
///
/// Format last change time
fn fmt_mtime(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
fn fmt_mtime(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
// Get date
let datetime: String = fmt_time(fsentry.get_last_change_time(), "%b %d %Y %H:%M");
let datetime: String = fmt_time(
fsentry.get_last_change_time(),
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
},
);
// Add to cur str, prefix and the key value
format!("{}{}{:17}", cur_str, prefix, datetime)
format!(
"{}{}{:0width$}",
cur_str,
prefix,
datetime,
width = fmt_len.unwrap_or(&17)
)
}
/// ### fmt_name
///
/// Format file name
fn fmt_name(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
fn fmt_name(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get file name (or elide if too long)
let file_len: usize = match fmt_len {
Some(l) => *l,
None => 24,
};
let name: &str = fsentry.get_name();
let last_idx: usize = match fsentry.is_dir() {
// NOTE: For directories is 19, since we push '/' to name
true => 19,
false => 20,
true => file_len - 5,
false => file_len - 4,
};
let mut name: String = match name.len() >= 24 {
let mut name: String = match name.len() >= file_len {
false => name.to_string(),
true => format!("{}...", &name[0..last_idx]),
};
@@ -216,13 +331,20 @@ impl Formatter {
name.push('/');
}
// Add to cur str, prefix and the key value
format!("{}{}{:24}", cur_str, prefix, name)
format!("{}{}{:0width$}", cur_str, prefix, name, width = file_len)
}
/// ### fmt_pex
///
/// Format file permissions
fn fmt_pex(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
fn fmt_pex(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Create mode string
let mut pex: String = String::with_capacity(10);
let file_type: char = match fsentry.is_symlink() {
@@ -244,7 +366,14 @@ impl Formatter {
/// ### fmt_size
///
/// Format file size
fn fmt_size(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
fn fmt_size(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
if fsentry.is_file() {
// Get byte size
let size: ByteSize = ByteSize(fsentry.get_size() as u64);
@@ -259,16 +388,31 @@ impl Formatter {
/// ### fmt_symlink
///
/// Format file symlink (if any)
fn fmt_symlink(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
fn fmt_symlink(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get file name (or elide if too long)
let file_len: usize = match fmt_len {
Some(l) => *l,
None => 21,
};
// Replace `FMT_KEY_NAME` with name
match fsentry.is_symlink() {
false => format!("{}{} ", cur_str, prefix),
true => format!(
"{}{}-> {:21}",
"{}{}-> {:0width$}",
cur_str,
prefix,
fmt_path_elide(fsentry.get_realfile().get_abs_path().as_path(), 20)
fmt_path_elide(
fsentry.get_realfile().get_abs_path().as_path(),
file_len - 1
),
width = file_len
),
}
}
@@ -276,7 +420,14 @@ impl Formatter {
/// ### fmt_user
///
/// Format owner user
fn fmt_user(&self, fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
fn fmt_user(
&self,
fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Get username
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
let username: String = match fsentry.get_user() {
@@ -299,7 +450,14 @@ impl Formatter {
///
/// Fallback function in case the format key is unknown
/// It does nothing, just returns cur_str
fn fmt_fallback(&self, _fsentry: &FsEntry, cur_str: &str, prefix: &str) -> String {
fn fmt_fallback(
&self,
_fsentry: &FsEntry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
// Add to cur str and prefix
format!("{}{}", cur_str, prefix)
}
@@ -322,29 +480,54 @@ impl Formatter {
let prefix: String = String::from(&fmt_str[last_index..index]);
// Increment last index (sum prefix lenght and the length of the key)
last_index += prefix.len() + regex_match[0].len();
// Match the match (I guess...)
let callback: FmtCallback = match &regex_match[0] {
FMT_KEY_ATIME => Self::fmt_atime,
FMT_KEY_CTIME => Self::fmt_ctime,
FMT_KEY_GROUP => Self::fmt_group,
FMT_KEY_MTIME => Self::fmt_mtime,
FMT_KEY_NAME => Self::fmt_name,
FMT_KEY_PEX => Self::fmt_pex,
FMT_KEY_SIZE => Self::fmt_size,
FMT_KEY_SYMLINK => Self::fmt_symlink,
FMT_KEY_USER => Self::fmt_user,
_ => Self::fmt_fallback,
};
// Create a callchain or push new element to its back
match callchain.as_mut() {
None => callchain = Some(CallChainBlock::new(callback, prefix)),
Some(chain_block) => chain_block.push(callback, prefix),
// Match attributes
match FMT_ATTR_REGEX.captures(&regex_match[1]) {
Some(regex_match) => {
// Match group 0 (which is name)
let callback: FmtCallback = match &regex_match.get(1) {
Some(key) => match key.as_str() {
FMT_KEY_ATIME => Self::fmt_atime,
FMT_KEY_CTIME => Self::fmt_ctime,
FMT_KEY_GROUP => Self::fmt_group,
FMT_KEY_MTIME => Self::fmt_mtime,
FMT_KEY_NAME => Self::fmt_name,
FMT_KEY_PEX => Self::fmt_pex,
FMT_KEY_SIZE => Self::fmt_size,
FMT_KEY_SYMLINK => Self::fmt_symlink,
FMT_KEY_USER => Self::fmt_user,
_ => Self::fmt_fallback,
},
None => Self::fmt_fallback,
};
// Match format length: group 3
let fmt_len: Option<usize> = match &regex_match.get(3) {
Some(len) => match len.as_str().parse::<usize>() {
Ok(len) => Some(len),
Err(_) => None,
},
None => None,
};
// Match format extra: group 2 + 1
let fmt_extra: Option<String> = match &regex_match.get(5) {
Some(extra) => Some(extra.as_str().to_string()),
None => None,
};
// Create a callchain or push new element to its back
match callchain.as_mut() {
None => {
callchain =
Some(CallChainBlock::new(callback, prefix, fmt_len, fmt_extra))
}
Some(chain_block) => chain_block.push(callback, prefix, fmt_len, fmt_extra),
}
}
None => continue,
}
}
// Finalize and return
match callchain {
Some(callchain) => callchain,
None => CallChainBlock::new(Self::fmt_fallback, String::new()),
None => CallChainBlock::new(Self::fmt_fallback, String::new(), None, None),
}
}
}
@@ -378,7 +561,7 @@ mod tests {
unix_pex: Some((6, 4, 4)), // UNIX only
});
let prefix: String = String::from("h");
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix);
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
assert!(callchain.next_block.is_none());
assert_eq!(callchain.prefix, String::from("h"));
// Execute
@@ -387,10 +570,10 @@ mod tests {
String::from("hA")
);
// Push 4 new blocks
callchain.push(dummy_fmt, String::from("h"));
callchain.push(dummy_fmt, String::from("h"));
callchain.push(dummy_fmt, String::from("h"));
callchain.push(dummy_fmt, String::from("h"));
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
callchain.push(dummy_fmt, String::from("h"), None, None);
// Verify
assert_eq!(
callchain.next(&dummy_formatter, &dummy_entry, ""),
@@ -597,7 +780,7 @@ mod tests {
#[test]
fn test_fs_explorer_formatter_all_together_now() {
let formatter: Formatter =
Formatter::new("{NAME} {SYMLINK} {GROUP} {USER} {PEX} {SIZE} {ATIME} {CTIME} {MTIME}");
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
// Directory (with symlink)
let t: SystemTime = SystemTime::now();
let pointer: FsEntry = FsEntry::File(FsFile {
@@ -627,10 +810,10 @@ mod tests {
unix_pex: Some((7, 5, 5)), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"projects/ -> /project.info 0 0 lrwxr-xr-x {} {} {}",
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
"projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// Directory without symlink
let entry: FsEntry = FsEntry::Directory(FsDirectory {
@@ -646,10 +829,10 @@ mod tests {
unix_pex: Some((7, 5, 5)), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"projects/ 0 0 drwxr-xr-x {} {} {}",
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
"projects/ 0 0 drwxr-xr-x {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// File with symlink
let pointer: FsEntry = FsEntry::File(FsFile {
@@ -681,10 +864,10 @@ mod tests {
unix_pex: Some((6, 4, 4)), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt -> /project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
"bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// File without symlink
let entry: FsEntry = FsEntry::File(FsFile {
@@ -702,17 +885,24 @@ mod tests {
unix_pex: Some((6, 4, 4)), // UNIX only
});
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
fmt_time(t, "%b %d %Y %H:%M"),
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
fmt_time(t, "%a %b %d %Y %H:%M"),
));
}
/// ### dummy_fmt
///
/// Dummy formatter, just yelds an 'A' at the end of the current string
fn dummy_fmt(_fmt: &Formatter, _entry: &FsEntry, cur_str: &str, prefix: &str) -> String {
fn dummy_fmt(
_fmt: &Formatter,
_entry: &FsEntry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
_fmt_extra: Option<&String>,
) -> String {
format!("{}{}A", cur_str, prefix)
}
}

View File

@@ -58,7 +58,9 @@ use filetransfer::FileTransferProtocol;
/// Print usage
fn print_usage(opts: Options) {
let brief = String::from("Usage: termscp [options]... [protocol://user@address:port]");
let brief = String::from(
"Usage: termscp [options]... [protocol://user@address:port:wrkdir] [local-wrkdir]",
);
print!("{}", opts.usage(&brief));
println!("\nPlease, report issues to <https://github.com/veeso/termscp>");
}
@@ -70,6 +72,7 @@ fn main() {
let mut port: u16 = 22; // Default port
let mut username: Option<String> = None; // Default username
let mut password: Option<String> = None; // Default password
let mut remote_wrkdir: Option<PathBuf> = None;
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp; // Default protocol
let mut ticks: Duration = Duration::from_millis(10);
//Process options
@@ -120,15 +123,17 @@ fn main() {
}
// Check free args
let extra_args: Vec<String> = matches.free;
// Remote argument
if let Some(remote) = extra_args.get(0) {
// Parse address
match utils::parser::parse_remote_opt(remote) {
Ok((addr, portn, proto, user)) => {
Ok(host_opts) => {
// Set params
address = Some(addr);
port = portn;
protocol = proto;
username = user;
address = Some(host_opts.hostname);
port = host_opts.port;
protocol = host_opts.protocol;
username = host_opts.username;
remote_wrkdir = host_opts.wrkdir;
}
Err(err) => {
eprintln!("Bad address option: {}", err);
@@ -137,6 +142,15 @@ fn main() {
}
}
}
// Local directory
if let Some(localdir) = extra_args.get(1) {
// Change working directory if local dir is set
let localdir: PathBuf = PathBuf::from(localdir);
if let Err(err) = env::set_current_dir(localdir.as_path()) {
eprintln!("Bad working directory argument: {}", err);
std::process::exit(255);
}
}
// Get working directory
let wrkdir: PathBuf = match env::current_dir() {
Ok(dir) => dir,
@@ -174,7 +188,7 @@ fn main() {
};
// Set file transfer params if set
if let Some(address) = address {
manager.set_filetransfer_params(address, port, protocol, username, password);
manager.set_filetransfer_params(address, port, protocol, username, password, remote_wrkdir);
}
// Run
manager.run(start_activity);

View File

@@ -138,6 +138,20 @@ impl ConfigClient {
self.config.user_interface.show_hidden_files = value;
}
/// ### get_check_for_updates
///
/// Get value of `check_for_updates`
pub fn get_check_for_updates(&self) -> bool {
self.config.user_interface.check_for_updates.unwrap_or(true)
}
/// ### set_check_for_updates
///
/// Set new value for `check_for_updates`
pub fn set_check_for_updates(&mut self, value: bool) {
self.config.user_interface.check_for_updates = Some(value);
}
/// ### get_group_dirs
///
/// Get GroupDirs value from configuration (will be converted from string)
@@ -455,6 +469,20 @@ mod tests {
assert_eq!(client.get_show_hidden_files(), true);
}
#[test]
fn test_system_config_check_for_updates() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
.unwrap();
assert_eq!(client.get_check_for_updates(), true); // Null ?
client.set_check_for_updates(true);
assert_eq!(client.get_check_for_updates(), true);
client.set_check_for_updates(false);
assert_eq!(client.get_check_for_updates(), false);
}
#[test]
fn test_system_config_group_dirs() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();

View File

@@ -41,18 +41,14 @@ impl AuthActivity {
pub(super) fn del_bookmark(&mut self, idx: usize) {
if let Some(bookmarks_cli) = self.bookmarks_client.as_mut() {
// Iterate over kyes
let mut name: Option<String> = None;
for (i, key) in bookmarks_cli.iter_bookmarks().enumerate() {
if i == idx {
name = Some(key.clone());
break;
}
}
let name: Option<&String> = self.bookmarks_list.get(idx);
if let Some(name) = name {
bookmarks_cli.del_bookmark(&name);
// Write bookmarks
self.write_bookmarks();
}
// Delete element from vec
self.recents_list.remove(idx);
}
}
@@ -62,20 +58,16 @@ impl AuthActivity {
pub(super) fn load_bookmark(&mut self, idx: usize) {
if let Some(bookmarks_cli) = self.bookmarks_client.as_ref() {
// Iterate over bookmarks
for (i, key) in bookmarks_cli.iter_bookmarks().enumerate() {
if i == idx {
if let Some(bookmark) = bookmarks_cli.get_bookmark(&key) {
// Load parameters
self.address = bookmark.0;
self.port = bookmark.1.to_string();
self.protocol = bookmark.2;
self.username = bookmark.3;
if let Some(password) = bookmark.4 {
self.password = password;
}
if let Some(key) = self.bookmarks_list.get(idx) {
if let Some(bookmark) = bookmarks_cli.get_bookmark(&key) {
// Load parameters
self.address = bookmark.0;
self.port = bookmark.1.to_string();
self.protocol = bookmark.2;
self.username = bookmark.3;
if let Some(password) = bookmark.4 {
self.password = password;
}
// Break
break;
}
}
}
@@ -112,7 +104,7 @@ impl AuthActivity {
DialogYesNoOption::No => None,
};
bookmarks_cli.add_bookmark(
name,
name.clone(),
self.address.clone(),
port,
self.protocol,
@@ -121,6 +113,9 @@ impl AuthActivity {
);
// Save bookmarks
self.write_bookmarks();
// Push bookmark to list and sort
self.bookmarks_list.push(name);
self.sort_bookmarks();
}
}
/// ### del_recent
@@ -128,19 +123,14 @@ impl AuthActivity {
/// Delete recent
pub(super) fn del_recent(&mut self, idx: usize) {
if let Some(client) = self.bookmarks_client.as_mut() {
// Iterate over kyes
let mut name: Option<String> = None;
for (i, key) in client.iter_recents().enumerate() {
if i == idx {
name = Some(key.clone());
break;
}
}
let name: Option<&String> = self.recents_list.get(idx);
if let Some(name) = name {
client.del_recent(&name);
// Save bookmarks
// Write bookmarks
self.write_bookmarks();
}
// Delete element from vec
self.recents_list.remove(idx);
}
}
@@ -150,17 +140,13 @@ impl AuthActivity {
pub(super) fn load_recent(&mut self, idx: usize) {
if let Some(client) = self.bookmarks_client.as_ref() {
// Iterate over bookmarks
for (i, key) in client.iter_recents().enumerate() {
if i == idx {
if let Some(bookmark) = client.get_recent(key) {
// Load parameters
self.address = bookmark.0;
self.port = bookmark.1.to_string();
self.protocol = bookmark.2;
self.username = bookmark.3;
// Break
break;
}
if let Some(key) = self.recents_list.get(idx) {
if let Some(bookmark) = client.get_recent(key) {
// Load parameters
self.address = bookmark.0;
self.port = bookmark.1.to_string();
self.protocol = bookmark.2;
self.username = bookmark.3;
}
}
}
@@ -233,7 +219,26 @@ impl AuthActivity {
config_dir_path.as_path(),
16,
) {
Ok(cli) => self.bookmarks_client = Some(cli),
Ok(cli) => {
// Load bookmarks into list
let mut bookmarks_list: Vec<String> =
Vec::with_capacity(cli.iter_bookmarks().count());
for bookmark in cli.iter_bookmarks() {
bookmarks_list.push(bookmark.clone());
}
// Load recents into list
let mut recents_list: Vec<String> =
Vec::with_capacity(cli.iter_recents().count());
for recent in cli.iter_recents() {
recents_list.push(recent.clone());
}
self.bookmarks_client = Some(cli);
self.bookmarks_list = bookmarks_list;
self.recents_list = recents_list;
// Sort bookmark list
self.sort_bookmarks();
self.sort_recents();
}
Err(err) => {
self.popup = Some(Popup::Alert(
Color::Red,
@@ -256,4 +261,21 @@ impl AuthActivity {
}
}
}
/// ### sort_bookmarks
///
/// Sort bookmarks in list
fn sort_bookmarks(&mut self) {
// Conver to lowercase when sorting
self.bookmarks_list
.sort_by(|a, b| a.to_lowercase().as_str().cmp(b.to_lowercase().as_str()));
}
/// ### sort_recents
///
/// Sort recents in list
fn sort_recents(&mut self) {
// Reverse order
self.recents_list.sort_by(|a, b| b.cmp(a));
}
}

View File

@@ -62,6 +62,7 @@ impl AuthActivity {
.constraints(
[
Constraint::Length(5),
Constraint::Length(1), // Version
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
@@ -80,32 +81,33 @@ impl AuthActivity {
.split(chunks[1]);
// Draw header
f.render_widget(self.draw_header(), auth_chunks[0]);
f.render_widget(self.draw_new_version(), auth_chunks[1]);
// Draw input fields
f.render_widget(self.draw_remote_address(), auth_chunks[1]);
f.render_widget(self.draw_remote_port(), auth_chunks[2]);
f.render_widget(self.draw_protocol_select(), auth_chunks[3]);
f.render_widget(self.draw_protocol_username(), auth_chunks[4]);
f.render_widget(self.draw_protocol_password(), auth_chunks[5]);
f.render_widget(self.draw_remote_address(), auth_chunks[2]);
f.render_widget(self.draw_remote_port(), auth_chunks[3]);
f.render_widget(self.draw_protocol_select(), auth_chunks[4]);
f.render_widget(self.draw_protocol_username(), auth_chunks[5]);
f.render_widget(self.draw_protocol_password(), auth_chunks[6]);
// Draw footer
f.render_widget(self.draw_footer(), auth_chunks[6]);
f.render_widget(self.draw_footer(), auth_chunks[7]);
// Set cursor
if let InputForm::AuthCredentials = self.input_form {
match self.selected_field {
InputField::Address => f.set_cursor(
auth_chunks[1].x + self.address.width() as u16 + 1,
auth_chunks[1].y + 1,
),
InputField::Port => f.set_cursor(
auth_chunks[2].x + self.port.width() as u16 + 1,
auth_chunks[2].x + self.address.width() as u16 + 1,
auth_chunks[2].y + 1,
),
InputField::Port => f.set_cursor(
auth_chunks[3].x + self.port.width() as u16 + 1,
auth_chunks[3].y + 1,
),
InputField::Username => f.set_cursor(
auth_chunks[4].x + self.username.width() as u16 + 1,
auth_chunks[4].y + 1,
auth_chunks[5].x + self.username.width() as u16 + 1,
auth_chunks[5].y + 1,
),
InputField::Password => f.set_cursor(
auth_chunks[5].x + self.password_placeholder.width() as u16 + 1,
auth_chunks[5].y + 1,
auth_chunks[6].x + self.password_placeholder.width() as u16 + 1,
auth_chunks[6].y + 1,
),
_ => {}
}
@@ -284,6 +286,21 @@ impl AuthActivity {
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD))
}
/// ### draw_new_version
///
/// Draw new version disclaimer
fn draw_new_version(&self) -> Paragraph {
let content: String = match self.new_version.as_ref() {
Some(ver) => format!("TermSCP {} is now available! Download it from <https://github.com/veeso/termscp/releases/latest>", ver),
None => String::new(),
};
Paragraph::new(content).style(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
}
/// ### draw_footer
///
/// Draw authentication page footer
@@ -320,10 +337,8 @@ impl AuthActivity {
fn draw_bookmarks_tab(&self) -> Option<List> {
self.bookmarks_client.as_ref()?;
let hosts: Vec<ListItem> = self
.bookmarks_client
.as_ref()
.unwrap()
.iter_bookmarks()
.bookmarks_list
.iter()
.map(|key: &String| {
let entry: (String, u16, FileTransferProtocol, String, _) = self
.bookmarks_client
@@ -368,10 +383,8 @@ impl AuthActivity {
fn draw_recents_tab(&self) -> Option<List> {
self.bookmarks_client.as_ref()?;
let hosts: Vec<ListItem> = self
.bookmarks_client
.as_ref()
.unwrap()
.iter_recents()
.recents_list
.iter()
.map(|key: &String| {
let entry: (String, u16, FileTransferProtocol, String) = self
.bookmarks_client

View File

@@ -40,6 +40,7 @@ use crate::filetransfer::FileTransferProtocol;
use crate::system::bookmarks_client::BookmarksClient;
use crate::system::config_client::ConfigClient;
use crate::system::environment;
use crate::utils::git;
// Includes
use crossterm::event::Event as InputEvent;
@@ -115,7 +116,11 @@ pub struct AuthActivity {
input_txt: String, // Input text
choice_opt: DialogYesNoOption, // Dialog popup selected option
bookmarks_idx: usize, // Index of selected bookmark
bookmarks_list: Vec<String>, // List of bookmarks
recents_idx: usize, // Index of selected recent
recents_list: Vec<String>, // list of recents
// misc
new_version: Option<String>, // Contains new version of termscp
}
impl Default for AuthActivity {
@@ -149,7 +154,10 @@ impl AuthActivity {
input_txt: String::new(),
choice_opt: DialogYesNoOption::Yes,
bookmarks_idx: 0,
bookmarks_list: Vec::new(),
recents_idx: 0,
recents_list: Vec::new(),
new_version: None,
}
}
@@ -188,6 +196,27 @@ impl AuthActivity {
}
}
}
/// ### on_create
///
/// If enabled in configuration, check for updates from Github
fn check_for_updates(&mut self) {
if let Some(client) = self.config_client.as_ref() {
if client.get_check_for_updates() {
// Send request
match git::check_for_updates(env!("CARGO_PKG_VERSION")) {
Ok(version) => self.new_version = version,
Err(err) => {
// Report error
self.popup = Some(Popup::Alert(
Color::Red,
format!("Could not check for new updates: {}", err),
))
}
}
}
}
}
}
impl Activity for AuthActivity {
@@ -212,6 +241,8 @@ impl Activity for AuthActivity {
if self.config_client.is_none() {
self.init_config_client();
}
// If check for updates is enabled, check for updates
self.check_for_updates();
}
/// ### on_draw
@@ -224,13 +255,11 @@ impl Activity for AuthActivity {
return;
}
// Read one event
if let Ok(event) = self.context.as_ref().unwrap().input_hnd.read_event() {
if let Some(event) = event {
// Set redraw to true
self.redraw = true;
// Handle event
self.handle_input_event(&event);
}
if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
// Set redraw to true
self.redraw = true;
// Handle event
self.handle_input_event(&event);
}
// Redraw if necessary
if self.redraw {

View File

@@ -41,17 +41,11 @@ impl FileTransferActivity {
/// Read one event.
/// Returns whether at least one event has been handled
pub(super) fn read_input_event(&mut self) -> bool {
if let Ok(event) = self.context.as_ref().unwrap().input_hnd.read_event() {
// Iterate over input events
if let Some(event) = event {
// Handle event
self.handle_input_event(&event);
// Return true
true
} else {
// No event
false
}
if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
// Handle event
self.handle_input_event(&event);
// Return true
true
} else {
// Error
false
@@ -103,7 +97,7 @@ impl FileTransferActivity {
KeyCode::Esc => {
// Handle quit event
// Create quit prompt dialog
self.popup = self.create_disconnect_popup();
self.popup = Some(self.create_disconnect_popup());
}
KeyCode::Tab => self.switch_input_field(), // <TAB> switch tab
KeyCode::Right => self.tab = FileExplorerTab::Remote, // <RIGHT> switch to right tab
@@ -161,6 +155,8 @@ impl FileTransferActivity {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
// Default choice to NO for delete!
self.choice_opt = DialogYesNoOption::No;
// Show delete prompt
self.popup = Some(Popup::YesNo(
format!("Delete file \"{}\"", file_name),
@@ -200,6 +196,8 @@ impl FileTransferActivity {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
// Default choice to NO for delete!
self.choice_opt = DialogYesNoOption::No;
// Show delete prompt
self.popup = Some(Popup::YesNo(
format!("Delete file \"{}\"", file_name),
@@ -265,7 +263,7 @@ impl FileTransferActivity {
}
'q' | 'Q' => {
// Create quit prompt dialog
self.popup = self.create_quit_popup();
self.popup = Some(self.create_quit_popup());
}
'r' | 'R' => {
// Rename
@@ -322,7 +320,7 @@ impl FileTransferActivity {
KeyCode::Esc => {
// Handle quit event
// Create quit prompt dialog
self.popup = self.create_disconnect_popup();
self.popup = Some(self.create_disconnect_popup());
}
KeyCode::Tab => self.switch_input_field(), // <TAB> switch tab
KeyCode::Left => self.tab = FileExplorerTab::Local, // <LEFT> switch to local tab
@@ -380,6 +378,8 @@ impl FileTransferActivity {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
// Default choice to NO for delete!
self.choice_opt = DialogYesNoOption::No;
// Show delete prompt
self.popup = Some(Popup::YesNo(
format!("Delete file \"{}\"", file_name),
@@ -419,6 +419,8 @@ impl FileTransferActivity {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
// Default choice to NO for delete!
self.choice_opt = DialogYesNoOption::No;
// Show delete prompt
self.popup = Some(Popup::YesNo(
format!("Delete file \"{}\"", file_name),
@@ -482,7 +484,7 @@ impl FileTransferActivity {
}
'q' | 'Q' => {
// Create quit prompt dialog
self.popup = self.create_quit_popup();
self.popup = Some(self.create_quit_popup());
}
'r' | 'R' => {
// Rename
@@ -539,7 +541,7 @@ impl FileTransferActivity {
KeyCode::Esc => {
// Handle quit event
// Create quit prompt dialog
self.popup = self.create_disconnect_popup();
self.popup = Some(self.create_disconnect_popup());
}
KeyCode::Tab => self.switch_input_field(), // <TAB> switch tab
KeyCode::Down => {
@@ -578,7 +580,7 @@ impl FileTransferActivity {
KeyCode::Char(ch) => match ch {
'q' | 'Q' => {
// Create quit prompt dialog
self.popup = self.create_quit_popup();
self.popup = Some(self.create_quit_popup());
}
_ => { /* Nothing to do */ }
},

View File

@@ -62,23 +62,23 @@ impl FileTransferActivity {
/// ### create_quit_popup
///
/// Create quit popup input mode (since must be shared between different input handlers)
pub(super) fn create_disconnect_popup(&mut self) -> Option<Popup> {
Some(Popup::YesNo(
pub(super) fn create_disconnect_popup(&mut self) -> Popup {
Popup::YesNo(
String::from("Are you sure you want to disconnect?"),
FileTransferActivity::disconnect,
FileTransferActivity::callback_nothing_to_do,
))
)
}
/// ### create_quit_popup
///
/// Create quit popup input mode (since must be shared between different input handlers)
pub(super) fn create_quit_popup(&mut self) -> Option<Popup> {
Some(Popup::YesNo(
pub(super) fn create_quit_popup(&mut self) -> Popup {
Popup::YesNo(
String::from("Are you sure you want to quit?"),
FileTransferActivity::disconnect_and_quit,
FileTransferActivity::callback_nothing_to_do,
))
)
}
/// ### switch_input_field

View File

@@ -69,6 +69,7 @@ pub struct FileTransferParams {
pub protocol: FileTransferProtocol,
pub username: Option<String>,
pub password: Option<String>,
pub entry_directory: Option<PathBuf>,
}
/// ### InputField

View File

@@ -67,6 +67,14 @@ impl FileTransferActivity {
.as_ref(),
);
}
// Try to change directory to entry directory
let mut remote_chdir: Option<PathBuf> = None;
if let Some(entry_directory) = &self.params.entry_directory {
remote_chdir = Some(entry_directory.clone());
}
if let Some(entry_directory) = remote_chdir {
self.remote_changedir(entry_directory.as_path(), false);
}
// Set state to explorer
self.popup = None;
self.reload_remote_dir();
@@ -607,13 +615,14 @@ impl FileTransferActivity {
// Restore index
self.local.set_abs_index(prev_index);
// Set index; keep if possible, otherwise set to last item
self.local.set_abs_index(match self.local.get_current_file() {
Some(_) => self.local.get_index(),
None => match self.local.count() {
0 => 0,
_ => self.local.count() - 1,
},
});
self.local
.set_abs_index(match self.local.get_current_file() {
Some(_) => self.local.get_index(),
None => match self.local.count() {
0 => 0,
_ => self.local.count() - 1,
},
});
}
Err(err) => {
self.log_and_alert(
@@ -636,13 +645,14 @@ impl FileTransferActivity {
// Restore index
self.remote.set_abs_index(prev_index);
// Set index; keep if possible, otherwise set to last item
self.remote.set_abs_index(match self.remote.get_current_file() {
Some(_) => self.remote.get_index(),
None => match self.remote.count() {
0 => 0,
_ => self.remote.count() - 1,
},
});
self.remote
.set_abs_index(match self.remote.get_current_file() {
Some(_) => self.remote.get_index(),
None => match self.remote.count() {
0 => 0,
_ => self.remote.count() - 1,
},
});
}
Err(err) => {
self.log_and_alert(

View File

@@ -238,6 +238,10 @@ impl SetupActivity {
// Move left
config_cli.set_show_hidden_files(true);
}
UserInterfaceInputField::CheckForUpdates => {
// move left
config_cli.set_check_for_updates(true);
}
_ => { /* Not a tab field */ }
}
}
@@ -275,6 +279,10 @@ impl SetupActivity {
// Move right
config_cli.set_show_hidden_files(false);
}
UserInterfaceInputField::CheckForUpdates => {
// move right
config_cli.set_check_for_updates(false);
}
_ => { /* Not a tab field */ }
}
}
@@ -284,6 +292,9 @@ impl SetupActivity {
self.tab = SetupTab::UserInterface(match field {
UserInterfaceInputField::FileFmt => UserInterfaceInputField::GroupDirs,
UserInterfaceInputField::GroupDirs => {
UserInterfaceInputField::CheckForUpdates
}
UserInterfaceInputField::CheckForUpdates => {
UserInterfaceInputField::ShowHiddenFiles
}
UserInterfaceInputField::ShowHiddenFiles => {
@@ -305,6 +316,9 @@ impl SetupActivity {
UserInterfaceInputField::ShowHiddenFiles
}
UserInterfaceInputField::ShowHiddenFiles => {
UserInterfaceInputField::CheckForUpdates
}
UserInterfaceInputField::CheckForUpdates => {
UserInterfaceInputField::GroupDirs
}
UserInterfaceInputField::GroupDirs => UserInterfaceInputField::FileFmt,
@@ -354,7 +368,8 @@ impl SetupActivity {
}
UserInterfaceInputField::FileFmt => {
// Push char to current file fmt
let mut file_fmt = config_cli.get_file_fmt().unwrap_or_default();
let mut file_fmt =
config_cli.get_file_fmt().unwrap_or_default();
file_fmt.push(ch);
// update value
config_cli.set_file_fmt(file_fmt);

View File

@@ -90,6 +90,7 @@ impl SetupActivity {
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(1),
]
.as_ref(),
@@ -105,12 +106,15 @@ impl SetupActivity {
if let Some(tab) = self.draw_hidden_files_tab() {
f.render_widget(tab, ui_cfg_chunks[2]);
}
if let Some(tab) = self.draw_default_group_dirs_tab() {
if let Some(tab) = self.draw_check_for_updates_tab() {
f.render_widget(tab, ui_cfg_chunks[3]);
}
if let Some(tab) = self.draw_file_fmt_input() {
if let Some(tab) = self.draw_default_group_dirs_tab() {
f.render_widget(tab, ui_cfg_chunks[4]);
}
if let Some(tab) = self.draw_file_fmt_input() {
f.render_widget(tab, ui_cfg_chunks[5]);
}
// Set cursor
if let Some(cli) = &self.config_cli {
match form_field {
@@ -317,9 +321,9 @@ impl SetupActivity {
}
}
/// ### draw_default_protocol_tab
/// ### draw_hidden_files_tab
///
/// Draw default protocol input tab
/// Draw default hidden files tab
fn draw_hidden_files_tab(&self) -> Option<Tabs> {
// Check if config client is some
match &self.config_cli {
@@ -358,6 +362,47 @@ impl SetupActivity {
}
}
/// ### draw_check_for_updates_tab
///
/// Draw check for updates tab
fn draw_check_for_updates_tab(&self) -> Option<Tabs> {
// Check if config client is some
match &self.config_cli {
Some(cli) => {
let choices: Vec<Spans> = vec![Spans::from("Yes"), Spans::from("No")];
let index: usize = match cli.get_check_for_updates() {
true => 0,
false => 1,
};
let (bg, fg, block_fg): (Color, Color, Color) = match &self.tab {
SetupTab::UserInterface(field) => match field {
UserInterfaceInputField::CheckForUpdates => {
(Color::LightYellow, Color::Black, Color::LightYellow)
}
_ => (Color::Reset, Color::LightYellow, Color::Reset),
},
_ => (Color::Reset, Color::Reset, Color::Reset),
};
Some(
Tabs::new(choices)
.block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.style(Style::default().fg(block_fg))
.title("Check for updates?"),
)
.select(index)
.style(Style::default())
.highlight_style(
Style::default().add_modifier(Modifier::BOLD).fg(fg).bg(bg),
),
)
}
None => None,
}
}
/// ### draw_default_group_dirs_tab
///
/// Draw group dirs input tab
@@ -441,15 +486,11 @@ impl SetupActivity {
// Iterate over ssh keys
let mut ssh_keys: Vec<ListItem> = Vec::with_capacity(cli.iter_ssh_keys().count());
for key in cli.iter_ssh_keys() {
if let Ok(host) = cli.get_ssh_key(key) {
if let Some((addr, username, _)) = host {
ssh_keys.push(ListItem::new(Span::from(format!(
"{} at {}",
username, addr,
))));
} else {
continue;
}
if let Ok(Some((addr, username, _))) = cli.get_ssh_key(key) {
ssh_keys.push(ListItem::new(Span::from(format!(
"{} at {}",
username, addr,
))));
} else {
continue;
}

View File

@@ -54,6 +54,7 @@ enum UserInterfaceInputField {
DefaultProtocol,
TextEditor,
ShowHiddenFiles,
CheckForUpdates,
GroupDirs,
FileFmt,
}
@@ -168,13 +169,11 @@ impl Activity for SetupActivity {
return;
}
// Read one event
if let Ok(event) = self.context.as_ref().unwrap().input_hnd.read_event() {
if let Some(event) = event {
// Set redraw to true
self.redraw = true;
// Handle event
self.handle_input_event(&event);
}
if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
// Set redraw to true
self.redraw = true;
// Handle event
self.handle_input_event(&event);
}
// Redraw if necessary
if self.redraw {

85
src/utils/git.rs Normal file
View File

@@ -0,0 +1,85 @@
//! ## git
//!
//! `git` is the module which provides utilities to interact through the GIT API and to perform some stuff at git level
/*
*
* Copyright (C) 2020-2021 Christian Visintin - christian.visintin1997@gmail.com
*
* This file is part of "TermSCP"
*
* TermSCP is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* TermSCP is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with TermSCP. If not, see <http://www.gnu.org/licenses/>.
*
*/
// Deps
extern crate ureq;
// Locals
use super::parser::parse_semver;
// Others
use serde::Deserialize;
#[derive(Deserialize)]
struct TagInfo {
tag_name: String,
}
/// ### check_for_updates
///
/// Check if there is a new version available for termscp.
/// This is performed through the Github API
/// In case of success returns Ok(Option<String>), where the Option is Some(new_version); otherwise if no version is available, return None
/// In case of error returns Error with the error description
pub fn check_for_updates(current_version: &str) -> Result<Option<String>, String> {
// Send request
let github_version: Result<String, String> =
match ureq::get("https://api.github.com/repos/veeso/termscp/releases/latest").call() {
Ok(response) => match response.into_json::<TagInfo>() {
Ok(tag_info) => Ok(tag_info.tag_name),
Err(err) => Err(err.to_string()),
},
Err(err) => Err(err.to_string()),
};
// Check version
match github_version {
Err(err) => Err(err),
Ok(version) => {
// Parse version
match parse_semver(version.as_str()) {
Some(new_version) => {
// Check if version is different
if new_version.as_str() > current_version {
Ok(Some(new_version)) // New version is available
} else {
Ok(None) // No new version
}
}
None => Err(String::from("Got bad response from Github")),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_utils_git_check_for_updates() {
assert!(check_for_updates("100.0.0").ok().unwrap().is_none());
assert!(check_for_updates("0.0.1").ok().unwrap().is_some());
}
}

View File

@@ -26,5 +26,6 @@
// modules
pub mod crypto;
pub mod fmt;
pub mod git;
pub mod parser;
pub mod random;

View File

@@ -25,29 +25,62 @@
// Dependencies
extern crate chrono;
extern crate regex;
extern crate whoami;
// Locals
use crate::filetransfer::FileTransferProtocol;
#[cfg(not(test))] // NOTE: don't use configuration during tests
use crate::system::config_client::ConfigClient;
#[cfg(not(test))] // NOTE: don't use configuration during tests
use crate::system::environment;
// Ext
use chrono::format::ParseError;
use chrono::prelude::*;
use regex::Regex;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
// Regex
lazy_static! {
/**
* Regex matches:
* - group 1: Some(protocol) | None
* - group 2: Some(user) | None
* - group 3: Address
* - group 4: Some(port) | None
* - group 5: Some(path) | None
*/
static ref REMOTE_OPT_REGEX: Regex = Regex::new(r"(?:([a-z]+)://)?(?:([^@]+)@)?(?:([^:]+))(?::((?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])))?(?::([^:]+))?").ok().unwrap();
/**
* Regex matches:
* - group 1: Version
* E.g. termscp-0.3.2 => 0.3.2
* v0.4.0 => 0.4.0
*/
static ref SEMVER_REGEX: Regex = Regex::new(r".*(:?[0-9]\.[0-9]\.[0-9])").unwrap();
}
pub struct RemoteOptions {
pub hostname: String,
pub port: u16,
pub protocol: FileTransferProtocol,
pub username: Option<String>,
pub wrkdir: Option<PathBuf>,
}
/// ### parse_remote_opt
///
/// Parse remote option string. Returns in case of success a tuple made of (address, port, protocol, username)
/// Parse remote option string. Returns in case of success a RemoteOptions struct
/// For ssh if username is not provided, current user will be used.
/// In case of error, message is returned
/// If port is missing default port will be used for each protocol
/// SFTP => 22
/// FTP => 21
/// The option string has the following syntax
/// [protocol]://[username]@{address}:[port]
/// [protocol://][username@]{address}[:port][:path]
/// The only argument which is mandatory is address
/// NOTE: possible strings
/// - 172.26.104.1
@@ -57,14 +90,9 @@ use std::time::{Duration, SystemTime};
/// - sftp://172.26.104.1
/// - ...
///
pub fn parse_remote_opt(
remote: &str,
) -> Result<(String, u16, FileTransferProtocol, Option<String>), String> {
let mut wrkstr: String = remote.to_string();
let address: String;
let mut port: u16 = 22;
let mut username: Option<String> = None;
pub fn parse_remote_opt(remote: &str) -> Result<RemoteOptions, String> {
// Set protocol to default protocol
#[cfg(not(test))] // NOTE: don't use configuration during tests
let mut protocol: FileTransferProtocol = match environment::init_config_dir() {
Ok(p) => match p {
Some(p) => {
@@ -79,71 +107,65 @@ pub fn parse_remote_opt(
},
Err(_) => FileTransferProtocol::Sftp,
};
// Split string by '://'
let tokens: Vec<&str> = wrkstr.split("://").collect();
// If length is > 1, then token[0] is protocol
match tokens.len() {
1 => {}
2 => {
// Parse protocol
let (m_protocol, m_port) = match FileTransferProtocol::from_str(tokens[0]) {
Ok(proto) => match proto {
FileTransferProtocol::Ftp(_) => (proto, 21),
FileTransferProtocol::Scp => (proto, 22),
FileTransferProtocol::Sftp => (proto, 22),
#[cfg(test)] // NOTE: during test set protocol just to Sftp
let mut protocol: FileTransferProtocol = FileTransferProtocol::Sftp;
// Match against regex
match REMOTE_OPT_REGEX.captures(remote) {
Some(groups) => {
// Match protocol
let mut port: u16 = 22;
if let Some(group) = groups.get(1) {
// Set protocol from group
let (m_protocol, m_port) = match FileTransferProtocol::from_str(group.as_str()) {
Ok(proto) => match proto {
FileTransferProtocol::Ftp(_) => (proto, 21),
FileTransferProtocol::Scp => (proto, 22),
FileTransferProtocol::Sftp => (proto, 22),
},
Err(_) => return Err(format!("Unknown protocol \"{}\"", group.as_str())),
};
// NOTE: tuple destructuring assignment is not supported yet :(
protocol = m_protocol;
port = m_port;
}
// Match user
let username: Option<String> = match groups.get(2) {
Some(group) => Some(group.as_str().to_string()),
None => match protocol {
// If group is empty, set to current user
FileTransferProtocol::Scp | FileTransferProtocol::Sftp => {
Some(whoami::username())
}
_ => None,
},
Err(_) => return Err(format!("Unknown protocol '{}'", tokens[0])),
};
protocol = m_protocol;
port = m_port;
wrkstr = String::from(tokens[1]); // Wrkstr becomes tokens[1]
}
_ => return Err(String::from("Bad syntax")), // Too many tokens...
}
// Set username to default if sftp or scp
if matches!(
protocol,
FileTransferProtocol::Sftp | FileTransferProtocol::Scp
) {
// Set username to current username
username = Some(whoami::username());
}
// Split wrkstring by '@'
let tokens: Vec<&str> = wrkstr.split('@').collect();
match tokens.len() {
1 => {}
2 => {
// Username is first token
username = Some(String::from(tokens[0]));
// Update wrkstr
wrkstr = String::from(tokens[1]);
}
_ => return Err(String::from("Bad syntax")), // Too many tokens...
}
// Split wrkstring by ':'
let tokens: Vec<&str> = wrkstr.split(':').collect();
match tokens.len() {
1 => {
// Address is wrkstr
address = wrkstr.clone();
}
2 => {
// Address is first token
address = String::from(tokens[0]);
// Port is second str
port = match tokens[1].parse::<u16>() {
Ok(val) => val,
Err(_) => {
return Err(format!(
"Port must be a number in range [0-65535], but is '{}'",
tokens[1]
))
}
// Get address
let hostname: String = match groups.get(3) {
Some(group) => group.as_str().to_string(),
None => return Err(String::from("Missing address")),
};
// Get port
if let Some(group) = groups.get(4) {
port = match group.as_str().parse::<u16>() {
Ok(p) => p,
Err(err) => return Err(format!("Bad port \"{}\": {}", group.as_str(), err)),
};
}
// Get workdir
let wrkdir: Option<PathBuf> = match groups.get(5) {
Some(group) => Some(PathBuf::from(group.as_str())),
None => None,
};
Ok(RemoteOptions {
hostname,
port,
protocol,
username,
wrkdir,
})
}
_ => return Err(String::from("Bad syntax")), // Too many tokens...
None => Err(String::from("Bad remote host syntax!")),
}
Ok((address, port, protocol, username))
}
/// ### parse_lstime
@@ -196,6 +218,19 @@ pub fn parse_datetime(tm: &str, fmt: &str) -> Result<SystemTime, ParseError> {
}
}
/// ### parse_semver
///
/// Parse semver string
pub fn parse_semver(haystack: &str) -> Option<String> {
match SEMVER_REGEX.captures(haystack) {
Some(groups) => match groups.get(1) {
Some(version) => Some(version.as_str().to_string()),
None => None,
},
None => None,
}
}
#[cfg(test)]
mod tests {
@@ -205,90 +240,107 @@ mod tests {
#[test]
fn test_utils_parse_remote_opt() {
// Base case
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 22);
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert!(result.3.is_some());
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 22);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some());
// User case
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("root@172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 22);
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert_eq!(result.3.unwrap(), String::from("root"));
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 22);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(result.username.unwrap(), String::from("root"));
assert!(result.wrkdir.is_none());
// User + port
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("root@172.26.104.1:8022"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 8022);
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert_eq!(result.3.unwrap(), String::from("root"));
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1:8022"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 8022);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(result.username.unwrap(), String::from("root"));
assert!(result.wrkdir.is_none());
// Port only
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("172.26.104.1:4022"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 4022);
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert!(result.3.is_some());
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1:4022"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 4022);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some());
assert!(result.wrkdir.is_none());
// Protocol
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("ftp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 21); // Fallback to ftp default
assert_eq!(result.2, FileTransferProtocol::Ftp(false));
assert!(result.3.is_none()); // Doesn't fall back
// Protocol
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("sftp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 22); // Fallback to sftp default
assert_eq!(result.2, FileTransferProtocol::Sftp);
assert!(result.3.is_some()); // Doesn't fall back
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("scp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 22); // Fallback to scp default
assert_eq!(result.2, FileTransferProtocol::Scp);
assert!(result.3.is_some()); // Doesn't fall back
// Protocol + user
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 21); // Fallback to ftp default
assert_eq!(result.2, FileTransferProtocol::Ftp(true));
assert_eq!(result.3.unwrap(), String::from("anon"));
let result: RemoteOptions = parse_remote_opt(&String::from("ftp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 21); // Fallback to ftp default
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
assert!(result.username.is_none()); // Doesn't fall back
assert!(result.wrkdir.is_none());
// Protocol
let result: RemoteOptions = parse_remote_opt(&String::from("sftp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 22); // Fallback to sftp default
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some()); // Doesn't fall back
assert!(result.wrkdir.is_none());
let result: RemoteOptions = parse_remote_opt(&String::from("scp://172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 22); // Fallback to scp default
assert_eq!(result.protocol, FileTransferProtocol::Scp);
assert!(result.username.is_some()); // Doesn't fall back
assert!(result.wrkdir.is_none());
// Protocol + user
let result: RemoteOptions = parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 21); // Fallback to ftp default
assert_eq!(result.protocol, FileTransferProtocol::Ftp(true));
assert_eq!(result.username.unwrap(), String::from("anon"));
assert!(result.wrkdir.is_none());
// Path
let result: RemoteOptions = parse_remote_opt(&String::from("root@172.26.104.1:8022:/var"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 8022);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert_eq!(result.username.unwrap(), String::from("root"));
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("/var"));
// Port only
let result: RemoteOptions = parse_remote_opt(&String::from("172.26.104.1:home"))
.ok()
.unwrap();
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 22);
assert_eq!(result.protocol, FileTransferProtocol::Sftp);
assert!(result.username.is_some());
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("home"));
// All together now
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021"))
let result: RemoteOptions =
parse_remote_opt(&String::from("ftp://anon@172.26.104.1:8021:/tmp"))
.ok()
.unwrap();
assert_eq!(result.0, String::from("172.26.104.1"));
assert_eq!(result.1, 8021); // Fallback to ftp default
assert_eq!(result.2, FileTransferProtocol::Ftp(false));
assert_eq!(result.3.unwrap(), String::from("anon"));
assert_eq!(result.hostname, String::from("172.26.104.1"));
assert_eq!(result.port, 8021); // Fallback to ftp default
assert_eq!(result.protocol, FileTransferProtocol::Ftp(false));
assert_eq!(result.username.unwrap(), String::from("anon"));
assert_eq!(result.wrkdir.unwrap(), PathBuf::from("/tmp"));
// bad syntax
assert!(parse_remote_opt(&String::from("://172.26.104.1")).is_err()); // Missing protocol
assert!(parse_remote_opt(&String::from("omar://172.26.104.1")).is_err()); // Bad protocol
assert!(parse_remote_opt(&String::from("172.26.104.1:abc")).is_err()); // Bad port
assert!(parse_remote_opt(&String::from("omar://172.26.104.1:650000")).is_err());
// Bad port
}
#[test]
@@ -352,4 +404,15 @@ mod tests {
// Not enough argument for datetime
assert!(parse_datetime("04-08-14", "%d-%m-%y").is_err());
}
#[test]
fn test_utils_parse_semver() {
assert_eq!(
parse_semver("termscp-0.3.2").unwrap(),
String::from("0.3.2")
);
assert_eq!(parse_semver("v0.4.1").unwrap(), String::from("0.4.1"),);
assert_eq!(parse_semver("1.0.0").unwrap(), String::from("1.0.0"),);
assert!(parse_semver("v1.1").is_none());
}
}