24 Commits

Author SHA1 Message Date
ChristianVisintin
20872829f1 changelog 2020-12-10 12:38:09 +01:00
ChristianVisintin
2d37607d2c 0.1.1 2020-12-10 12:36:09 +01:00
ChristianVisintin
c9c35d027b Removed unused import 2020-12-10 12:13:45 +01:00
ChristianVisintin
11475b64ea Removed ctrl to 'Q' 2020-12-10 12:06:32 +01:00
ChristianVisintin
53249c8bc5 Break on recv/send errors 2020-12-10 11:52:00 +01:00
ChristianVisintin
843c5ab6d0 Break on recv/send errors 2020-12-10 11:51:49 +01:00
ChristianVisintin
66f17c93c2 Finalize get stream for FTP 2020-12-10 11:27:00 +01:00
ChristianVisintin
69df9f0aa9 ftp4 0.4.1 2020-12-10 11:26:52 +01:00
ChristianVisintin
5c5a93cc41 use macos-latest 2020-12-10 11:10:42 +01:00
ChristianVisintin
040b684398 Typo in windows version 2020-12-10 11:04:53 +01:00
ChristianVisintin
d75867ed7b Build all branches 2020-12-10 10:47:26 +01:00
ChristianVisintin
e36ae7e32d Updated textwrap to 0.13.0 2020-12-10 10:41:13 +01:00
ChristianVisintin
4aa262a273 Build all branches 2020-12-10 10:40:36 +01:00
ChristianVisintin
7e6044a41b Removed CTRL key; just press associated key to perform command 2020-12-10 10:28:40 +01:00
ChristianVisintin
145b778ff3 Added FileInfo popup ('I') 2020-12-10 10:28:13 +01:00
ChristianVisintin
ca46c872cf fmt_pex is now a method 2020-12-10 10:05:30 +01:00
ChristianVisintin
98e3866447 Elide paths in explorer tabs if they are too long 2020-12-10 09:22:58 +01:00
ChristianVisintin
95ab3daa86 Elide file names longer than 24 2020-12-10 08:44:35 +01:00
ChristianVisintin
93977cc714 Ignore capital letters when sorting files 2020-12-09 16:35:16 +01:00
ChristianVisintin
b3537afd2e Check value in set_progress 2020-12-09 16:29:35 +01:00
ChristianVisintin
939f741a1b some stuff in readme 2020-12-09 16:20:55 +01:00
ChristianVisintin
5a96091258 Fixed help 2020-12-09 16:19:27 +01:00
ChristianVisintin
2d20f0c534 Working on 0.1.1 2020-12-09 16:18:13 +01:00
ChristianVisintin
8f3e416144 Ask password before cleaning screen 2020-12-09 16:17:11 +01:00
16 changed files with 607 additions and 389 deletions

View File

@@ -1,10 +1,6 @@
name: Linux
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always

View File

@@ -1,10 +1,6 @@
name: MacOS
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always
@@ -12,7 +8,7 @@ env:
jobs:
build:
runs-on: macos-10.14
runs-on: macos-latest
steps:
- uses: actions/checkout@v2

View File

@@ -1,10 +1,6 @@
name: Windows
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
on: [push, pull_request]
env:
CARGO_TERM_COLOR: always

View File

@@ -1,10 +1,31 @@
# Changelog
- [Changelog](#changelog)
- [0.1.1](#011)
- [0.1.0](#010)
---
## 0.1.1
Released on 10/12/2020
- enhancements:
- password prompt: ask before performing terminal clear
- file explorer:
- file names are now sorted ignoring capital letters
- file names longer than 23, are now cut to 20 and followed by `...`
- paths which exceed tab size in explorer are elided with the following formato `ANCESTOR[1]/.../PARENT/DIRNAME`
- keybindings:
- `I`: show info about selected file or directory
- Removed `CTRL`; just use keys now.
- bugfix:
- prevent panic in set_progress, for progress values `> 100.0 or < 0.0`
- Fixed FTP get, which didn't finalize the reader
- dependencies:
- updated `textwrap` to `0.13.0`
- updated `ftp4` to `4.0.1`
## 0.1.0
Released on 06/12/2020

17
Cargo.lock generated
View File

@@ -137,9 +137,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "ftp4"
version = "4.0.0"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "412a5da44e30489a1790749f34044d53bbc4ff0cf8d789ee37be4b6befeb7c23"
checksum = "6318bd155755b6e07ccb7bf8e5b1b7cb221c74fff7c6440692ef38eb2ec1d42c"
dependencies = [
"chrono",
"lazy_static",
@@ -603,6 +603,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75"
[[package]]
name = "smawk"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1bc737c97d093feb72e67f4926d9b22d717ce8580cd25f0ce86d74e859c466d"
[[package]]
name = "socket2"
version = "0.3.17"
@@ -654,7 +660,7 @@ dependencies = [
[[package]]
name = "termscp"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"bytesize",
"chrono",
@@ -676,10 +682,11 @@ dependencies = [
[[package]]
name = "textwrap"
version = "0.12.1"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
checksum = "b1bca196a5c5a7bc57a5c92809cf5670e16bcbca3bf0d09ef47150bf97221f6f"
dependencies = [
"smawk",
"unicode-width",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "termscp"
version = "0.1.0"
version = "0.1.1"
authors = ["Christian Visintin"]
edition = "2018"
license = "GPL-3.0"
@@ -16,7 +16,7 @@ readme = "README.md"
[dependencies]
crossterm = "0.18.2"
ftp4 = { version = "^4.0.0", features = ["secure"] }
ftp4 = { version = "^4.0.1", features = ["secure"] }
getopts = "0.2.21"
ssh2 = "0.9.0"
tui = { version = "0.13.0", features = ["crossterm"], default-features = false }
@@ -25,7 +25,7 @@ rpassword = "5.0.0"
unicode-width = "0.1.7"
chrono = "0.4.19"
bytesize = "1.0.1"
textwrap = "0.12.1"
textwrap = "0.13.0"
regex = "1.4.2"
lazy_static = "1.4.0"
hostname = "0.3.1"

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/ChristianVisintin/TermSCP.svg)](https://github.com/ChristianVisintin/TermSCP) [![Issues](https://img.shields.io/github/issues/ChristianVisintin/TermSCP.svg)](https://github.com/ChristianVisintin/TermSCP/issues) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.1.0-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) [![Stars](https://img.shields.io/github/stars/ChristianVisintin/TermSCP.svg)](https://github.com/ChristianVisintin/TermSCP) [![Issues](https://img.shields.io/github/issues/ChristianVisintin/TermSCP.svg)](https://github.com/ChristianVisintin/TermSCP/issues) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.1.1-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![Build](https://github.com/ChristianVisintin/TermSCP/workflows/Linux/badge.svg)](https://github.com/ChristianVisintin/TermSCP/actions) [![Build](https://github.com/ChristianVisintin/TermSCP/workflows/MacOS/badge.svg)](https://github.com/ChristianVisintin/TermSCP/actions) [![Build](https://github.com/ChristianVisintin/TermSCP/workflows/Windows/badge.svg)](https://github.com/ChristianVisintin/TermSCP/actions)
~ Basically, WinSCP on a terminal ~
Developed by Christian Visintin
Current version: 0.1.0 (06/12/2020)
Current version: 0.1.1 (10/12/2020)
---
@@ -37,7 +37,7 @@ Current version: 0.1.0 (06/12/2020)
## About TermSCP 🖥
TermSCP is basically a porting of WinSCP to terminal. 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 works both on **Linux**, **MacOS**, **UNIX** and **Windows** and supports SFTP, SCP, FTP and FTPS.
TermSCP is basically a porting of WinSCP to terminal. 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 works both on **Linux**, **MacOS**, **BSD** and **Windows** and supports SFTP, SCP, FTP and FTPS.
![Explorer](assets/images/explorer.gif)
@@ -45,7 +45,7 @@ TermSCP is basically a porting of WinSCP to terminal. So basically is a terminal
### Why TermSCP 🤔
It happens very often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me then to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there midnight commander too, but actually I don't like it very much tbh (and doesn't support scp).
It happens quite often to me, when using SCP at work to forget the path of a file on a remote machine, which forces me then to connect through SSH, gather the file path and finally download it through SCP. I could use WinSCP, but I use Linux and I pratically use the terminal for everything, so I wanted something like WinSCP on my terminal. Yeah, I know there midnight commander too, but actually I don't like it very much tbh (and hasn't a decent support for scp).
## Features 🎁
@@ -54,7 +54,7 @@ It happens very often to me, when using SCP at work to forget the path of a file
- SCP
- FTP and FTPS
- Practical user interface to explore and operate on the remote and on the local machine file system
- Compatible with Windows, Linux, UNIX and MacOS
- Compatible with Windows, Linux, BSD and MacOS
- Written in Rust
- Easy to extend with new file transfers protocols
@@ -74,8 +74,8 @@ cargo install termscp
### Deb package 📦
Get `deb` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.0_amd64.deb)
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.0_amd64.deb`
Get `deb` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.1_amd64.deb)
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp_0.1.1_amd64.deb`
then install through dpkg:
@@ -87,8 +87,8 @@ gdebi termscp_*.deb
### RPM Package 📦
Get `rpm` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.0-1.x86_64.rpm)
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.0-1.x86_64.rpm`
Get `rpm` package from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.1-1.x86_64.rpm)
or run `wget https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp-0.1.1-1.x86_64.rpm`
then install through rpm:
@@ -106,7 +106,7 @@ Start PowerShell as administrator and run
choco install termscp
```
Alternatively you can download the ZIP file from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp.0.1.0.nupkg)
Alternatively you can download the ZIP file from [HERE](https://github.com/ChristianVisintin/TermSCP/releases/download/latest/termscp.0.1.1.nupkg)
and then with PowerShell started with administrator previleges, run:
@@ -193,12 +193,13 @@ Password can be basically provided through 3 ways when address argument is provi
| `<PGDOWN>` | Move down in selected list by 8 rows |
| `<ENTER>` | Enter directory |
| `<SPACE>` | Upload / download selected file |
| `<CTRL+D>` | Make directory |
| `<CTRL+G>` | Go to supplied path |
| `<CTRL+H>` | Show help |
| `<CTRL+Q>` | Quit TermSCP |
| `<CTRL+R>` | Rename file |
| `<CTRL+U>` | Go to parent directory |
| `<D>` | Make directory |
| `<G>` | Go to supplied path |
| `<H>` | Show help |
| `<H>` | Show info about selected file or directory |
| `<Q>` | Quit TermSCP |
| `<R>` | Rename file |
| `<U>` | Go to parent directory |
| `<CANC>` | Delete file |
---

View File

@@ -574,8 +574,19 @@ 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> {
Ok(())
fn on_recv(&mut self, readable: Box<dyn Read>) -> Result<(), FileTransferError> {
match &mut self.stream {
Some(stream) => match stream.finalize_get(readable) {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("{}", err),
)),
},
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
}

View File

@@ -27,7 +27,7 @@ extern crate bytesize;
#[cfg(any(unix, macos, linux))]
extern crate users;
use crate::utils::time_to_str;
use crate::utils::{fmt_pex, time_to_str};
use bytesize::ByteSize;
use std::path::PathBuf;
@@ -101,51 +101,7 @@ impl std::fmt::Display for FsEntry {
match dir.unix_pex {
None => mode.push_str("?????????"),
Some((owner, group, others)) => {
let read: u8 = (owner >> 2) & 0x1;
let write: u8 = (owner >> 1) & 0x1;
let exec: u8 = owner & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
let read: u8 = (group >> 2) & 0x1;
let write: u8 = (group >> 1) & 0x1;
let exec: u8 = group & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
let read: u8 = (others >> 2) & 0x1;
let write: u8 = (others >> 1) & 0x1;
let exec: u8 = others & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
mode.push_str(fmt_pex(owner, group, others).as_str())
}
}
// Get username
@@ -170,14 +126,15 @@ impl std::fmt::Display for FsEntry {
let size: String = String::from("4096");
// Get date
let datetime: String = time_to_str(dir.last_change_time, "%b %d %Y %H:%M");
// Set file name (or elide if too long)
let dir_name: String = match dir.name.len() >= 24 {
false => dir.name.clone(),
true => format!("{}...", &dir.name.as_str()[0..20]),
};
write!(
f,
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
dir.name.as_str(),
mode,
username,
size,
datetime
dir_name, mode, username, size, datetime
)
}
FsEntry::File(file) => {
@@ -191,51 +148,7 @@ impl std::fmt::Display for FsEntry {
match file.unix_pex {
None => mode.push_str("?????????"),
Some((owner, group, others)) => {
let read: u8 = (owner >> 2) & 0x1;
let write: u8 = (owner >> 1) & 0x1;
let exec: u8 = owner & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
let read: u8 = (group >> 2) & 0x1;
let write: u8 = (group >> 1) & 0x1;
let exec: u8 = group & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
let read: u8 = (others >> 2) & 0x1;
let write: u8 = (others >> 1) & 0x1;
let exec: u8 = others & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
mode.push_str(fmt_pex(owner, group, others).as_str())
}
}
// Get username
@@ -260,14 +173,15 @@ impl std::fmt::Display for FsEntry {
let size: ByteSize = ByteSize(file.size as u64);
// Get date
let datetime: String = time_to_str(file.last_change_time, "%b %d %Y %H:%M");
// Set file name (or elide if too long)
let file_name: String = match file.name.len() >= 24 {
false => file.name.clone(),
true => format!("{}...", &file.name.as_str()[0..20]),
};
write!(
f,
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
file.name.as_str(),
mode,
username,
size,
datetime
file_name, mode, username, size, datetime
)
}
}
@@ -291,51 +205,7 @@ impl std::fmt::Display for FsEntry {
match dir.unix_pex {
None => mode.push_str("?????????"),
Some((owner, group, others)) => {
let read: u8 = (owner >> 2) & 0x1;
let write: u8 = (owner >> 1) & 0x1;
let exec: u8 = owner & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
let read: u8 = (group >> 2) & 0x1;
let write: u8 = (group >> 1) & 0x1;
let exec: u8 = group & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
let read: u8 = (others >> 2) & 0x1;
let write: u8 = (others >> 1) & 0x1;
let exec: u8 = others & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
mode.push_str(fmt_pex(owner, group, others).as_str())
}
}
// Get username
@@ -354,14 +224,15 @@ impl std::fmt::Display for FsEntry {
let size: String = String::from("4096");
// Get date
let datetime: String = time_to_str(dir.last_change_time, "%b %d %Y %H:%M");
// Set file name (or elide if too long)
let dir_name: String = match dir.name.len() >= 24 {
false => dir.name.clone(),
true => format!("{}...", &dir.name.as_str()[0..20]),
};
write!(
f,
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
dir.name.as_str(),
mode,
username,
size,
datetime
dir_name, mode, username, size, datetime
)
}
FsEntry::File(file) => {
@@ -375,51 +246,7 @@ impl std::fmt::Display for FsEntry {
match file.unix_pex {
None => mode.push_str("?????????"),
Some((owner, group, others)) => {
let read: u8 = (owner >> 2) & 0x1;
let write: u8 = (owner >> 1) & 0x1;
let exec: u8 = owner & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
let read: u8 = (group >> 2) & 0x1;
let write: u8 = (group >> 1) & 0x1;
let exec: u8 = group & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
let read: u8 = (others >> 2) & 0x1;
let write: u8 = (others >> 1) & 0x1;
let exec: u8 = others & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
mode.push_str(fmt_pex(owner, group, others).as_str())
}
}
// Get username
@@ -438,14 +265,15 @@ impl std::fmt::Display for FsEntry {
let size: ByteSize = ByteSize(file.size as u64);
// Get date
let datetime: String = time_to_str(file.last_change_time, "%b %d %Y %H:%M");
// Set file name (or elide if too long)
let file_name: String = match file.name.len() >= 24 {
false => file.name.clone(),
true => format!("{}...", &file.name.as_str()[0..20]),
};
write!(
f,
"{:24}\t{:12}\t{:12}\t{:9}\t{:17}",
file.name.as_str(),
mode,
username,
size,
datetime
file_name, mode, username, size, datetime
)
}
}

View File

@@ -50,7 +50,7 @@ use filetransfer::FileTransferProtocol;
/// Print usage
fn print_usage(opts: Options) {
let brief = format!("Usage: termscp [Options]... [protocol:user@address:port]");
let brief = format!("Usage: termscp [options]... [protocol://user@address:port]");
print!("{}", opts.usage(&brief));
println!("\nPlease, report issues to <https://github.com/ChristianVisintin/TermSCP>");
}
@@ -134,17 +134,9 @@ fn main() {
Ok(dir) => dir,
Err(_) => PathBuf::from("/"),
};
// Create activity manager
let mut manager: ActivityManager = match ActivityManager::new(&wrkdir, ticks) {
Ok(m) => m,
Err(_) => {
eprintln!("Invalid directory '{}'", wrkdir.display());
std::process::exit(255);
}
};
// Initialize client if necessary
let mut start_activity: NextActivity = NextActivity::Authentication;
if let Some(address) = address {
if address.is_some() {
if password.is_none() {
// Ask password if unspecified
password = match rpassword::read_password_from_tty(Some("Password: ")) {
@@ -163,6 +155,17 @@ fn main() {
}
// In this case the first activity will be FileTransfer
start_activity = NextActivity::FileTransfer;
}
// Create activity manager (and context too)
let mut manager: ActivityManager = match ActivityManager::new(&wrkdir, ticks) {
Ok(m) => m,
Err(_) => {
eprintln!("Invalid directory '{}'", wrkdir.display());
std::process::exit(255);
}
};
// Set file transfer params if set
if let Some(address) = address {
manager.set_filetransfer_params(address, port, protocol, username, password);
}
// Run

View File

@@ -24,7 +24,7 @@ use super::{
InputField, InputMode, LogLevel, OnInputSubmitCallback, PopupType,
};
use crossterm::event::{KeyCode, KeyModifiers};
use crossterm::event::KeyCode;
use std::path::PathBuf;
use tui::style::Color;
@@ -187,69 +187,53 @@ impl FileTransferActivity {
}
KeyCode::Char(ch) => match ch {
'q' | 'Q' => {
if key.modifiers.intersects(KeyModifiers::CONTROL) {
// Create quit prompt dialog
self.input_mode = self.create_quit_popup();
}
// Create quit prompt dialog
self.input_mode = self.create_quit_popup();
}
'g' | 'G' => {
// Goto
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
// Show input popup
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Change working directory"),
FileTransferActivity::callback_change_directory,
));
}
// Show input popup
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Change working directory"),
FileTransferActivity::callback_change_directory,
));
}
'd' | 'D' => {
// Make directory
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert directory name"),
FileTransferActivity::callback_mkdir,
));
}
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert directory name"),
FileTransferActivity::callback_mkdir,
));
}
'h' | 'H' => {
// Show help
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
self.input_mode = InputMode::Popup(PopupType::Help);
}
self.input_mode = InputMode::Popup(PopupType::Help);
}
'i' | 'I' => {
// Show file info
self.input_mode = InputMode::Popup(PopupType::FileInfo);
}
'r' | 'R' => {
// Rename
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert new name"),
FileTransferActivity::callback_rename,
));
}
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert new name"),
FileTransferActivity::callback_rename,
));
}
's' | 'S' => {
// Save as...
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
// Ask for input
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Save as..."),
FileTransferActivity::callback_save_as,
));
}
// Ask for input
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Save as..."),
FileTransferActivity::callback_save_as,
));
}
'u' | 'U' => {
// Go to parent directory
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
// Get pwd
let path: PathBuf = self.context.as_ref().unwrap().local.pwd();
if let Some(parent) = path.as_path().parent() {
self.local_changedir(parent, true);
}
// Get pwd
let path: PathBuf = self.context.as_ref().unwrap().local.pwd();
if let Some(parent) = path.as_path().parent() {
self.local_changedir(parent, true);
}
}
' ' => {
@@ -403,78 +387,62 @@ impl FileTransferActivity {
}
KeyCode::Char(ch) => match ch {
'q' | 'Q' => {
if key.modifiers.intersects(KeyModifiers::CONTROL) {
// Create quit prompt dialog
self.input_mode = self.create_quit_popup();
}
// Create quit prompt dialog
self.input_mode = self.create_quit_popup();
}
'g' | 'G' => {
// Goto
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
// Show input popup
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Change working directory"),
FileTransferActivity::callback_change_directory,
));
}
// Show input popup
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Change working directory"),
FileTransferActivity::callback_change_directory,
));
}
'd' | 'D' => {
// Make directory
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert directory name"),
FileTransferActivity::callback_mkdir,
));
}
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert directory name"),
FileTransferActivity::callback_mkdir,
));
}
'h' | 'H' => {
// Show help
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
self.input_mode = InputMode::Popup(PopupType::Help);
}
self.input_mode = InputMode::Popup(PopupType::Help);
}
'i' | 'I' => {
// Show file info
self.input_mode = InputMode::Popup(PopupType::FileInfo);
}
'r' | 'R' => {
// Rename
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert new name"),
FileTransferActivity::callback_rename,
));
}
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Insert new name"),
FileTransferActivity::callback_rename,
));
}
's' | 'S' => {
// Save as...
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
// Ask for input
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Save as..."),
FileTransferActivity::callback_save_as,
));
}
// Ask for input
self.input_mode = InputMode::Popup(PopupType::Input(
String::from("Save as..."),
FileTransferActivity::callback_save_as,
));
}
'u' | 'U' => {
// Go to parent directory
// If ctrl is enabled...
if key.modifiers.intersects(KeyModifiers::CONTROL) {
// Get pwd
match self.client.pwd() {
Ok(path) => {
if let Some(parent) = path.as_path().parent() {
self.remote_changedir(parent, true);
}
}
Err(err) => {
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not change working directory: {}", err),
))
// Get pwd
match self.client.pwd() {
Ok(path) => {
if let Some(parent) = path.as_path().parent() {
self.remote_changedir(parent, true);
}
}
Err(err) => {
self.input_mode = InputMode::Popup(PopupType::Alert(
Color::Red,
format!("Could not change working directory: {}", err),
))
}
}
}
' ' => {
@@ -549,10 +517,8 @@ impl FileTransferActivity {
}
KeyCode::Char(ch) => match ch {
'q' | 'Q' => {
if key.modifiers.intersects(KeyModifiers::CONTROL) {
// Create quit prompt dialog
self.input_mode = self.create_quit_popup();
}
// Create quit prompt dialog
self.input_mode = self.create_quit_popup();
}
_ => { /* Nothing to do */ }
},
@@ -569,6 +535,7 @@ impl FileTransferActivity {
pub(super) fn handle_input_event_mode_popup(&mut self, ev: &InputEvent, popup: PopupType) {
match popup {
PopupType::Alert(_, _) => self.handle_input_event_mode_popup_alert(ev),
PopupType::FileInfo => self.handle_input_event_mode_popup_fileinfo(ev),
PopupType::Help => self.handle_input_event_mode_popup_help(ev),
PopupType::Fatal(_) => self.handle_input_event_mode_popup_fatal(ev),
PopupType::Input(_, cb) => self.handle_input_event_mode_popup_input(ev, cb),
@@ -599,6 +566,25 @@ impl FileTransferActivity {
}
}
/// ### handle_input_event_mode_popup_fileinfo
///
/// Input event handler for popup fileinfo
pub(super) fn handle_input_event_mode_popup_fileinfo(&mut self, ev: &InputEvent) {
// If enter, close popup
match ev {
InputEvent::Key(key) => {
match key.code {
KeyCode::Enter | KeyCode::Esc => {
// Set input mode back to explorer
self.input_mode = InputMode::Explorer;
}
_ => { /* Nothing to do */ }
}
}
_ => { /* Nothing to do */ }
}
}
/// ### handle_input_event_mode_popup_help
///
/// Input event handler for popup help

View File

@@ -19,13 +19,19 @@
*
*/
extern crate bytesize;
extern crate hostname;
#[cfg(any(unix, macos, linux))]
extern crate users;
use super::{
Context, DialogYesNoOption, FileExplorerTab, FileTransferActivity, FsEntry, InputField,
InputMode, LogLevel, LogRecord, PopupType,
};
use std::path::PathBuf;
use crate::utils::time_to_str;
use bytesize::ByteSize;
use std::path::{Path, PathBuf};
use tui::{
layout::{Constraint, Corner, Direction, Layout, Rect},
style::{Color, Modifier, Style},
@@ -33,6 +39,8 @@ use tui::{
widgets::{Block, Borders, Clear, Gauge, List, ListItem, ListState, Paragraph, Tabs},
};
use unicode_width::UnicodeWidthStr;
#[cfg(any(unix, macos, linux))]
use users::{get_group_by_gid, get_user_by_uid};
impl FileTransferActivity {
/// ### draw
@@ -67,7 +75,7 @@ impl FileTransferActivity {
remote_state.select(Some(self.remote.index));
// Draw tabs
f.render_stateful_widget(
self.draw_local_explorer(local_wrkdir),
self.draw_local_explorer(local_wrkdir, tabs_chunks[0].width),
tabs_chunks[0],
&mut localhost_state,
);
@@ -77,7 +85,7 @@ impl FileTransferActivity {
Err(_) => PathBuf::from("/"),
};
f.render_stateful_widget(
self.draw_remote_explorer(remote_wrkdir),
self.draw_remote_explorer(remote_wrkdir, tabs_chunks[1].width),
tabs_chunks[1],
&mut remote_state,
);
@@ -96,8 +104,9 @@ impl FileTransferActivity {
let (width, height): (u16, u16) = match popup {
PopupType::Alert(_, _) => (50, 10),
PopupType::Fatal(_) => (50, 10),
PopupType::FileInfo => (50, 50),
PopupType::Help => (50, 70),
PopupType::Input(_, _) => (30, 10),
PopupType::Input(_, _) => (40, 10),
PopupType::Progress(_) => (40, 10),
PopupType::Wait(_) => (50, 10),
PopupType::YesNo(_, _, _) => (30, 10),
@@ -113,6 +122,7 @@ impl FileTransferActivity {
self.draw_popup_fatal(txt.clone(), popup_area.width),
popup_area,
),
PopupType::FileInfo => f.render_widget(self.draw_popup_fileinfo(), popup_area),
PopupType::Help => f.render_widget(self.draw_popup_help(), popup_area),
PopupType::Input(txt, _) => {
f.render_widget(self.draw_popup_input(txt.clone()), popup_area);
@@ -141,7 +151,7 @@ impl FileTransferActivity {
/// ### draw_local_explorer
///
/// Draw local explorer list
pub(super) fn draw_local_explorer(&self, local_wrkdir: PathBuf) -> List {
pub(super) fn draw_local_explorer(&self, local_wrkdir: PathBuf, width: u16) -> List {
let hostname: String = match hostname::get() {
Ok(h) => String::from(h.as_os_str().to_string_lossy()),
Err(_) => String::from("localhost"),
@@ -163,7 +173,16 @@ impl FileTransferActivity {
},
_ => Style::default(),
})
.title(format!("{}:{} ", hostname, local_wrkdir.display())),
.title(format!(
"{}:{} ",
hostname,
FileTransferActivity::elide_wrkdir_path(
local_wrkdir.as_path(),
hostname.as_str(),
width
)
.display()
)),
)
.start_corner(Corner::TopLeft)
.highlight_style(
@@ -176,7 +195,7 @@ impl FileTransferActivity {
/// ### draw_remote_explorer
///
/// Draw remote explorer list
pub(super) fn draw_remote_explorer(&self, remote_wrkdir: PathBuf) -> List {
pub(super) fn draw_remote_explorer(&self, remote_wrkdir: PathBuf, width: u16) -> List {
let files: Vec<ListItem> = self
.remote
.files
@@ -197,7 +216,12 @@ impl FileTransferActivity {
.title(format!(
"{}:{} ",
self.params.address,
remote_wrkdir.display()
FileTransferActivity::elide_wrkdir_path(
remote_wrkdir.as_path(),
self.params.address.as_str(),
width
)
.display()
)),
)
.start_corner(Corner::TopLeft)
@@ -305,7 +329,7 @@ impl FileTransferActivity {
let mut lines: Vec<ListItem> = Vec::new();
for msg in message_rows.iter() {
lines.push(ListItem::new(Spans::from(
self.align_text_center(msg, width),
FileTransferActivity::align_text_center(msg, width),
)));
}
List::new(lines)
@@ -328,7 +352,7 @@ impl FileTransferActivity {
let mut lines: Vec<ListItem> = Vec::new();
for msg in message_rows.iter() {
lines.push(ListItem::new(Spans::from(
self.align_text_center(msg, width),
FileTransferActivity::align_text_center(msg, width),
)));
}
List::new(lines)
@@ -386,7 +410,7 @@ impl FileTransferActivity {
let mut lines: Vec<ListItem> = Vec::new();
for msg in message_rows.iter() {
lines.push(ListItem::new(Spans::from(
self.align_text_center(msg, width),
FileTransferActivity::align_text_center(msg, width),
)));
}
List::new(lines)
@@ -420,6 +444,225 @@ impl FileTransferActivity {
)
}
/// ### draw_popup_fileinfo
///
/// Draw popup containing info about selected fsentry
pub(super) fn draw_popup_fileinfo(&self) -> List {
let mut info: Vec<ListItem> = Vec::new();
// Get current fsentry
let fsentry: Option<&FsEntry> = match self.tab {
FileExplorerTab::Local => {
// Get selected file
match self.local.files.get(self.local.index) {
Some(entry) => Some(entry),
None => None,
}
}
FileExplorerTab::Remote => match self.remote.files.get(self.remote.index) {
Some(entry) => Some(entry),
None => None,
},
};
// Get file_name and fill info list
let file_name: String = match fsentry {
Some(fsentry) => match fsentry {
FsEntry::Directory(dir) => {
// Push path
info.push(ListItem::new(Spans::from(vec![
Span::styled("Path: ", Style::default()),
Span::styled(
match &dir.symlink {
Some(symlink) => {
format!("{} => {}", dir.abs_path.display(), symlink.display())
}
None => dir.abs_path.to_string_lossy().to_string(),
},
Style::default()
.fg(Color::LightYellow)
.add_modifier(Modifier::BOLD),
),
])));
// Push creation time
info.push(ListItem::new(Spans::from(vec![
Span::styled("Creation time: ", Style::default()),
Span::styled(
time_to_str(dir.creation_time, "%b %d %Y %H:%M:%S"),
Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
),
])));
// Push Last change
info.push(ListItem::new(Spans::from(vec![
Span::styled("Last change time: ", Style::default()),
Span::styled(
time_to_str(dir.last_change_time, "%b %d %Y %H:%M:%S"),
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
),
])));
// Push Last access
info.push(ListItem::new(Spans::from(vec![
Span::styled("Last access time: ", Style::default()),
Span::styled(
time_to_str(dir.last_access_time, "%b %d %Y %H:%M:%S"),
Style::default()
.fg(Color::LightMagenta)
.add_modifier(Modifier::BOLD),
),
])));
// User
#[cfg(any(unix, macos, linux))]
let username: String = match dir.user {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
},
None => String::from("0"),
};
#[cfg(target_os = "windows")]
let username: String = format!("{}", dir.user.unwrap_or(0));
info.push(ListItem::new(Spans::from(vec![
Span::styled("User: ", Style::default()),
Span::styled(
username,
Style::default()
.fg(Color::LightRed)
.add_modifier(Modifier::BOLD),
),
])));
// Group
#[cfg(any(unix, macos, linux))]
let group: String = match dir.group {
Some(gid) => match get_group_by_gid(gid) {
Some(group) => group.name().to_string_lossy().to_string(),
None => gid.to_string(),
},
None => String::from("0"),
};
#[cfg(target_os = "windows")]
let group: String = format!("{}", dir.group.unwrap_or(0));
info.push(ListItem::new(Spans::from(vec![
Span::styled("Group: ", Style::default()),
Span::styled(
group,
Style::default()
.fg(Color::LightBlue)
.add_modifier(Modifier::BOLD),
),
])));
// Finally return file name
dir.name.clone()
}
FsEntry::File(file) => {
// Push path
info.push(ListItem::new(Spans::from(vec![
Span::styled("Path: ", Style::default()),
Span::styled(
match &file.symlink {
Some(symlink) => {
format!("{} => {}", file.abs_path.display(), symlink.display())
}
None => file.abs_path.to_string_lossy().to_string(),
},
Style::default()
.fg(Color::LightYellow)
.add_modifier(Modifier::BOLD),
),
])));
// Push size
info.push(ListItem::new(Spans::from(vec![
Span::styled("Size: ", Style::default()),
Span::styled(
format!("{} ({})", ByteSize(file.size as u64), file.size),
Style::default()
.fg(Color::LightBlue)
.add_modifier(Modifier::BOLD),
),
])));
// Push creation time
info.push(ListItem::new(Spans::from(vec![
Span::styled("Creation time: ", Style::default()),
Span::styled(
time_to_str(file.creation_time, "%b %d %Y %H:%M:%S"),
Style::default()
.fg(Color::LightGreen)
.add_modifier(Modifier::BOLD),
),
])));
// Push Last change
info.push(ListItem::new(Spans::from(vec![
Span::styled("Last change time: ", Style::default()),
Span::styled(
time_to_str(file.last_change_time, "%b %d %Y %H:%M:%S"),
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
),
])));
// Push Last access
info.push(ListItem::new(Spans::from(vec![
Span::styled("Last access time: ", Style::default()),
Span::styled(
time_to_str(file.last_access_time, "%b %d %Y %H:%M:%S"),
Style::default()
.fg(Color::LightMagenta)
.add_modifier(Modifier::BOLD),
),
])));
// User
#[cfg(any(unix, macos, linux))]
let username: String = match file.user {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
},
None => String::from("0"),
};
#[cfg(target_os = "windows")]
let username: String = format!("{}", file.user.unwrap_or(0));
info.push(ListItem::new(Spans::from(vec![
Span::styled("User: ", Style::default()),
Span::styled(
username,
Style::default()
.fg(Color::LightRed)
.add_modifier(Modifier::BOLD),
),
])));
// Group
#[cfg(any(unix, macos, linux))]
let group: String = match file.group {
Some(gid) => match get_group_by_gid(gid) {
Some(group) => group.name().to_string_lossy().to_string(),
None => gid.to_string(),
},
None => String::from("0"),
};
#[cfg(target_os = "windows")]
let group: String = format!("{}", file.group.unwrap_or(0));
info.push(ListItem::new(Spans::from(vec![
Span::styled("Group: ", Style::default()),
Span::styled(
group,
Style::default()
.fg(Color::LightBlue)
.add_modifier(Modifier::BOLD),
),
])));
// Finally return file name
file.name.clone()
}
},
None => String::from(""),
};
List::new(info)
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default())
.title(file_name),
)
.start_corner(Corner::TopLeft)
}
/// ### draw_footer
///
/// Draw authentication page footer
@@ -518,62 +761,72 @@ impl FileTransferActivity {
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+D>",
"<D>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw(" "),
Span::raw("make directory"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+G>",
"<G>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw(" "),
Span::raw("goto path"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+H>",
"<H>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw(" "),
Span::raw("show help"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+Q>",
"<I>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw(" "),
Span::raw("show info about the selected file or directory"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<Q>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw("Quit TermSCP"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+R>",
"<R>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw(" "),
Span::raw("rename file"),
])),
ListItem::new(Spans::from(vec![
Span::styled(
"<CTRL+U>",
"<U>",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::raw(" "),
Span::raw("go to parent directory"),
])),
];
@@ -590,7 +843,7 @@ impl FileTransferActivity {
/// align_text_center
///
/// Align text to center for a given width
fn align_text_center(&self, text: &str, width: u16) -> String {
fn align_text_center(text: &str, width: u16) -> String {
let indent_size: usize = match (width as usize) >= text.len() {
// NOTE: The check prevents underflow
true => (width as usize - text.len()) / 2,
@@ -601,4 +854,38 @@ impl FileTransferActivity {
(0..indent_size).map(|_| " ").collect::<String>().as_str(),
)
}
/// ### elide_wrkdir_path
///
/// Elide working directory path if longer than width + host.len
/// In this case, the path is formatted to {ANCESTOR[0]}/.../{PARENT[0]}/{BASENAME}
fn elide_wrkdir_path(wrkdir: &Path, host: &str, width: u16) -> PathBuf {
let fmt_path: String = format!("{}", wrkdir.display());
// NOTE: +5 is const
match fmt_path.len() + host.len() + 5 > width as usize {
false => PathBuf::from(wrkdir),
true => {
// Elide
let ancestors_len: usize = wrkdir.ancestors().count();
let mut ancestors = wrkdir.ancestors();
let mut elided_path: PathBuf = PathBuf::new();
// If ancestors_len's size is bigger than 2, push count - 2
if ancestors_len > 2 {
elided_path.push(ancestors.nth(ancestors_len - 2).unwrap());
}
// If ancestors_len is bigger than 3, push '...' and parent too
if ancestors_len > 3 {
elided_path.push("...");
if let Some(parent) = wrkdir.ancestors().nth(1) {
elided_path.push(parent.file_name().unwrap());
}
}
// Push file_name
if let Some(name) = wrkdir.file_name() {
elided_path.push(name);
}
elided_path
}
}
}
}

View File

@@ -74,6 +74,13 @@ impl FileTransferActivity {
///
/// Calculate progress percentage based on current progress
pub(super) fn set_progress(&mut self, it: usize, sz: usize) {
self.transfer_progress = ((it as f64) * 100.0) / (sz as f64);
let mut prog: f64 = ((it as f64) * 100.0) / (sz as f64);
// Check value
if prog > 100.0 {
prog = 100.0;
} else if prog < 0.0 {
prog = 0.0;
}
self.transfer_progress = prog;
}
}

View File

@@ -97,6 +97,7 @@ enum DialogYesNoOption {
enum PopupType {
Alert(Color, String), // Block color; Block text
Fatal(String), // Must quit after being hidden
FileInfo, // Show info about current file
Help, // Show Help
Input(String, OnInputSubmitCallback), // Input description; Callback for submit
Progress(String), // Progress block text
@@ -159,8 +160,8 @@ impl FileExplorer {
/// Sort explorer files by their name
pub fn sort_files_by_name(&mut self) {
self.files.sort_by_key(|x: &FsEntry| match x {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
FsEntry::Directory(dir) => dir.name.as_str().to_lowercase(),
FsEntry::File(file) => file.name.as_str().to_lowercase(),
});
}
}

View File

@@ -171,7 +171,9 @@ impl FileTransferActivity {
while buf_start < bytes_read {
// Write bytes
match rhnd.write(&buffer[buf_start..bytes_read]) {
Ok(bytes) => buf_start += bytes,
Ok(bytes) => {
buf_start += bytes;
}
Err(err) => {
self.log(
LogLevel::Error,
@@ -204,6 +206,7 @@ impl FileTransferActivity {
Color::Red,
format!("Could not read local file: {}", err),
));
break;
}
}
// Increase progress
@@ -441,6 +444,7 @@ impl FileTransferActivity {
err
),
));
break;
}
}
}
@@ -456,6 +460,7 @@ impl FileTransferActivity {
Color::Red,
format!("Could not read remote file: {}", err),
));
break;
}
}
// Set progress

View File

@@ -141,6 +141,59 @@ pub fn parse_remote_opt(
Ok((address, port, protocol, username))
}
/// ### fmt_pex
///
/// Convert 3 bytes of permissions value into ls notation (e.g. rwx-wx--x)
pub fn fmt_pex(owner: u8, group: u8, others: u8) -> String {
let mut mode: String = String::with_capacity(9);
let read: u8 = (owner >> 2) & 0x1;
let write: u8 = (owner >> 1) & 0x1;
let exec: u8 = owner & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
let read: u8 = (group >> 2) & 0x1;
let write: u8 = (group >> 1) & 0x1;
let exec: u8 = group & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
let read: u8 = (others >> 2) & 0x1;
let write: u8 = (others >> 1) & 0x1;
let exec: u8 = others & 0x1;
mode.push_str(match read {
1 => "r",
_ => "-",
});
mode.push_str(match write {
1 => "w",
_ => "-",
});
mode.push_str(match exec {
1 => "x",
_ => "-",
});
mode
}
/// ### instant_to_str
///
/// Format a `Instant` into a time string
@@ -240,7 +293,7 @@ mod tests {
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
// Protocol
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("scp://172.26.104.1"))
.ok()
@@ -249,7 +302,7 @@ mod tests {
assert_eq!(result.1, 22); // Fallback to scp default
assert_eq!(result.2, FileTransferProtocol::Scp);
assert!(result.3.is_none()); // Doesn't fall back
// Protocol + user
// Protocol + user
let result: (String, u16, FileTransferProtocol, Option<String>) =
parse_remote_opt(&String::from("ftps://anon@172.26.104.1"))
.ok()
@@ -274,6 +327,18 @@ mod tests {
assert!(parse_remote_opt(&String::from("172.26.104.1:abc")).is_err()); // Bad port
}
#[test]
fn test_utils_fmt_pex() {
assert_eq!(fmt_pex(7, 7, 7), String::from("rwxrwxrwx"));
assert_eq!(fmt_pex(7, 5, 5), String::from("rwxr-xr-x"));
assert_eq!(fmt_pex(6, 6, 6), String::from("rw-rw-rw-"));
assert_eq!(fmt_pex(6, 4, 4), String::from("rw-r--r--"));
assert_eq!(fmt_pex(6, 0, 0), String::from("rw-------"));
assert_eq!(fmt_pex(0, 0, 0), String::from("---------"));
assert_eq!(fmt_pex(4, 4, 4), String::from("r--r--r--"));
assert_eq!(fmt_pex(1, 2, 1), String::from("--x-w---x"));
}
#[test]
fn test_utils_time_to_str() {
let system_time: SystemTime = SystemTime::from(SystemTime::UNIX_EPOCH);
@@ -290,28 +355,36 @@ mod tests {
lstime_to_systime("Nov 5 16:32", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH).ok().unwrap(),
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1604593920)
);
assert_eq!(
lstime_to_systime("Dec 2 21:32", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH).ok().unwrap(),
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1606944720)
);
assert_eq!(
lstime_to_systime("Nov 5 2018", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH).ok().unwrap(),
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
lstime_to_systime("Mar 18 2018", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH).ok().unwrap(),
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1521331200)
);
// bad cases