Merge branch '0.6.0' into keyring-rs-linux

This commit is contained in:
veeso 2021-06-21 20:51:24 +02:00
commit 2d8ce475f8
55 changed files with 2387 additions and 1638 deletions

View file

@ -32,3 +32,9 @@ Please select relevant options.
- [ ] I have introduced no new *C-bindings*
- [ ] The changes I've made are Windows, MacOS, UNIX, Linux compatible (or I've handled them using `cfg target_os`)
- [ ] I increased or maintained the code coverage for the project, compared to the previous commit
## Acceptance tests
wait for a *project maintainer* to fulfill this section...
- [ ] regression test: ...

View file

@ -1,4 +1,4 @@
name: coverage
name: Coverage
on: [push, pull_request]
@ -6,22 +6,23 @@ env:
CARGO_TERM_COLOR: always
jobs:
coverage:
name: Generate coverage
build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup rust toolchain
- uses: actions/checkout@v2
- name: Setup containers
run: docker-compose -f "tests/docker-compose.yml" up -d --build
- name: Setup nightly toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
override: true
- name: Run tests
- name: Run tests (nightly)
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features githubActions --features with-containers --no-fail-fast
args: --no-default-features --features github-actions --features with-containers --no-fail-fast
env:
CARGO_INCREMENTAL: "0"
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"

22
.github/workflows/freebsd.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: FreeBSD
on: [push, pull_request]
jobs:
build:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: FreeBSD build
id: test
uses: vmactions/freebsd-vm@v0.1.4
with:
usesh: true
prepare: pkg install -y curl wget libssh gcc vim
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > /tmp/rustup.sh && \
chmod +x /tmp/rustup.sh && \
/tmp/rustup.sh -y
. $HOME/.cargo/env
cargo build --no-default-features
cargo test --no-default-features --verbose --lib --features github-actions -- --test-threads 1

View file

@ -11,15 +11,18 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Setup containers
run: docker-compose -f "tests/docker-compose.yml" up -d --build
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- uses: actions-rs/cargo@v1
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
args: --no-default-features --features githubActions --features with-containers --no-fail-fast
args: --no-default-features --features github-actions --features with-containers --no-fail-fast
- name: Format
run: cargo fmt --all -- --check
- name: Clippy

View file

@ -12,8 +12,8 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
run: cargo build
- name: Run tests
run: cargo test --verbose --features githubActions -- --test-threads 1
run: cargo test --verbose --lib --features github-actions -- --test-threads 1
- name: Clippy
run: cargo clippy

View file

@ -12,8 +12,8 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Build
run: cargo build --verbose
run: cargo build
- name: Run tests
run: cargo test --verbose --features githubActions -- --test-threads 1
run: cargo test --verbose --lib --features github-actions -- --test-threads 1
- name: Clippy
run: cargo clippy

View file

@ -2,6 +2,7 @@
- [Changelog](#changelog)
- [0.6.0](#060)
- [0.5.1](#051)
- [0.5.0](#050)
- [0.4.2](#042)
- [0.4.1](#041)
@ -40,6 +41,35 @@ Released on FIXME: ??
- Updated `textwrap` to `0.14.0`
- Updated `tui-realm` to `0.4.1`
## 0.5.1
Released on 21/06/2021
- Enhancements:
- **CI now uses containers to test file transfers (SSH/FTP)**
- Improved coverage
- Found many bugs which has now been fixed
- Build in CI won't fail due to test servers not responding
- We're now able to test all the functionalities of the file transfers
- **Status bar improvements**
- "Show hidden files" in status bar
- Status bar has now been splitted into two, one for each explorer tab
- **Error message if terminal window is too small**
- If the terminal window has less than 24 lines, then an error message is displayed in the auth activity
- Changed auth layout to absolute sizes
- Bugfix:
- Fixed UI not showing connection errors
- Fixed termscp on Windows dying whenever opening a file with text editor
- Fixed broken input cursor when typing UTF8 characters (tui-realm 0.3.2)
- Fixed [Issue 44](https://github.com/veeso/termscp/issues/44): Could not move files to other paths in FTP
- Fixed [Issue 43](https://github.com/veeso/termscp/issues/43): Could not remove non-empty directories in FTP
- Fixed [Issue 39](https://github.com/veeso/termscp/issues/39): Help panels as `ScrollTable` to allow displaying entire content on small screens
- Fixed [Issue 38](https://github.com/veeso/termscp/issues/38): Transfer size was wrong when transferring "selected" files (with mark)
- Fixed [Issue 37](https://github.com/veeso/termscp/issues/37): progress bar not visible when editing remote files
- Dependencies:
- Updated `textwrap` to `0.14.0`
- Updated `tui-realm` to `0.4.2`
## 0.5.0
Released on 23/05/2021

View file

@ -115,7 +115,9 @@ Let's make it simple and clear:
6. Report changes to the PR you opened, writing a report of what you changed and what you have introduced.
7. Update the `CHANGELOG.md` file with details of changes to the application. In changelog report changes under a chapter called `PR{PULL_REQUEST_NUMBER}` (e.g. PR12).
8. Assign a maintainer to the reviewers.
9. Request maintainers to merge your changes.
9. Wait for a maintainer to fullfil the acceptance tests
10. Wait for a maintainer to complete the acceptance tests
11. Request maintainers to merge your changes.
### Software guidelines

4
Cargo.lock generated
View file

@ -1451,9 +1451,9 @@ dependencies = [
[[package]]
name = "tuirealm"
version = "0.4.1"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bee2a1c050878fac02ba3a6c2e93aa92a1de56849d5deec00d4ab4bc7928c0a"
checksum = "9897335542e4a4a87ad391419c35e54b4088661e671ba53e578fbbb1154740c2"
dependencies = [
"crossterm",
"textwrap",

View file

@ -34,6 +34,7 @@ content_inspector = "0.2.4"
crossterm = "0.19.0"
dirs = "3.0.1"
edit = "0.1.3"
ftp4 = { version = "4.0.2", features = [ "secure" ] }
getopts = "0.2.21"
hostname = "0.3.1"
keyring = { version = "0.10.1", optional = true }
@ -44,39 +45,29 @@ open = "1.7.0"
rand = "0.8.3"
regex = "1.5.4"
rpassword = "5.0.1"
serde = { version = "^1.0.0", features = [ "derive" ] }
simplelog = "0.10.0"
ssh2 = "0.9.0"
tempfile = "3.1.0"
textwrap = "0.14.0"
thiserror = "^1.0.0"
toml = "0.5.8"
tuirealm = { version = "0.4.1", features = [ "with-components" ] }
tuirealm = { version = "0.4.2", features = [ "with-components" ] }
ureq = { version = "2.1.0", features = [ "json" ] }
whoami = "1.1.1"
wildmatch = "2.0.0"
[dev-dependencies]
pretty_assertions = "0.7.2"
[dependencies.ftp4]
features = ["secure"]
version = "^4.0.2"
[dependencies.serde]
features = ["derive"]
version = "^1.0.0"
[dependencies.ureq]
features = ["json"]
version = "2.1.0"
[features]
default = [ "with-keyring" ]
githubActions = []
github-actions = []
with-containers = []
with-keyring = [ "keyring" ]
[target."cfg(any(target_os = \"unix\", target_os = \"macos\", target_os = \"linux\"))"]
[target."cfg(any(target_os = \"unix\", target_os = \"macos\", target_os = \"linux\"))".dependencies]
[target."cfg(target_family = \"unix\")"]
[target."cfg(target_family = \"unix\")".dependencies]
users = "0.11.0"
[target."cfg(target_os = \"windows\")"]

View file

@ -14,11 +14,11 @@
</p>
<p align="center">Developed by <a href="https://veeso.github.io/">@veeso</a></p>
<p align="center">Current version: 0.6.0 FIXME: (23/05/2021)</p>
<p align="center">Current version: 0.6.0 FIXME: (21/06/2021)</p>
[![License: MIT](https://img.shields.io/badge/License-MIT-teal.svg)](https://opensource.org/licenses/MIT) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.6.0-orange.svg)](https://crates.io/crates/termscp) [![Docs](https://docs.rs/termscp/badge.svg)](https://docs.rs/termscp)
[![License: MIT](https://img.shields.io/badge/License-MIT-teal.svg)](https://opensource.org/licenses/MIT) [![Stars](https://img.shields.io/github/stars/veeso/termscp.svg)](https://github.com/veeso/termscp) [![Downloads](https://img.shields.io/crates/d/termscp.svg)](https://crates.io/crates/termscp) [![Crates.io](https://img.shields.io/badge/crates.io-v0.5.1-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) [![Coverage Status](https://coveralls.io/repos/github/veeso/termscp/badge.svg)](https://coveralls.io/github/veeso/termscp)
[![Linux](https://github.com/veeso/termscp/workflows/Linux/badge.svg)](https://github.com/veeso/termscp/actions) [![MacOs](https://github.com/veeso/termscp/workflows/MacOS/badge.svg)](https://github.com/veeso/termscp/actions) [![Windows](https://github.com/veeso/termscp/workflows/Windows/badge.svg)](https://github.com/veeso/termscp/actions) [![FreeBSD](https://github.com/veeso/termscp/workflows/FreeBSD/badge.svg)](https://github.com/veeso/termscp/actions) [![Coverage Status](https://coveralls.io/repos/github/veeso/termscp/badge.svg)](https://coveralls.io/github/veeso/termscp)
---
@ -59,7 +59,7 @@ Termscp is a feature rich terminal file transfer and explorer, with support for
If you're considering to install termscp I want to thank you 💜 ! I hope you will enjoy termscp!
If you want to contribute to this project, don't forget to check out our contribute guide. [Read More](CONTRIBUTING.md)
If you are a Linux or a MacOS user this simple shell script will install termscp on your system with a single command:
If you are a Linux, a FreeBSD or a MacOS user this simple shell script will install termscp on your system with a single command:
```sh
curl --proto '=https' --tlsv1.2 -sSf "https://raw.githubusercontent.com/veeso/termscp/main/install.sh" | sh

View file

@ -1,7 +0,0 @@
ignore:
- src/main.rs
- src/lib.rs
- src/activity_manager.rs
- src/ui/activities/
- src/ui/context.rs
- src/ui/input.rs

View file

@ -1,4 +1,4 @@
FROM archlinux:base-20210120.0.13969 as builder
FROM archlinux:latest as builder
WORKDIR /usr/src/
# Install dependencies

View file

@ -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=("279b4cab7da68c6db0efc054ddf72e36de85910110721b66d5cdc55833c99ccf")
sha256sums=("f66a1d1602dc8ea336ba4a42bfbe818edc9c20722e1761b471b76109c272094c")
package() {
install -Dm755 termscp -t "$pkgdir/usr/bin/"

17
dist/pkgs/freebsd/manifest vendored Executable file
View file

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

View file

@ -3,15 +3,25 @@
Document audience: developers
- [Developer Manual](#developer-manual)
- [How to test](#how-to-test)
- [How termscp works](#how-termscp-works)
- [Activities](#activities)
- [The Context](#the-context)
- [Tests fails due to receivers](#tests-fails-due-to-receivers)
- [Implementing File Transfers](#implementing-file-transfers)
Welcome to the developer manual for termscp. This chapter DOESN'T contain the documentation for termscp modules, which can instead be found on Rust Docs at <https://docs.rs/termscp>
This chapter describes how termscp works and the guide lines to implement stuff such as file transfers and add features to the user interface.
## How to test
First an introduction to tests.
Usually it's enough to run `cargo test`, but please note that whenever you're working on file transfer you'll need one more step.
In order to run tests with file transfers, you need to start the file transfer server containers, which can be started via `docker`.
To run all tests with file transfers just run: `./tests/test.sh`
---
## How termscp works
termscp is basically made up of 4 components:
@ -61,146 +71,3 @@ The context basically holds the following data:
- The **Terminal**: the terminal is used to view the tui on the terminal
---
## 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.
In the following steps I will describe how to implement a new file transfer, in this case I will be implementing the SCP file transfer (which I'm actually implementing the moment I'm writing this lines).
1. Add the Scp protocol to the `FileTransferProtocol` enum.
Move to `src/filetransfer/mod.rs` and add `Scp` to the `FileTransferProtocol` enum
```rs
/// ## FileTransferProtocol
///
/// This enum defines the different transfer protocol available in termscp
#[derive(std::cmp::PartialEq, std::fmt::Debug, std::clone::Clone)]
pub enum FileTransferProtocol {
Sftp,
Ftp(bool), // Bool is for secure (true => ftps)
Scp, // <-- here
}
```
In this case Scp is a "plain" enum type. If you need particular options, follow the implementation of `Ftp` which uses a boolean flag for indicating if using FTPS or FTP.
2. Implement the FileTransfer struct
Create a file at `src/filetransfer/mytransfer.rs`
Declare your file transfer struct
```rs
/// ## ScpFileTransfer
///
/// SFTP file transfer structure
pub struct ScpFileTransfer {
session: Option<Session>,
sftp: Option<Sftp>,
wrkdir: PathBuf,
}
```
3. Implement the `FileTransfer` trait for it
You'll have to implement the following methods for your file transfer:
- connect: connect to remote server
- disconnect: disconnect from remote server
- is_connected: returns whether the file transfer is connected to remote
- pwd: get working directory
- change_dir: change working directory.
- list_dir: get files and directories at a certain path
- mkdir: make a new directory. Return an error in case the directory already exists
- remove: remove a file or a directory. In case the protocol doesn't support recursive removing of directories you MUST implement this through a recursive algorithm
- rename: rename a file or a directory
- stat: returns detail for a certain path
- send_file: opens a stream to a remote path for write purposes (write a remote file)
- recv_file: opens a stream to a remote path for read purposes (write a local file)
- on_sent: finalize a stream when writing a remote file. In case it's not necessary just return `Ok(())`
- on_recv: fianlize a stream when reading a remote file. In case it's not necessary just return `Ok(())`
In case the protocol you're working on doesn't support any of this features, just return `Err(FileTransferError::new(FileTransferErrorType::UnsupportedFeature))`
4. Add your transfer to filetransfers:
Move to `src/filetransfer/mod.rs` and declare your file transfer:
```rs
// Transfers
pub mod ftp_transfer;
pub mod scp_transfer; // <-- here
pub mod sftp_transfer;
```
5. Handle FileTransfer in `FileTransferActivity::new`
Move to `src/ui/activities/filetransfer_activity/mod.rs` and add the new protocol to the client match
```rs
client: match protocol {
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new()),
FileTransferProtocol::Ftp(ftps) => Box::new(FtpFileTransfer::new(ftps)),
FileTransferProtocol::Scp => Box::new(ScpFileTransfer::new()), // <--- here
},
```
6. Handle right/left input events in `AuthActivity`:
Move to `src/ui/activities/auth_activity.rs` and handle the new protocol in `handle_input_event_mode_text` for `KeyCode::Left` and `KeyCode::Right`.
Consider that the order they "rotate" must match the way they will be drawned in the interface.
For newer protocols, please put them always at the end of the list. In this list I won't, because Scp is more important than Ftp imo.
```rs
KeyCode::Left => {
// If current field is Protocol handle event... (move element left)
if self.selected_field == InputField::Protocol {
self.protocol = match self.protocol {
FileTransferProtocol::Sftp => FileTransferProtocol::Ftp(true), // End of list (wrap)
FileTransferProtocol::Scp => FileTransferProtocol::Sftp,
FileTransferProtocol::Ftp(ftps) => match ftps {
false => FileTransferProtocol::Scp,
true => FileTransferProtocol::Ftp(false),
}
};
}
}
KeyCode::Right => {
// If current field is Protocol handle event... ( move element right )
if self.selected_field == InputField::Protocol {
self.protocol = match self.protocol {
FileTransferProtocol::Sftp => FileTransferProtocol::Scp,
FileTransferProtocol::Scp => FileTransferProtocol::Ftp(false),
FileTransferProtocol::Ftp(ftps) => match ftps {
false => FileTransferProtocol::Ftp(true),
true => FileTransferProtocol::Sftp, // End of list (wrap)
}
};
}
}
```
7. Add your new file transfer to the protocol input field
Move to `AuthActivity::draw_protocol_select` method.
Here add your new protocol to the `Spans` vector and to the match case, which chooses which element to highlight.
```rs
let protocols: Vec<Spans> = vec![Spans::from("SFTP"), Spans::from("SCP"), Spans::from("FTP"), Spans::from("FTPS")];
let index: usize = match self.protocol {
FileTransferProtocol::Sftp => 0,
FileTransferProtocol::Scp => 1,
FileTransferProtocol::Ftp(ftps) => match ftps {
false => 2,
true => 3,
}
};
```

View file

@ -11,6 +11,7 @@
TERMSCP_VERSION="0.6.0"
GITHUB_URL="https://github.com/veeso/termscp/releases/download/v${TERMSCP_VERSION}"
DEB_URL="${GITHUB_URL}/termscp_${TERMSCP_VERSION}_amd64.deb"
FREEBSD_URL="${GITHUB_URL}/termscp-${TERMSCP_VERSION}.txz"
RPM_URL="${GITHUB_URL}/termscp-${TERMSCP_VERSION}-1.x86_64.rpm"
set -eu
@ -170,7 +171,21 @@ confirm() {
# Installers
install_on_bsd() {
try_with_cargo "we currently don't distribute any pre-built package for BSD"
info "Installing termscp via FreeBSD pkg"
archive=$(get_tmpfile "txz")
download "${archive}" "${FREEBSD_URL}"
info "Downloaded FreeBSD package to ${archive}"
if test_writeable "/usr/local/bin"; then
sudo=""
msg="Installing termscp, please wait…"
else
warn "Root permissions are required to install termscp…"
elevate_priv
sudo="sudo"
msg="Installing termscp as root, please wait…"
fi
info "$msg"
$sudo pkg install -y "${archive}"
}
install_on_linux() {

View file

@ -183,6 +183,12 @@ mod tests {
file_fmt: Some(String::from("{NAME}")),
remote_file_fmt: Some(String::from("{USER}")),
};
assert_eq!(ui.default_protocol, String::from("SFTP"));
assert_eq!(ui.text_editor, PathBuf::from("nano"));
assert_eq!(ui.show_hidden_files, true);
assert_eq!(ui.check_for_updates, Some(true));
assert_eq!(ui.group_dirs, Some(String::from("first")));
assert_eq!(ui.file_fmt, Some(String::from("{NAME}")));
let cfg: UserConfig = UserConfig {
user_interface: ui,
remote: remote,
@ -219,7 +225,7 @@ mod tests {
PathBuf::from(cfg.user_interface.text_editor.file_name().unwrap()), // NOTE: since edit 0.1.3 real path is used
PathBuf::from("vim.EXE")
);
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
assert_eq!(
PathBuf::from(cfg.user_interface.text_editor.file_name().unwrap()), // NOTE: since edit 0.1.3 real path is used
PathBuf::from("vim")

View file

@ -208,6 +208,25 @@ mod tests {
assert!(serializer.deserialize(Box::new(toml_file)).is_ok());
}
#[test]
fn test_config_serializer_fail_write() {
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
let writer: Box<dyn Write> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
// Try to write unexisting file
let serializer: ConfigSerializer = ConfigSerializer {};
let cfg: UserConfig = UserConfig::default();
assert!(serializer.serialize(writer, &cfg).is_err());
}
#[test]
fn test_config_serializer_fail_read() {
let toml_file: tempfile::NamedTempFile = tempfile::NamedTempFile::new().ok().unwrap();
let reader: Box<dyn Read> = Box::new(std::fs::File::open(toml_file.path()).unwrap());
// Try to write unexisting file
let serializer: ConfigSerializer = ConfigSerializer {};
assert!(serializer.deserialize(reader).is_err());
}
fn create_good_toml() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();

View file

@ -73,7 +73,7 @@ impl FtpFileTransfer {
PathBuf::from(path_slash::PathExt::to_slash_lossy(p).as_str())
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn resolve(p: &Path) -> PathBuf {
p.to_path_buf()
}
@ -491,7 +491,10 @@ impl FileTransfer for FtpFileTransfer {
info!("Disconnecting from FTP server...");
match &mut self.stream {
Some(stream) => match stream.quit() {
Ok(_) => Ok(()),
Ok(_) => {
self.stream = None;
Ok(())
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
@ -629,23 +632,36 @@ impl FileTransfer for FtpFileTransfer {
));
}
info!("Removing entry {}", fsentry.get_abs_path().display());
let wrkdir: PathBuf = self.pwd()?;
match fsentry {
// Match fs entry...
FsEntry::File(file) => {
debug!("entry is a file; removing file");
// Go to parent directory
if let Some(parent_dir) = file.abs_path.parent() {
debug!("Changing wrkdir to {}", parent_dir.display());
self.change_dir(parent_dir)?;
}
debug!("entry is a file; removing file {}", file.abs_path.display());
// Remove file directly
match self.stream.as_mut().unwrap().rm(file.name.as_ref()) {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::PexError,
err.to_string(),
)),
let result = self
.stream
.as_mut()
.unwrap()
.rm(file.name.as_ref())
.map(|_| ())
.map_err(|e| {
FileTransferError::new_ex(FileTransferErrorType::PexError, e.to_string())
});
// Go to source directory
match self.change_dir(wrkdir.as_path()) {
Err(err) => Err(err),
Ok(_) => result,
}
}
FsEntry::Directory(dir) => {
// Get directory files
debug!("Entry is a directory; iterating directory entries");
match self.list_dir(dir.abs_path.as_path()) {
let result = match self.list_dir(dir.abs_path.as_path()) {
Ok(files) => {
// Remove recursively files
debug!("Removing {} entries from directory...", files.len());
@ -658,9 +674,21 @@ impl FileTransfer for FtpFileTransfer {
}
}
// Once all files in directory have been deleted, remove directory
debug!("Finally removing directory {}", dir.name);
debug!("Finally removing directory {}...", dir.name);
// Enter parent directory
if let Some(parent_dir) = dir.abs_path.parent() {
debug!(
"Changing wrkdir to {} to delete directory {}",
parent_dir.display(),
dir.name
);
self.change_dir(parent_dir)?;
}
match self.stream.as_mut().unwrap().rmdir(dir.name.as_str()) {
Ok(_) => Ok(()),
Ok(_) => {
debug!("Removed {}", dir.abs_path.display());
Ok(())
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::PexError,
err.to_string(),
@ -671,6 +699,11 @@ impl FileTransfer for FtpFileTransfer {
FileTransferErrorType::DirStatFailed,
err.to_string(),
)),
};
// Restore directory
match self.change_dir(wrkdir.as_path()) {
Err(err) => Err(err),
Ok(_) => result,
}
}
}
@ -693,17 +726,8 @@ impl FileTransfer for FtpFileTransfer {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
let dst_name: PathBuf = match dst.file_name() {
Some(p) => PathBuf::from(p),
None => {
return Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
String::from("Invalid destination name"),
))
}
};
// Only names are supported
match stream.rename(src_name.as_str(), &dst_name.as_path().to_string_lossy()) {
match stream.rename(src_name.as_str(), &dst.as_path().to_string_lossy()) {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
@ -838,9 +862,14 @@ impl FileTransfer for FtpFileTransfer {
mod tests {
use super::*;
use crate::utils::file::open_file;
use crate::utils::fmt::fmt_time;
#[cfg(feature = "with-containers")]
use crate::utils::test_helpers::write_file;
use crate::utils::test_helpers::{create_sample_file_entry, make_fsentry};
use pretty_assertions::assert_eq;
use std::io::{Read, Write};
use std::time::Duration;
#[test]
@ -854,120 +883,281 @@ mod tests {
assert!(ftp.stream.is_none());
}
#[test]
#[cfg(feature = "with-containers")]
fn test_filetransfer_ftp_server() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Sample file
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect
#[cfg(not(feature = "github-actions"))]
let hostname: String = String::from("127.0.0.1");
#[cfg(feature = "github-actions")]
let hostname: String = String::from("127.0.0.1");
assert!(ftp
.connect(
hostname,
10021,
Some(String::from("test")),
Some(String::from("test")),
)
.is_ok());
assert_eq!(ftp.is_connected(), true);
// Get pwd
assert_eq!(ftp.pwd().unwrap(), PathBuf::from("/"));
// List dir (dir is empty)
assert_eq!(ftp.list_dir(&Path::new("/")).unwrap().len(), 0);
// Make directory
assert!(ftp.mkdir(PathBuf::from("/home").as_path()).is_ok());
// Make directory (err)
assert!(ftp.mkdir(PathBuf::from("/root/pommlar").as_path()).is_err());
// Change directory
assert!(ftp.change_dir(PathBuf::from("/home").as_path()).is_ok());
// Change directory (err)
assert!(ftp
.change_dir(PathBuf::from("/tmp/oooo/aaaa/eee").as_path())
.is_err());
// Copy (not supported)
assert!(ftp
.copy(&FsEntry::File(entry.clone()), PathBuf::from("/").as_path())
.is_err());
// Exec (not supported)
assert!(ftp.exec("echo 1;").is_err());
// Upload 2 files
let mut writable = ftp
.send_file(&entry, PathBuf::from("omar.txt").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(ftp.on_sent(writable).is_ok());
let mut writable = ftp
.send_file(&entry, PathBuf::from("README.md").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(ftp.on_sent(writable).is_ok());
// Upload file (err)
assert!(ftp
.send_file(&entry, PathBuf::from("/ommlar/omarone").as_path())
.is_err());
// List dir
let list: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/home").as_path()).ok().unwrap();
assert_eq!(list.len(), 2);
// Find
assert!(ftp.change_dir(PathBuf::from("/").as_path()).is_ok());
assert_eq!(ftp.find("*.txt").ok().unwrap().len(), 1);
assert_eq!(ftp.find("*.md").ok().unwrap().len(), 1);
assert_eq!(ftp.find("*.jpeg").ok().unwrap().len(), 0);
assert!(ftp.change_dir(PathBuf::from("/home").as_path()).is_ok());
// Rename
assert!(ftp.mkdir(PathBuf::from("/uploads").as_path()).is_ok());
assert!(ftp
.rename(
list.get(0).unwrap(),
PathBuf::from("/uploads/README.txt").as_path()
)
.is_ok());
// Rename (err)
assert!(ftp
.rename(list.get(0).unwrap(), PathBuf::from("OMARONE").as_path())
.is_err());
let dummy: FsEntry = FsEntry::File(FsFile {
name: String::from("cucumber.txt"),
abs_path: PathBuf::from("/cucumber.txt"),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
readonly: true,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
assert!(ftp
.rename(&dummy, PathBuf::from("/a/b/c").as_path())
.is_err());
// Remove
assert!(ftp.remove(list.get(1).unwrap()).is_ok());
assert!(ftp.remove(list.get(1).unwrap()).is_err());
// Receive file
let mut writable = ftp
.send_file(&entry, PathBuf::from("/uploads/README.txt").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(ftp.on_sent(writable).is_ok());
let file: FsFile = ftp
.list_dir(PathBuf::from("/uploads").as_path())
.ok()
.unwrap()
.get(0)
.unwrap()
.clone()
.unwrap_file();
let mut readable = ftp.recv_file(&file).ok().unwrap();
let mut data: Vec<u8> = vec![0; 1024];
assert!(readable.read(&mut data).is_ok());
assert!(ftp.on_recv(readable).is_ok());
// Receive file (err)
assert!(ftp.recv_file(&entry).is_err());
// Cleanup
assert!(ftp.change_dir(PathBuf::from("/").as_path()).is_ok());
assert!(ftp
.remove(&make_fsentry(PathBuf::from("/home"), true))
.is_ok());
assert!(ftp
.remove(&make_fsentry(PathBuf::from("/uploads"), true))
.is_ok());
// Disconnect
assert!(ftp.disconnect().is_ok());
assert_eq!(ftp.is_connected(), false);
}
#[test]
#[cfg(feature = "with-containers")]
fn test_filetransfer_ftp_server_bad_auth() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(
String::from("127.0.0.1"),
10021,
Some(String::from("omar")),
Some(String::from("ommlar")),
)
.is_err());
}
#[test]
#[cfg(feature = "with-containers")]
fn test_filetransfer_ftp_no_credentials() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
assert!(ftp
.connect(String::from("127.0.0.1"), 10021, None, None)
.is_err());
}
#[test]
fn test_filetransfer_ftp_server_bad_server() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(
String::from("mybadserver.veryverybad.awful"),
21,
Some(String::from("omar")),
Some(String::from("ommlar")),
)
.is_err());
}
#[test]
fn test_filetransfer_ftp_parse_list_line_unix() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Simple file
let fs_entry: FsEntry = ftp
let file: FsFile = ftp
.parse_list_line(
PathBuf::from("/tmp").as_path(),
"-rw-rw-r-- 1 root dialout 8192 Nov 5 2018 omar.txt",
)
.ok()
.unwrap();
if let FsEntry::File(file) = fs_entry {
assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt"));
assert_eq!(file.name, String::from("omar.txt"));
assert_eq!(file.size, 8192);
assert!(file.symlink.is_none());
assert_eq!(file.user, None);
assert_eq!(file.group, None);
assert_eq!(file.unix_pex.unwrap(), (6, 6, 4));
assert_eq!(
file.last_access_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
file.last_change_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
file.creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
} else {
panic!("Expected file, got directory");
}
.unwrap()
.unwrap_file();
assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt"));
assert_eq!(file.name, String::from("omar.txt"));
assert_eq!(file.size, 8192);
assert!(file.symlink.is_none());
assert_eq!(file.user, None);
assert_eq!(file.group, None);
assert_eq!(file.unix_pex.unwrap(), (6, 6, 4));
assert_eq!(
file.last_access_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
file.last_change_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
file.creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
// Simple file with number as gid, uid
let fs_entry: FsEntry = ftp
let file: FsFile = ftp
.parse_list_line(
PathBuf::from("/tmp").as_path(),
"-rwxr-xr-x 1 0 9 4096 Nov 5 16:32 omar.txt",
)
.ok()
.unwrap();
if let FsEntry::File(file) = fs_entry {
assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt"));
assert_eq!(file.name, String::from("omar.txt"));
assert_eq!(file.size, 4096);
assert!(file.symlink.is_none());
assert_eq!(file.user, Some(0));
assert_eq!(file.group, Some(9));
assert_eq!(file.unix_pex.unwrap(), (7, 5, 5));
assert_eq!(
fmt_time(file.last_access_time, "%m %d %M").as_str(),
"11 05 32"
);
assert_eq!(
fmt_time(file.last_change_time, "%m %d %M").as_str(),
"11 05 32"
);
assert_eq!(
fmt_time(file.creation_time, "%m %d %M").as_str(),
"11 05 32"
);
} else {
panic!("Expected file, got directory");
}
.unwrap()
.unwrap_file();
assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt"));
assert_eq!(file.name, String::from("omar.txt"));
assert_eq!(file.size, 4096);
assert!(file.symlink.is_none());
assert_eq!(file.user, Some(0));
assert_eq!(file.group, Some(9));
assert_eq!(file.unix_pex.unwrap(), (7, 5, 5));
assert_eq!(
fmt_time(file.last_access_time, "%m %d %M").as_str(),
"11 05 32"
);
assert_eq!(
fmt_time(file.last_change_time, "%m %d %M").as_str(),
"11 05 32"
);
assert_eq!(
fmt_time(file.creation_time, "%m %d %M").as_str(),
"11 05 32"
);
// Directory
let fs_entry: FsEntry = ftp
let dir: FsDirectory = ftp
.parse_list_line(
PathBuf::from("/tmp").as_path(),
"drwxrwxr-x 1 0 9 4096 Nov 5 2018 docs",
)
.ok()
.unwrap();
if let FsEntry::Directory(dir) = fs_entry {
assert_eq!(dir.abs_path, PathBuf::from("/tmp/docs"));
assert_eq!(dir.name, String::from("docs"));
assert!(dir.symlink.is_none());
assert_eq!(dir.user, Some(0));
assert_eq!(dir.group, Some(9));
assert_eq!(dir.unix_pex.unwrap(), (7, 7, 5));
assert_eq!(
dir.last_access_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
dir.last_change_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
dir.creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(dir.readonly, false);
} else {
panic!("Expected directory, got directory");
}
.unwrap()
.unwrap_dir();
assert_eq!(dir.abs_path, PathBuf::from("/tmp/docs"));
assert_eq!(dir.name, String::from("docs"));
assert!(dir.symlink.is_none());
assert_eq!(dir.user, Some(0));
assert_eq!(dir.group, Some(9));
assert_eq!(dir.unix_pex.unwrap(), (7, 7, 5));
assert_eq!(
dir.last_access_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
dir.last_change_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
dir.creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(dir.readonly, false);
// Error
assert!(ftp
.parse_list_line(
@ -981,186 +1171,85 @@ mod tests {
fn test_filetransfer_ftp_parse_list_line_dos() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Simple file
let fs_entry: FsEntry = ftp
let file: FsFile = ftp
.parse_list_line(
PathBuf::from("/tmp").as_path(),
"04-08-14 03:09PM 8192 omar.txt",
)
.ok()
.unwrap();
if let FsEntry::File(file) = fs_entry {
assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt"));
assert_eq!(file.name, String::from("omar.txt"));
assert_eq!(file.size, 8192);
assert!(file.symlink.is_none());
assert_eq!(file.user, None);
assert_eq!(file.group, None);
assert_eq!(file.unix_pex, None);
assert_eq!(
file.last_access_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
assert_eq!(
file.last_change_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
assert_eq!(
file.creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
} else {
panic!("Expected file, got directory");
}
.unwrap()
.unwrap_file();
assert_eq!(file.abs_path, PathBuf::from("/tmp/omar.txt"));
assert_eq!(file.name, String::from("omar.txt"));
assert_eq!(file.size, 8192);
assert!(file.symlink.is_none());
assert_eq!(file.user, None);
assert_eq!(file.group, None);
assert_eq!(file.unix_pex, None);
assert_eq!(
file.last_access_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
assert_eq!(
file.last_change_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
assert_eq!(
file.creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
// Directory
let fs_entry: FsEntry = ftp
let dir: FsDirectory = ftp
.parse_list_line(
PathBuf::from("/tmp").as_path(),
"04-08-14 03:09PM <DIR> docs",
)
.ok()
.unwrap();
if let FsEntry::Directory(dir) = fs_entry {
assert_eq!(dir.abs_path, PathBuf::from("/tmp/docs"));
assert_eq!(dir.name, String::from("docs"));
assert!(dir.symlink.is_none());
assert_eq!(dir.user, None);
assert_eq!(dir.group, None);
assert_eq!(dir.unix_pex, None);
assert_eq!(
dir.last_access_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
assert_eq!(
dir.last_change_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
assert_eq!(
dir.creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
assert_eq!(dir.readonly, false);
} else {
panic!("Expected directory, got directory");
}
.unwrap()
.unwrap_dir();
assert_eq!(dir.abs_path, PathBuf::from("/tmp/docs"));
assert_eq!(dir.name, String::from("docs"));
assert!(dir.symlink.is_none());
assert_eq!(dir.user, None);
assert_eq!(dir.group, None);
assert_eq!(dir.unix_pex, None);
assert_eq!(
dir.last_access_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
assert_eq!(
dir.last_change_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
assert_eq!(
dir.creation_time
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
assert_eq!(dir.readonly, false);
// Error
assert!(ftp
.parse_list_line(PathBuf::from("/").as_path(), "04-08-14 omar.txt")
.is_err());
}
#[test]
fn test_filetransfer_ftp_connect_unsecure_anonymous() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(String::from("speedtest.tele2.net"), 21, None, None)
.is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// Disconnect
assert!(ftp.disconnect().is_ok());
}
#[test]
fn test_filetransfer_ftp_connect_unsecure_username() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(
String::from("test.rebex.net"),
21,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// Disconnect
assert!(ftp.disconnect().is_ok());
}
#[test]
fn test_filetransfer_ftp_connect_secure() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(true);
// Connect
assert!(ftp
.connect(
String::from("test.rebex.net"),
21,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// Disconnect
assert!(ftp.disconnect().is_ok());
}
#[test]
fn test_filetransfer_ftp_change_dir() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(String::from("speedtest.tele2.net"), 21, None, None)
.is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// Cwd
assert!(ftp.change_dir(PathBuf::from("upload/").as_path()).is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/upload"));
// Disconnect
assert!(ftp.disconnect().is_ok());
}
#[test]
fn test_filetransfer_ftp_copy() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(String::from("speedtest.tele2.net"), 21, None, None)
.is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// Copy
let file: FsFile = FsFile {
name: String::from("readme.txt"),
abs_path: PathBuf::from("/readme.txt"),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
readonly: true,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
};
assert!(ftp
.copy(&FsEntry::File(file), &Path::new("/tmp/dest.txt"))
.is_err());
}
#[test]
fn test_filetransfer_ftp_list_dir_dos_syntax() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
@ -1184,94 +1273,16 @@ mod tests {
}
#[test]
#[cfg(not(target_os = "macos"))]
fn test_filetransfer_ftp_list_dir_unix_syntax() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(String::from("speedtest.tele2.net"), 21, None, None)
.is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// List dir
let files: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap();
// There should be at least 1 file
assert!(files.len() > 0);
// Disconnect
assert!(ftp.disconnect().is_ok());
}
/* NOTE: they don't work
#[test]
fn test_filetransfer_ftp_recv() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp.connect(String::from("test.rebex.net"), 21, Some(String::from("demo")), Some(String::from("password"))).is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// Recv 100KB
assert!(ftp.recv_file(PathBuf::from("readme.txt").as_path()).is_ok());
// Disconnect
assert!(ftp.disconnect().is_ok());
}
#[test]
fn test_filetransfer_ftp_send() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp.connect(String::from("speedtest.tele2.net"), 21, None, None).is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// Cwd
assert!(ftp.change_dir(PathBuf::from("upload/").as_path()).is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/upload"));
// Send a sample file 100KB
assert!(ftp.send_file(PathBuf::from("test.txt").as_path()).is_ok());
// Disconnect
assert!(ftp.disconnect().is_ok());
}*/
#[test]
fn test_filetransfer_ftp_exec() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(String::from("speedtest.tele2.net"), 21, None, None)
.is_ok());
// Pwd
assert!(ftp.exec("echo 1;").is_err());
// Disconnect
assert!(ftp.disconnect().is_ok());
}
#[test]
fn test_filetransfer_ftp_find() {
let mut client: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(client
.connect(
String::from("test.rebex.net"),
21,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Pwd
assert_eq!(client.pwd().ok().unwrap(), PathBuf::from("/"));
// Search for file (let's search for pop3-*.png); there should be 2
let search_res: Vec<FsEntry> = client.find("pop3-*.png").ok().unwrap();
assert_eq!(search_res.len(), 2);
// verify names
assert_eq!(search_res[0].get_name(), "pop3-browser.png");
assert_eq!(search_res[1].get_name(), "pop3-console-client.png");
// Search directory
let search_res: Vec<FsEntry> = client.find("pub").ok().unwrap();
assert_eq!(search_res.len(), 1);
// Disconnect
assert!(client.disconnect().is_ok());
// Verify err
assert!(client.find("pippo").is_err());
fn test_filetransfer_ftp_get_name_and_link() {
let client: FtpFileTransfer = FtpFileTransfer::new(false);
assert_eq!(
client.get_name_and_link("Cargo.toml"),
(String::from("Cargo.toml"), None)
);
assert_eq!(
client.get_name_and_link("Cargo -> Cargo.toml"),
(String::from("Cargo"), Some(PathBuf::from("Cargo.toml")))
);
}
#[test]
@ -1295,9 +1306,25 @@ mod tests {
assert!(ftp.disconnect().is_err());
assert!(ftp.list_dir(Path::new("/tmp")).is_err());
assert!(ftp.mkdir(Path::new("/tmp")).is_err());
assert!(ftp
.remove(&make_fsentry(PathBuf::from("/nowhere"), false))
.is_err());
assert!(ftp
.rename(
&make_fsentry(PathBuf::from("/nowhere"), false),
PathBuf::from("/culonia").as_path()
)
.is_err());
assert!(ftp.pwd().is_err());
assert!(ftp.stat(Path::new("/tmp")).is_err());
assert!(ftp.recv_file(&file).is_err());
assert!(ftp.send_file(&file, Path::new("/tmp/omar.txt")).is_err());
let (_, temp): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
let readable: Box<dyn Read> = Box::new(std::fs::File::open(temp.path()).unwrap());
assert!(ftp.on_recv(readable).is_err());
let (_, temp): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
let writable: Box<dyn Write> =
Box::new(open_file(temp.path(), true, true, true).ok().unwrap());
assert!(ftp.on_sent(writable).is_err());
}
}

View file

@ -284,10 +284,7 @@ pub trait FileTransfer {
if filter.matches(dir.name.as_str()) {
drained.push(FsEntry::Directory(dir.clone()));
}
match self.iter_search(dir.abs_path.as_path(), filter) {
Ok(mut filtered) => drained.append(&mut filtered),
Err(err) => return Err(err),
}
drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?);
}
FsEntry::File(file) => {
if filter.matches(file.name.as_str()) {

View file

@ -77,7 +77,7 @@ impl ScpFileTransfer {
PathBuf::from(path_slash::PathExt::to_slash_lossy(p).as_str())
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn resolve(p: &Path) -> PathBuf {
p.to_path_buf()
}
@ -445,10 +445,9 @@ impl FileTransfer for ScpFileTransfer {
self.session = Some(session);
// Get working directory
debug!("Getting working directory...");
match self.perform_shell_cmd("pwd") {
Ok(output) => self.wrkdir = PathBuf::from(output.as_str().trim()),
Err(err) => return Err(err),
}
self.wrkdir = self
.perform_shell_cmd("pwd")
.map(|x| PathBuf::from(x.as_str().trim()))?;
info!(
"Connection established; working directory: {}",
self.wrkdir.display()
@ -486,7 +485,7 @@ impl FileTransfer for ScpFileTransfer {
///
/// Indicates whether the client is connected to remote
fn is_connected(&self) -> bool {
self.session.as_ref().is_some()
self.session.is_some()
}
/// ### pwd
@ -853,7 +852,15 @@ impl FileTransfer for ScpFileTransfer {
) -> Result<Box<dyn Write>, FileTransferError> {
match self.session.as_ref() {
Some(session) => {
let file_name: PathBuf = Self::resolve(file_name);
let file_name: PathBuf = match file_name.is_absolute() {
true => PathBuf::from(file_name),
false => {
let mut p: PathBuf = self.wrkdir.clone();
p.push(file_name);
Self::resolve(p.as_path())
}
};
let file_name: PathBuf = Self::resolve(file_name.as_path());
info!(
"Sending file {} to {}",
local.abs_path.display(),
@ -963,8 +970,12 @@ impl FileTransfer for ScpFileTransfer {
mod tests {
use super::*;
use crate::utils::test_helpers::make_fsentry;
use pretty_assertions::assert_eq;
#[cfg(feature = "with-containers")]
use crate::utils::test_helpers::{create_sample_file_entry, write_file, write_ssh_key};
#[test]
fn test_filetransfer_scp_new() {
let client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
@ -973,31 +984,210 @@ mod tests {
}
#[test]
fn test_filetransfer_scp_connect() {
#[cfg(feature = "with-containers")]
fn test_filetransfer_scp_server() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert_eq!(client.is_connected(), false);
// Sample file
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
String::from("127.0.0.1"),
10222,
Some(String::from("sftp")),
Some(String::from("password"))
)
.is_ok());
// Check session and scp
// Check session and sftp
assert!(client.session.is_some());
assert_eq!(client.wrkdir, PathBuf::from("/config"));
assert_eq!(client.is_connected(), true);
// Pwd
assert_eq!(client.wrkdir.clone(), client.pwd().ok().unwrap());
// Stat
let stat: FsFile = client
.stat(PathBuf::from("sshd.pid").as_path())
.ok()
.unwrap()
.unwrap_file();
assert_eq!(stat.abs_path, PathBuf::from("/config/sshd.pid"));
let stat: FsDirectory = client
.stat(PathBuf::from("/config/").as_path())
.ok()
.unwrap()
.unwrap_dir();
assert_eq!(stat.abs_path, PathBuf::from("/config/"));
// Stat (err)
assert!(client
.stat(PathBuf::from("/config/5t0ca220.log").as_path())
.is_err());
// List dir (dir has 4 (one is hidden :D) entries)
assert!(client.list_dir(&Path::new("/config")).unwrap().len() >= 4);
// Make directory
assert!(client.mkdir(PathBuf::from("/tmp/omar").as_path()).is_ok());
// Make directory (err)
assert!(client
.mkdir(PathBuf::from("/root/aaaaa/pommlar").as_path())
.is_err());
// Change directory
assert!(client
.change_dir(PathBuf::from("/tmp/omar").as_path())
.is_ok());
// Change directory (err)
assert!(client
.change_dir(PathBuf::from("/tmp/oooo/aaaa/eee").as_path())
.is_err());
// Copy file
assert!(client
.copy(
&make_fsentry(PathBuf::from("/config/sshd.pid"), false),
PathBuf::from("/tmp/sshd.pid").as_path()
)
.is_ok());
// Copy dir
assert!(client
.copy(
&make_fsentry(PathBuf::from("/tmp/omar"), true),
PathBuf::from("/tmp/ommlar").as_path()
)
.is_ok());
// Copy (err)
assert!(client
.copy(
&make_fsentry(PathBuf::from("/tmp/zattera"), false),
PathBuf::from("/").as_path()
)
.is_err());
// Exec
assert_eq!(client.exec("echo 5").ok().unwrap().as_str(), "5\n");
// Change dir to ommlar
assert!(client
.change_dir(PathBuf::from("/tmp/ommlar/").as_path())
.is_ok());
// Upload 2 files
let mut writable = client
.send_file(&entry, PathBuf::from("omar.txt").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(client.on_sent(writable).is_ok());
let mut writable = client
.send_file(&entry, PathBuf::from("README.md").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(client.on_sent(writable).is_ok());
// Upload file (err)
assert!(client
.send_file(&entry, PathBuf::from("/ommlar/omarone").as_path())
.is_err());
// List dir
let list: Vec<FsEntry> = client
.list_dir(PathBuf::from("/tmp/ommlar").as_path())
.ok()
.unwrap();
assert_eq!(list.len(), 2);
// Find
assert_eq!(client.find("*.txt").ok().unwrap().len(), 1);
assert_eq!(client.find("*.md").ok().unwrap().len(), 1);
assert_eq!(client.find("*.jpeg").ok().unwrap().len(), 0);
// Rename
assert!(client
.mkdir(PathBuf::from("/tmp/uploads").as_path())
.is_ok());
assert!(client
.rename(
list.get(0).unwrap(),
PathBuf::from("/tmp/uploads/README.txt").as_path()
)
.is_ok());
// Rename (err)
assert!(client
.rename(list.get(0).unwrap(), PathBuf::from("OMARONE").as_path())
.is_err());
let dummy: FsEntry = FsEntry::File(FsFile {
name: String::from("cucumber.txt"),
abs_path: PathBuf::from("/cucumber.txt"),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
readonly: true,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
assert!(client
.rename(&dummy, PathBuf::from("/a/b/c").as_path())
.is_err());
// Remove
assert!(client.remove(list.get(1).unwrap()).is_ok());
// Receive file
let mut writable = client
.send_file(&entry, PathBuf::from("/tmp/uploads/README.txt").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(client.on_sent(writable).is_ok());
let file: FsFile = client
.list_dir(PathBuf::from("/tmp/uploads").as_path())
.ok()
.unwrap()
.get(0)
.unwrap()
.clone()
.unwrap_file();
let mut readable = client.recv_file(&file).ok().unwrap();
let mut data: Vec<u8> = vec![0; 1024];
assert!(readable.read(&mut data).is_ok());
assert!(client.on_recv(readable).is_ok());
// Receive file (err)
assert!(client.recv_file(&entry).is_err());
// Cleanup
assert!(client.change_dir(PathBuf::from("/").as_path()).is_ok());
assert!(client
.remove(&make_fsentry(PathBuf::from("/tmp/ommlar"), true))
.is_ok());
assert!(client
.remove(&make_fsentry(PathBuf::from("/tmp/omar"), true))
.is_ok());
assert!(client
.remove(&make_fsentry(PathBuf::from("/tmp/uploads"), true))
.is_ok());
// Disconnect
assert!(client.disconnect().is_ok());
assert_eq!(client.is_connected(), false);
}
#[test]
#[cfg(feature = "with-containers")]
fn test_filetransfer_scp_ssh_storage() {
let mut storage: SshKeyStorage = SshKeyStorage::empty();
let key_file: tempfile::NamedTempFile = write_ssh_key();
storage.add_key("127.0.0.1", "sftp", key_file.path().to_path_buf());
let mut client: ScpFileTransfer = ScpFileTransfer::new(storage);
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10222,
Some(String::from("sftp")),
None,
)
.is_ok());
assert_eq!(client.is_connected(), true);
assert!(client.disconnect().is_ok());
}
#[test]
fn test_filetransfer_scp_bad_auth() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
String::from("127.0.0.1"),
10222,
Some(String::from("demo")),
Some(String::from("badpassword"))
)
@ -1005,10 +1195,11 @@ mod tests {
}
#[test]
#[cfg(feature = "with-containers")]
fn test_filetransfer_scp_no_credentials() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(String::from("test.rebex.net"), 22, None, None)
.connect(String::from("127.0.0.1"), 10222, None, None)
.is_err());
}
@ -1024,245 +1215,92 @@ mod tests {
)
.is_err());
}
#[test]
fn test_filetransfer_scp_pwd() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and scp
assert!(client.session.is_some());
// Pwd
assert_eq!(client.pwd().ok().unwrap(), PathBuf::from("/"));
// Disconnect
assert!(client.disconnect().is_ok());
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
fn test_filetransfer_scp_cwd() {
fn test_filetransfer_scp_parse_ls() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
// File
let entry: FsFile = client
.parse_ls_output(
PathBuf::from("/tmp").as_path(),
"-rw-r--r-- 1 root root 2056 giu 13 21:11 Cargo.toml",
)
.is_ok());
// Check session and scp
assert!(client.session.is_some());
// Cwd (relative)
assert!(client.change_dir(PathBuf::from("pub/").as_path()).is_ok());
// Cwd (absolute)
assert!(client.change_dir(PathBuf::from("/pub").as_path()).is_ok());
// Disconnect
assert!(client.disconnect().is_ok());
}
#[test]
fn test_filetransfer_scp_cwd_error() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Cwd (abs)
assert!(client
.change_dir(PathBuf::from("/omar/gabber").as_path())
.is_err());
// Cwd (rel)
assert!(client
.change_dir(PathBuf::from("gomar/pett").as_path())
.is_err());
// Disconnect
assert!(client.disconnect().is_ok());
}
#[test]
fn test_filetransfer_scp_ls() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and scp
assert!(client.session.is_some());
// List dir
let pwd: PathBuf = client.pwd().ok().unwrap();
let files: Vec<FsEntry> = client.list_dir(pwd.as_path()).ok().unwrap();
assert_eq!(files.len(), 3); // There are 3 files
// Disconnect
assert!(client.disconnect().is_ok());
}
#[test]
fn test_filetransfer_scp_stat() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and scp
assert!(client.session.is_some());
let file: FsEntry = client
.stat(PathBuf::from("readme.txt").as_path())
.ok()
.unwrap();
if let FsEntry::File(file) = file {
assert_eq!(file.abs_path, PathBuf::from("/readme.txt"));
} else {
panic!("Expected readme.txt to be a file");
}
.unwrap()
.unwrap_file();
assert_eq!(entry.name.as_str(), "Cargo.toml");
assert_eq!(entry.abs_path, PathBuf::from("/tmp/Cargo.toml"));
assert_eq!(entry.unix_pex.unwrap(), (6, 4, 4));
assert_eq!(entry.size, 2056);
assert_eq!(entry.readonly, false);
assert_eq!(entry.ftype.unwrap().as_str(), "toml");
assert!(entry.symlink.is_none());
// File (year)
let entry: FsFile = client
.parse_ls_output(
PathBuf::from("/tmp").as_path(),
"-rw-rw-rw- 1 root root 3368 nov 7 2020 CODE_OF_CONDUCT.md",
)
.ok()
.unwrap()
.unwrap_file();
assert_eq!(entry.name.as_str(), "CODE_OF_CONDUCT.md");
assert_eq!(entry.abs_path, PathBuf::from("/tmp/CODE_OF_CONDUCT.md"));
assert_eq!(entry.unix_pex.unwrap(), (6, 6, 6));
assert_eq!(entry.size, 3368);
assert_eq!(entry.readonly, false);
assert_eq!(entry.ftype.unwrap().as_str(), "md");
assert!(entry.symlink.is_none());
// Directory
let entry: FsDirectory = client
.parse_ls_output(
PathBuf::from("/tmp").as_path(),
"drwxr-xr-x 1 root root 512 giu 13 21:11 docs",
)
.ok()
.unwrap()
.unwrap_dir();
assert_eq!(entry.name.as_str(), "docs");
assert_eq!(entry.abs_path, PathBuf::from("/tmp/docs"));
assert_eq!(entry.unix_pex.unwrap(), (7, 5, 5));
assert_eq!(entry.readonly, false);
assert!(entry.symlink.is_none());
// Short metadata
assert!(client
.parse_ls_output(
PathBuf::from("/tmp").as_path(),
"drwxr-xr-x 1 root root 512 giu 13 21:11",
)
.is_err());
// Special file
assert!(client
.parse_ls_output(
PathBuf::from("/tmp").as_path(),
"crwxr-xr-x 1 root root 512 giu 13 21:11 ttyS1",
)
.is_err());
// Bad pex
assert!(client
.parse_ls_output(
PathBuf::from("/tmp").as_path(),
"-rwxr-xr 1 root root 512 giu 13 21:11 ttyS1",
)
.is_err());
}
#[test]
fn test_filetransfer_scp_exec() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and scp
assert!(client.session.is_some());
// Exec
assert_eq!(client.exec("echo 5").ok().unwrap().as_str(), "5\n");
// Disconnect
assert!(client.disconnect().is_ok());
fn test_filetransfer_scp_get_name_and_link() {
let client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert_eq!(
client.get_name_and_link("Cargo.toml"),
(String::from("Cargo.toml"), None)
);
assert_eq!(
client.get_name_and_link("Cargo -> Cargo.toml"),
(String::from("Cargo"), Some(PathBuf::from("Cargo.toml")))
);
}
#[test]
//#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
fn test_filetransfer_scp_find() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and scp
assert!(client.session.is_some());
// Search for file (let's search for pop3-*.png); there should be 2
let search_res: Vec<FsEntry> = client.find("pop3-*.png").ok().unwrap();
assert_eq!(search_res.len(), 2);
// verify names
assert_eq!(search_res[0].get_name(), "pop3-browser.png");
assert_eq!(search_res[1].get_name(), "pop3-console-client.png");
// Search directory
let search_res: Vec<FsEntry> = client.find("pub").ok().unwrap();
assert_eq!(search_res.len(), 1);
// Disconnect
assert!(client.disconnect().is_ok());
// Verify err
assert!(client.find("pippo").is_err());
}
#[test]
fn test_filetransfer_scp_recv() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and scp
assert!(client.session.is_some());
let file: FsFile = FsFile {
name: String::from("readme.txt"),
abs_path: PathBuf::from("/readme.txt"),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
readonly: true,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
};
// Receive file
assert!(client.recv_file(&file).is_ok());
// Disconnect
assert!(client.disconnect().is_ok());
}
#[test]
fn test_filetransfer_scp_recv_failed_nosuchfile() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and scp
assert!(client.session.is_some());
// Receive file
let file: FsFile = FsFile {
name: String::from("omar.txt"),
abs_path: PathBuf::from("/omar.txt"),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
readonly: true,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
};
assert!(client.recv_file(&file).is_err());
// Disconnect
assert!(client.disconnect().is_ok());
}
// NOTE: other functions doesn't work with this test scp server
/* NOTE: the server doesn't allow you to create directories
#[test]
fn test_filetransfer_scp_mkdir() {
let mut client: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(client.connect(String::from("test.rebex.net"), 22, Some(String::from("demo")), Some(String::from("password"))).is_ok());
let dir: String = String::from("foo");
// Mkdir
assert!(client.mkdir(dir).is_ok());
// cwd
assert!(client.change_dir(PathBuf::from("foo/").as_path()).is_ok());
assert_eq!(client.wrkdir, PathBuf::from("/foo"));
// Disconnect
assert!(client.disconnect().is_ok());
}
*/
#[test]
fn test_filetransfer_scp_uninitialized() {
let file: FsFile = FsFile {
@ -1282,9 +1320,19 @@ mod tests {
let mut scp: ScpFileTransfer = ScpFileTransfer::new(SshKeyStorage::empty());
assert!(scp.change_dir(Path::new("/tmp")).is_err());
assert!(scp.disconnect().is_err());
assert!(scp.exec("echo 5").is_err());
assert!(scp.list_dir(Path::new("/tmp")).is_err());
assert!(scp.mkdir(Path::new("/tmp")).is_err());
assert!(scp.pwd().is_err());
assert!(scp
.remove(&make_fsentry(PathBuf::from("/nowhere"), false))
.is_err());
assert!(scp
.rename(
&make_fsentry(PathBuf::from("/nowhere"), false),
PathBuf::from("/culonia").as_path()
)
.is_err());
assert!(scp.stat(Path::new("/tmp")).is_err());
assert!(scp.recv_file(&file).is_err());
assert!(scp.send_file(&file, Path::new("/tmp/omar.txt")).is_err());

View file

@ -466,10 +466,7 @@ impl FileTransfer for SftpFileTransfer {
match self.sftp.as_ref() {
Some(_) => {
// Change working directory
self.wrkdir = match self.get_remote_path(dir) {
Ok(p) => p,
Err(err) => return Err(err),
};
self.wrkdir = self.get_remote_path(dir)?;
info!("Changed working directory to {}", self.wrkdir.display());
Ok(self.wrkdir.clone())
}
@ -532,10 +529,7 @@ impl FileTransfer for SftpFileTransfer {
match self.sftp.as_ref() {
Some(sftp) => {
// Get path
let dir: PathBuf = match self.get_remote_path(path) {
Ok(p) => p,
Err(err) => return Err(err),
};
let dir: PathBuf = self.get_remote_path(path)?;
info!("Getting file entries in {}", path.display());
// Get files
match sftp.readdir(dir.as_path()) {
@ -609,10 +603,7 @@ impl FileTransfer for SftpFileTransfer {
// Remove recursively
debug!("{} is a directory; removing all directory entries", d.name);
// Get directory files
let directory_content: Vec<FsEntry> = match self.list_dir(d.abs_path.as_path()) {
Ok(entries) => entries,
Err(err) => return Err(err),
};
let directory_content: Vec<FsEntry> = self.list_dir(d.abs_path.as_path())?;
for entry in directory_content.iter() {
if let Err(err) = self.remove(&entry) {
return Err(err);
@ -666,10 +657,7 @@ impl FileTransfer for SftpFileTransfer {
match self.sftp.as_ref() {
Some(sftp) => {
// Get path
let dir: PathBuf = match self.get_remote_path(path) {
Ok(p) => p,
Err(err) => return Err(err),
};
let dir: PathBuf = self.get_remote_path(path)?;
info!("Stat file {}", dir.display());
// Get file
match sftp.stat(dir.as_path()) {
@ -758,10 +746,7 @@ impl FileTransfer for SftpFileTransfer {
)),
Some(sftp) => {
// Get remote file name
let remote_path: PathBuf = match self.get_remote_path(file.abs_path.as_path()) {
Ok(p) => p,
Err(err) => return Err(err),
};
let remote_path: PathBuf = self.get_remote_path(file.abs_path.as_path())?;
info!("Receiving file {}", remote_path.display());
// Open remote file
match sftp.open(remote_path.as_path()) {
@ -800,7 +785,9 @@ impl FileTransfer for SftpFileTransfer {
mod tests {
use super::*;
use crate::utils::test_helpers::make_fsentry;
#[cfg(feature = "with-containers")]
use crate::utils::test_helpers::{create_sample_file_entry, write_file, write_ssh_key};
use pretty_assertions::assert_eq;
#[test]
@ -813,34 +800,188 @@ mod tests {
}
#[test]
fn test_filetransfer_sftp_connect() {
#[cfg(feature = "with-containers")]
fn test_filetransfer_sftp_server() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert_eq!(client.is_connected(), false);
// Sample file
let (entry, file): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
// Connect
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
String::from("127.0.0.1"),
10022,
Some(String::from("sftp")),
Some(String::from("password"))
)
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
assert!(client.sftp.is_some());
assert_eq!(client.wrkdir, PathBuf::from("/"));
assert_eq!(client.wrkdir, PathBuf::from("/config"));
assert_eq!(client.is_connected(), true);
// Pwd
assert_eq!(client.wrkdir.clone(), client.pwd().ok().unwrap());
// Stat
let stat: FsFile = client
.stat(PathBuf::from("/config/sshd.pid").as_path())
.ok()
.unwrap()
.unwrap_file();
assert_eq!(stat.name.as_str(), "sshd.pid");
let stat: FsDirectory = client
.stat(PathBuf::from("/config").as_path())
.ok()
.unwrap()
.unwrap_dir();
assert_eq!(stat.name.as_str(), "config");
// Stat (err)
assert!(client
.stat(PathBuf::from("/config/5t0ca220.log").as_path())
.is_err());
// List dir (dir has 4 (one is hidden :D) entries)
assert!(client.list_dir(&Path::new("/config")).unwrap().len() >= 4);
// Make directory
assert!(client.mkdir(PathBuf::from("/tmp/omar").as_path()).is_ok());
// Make directory (err)
assert!(client
.mkdir(PathBuf::from("/root/aaaaa/pommlar").as_path())
.is_err());
// Change directory
assert!(client
.change_dir(PathBuf::from("/tmp/omar").as_path())
.is_ok());
// Change directory (err)
assert!(client
.change_dir(PathBuf::from("/tmp/oooo/aaaa/eee").as_path())
.is_err());
// Copy (not supported)
assert!(client
.copy(&FsEntry::File(entry.clone()), PathBuf::from("/").as_path())
.is_err());
// Exec
assert_eq!(client.exec("echo 5").ok().unwrap().as_str(), "5\n");
// Upload 2 files
let mut writable = client
.send_file(&entry, PathBuf::from("omar.txt").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(client.on_sent(writable).is_ok());
let mut writable = client
.send_file(&entry, PathBuf::from("README.md").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(client.on_sent(writable).is_ok());
// Upload file (err)
assert!(client
.send_file(&entry, PathBuf::from("/ommlar/omarone").as_path())
.is_err());
// List dir
let list: Vec<FsEntry> = client
.list_dir(PathBuf::from("/tmp/omar").as_path())
.ok()
.unwrap();
assert_eq!(list.len(), 2);
// Find
assert_eq!(client.find("*.txt").ok().unwrap().len(), 1);
assert_eq!(client.find("*.md").ok().unwrap().len(), 1);
assert_eq!(client.find("*.jpeg").ok().unwrap().len(), 0);
// Rename
assert!(client
.mkdir(PathBuf::from("/tmp/uploads").as_path())
.is_ok());
assert!(client
.rename(
list.get(0).unwrap(),
PathBuf::from("/tmp/uploads/README.txt").as_path()
)
.is_ok());
// Rename (err)
assert!(client
.rename(list.get(0).unwrap(), PathBuf::from("OMARONE").as_path())
.is_err());
let dummy: FsEntry = FsEntry::File(FsFile {
name: String::from("cucumber.txt"),
abs_path: PathBuf::from("/cucumber.txt"),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
readonly: true,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
assert!(client
.rename(&dummy, PathBuf::from("/a/b/c").as_path())
.is_err());
// Remove
assert!(client.remove(list.get(1).unwrap()).is_ok());
assert!(client.remove(list.get(1).unwrap()).is_err());
// Receive file
let mut writable = client
.send_file(&entry, PathBuf::from("/tmp/uploads/README.txt").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(client.on_sent(writable).is_ok());
let file: FsFile = client
.list_dir(PathBuf::from("/tmp/uploads").as_path())
.ok()
.unwrap()
.get(0)
.unwrap()
.clone()
.unwrap_file();
let mut readable = client.recv_file(&file).ok().unwrap();
let mut data: Vec<u8> = vec![0; 1024];
assert!(readable.read(&mut data).is_ok());
assert!(client.on_recv(readable).is_ok());
// Receive file (err)
assert!(client.recv_file(&entry).is_err());
// Cleanup
assert!(client.change_dir(PathBuf::from("/").as_path()).is_ok());
assert!(client
.remove(&make_fsentry(PathBuf::from("/tmp/omar"), true))
.is_ok());
assert!(client
.remove(&make_fsentry(PathBuf::from("/tmp/uploads"), true))
.is_ok());
// Disconnect
assert!(client.disconnect().is_ok());
assert_eq!(client.is_connected(), false);
}
#[test]
#[cfg(feature = "with-containers")]
fn test_filetransfer_sftp_ssh_storage() {
let mut storage: SshKeyStorage = SshKeyStorage::empty();
let key_file: tempfile::NamedTempFile = write_ssh_key();
storage.add_key("127.0.0.1", "sftp", key_file.path().to_path_buf());
let mut client: SftpFileTransfer = SftpFileTransfer::new(storage);
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10022,
Some(String::from("sftp")),
None,
)
.is_ok());
assert_eq!(client.is_connected(), true);
assert!(client.disconnect().is_ok());
}
#[test]
fn test_filetransfer_sftp_bad_auth() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
String::from("127.0.0.1"),
10022,
Some(String::from("demo")),
Some(String::from("badpassword"))
)
@ -848,13 +989,52 @@ mod tests {
}
#[test]
#[cfg(feature = "with-containers")]
fn test_filetransfer_sftp_no_credentials() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(String::from("test.rebex.net"), 22, None, None)
.connect(String::from("127.0.0.1"), 10022, None, None)
.is_err());
}
#[test]
#[cfg(feature = "with-containers")]
fn test_filetransfer_sftp_get_remote_path() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
// Connect
assert!(client
.connect(
String::from("127.0.0.1"),
10022,
Some(String::from("sftp")),
Some(String::from("password"))
)
.is_ok());
// get realpath
assert!(client
.change_dir(PathBuf::from("/config").as_path())
.is_ok());
assert_eq!(
client
.get_remote_path(PathBuf::from("sshd.pid").as_path())
.ok()
.unwrap(),
PathBuf::from("/config/sshd.pid")
);
// No such file
assert!(client
.get_remote_path(PathBuf::from("omarone.txt").as_path())
.is_err());
// Ok abs path
assert_eq!(
client
.get_remote_path(PathBuf::from("/config/sshd.pid").as_path())
.ok()
.unwrap(),
PathBuf::from("/config/sshd.pid")
);
}
#[test]
fn test_filetransfer_sftp_bad_server() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
@ -868,302 +1048,6 @@ mod tests {
.is_err());
}
#[test]
fn test_filetransfer_sftp_pwd() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
assert!(client.sftp.is_some());
assert_eq!(client.wrkdir, PathBuf::from("/"));
// Pwd
assert_eq!(client.wrkdir.clone(), client.pwd().ok().unwrap());
// Disconnect
assert!(client.disconnect().is_ok());
}
#[test]
fn test_filetransfer_sftp_cwd() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
assert!(client.sftp.is_some());
assert_eq!(client.wrkdir, PathBuf::from("/"));
// Pwd
assert_eq!(client.wrkdir.clone(), client.pwd().ok().unwrap());
// Cwd (relative)
assert!(client.change_dir(PathBuf::from("pub/").as_path()).is_ok());
assert_eq!(client.wrkdir, PathBuf::from("/pub"));
// Cwd (absolute)
assert!(client.change_dir(PathBuf::from("/").as_path()).is_ok());
assert_eq!(client.wrkdir, PathBuf::from("/"));
// Disconnect
assert!(client.disconnect().is_ok());
}
#[test]
fn test_filetransfer_sftp_copy() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
assert!(client.sftp.is_some());
assert_eq!(client.wrkdir, PathBuf::from("/"));
// Copy
let file: FsFile = FsFile {
name: String::from("readme.txt"),
abs_path: PathBuf::from("/readme.txt"),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
readonly: true,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
};
assert!(client
.copy(&FsEntry::File(file), &Path::new("/tmp/dest.txt"))
.is_err());
}
#[test]
fn test_filetransfer_sftp_cwd_error() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Cwd (abs)
assert!(client
.change_dir(PathBuf::from("/omar/gabber").as_path())
.is_err());
// Cwd (rel)
assert!(client
.change_dir(PathBuf::from("gomar/pett").as_path())
.is_err());
// Disconnect
assert!(client.disconnect().is_ok());
assert!(client
.change_dir(PathBuf::from("gomar/pett").as_path())
.is_err());
}
#[test]
fn test_filetransfer_sftp_ls() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
assert!(client.sftp.is_some());
assert_eq!(client.wrkdir, PathBuf::from("/"));
// List dir
let pwd: PathBuf = client.pwd().ok().unwrap();
let files: Vec<FsEntry> = client.list_dir(pwd.as_path()).ok().unwrap();
assert_eq!(files.len(), 3); // There are 3 files
// Disconnect
assert!(client.disconnect().is_ok());
// Verify err
assert!(client.list_dir(pwd.as_path()).is_err());
}
#[test]
fn test_filetransfer_sftp_stat() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
assert!(client.sftp.is_some());
assert_eq!(client.wrkdir, PathBuf::from("/"));
let file: FsEntry = client
.stat(PathBuf::from("readme.txt").as_path())
.ok()
.unwrap();
if let FsEntry::File(file) = file {
assert_eq!(file.abs_path, PathBuf::from("/readme.txt"));
} else {
panic!("Expected readme.txt to be a file");
}
}
#[test]
fn test_filetransfer_sftp_exec() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and scp
assert!(client.session.is_some());
// Exec
assert_eq!(client.exec("echo 5").ok().unwrap().as_str(), "5\n");
// Disconnect
assert!(client.disconnect().is_ok());
// Verify err
assert!(client.exec("echo 1").is_err());
}
#[test]
fn test_filetransfer_sftp_find() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and scp
assert!(client.session.is_some());
// Search for file (let's search for pop3-*.png); there should be 2
let search_res: Vec<FsEntry> = client.find("pop3-*.png").ok().unwrap();
assert_eq!(search_res.len(), 2);
// verify names
assert_eq!(search_res[0].get_name(), "pop3-browser.png");
assert_eq!(search_res[1].get_name(), "pop3-console-client.png");
// Search directory
let search_res: Vec<FsEntry> = client.find("pub").ok().unwrap();
assert_eq!(search_res.len(), 1);
// Disconnect
assert!(client.disconnect().is_ok());
// Verify err
assert!(client.find("pippo").is_err());
}
#[test]
fn test_filetransfer_sftp_recv() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
assert!(client.sftp.is_some());
assert_eq!(client.wrkdir, PathBuf::from("/"));
let file: FsFile = FsFile {
name: String::from("readme.txt"),
abs_path: PathBuf::from("/readme.txt"),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
readonly: true,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
};
// Receive file
assert!(client.recv_file(&file).is_ok());
// Disconnect
assert!(client.disconnect().is_ok());
}
#[test]
fn test_filetransfer_sftp_recv_failed_nosuchfile() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client
.connect(
String::from("test.rebex.net"),
22,
Some(String::from("demo")),
Some(String::from("password"))
)
.is_ok());
// Check session and sftp
assert!(client.session.is_some());
assert!(client.sftp.is_some());
assert_eq!(client.wrkdir, PathBuf::from("/"));
// Receive file
let file: FsFile = FsFile {
name: String::from("omar.txt"),
abs_path: PathBuf::from("/omar.txt"),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
readonly: true,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
};
assert!(client.recv_file(&file).is_err());
// Disconnect
assert!(client.disconnect().is_ok());
}
// NOTE: other functions doesn't work with this test SFTP server
/* NOTE: the server doesn't allow you to create directories
#[test]
fn test_filetransfer_sftp_mkdir() {
let mut client: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(client.connect(String::from("test.rebex.net"), 22, Some(String::from("demo")), Some(String::from("password"))).is_ok());
let dir: String = String::from("foo");
// Mkdir
assert!(client.mkdir(dir).is_ok());
// cwd
assert!(client.change_dir(PathBuf::from("foo/").as_path()).is_ok());
assert_eq!(client.wrkdir, PathBuf::from("/foo"));
// Disconnect
assert!(client.disconnect().is_ok());
}
*/
#[test]
fn test_filetransfer_sftp_uninitialized() {
let file: FsFile = FsFile {
@ -1182,10 +1066,26 @@ mod tests {
};
let mut sftp: SftpFileTransfer = SftpFileTransfer::new(SshKeyStorage::empty());
assert!(sftp.change_dir(Path::new("/tmp")).is_err());
assert!(sftp
.copy(
&make_fsentry(PathBuf::from("/nowhere"), false),
PathBuf::from("/culonia").as_path()
)
.is_err());
assert!(sftp.exec("echo 5").is_err());
assert!(sftp.disconnect().is_err());
assert!(sftp.list_dir(Path::new("/tmp")).is_err());
assert!(sftp.mkdir(Path::new("/tmp")).is_err());
assert!(sftp.pwd().is_err());
assert!(sftp
.remove(&make_fsentry(PathBuf::from("/nowhere"), false))
.is_err());
assert!(sftp
.rename(
&make_fsentry(PathBuf::from("/nowhere"), false),
PathBuf::from("/culonia").as_path()
)
.is_err());
assert!(sftp.stat(Path::new("/tmp")).is_err());
assert!(sftp.recv_file(&file).is_err());
assert!(sftp.send_file(&file, Path::new("/tmp/omar.txt")).is_err());

View file

@ -28,7 +28,7 @@
// Deps
extern crate bytesize;
extern crate regex;
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
extern crate users;
// Locals
use super::FsEntry;
@ -36,7 +36,7 @@ use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
// Ext
use bytesize::ByteSize;
use regex::Regex;
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
use users::{get_group_by_gid, get_user_by_uid};
// Types
// FmtCallback: Formatter, fsentry: &FsEntry, cur_str, prefix, length, extra
@ -251,7 +251,7 @@ impl Formatter {
_fmt_extra: Option<&String>,
) -> String {
// Get username
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
let group: String = match fsentry.get_group() {
Some(gid) => match get_group_by_gid(gid) {
Some(user) => user.name().to_string_lossy().to_string(),
@ -431,7 +431,7 @@ impl Formatter {
_fmt_extra: Option<&String>,
) -> String {
// Get username
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
let username: String = match fsentry.get_user() {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
@ -605,7 +605,7 @@ mod tests {
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
@ -636,7 +636,7 @@ mod tests {
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
@ -667,7 +667,7 @@ mod tests {
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
@ -698,7 +698,7 @@ mod tests {
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
@ -734,7 +734,7 @@ mod tests {
group: Some(0), // UNIX only
unix_pex: Some((7, 5, 5)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
@ -763,7 +763,7 @@ mod tests {
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(

View file

@ -306,6 +306,13 @@ impl FileExplorer {
pub fn toggle_hidden_files(&mut self) {
self.opts.toggle(ExplorerOpts::SHOW_HIDDEN_FILES);
}
/// ### hidden_files_visible
///
/// Returns whether hidden files are visible
pub fn hidden_files_visible(&self) -> bool {
self.opts.intersects(ExplorerOpts::SHOW_HIDDEN_FILES)
}
}
// Traits
@ -411,6 +418,7 @@ mod tests {
let mut explorer: FileExplorer = FileExplorer::default();
// Don't show hidden files
explorer.opts.remove(ExplorerOpts::SHOW_HIDDEN_FILES);
assert_eq!(explorer.hidden_files_visible(), false);
// Create files
explorer.set_files(vec![
make_fs_entry("README.md", false),
@ -434,6 +442,7 @@ mod tests {
assert_eq!(explorer.iter_files().count(), 4);
// Toggle hidden
explorer.toggle_hidden_files();
assert_eq!(explorer.hidden_files_visible(), true);
assert_eq!(explorer.iter_files().count(), 6); // All files are returned now
}
@ -586,7 +595,7 @@ mod tests {
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
assert_eq!(
explorer.fmt_file(&entry),
format!(

View file

@ -226,6 +226,27 @@ impl FsEntry {
},
}
}
/// ### unwrap_file
///
/// Unwrap FsEntry as FsFile
pub fn unwrap_file(self) -> FsFile {
match self {
FsEntry::File(file) => file,
_ => panic!("unwrap_file: not a file"),
}
}
#[cfg(test)]
/// ### unwrap_dir
///
/// Unwrap FsEntry as FsDirectory
pub fn unwrap_dir(self) -> FsDirectory {
match self {
FsEntry::Directory(dir) => dir,
_ => panic!("unwrap_dir: not a directory"),
}
}
}
#[cfg(test)]
@ -262,6 +283,7 @@ mod tests {
assert_eq!(entry.is_dir(), true);
assert_eq!(entry.is_file(), false);
assert_eq!(entry.get_unix_pex(), Some((7, 5, 5)));
assert_eq!(entry.unwrap_dir().abs_path, PathBuf::from("/foo"));
}
#[test]
@ -294,6 +316,47 @@ mod tests {
assert_eq!(entry.is_symlink(), false);
assert_eq!(entry.is_dir(), false);
assert_eq!(entry.is_file(), true);
assert_eq!(entry.unwrap_file().abs_path, PathBuf::from("/bar.txt"));
}
#[test]
#[should_panic]
fn test_fs_fsentry_file_unwrap_bad() {
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8192,
readonly: false,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
});
entry.unwrap_dir();
}
#[test]
#[should_panic]
fn test_fs_fsentry_dir_unwrap_bad() {
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("foo"),
abs_path: PathBuf::from("/foo"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
readonly: false,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((7, 5, 5)), // UNIX only
});
entry.unwrap_file();
}
#[test]

View file

@ -34,9 +34,9 @@ use std::time::SystemTime;
use thiserror::Error;
use wildmatch::WildMatch;
// Metadata ext
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
use std::fs::set_permissions;
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
use std::os::unix::fs::{MetadataExt, PermissionsExt};
// Locals
@ -439,7 +439,7 @@ impl Localhost {
/// ### stat
///
/// Stat file and create a FsEntry
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
info!("Stating file {}", path.display());
let path: PathBuf = self.to_abs_path(path);
@ -605,7 +605,7 @@ impl Localhost {
/// ### chmod
///
/// Change file mode to file, according to UNIX permissions
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
pub fn chmod(&self, path: &Path, pex: (u8, u8, u8)) -> Result<(), HostError> {
let path: PathBuf = self.to_abs_path(path);
// Get metadta
@ -773,10 +773,7 @@ impl Localhost {
if filter.matches(dir.name.as_str()) {
drained.push(FsEntry::Directory(dir.clone()));
}
match self.iter_search(dir.abs_path.as_path(), filter) {
Ok(mut filtered) => drained.append(&mut filtered),
Err(err) => return Err(err),
}
drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?);
}
FsEntry::File(file) => {
if filter.matches(file.name.as_str()) {
@ -793,7 +790,7 @@ impl Localhost {
/// ### u32_to_mode
///
/// Return string with format xxxxxx to tuple of permissions (user, group, others)
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn u32_to_mode(&self, mode: u32) -> (u8, u8, u8) {
let user: u8 = ((mode >> 6) & 0x7) as u8;
let group: u8 = ((mode >> 3) & 0x7) as u8;
@ -804,7 +801,7 @@ impl Localhost {
/// mode_to_u32
///
/// Convert owner,group,others to u32
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn mode_to_u32(&self, mode: (u8, u8, u8)) -> u32 {
((mode.0 as u32) << 6) + ((mode.1 as u32) << 3) + mode.2 as u32
}
@ -829,12 +826,17 @@ impl Localhost {
mod tests {
use super::*;
#[cfg(target_family = "unix")]
use crate::utils::test_helpers::{create_sample_file, make_fsentry};
use crate::utils::test_helpers::{make_dir_at, make_file_at};
use pretty_assertions::assert_eq;
#[cfg(target_family = "unix")]
use std::fs::File;
#[cfg(target_family = "unix")]
use std::io::Write;
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
use std::os::unix::fs::{symlink, PermissionsExt};
#[test]
@ -846,7 +848,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn test_host_localhost_new() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
assert_eq!(host.wrkdir, PathBuf::from("/dev"));
@ -882,14 +884,14 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn test_host_localhost_pwd() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
assert_eq!(host.pwd(), PathBuf::from("/dev"));
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn test_host_localhost_list_files() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
// Scan dir
@ -902,7 +904,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn test_host_localhost_change_dir() {
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
let new_dir: PathBuf = PathBuf::from("/dev");
@ -918,7 +920,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
#[should_panic]
fn test_host_localhost_change_dir_failed() {
let mut host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
@ -927,7 +929,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn test_host_localhost_open_read() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
// Create temp file
@ -936,7 +938,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
#[should_panic]
fn test_host_localhost_open_read_err_no_such_file() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
@ -946,7 +948,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn test_host_localhost_open_read_err_not_accessible() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
let file: tempfile::NamedTempFile = create_sample_file();
@ -957,7 +959,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn test_host_localhost_open_write() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
// Create temp file
@ -966,7 +968,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(any(target_os = "macos", target_os = "linux"))]
fn test_host_localhost_open_write_err() {
let host: Localhost = Localhost::new(PathBuf::from("/dev")).ok().unwrap();
let file: tempfile::NamedTempFile = create_sample_file();
@ -975,7 +977,8 @@ mod tests {
//fs::set_permissions(file.path(), perms)?;
assert!(host.open_file_write(file.path()).is_err());
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
#[test]
fn test_host_localhost_symlinks() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@ -1023,7 +1026,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn test_host_localhost_mkdir() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
@ -1038,10 +1041,17 @@ mod tests {
assert!(host
.mkdir_ex(PathBuf::from("/tmp/test_dir_123456789").as_path(), true)
.is_ok());
// Fail
assert!(host
.mkdir_ex(
PathBuf::from("/aaaa/oooooo/tmp/test_dir_123456789").as_path(),
true
)
.is_err());
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn test_host_localhost_remove() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// Create sample file
@ -1060,10 +1070,17 @@ mod tests {
let files: Vec<FsEntry> = host.list_dir();
assert_eq!(files.len(), 1); // There should be 1 file now
assert!(host.remove(files.get(0).unwrap()).is_ok());
// Remove unexisting directory
assert!(host
.remove(&make_fsentry(PathBuf::from("/a/b/c/d"), true))
.is_err());
assert!(host
.remove(&make_fsentry(PathBuf::from("/aaaaaaa"), false))
.is_err());
}
#[test]
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
fn test_host_localhost_rename() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// Create sample file
@ -1073,7 +1090,7 @@ mod tests {
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<FsEntry> = host.list_dir();
assert_eq!(files.len(), 1); // There should be 1 file now
assert_eq!(get_filename(files.get(0).unwrap()), String::from("foo.txt"));
assert_eq!(files.get(0).unwrap().get_name(), "foo.txt");
// Rename file
let dst_path: PathBuf =
PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str());
@ -1083,7 +1100,7 @@ mod tests {
// There should be still 1 file now, but named bar.txt
let files: Vec<FsEntry> = host.list_dir();
assert_eq!(files.len(), 1); // There should be 0 files now
assert_eq!(get_filename(files.get(0).unwrap()), String::from("bar.txt"));
assert_eq!(files.get(0).unwrap().get_name(), "bar.txt");
// Fail
let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu");
assert!(host
@ -1091,7 +1108,7 @@ mod tests {
.is_err());
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
#[test]
fn test_host_chmod() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@ -1110,7 +1127,7 @@ mod tests {
.is_err());
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
#[test]
fn test_host_copy_file_absolute() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@ -1131,9 +1148,16 @@ mod tests {
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
// Verify host has two files
assert_eq!(host.files.len(), 2);
// Fail copy
assert!(host
.copy(
&make_fsentry(PathBuf::from("/a/a7/a/a7a"), false),
PathBuf::from("571k422i").as_path()
)
.is_err());
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
#[test]
fn test_host_copy_file_relative() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@ -1155,7 +1179,7 @@ mod tests {
assert_eq!(host.files.len(), 2);
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
#[test]
fn test_host_copy_directory_absolute() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@ -1186,7 +1210,7 @@ mod tests {
assert!(host.stat(test_file_path.as_path()).is_ok());
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
#[test]
fn test_host_copy_directory_relative() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
@ -1221,7 +1245,7 @@ mod tests {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
// Execute
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
assert_eq!(host.exec("echo 5").ok().unwrap().as_str(), "5\n");
#[cfg(target_os = "windows")]
assert_eq!(host.exec("echo 5").ok().unwrap().as_str(), "5\r\n");
@ -1232,16 +1256,16 @@ mod tests {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let dir_path: &Path = tmpdir.path();
// Make files
assert!(make_sample_file(dir_path, "pippo.txt").is_ok());
assert!(make_sample_file(dir_path, "foo.jpg").is_ok());
assert!(make_file_at(dir_path, "pippo.txt").is_ok());
assert!(make_file_at(dir_path, "foo.jpg").is_ok());
// Make nested struct
assert!(make_dir(dir_path, "examples").is_ok());
assert!(make_dir_at(dir_path, "examples").is_ok());
let mut subdir: PathBuf = PathBuf::from(dir_path);
subdir.push("examples/");
assert!(make_sample_file(subdir.as_path(), "omar.txt").is_ok());
assert!(make_sample_file(subdir.as_path(), "errors.txt").is_ok());
assert!(make_sample_file(subdir.as_path(), "screenshot.png").is_ok());
assert!(make_sample_file(subdir.as_path(), "examples.csv").is_ok());
assert!(make_file_at(subdir.as_path(), "omar.txt").is_ok());
assert!(make_file_at(subdir.as_path(), "errors.txt").is_ok());
assert!(make_file_at(subdir.as_path(), "screenshot.png").is_ok());
assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok());
let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
// Find txt files
let mut result: Vec<FsEntry> = host.find("*.txt").ok().unwrap();
@ -1300,50 +1324,4 @@ mod tests {
String::from("File already exists")
);
}
/// ### make_sample_file
///
/// Make a file with `name` in the current directory
fn make_sample_file(dir: &Path, filename: &str) -> std::io::Result<()> {
let mut p: PathBuf = PathBuf::from(dir);
p.push(filename);
let mut file: File = File::create(p.as_path())?;
write!(
file,
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nMauris ultricies consequat eros,\nnec scelerisque magna imperdiet metus.\n"
)?;
Ok(())
}
/// ### make_dir
///
/// Make a directory in `dir`
fn make_dir(dir: &Path, dirname: &str) -> std::io::Result<()> {
let mut p: PathBuf = PathBuf::from(dir);
p.push(dirname);
std::fs::create_dir(p.as_path())
}
/// ### create_sample_file
///
/// Create a sample file
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
fn create_sample_file() -> tempfile::NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
write!(
tmpfile,
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.\nMauris ultricies consequat eros,\nnec scelerisque magna imperdiet metus.\n"
)
.unwrap();
tmpfile
}
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
fn get_filename(entry: &FsEntry) -> String {
match entry {
FsEntry::Directory(d) => d.name.clone(),
FsEntry::File(f) => f.name.clone(),
}
}
}

View file

@ -427,11 +427,12 @@ mod tests {
use pretty_assertions::assert_eq;
use std::thread::sleep;
use std::time::Duration;
use tempfile::TempDir;
#[test]
fn test_system_bookmarks_new() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let client: BookmarksClient =
@ -445,7 +446,12 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "linux"))]
#[cfg(any(
target_os = "linux",
target_os = "freebsd",
target_os = "netbsd",
target_os = "netbsd"
))]
fn test_system_bookmarks_new_err() {
assert!(BookmarksClient::new(
Path::new("/tmp/oifoif/omar"),
@ -454,7 +460,7 @@ mod tests {
)
.is_err());
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, _): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
assert!(
BookmarksClient::new(cfg_path.as_path(), Path::new("/tmp/efnnu/omar"), 16).is_err()
@ -464,7 +470,7 @@ mod tests {
#[test]
fn test_system_bookmarks_new_from_existing() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
@ -510,7 +516,7 @@ mod tests {
#[test]
fn test_system_bookmarks_manipulate_bookmarks() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
@ -556,7 +562,7 @@ mod tests {
#[should_panic]
fn test_system_bookmarks_bad_bookmark_name() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
@ -575,7 +581,7 @@ mod tests {
#[test]
fn test_system_bookmarks_manipulate_recents() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
@ -610,7 +616,7 @@ mod tests {
#[test]
fn test_system_bookmarks_dup_recent() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
@ -635,7 +641,7 @@ mod tests {
#[test]
fn test_system_bookmarks_recents_more_than_limit() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
@ -681,9 +687,8 @@ mod tests {
#[test]
#[should_panic]
fn test_system_bookmarks_add_bookmark_empty() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: tempfile::TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
// Initialize a new bookmarks client
let mut client: BookmarksClient =
@ -702,19 +707,10 @@ mod tests {
/// ### get_paths
///
/// Get paths for configuration and key for bookmarks
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
let k: PathBuf = PathBuf::from(dir);
let mut c: PathBuf = k.clone();
c.push("bookmarks.toml");
(c, k)
}
/// ### create_tmp_dir
///
/// Create temporary directory
fn create_tmp_dir() -> tempfile::TempDir {
tempfile::TempDir::new().ok().unwrap()
}
}

View file

@ -408,10 +408,11 @@ mod tests {
use pretty_assertions::assert_eq;
use std::io::Read;
use tempfile::TempDir;
#[test]
fn test_system_config_new() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, ssh_keys_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let client: ConfigClient = ConfigClient::new(cfg_path.as_path(), ssh_keys_path.as_path())
.ok()
@ -437,14 +438,14 @@ mod tests {
ConfigClient::new(Path::new("/tmp/oifoif/omar"), Path::new("/tmp/efnnu/omar"),)
.is_err()
);
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, _): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
assert!(ConfigClient::new(cfg_path.as_path(), Path::new("/tmp/efnnu/omar")).is_err());
}
#[test]
fn test_system_config_from_existing() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
@ -477,7 +478,7 @@ mod tests {
#[test]
fn test_system_config_text_editor() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
@ -488,7 +489,7 @@ mod tests {
#[test]
fn test_system_config_default_protocol() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
@ -502,7 +503,7 @@ mod tests {
#[test]
fn test_system_config_show_hidden_files() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
@ -513,7 +514,7 @@ mod tests {
#[test]
fn test_system_config_check_for_updates() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
@ -527,7 +528,7 @@ mod tests {
#[test]
fn test_system_config_group_dirs() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
@ -540,7 +541,7 @@ mod tests {
#[test]
fn test_system_config_local_file_fmt() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
@ -555,7 +556,7 @@ mod tests {
#[test]
fn test_system_config_remote_file_fmt() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
@ -573,7 +574,7 @@ mod tests {
#[test]
fn test_system_config_ssh_keys() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
@ -637,13 +638,6 @@ mod tests {
(c, k)
}
/// ### create_tmp_dir
///
/// Create temporary directory
fn create_tmp_dir() -> tempfile::TempDir {
tempfile::TempDir::new().ok().unwrap()
}
fn get_sample_rsa_key() -> String {
format!(
"-----BEGIN OPENSSH PRIVATE KEY-----\n{}\n-----END OPENSSH PRIVATE KEY-----",

View file

@ -87,6 +87,16 @@ impl SshKeyStorage {
fn make_mapkey(host: &str, username: &str) -> String {
format!("{}@{}", username, host)
}
#[cfg(test)]
/// ### add_key
///
/// Add a key to storage
/// NOTE: available only for tests
pub fn add_key(&mut self, host: &str, username: &str, p: PathBuf) {
let key: String = Self::make_mapkey(host, username);
self.hosts.insert(key, p);
}
}
#[cfg(test)]
@ -100,7 +110,7 @@ mod tests {
#[test]
fn test_system_sshkey_storage_new() {
let tmp_dir: tempfile::TempDir = create_tmp_dir();
let tmp_dir: tempfile::TempDir = tempfile::TempDir::new().ok().unwrap();
let (cfg_path, key_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
let mut client: ConfigClient = ConfigClient::new(cfg_path.as_path(), key_path.as_path())
.ok()
@ -128,6 +138,16 @@ mod tests {
assert_eq!(storage.hosts.len(), 0);
}
#[test]
fn test_system_sshkey_storage_add() {
let mut storage: SshKeyStorage = SshKeyStorage::empty();
storage.add_key("deskichup", "veeso", PathBuf::from("/tmp/omar"));
assert_eq!(
*storage.resolve("deskichup", "veeso").unwrap(),
PathBuf::from("/tmp/omar")
);
}
/// ### get_paths
///
/// Get paths for configuration and keys directory
@ -138,11 +158,4 @@ mod tests {
c.push("config.toml");
(c, k)
}
/// ### create_tmp_dir
///
/// Create temporary directory
fn create_tmp_dir() -> tempfile::TempDir {
tempfile::TempDir::new().ok().unwrap()
}
}

View file

@ -68,4 +68,16 @@ impl AuthActivity {
pub(super) fn is_port_standard(port: u16) -> bool {
port < 1024
}
/// ### check_minimum_window_size
///
/// Check minimum window size window
pub(super) fn check_minimum_window_size(&mut self, height: u16) {
if height < 25 {
// Mount window error
self.mount_size_err();
} else {
self.umount_size_err();
}
}
}

View file

@ -43,6 +43,7 @@ use crate::ui::context::FileTransferParams;
use crate::utils::git;
// Includes
use crossterm::event::Event;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use tuirealm::{Update, View};
@ -53,6 +54,7 @@ const COMPONENT_TEXT_NEW_VERSION: &str = "TEXT_NEW_VERSION";
const COMPONENT_TEXT_FOOTER: &str = "TEXT_FOOTER";
const COMPONENT_TEXT_HELP: &str = "TEXT_HELP";
const COMPONENT_TEXT_ERROR: &str = "TEXT_ERROR";
const COMPONENT_TEXT_SIZE_ERR: &str = "TEXT_SIZE_ERR";
const COMPONENT_INPUT_ADDR: &str = "INPUT_ADDRESS";
const COMPONENT_INPUT_PORT: &str = "INPUT_PORT";
const COMPONENT_INPUT_USERNAME: &str = "INPUT_USERNAME";
@ -199,6 +201,10 @@ impl Activity for AuthActivity {
if let Ok(Some(event)) = self.context.as_ref().unwrap().input_hnd.read_event() {
// Set redraw to true
self.redraw = true;
// Handle on resize
if let Event::Resize(_, h) = event {
self.check_minimum_window_size(h);
}
// Handle event on view and update
let msg = self.view.on(event);
self.update(msg);

View file

@ -32,7 +32,7 @@ use super::{
COMPONENT_INPUT_PORT, COMPONENT_INPUT_USERNAME, COMPONENT_RADIO_BOOKMARK_DEL_BOOKMARK,
COMPONENT_RADIO_BOOKMARK_DEL_RECENT, COMPONENT_RADIO_BOOKMARK_SAVE_PWD,
COMPONENT_RADIO_PROTOCOL, COMPONENT_RADIO_QUIT, COMPONENT_RECENTS_LIST, COMPONENT_TEXT_ERROR,
COMPONENT_TEXT_HELP,
COMPONENT_TEXT_HELP, COMPONENT_TEXT_SIZE_ERR,
};
use crate::ui::keymap::*;
use tuirealm::components::InputPropsBuilder;
@ -306,6 +306,8 @@ impl Update for AuthActivity {
self.umount_quit();
None
}
// -- text size error; block everything
(COMPONENT_TEXT_SIZE_ERR, _) => None,
// On submit on any unhandled (connect)
(_, Msg::OnSubmit(_)) | (_, &MSG_KEY_ENTER) => {
// Match <ENTER> key for all other components

View file

@ -37,8 +37,8 @@ use tuirealm::components::{
input::{Input, InputPropsBuilder},
label::{Label, LabelPropsBuilder},
radio::{Radio, RadioPropsBuilder},
scrolltable::{ScrollTablePropsBuilder, Scrolltable},
span::{Span, SpanPropsBuilder},
table::{Table, TablePropsBuilder},
};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
@ -234,14 +234,17 @@ impl AuthActivity {
pub(super) fn view(&mut self) {
let mut ctx: Context = self.context.take().unwrap();
let _ = ctx.terminal.draw(|f| {
// Check window size
let height: u16 = f.size().height;
self.check_minimum_window_size(height);
// Prepare chunks
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Percentage(70), // Auth Form
Constraint::Percentage(30), // Bookmarks
Constraint::Length(21), // Auth Form
Constraint::Min(3), // Bookmarks
]
.as_ref(),
)
@ -303,6 +306,14 @@ impl AuthActivity {
self.view.render(super::COMPONENT_TEXT_ERROR, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_TEXT_SIZE_ERR) {
if props.visible {
let popup = draw_area_in(f.size(), 80, 20);
f.render_widget(Clear, popup);
// make popup
self.view.render(super::COMPONENT_TEXT_SIZE_ERR, f, popup);
}
}
if let Some(props) = self.view.get_props(super::COMPONENT_RADIO_QUIT) {
if props.visible {
// make popup
@ -478,6 +489,38 @@ impl AuthActivity {
self.view.umount(super::COMPONENT_TEXT_ERROR);
}
/// ### mount_size_err
///
/// Mount size error
pub(super) fn mount_size_err(&mut self) {
// Mount
self.view.mount(
super::COMPONENT_TEXT_SIZE_ERR,
Box::new(MsgBox::new(
MsgBoxPropsBuilder::default()
.with_foreground(Color::Red)
.with_borders(Borders::ALL, BorderType::Thick, Color::Red)
.bold()
.with_texts(
None,
vec![TextSpan::from(
"termscp requires at least 24 lines of height to run",
)],
)
.build(),
)),
);
// Give focus to error
self.view.active(super::COMPONENT_TEXT_SIZE_ERR);
}
/// ### umount_size_err
///
/// Umount error size error
pub(super) fn umount_size_err(&mut self) {
self.view.umount(super::COMPONENT_TEXT_SIZE_ERR);
}
/// ### mount_quit
///
/// Mount quit popup
@ -622,9 +665,12 @@ impl AuthActivity {
pub(super) fn mount_help(&mut self) {
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(Table::new(
TablePropsBuilder::default()
Box::new(Scrolltable::new(
ScrollTablePropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_highlighted_str(Some("?"))
.with_max_scroll_step(8)
.bold()
.with_table(
Some(String::from("Help")),
TableBuilder::default()

View file

@ -27,8 +27,9 @@
*/
extern crate tempfile;
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use crate::filetransfer::FileTransferErrorType;
use crate::fs::FsFile;
use std::path::{Path, PathBuf};
impl FileTransferActivity {
@ -66,7 +67,7 @@ impl FileTransferActivity {
match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => {
let dest_path: PathBuf = PathBuf::from(input);
self.remote_copy_file(&entry, dest_path.as_path());
self.remote_copy_file(entry, dest_path.as_path());
// Reload entries
self.reload_remote_dir();
}
@ -74,7 +75,7 @@ impl FileTransferActivity {
// Try to copy each file to Input/{FILE_NAME}
let base_path: PathBuf = PathBuf::from(input);
// Iter files
for entry in entries.iter() {
for entry in entries.into_iter() {
let mut dest_path: PathBuf = base_path.clone();
dest_path.push(entry.get_name());
self.remote_copy_file(entry, dest_path.as_path());
@ -110,8 +111,8 @@ impl FileTransferActivity {
}
}
fn remote_copy_file(&mut self, entry: &FsEntry, dest: &Path) {
match self.client.as_mut().copy(entry, dest) {
fn remote_copy_file(&mut self, entry: FsEntry, dest: &Path) {
match self.client.as_mut().copy(&entry, dest) {
Ok(_) => {
self.log(
LogLevel::Info,
@ -139,4 +140,123 @@ impl FileTransferActivity {
},
}
}
/// ### tricky_copy
///
/// Tricky copy will be used whenever copy command is not available on remote host
fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) {
// match entry
match entry {
FsEntry::File(entry) => {
// Create tempfile
let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() {
Ok(f) => f,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: could not create temporary file: {}", err),
);
return;
}
};
// Download file
let name = entry.name.clone();
let entry_path = entry.abs_path.clone();
if let Err(err) =
self.filetransfer_recv(TransferPayload::File(entry), tmpfile.path(), Some(name))
{
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: could not download to temporary file: {}", err),
);
return;
}
// Get local fs entry
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) {
Ok(e) => e.unwrap_file(),
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Copy failed: could not stat \"{}\": {}",
tmpfile.path().display(),
err
),
);
return;
}
};
// Upload file to destination
let wrkdir = self.remote().wrkdir.clone();
if let Err(err) = self.filetransfer_send(
TransferPayload::File(tmpfile_entry),
wrkdir.as_path(),
Some(String::from(dest.to_string_lossy())),
) {
self.log_and_alert(
LogLevel::Error,
format!(
"Copy failed: could not write file {}: {}",
entry_path.display(),
err
),
);
return;
}
}
FsEntry::Directory(_) => {
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
Ok(d) => d,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: could not create temporary directory: {}", err),
);
return;
}
};
// Get path of dest
let mut tempdir_path: PathBuf = tempdir.path().to_path_buf();
tempdir_path.push(entry.get_name());
// Download file
if let Err(err) =
self.filetransfer_recv(TransferPayload::Any(entry), tempdir.path(), None)
{
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: failed to download file: {}", err),
);
return;
}
// Stat dir
let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) {
Ok(e) => e,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Copy failed: could not stat \"{}\": {}",
tempdir.path().display(),
err
),
);
return;
}
};
// Upload to destination
let wrkdir: PathBuf = self.remote().wrkdir.clone();
if let Err(err) = self.filetransfer_send(
TransferPayload::Any(tempdir_entry),
wrkdir.as_path(),
Some(String::from(dest.to_string_lossy())),
) {
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: failed to send file: {}", err),
);
return;
}
}
}
}
}

View file

@ -26,7 +26,14 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use crate::fs::FsFile;
// ext
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::fs::OpenOptions;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
impl FileTransferActivity {
pub(crate) fn action_edit_local_file(&mut self) {
@ -60,15 +67,15 @@ impl FileTransferActivity {
SelectedEntry::None => vec![],
};
// Edit all entries
for entry in entries.iter() {
for entry in entries.into_iter() {
// Check if file
if let FsEntry::File(file) = entry {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"...", entry.get_abs_path().display()),
format!("Opening file \"{}\"...", file.abs_path.display()),
);
// Edit file
if let Err(err) = self.edit_remote_file(&file) {
if let Err(err) = self.edit_remote_file(file) {
self.log_and_alert(LogLevel::Error, err);
}
}
@ -76,4 +83,150 @@ impl FileTransferActivity {
// Reload entries
self.reload_remote_dir();
}
/// ### edit_local_file
///
/// Edit a file on localhost
fn edit_local_file(&mut self, path: &Path) -> Result<(), String> {
// Read first 2048 bytes or less from file to check if it is textual
match OpenOptions::new().read(true).open(path) {
Ok(mut f) => {
// Read
let mut buff: [u8; 2048] = [0; 2048];
match f.read(&mut buff) {
Ok(size) => {
if content_inspector::inspect(&buff[0..size]).is_binary() {
return Err("Could not open file in editor: file is binary".to_string());
}
}
Err(err) => {
return Err(format!("Could not read file: {}", err));
}
}
}
Err(err) => {
return Err(format!("Could not read file: {}", err));
}
}
// Put input mode back to normal
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
#[cfg(not(target_os = "windows"))]
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
}
// Open editor
match edit::edit_file(path) {
Ok(_) => self.log(
LogLevel::Info,
format!(
"Changes performed through editor saved to \"{}\"!",
path.display()
),
),
Err(err) => return Err(format!("Could not open editor: {}", err)),
}
#[cfg(not(target_os = "windows"))]
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();
// Enter alternate mode
ctx.enter_alternate_screen();
}
// Re-enable raw mode
let _ = enable_raw_mode();
Ok(())
}
/// ### edit_remote_file
///
/// Edit file on remote host
fn edit_remote_file(&mut self, file: FsFile) -> Result<(), String> {
// Create temp file
let tmpfile: PathBuf = match self.download_file_as_temp(&file) {
Ok(p) => p,
Err(err) => return Err(err),
};
// Download file
let file_name = file.name.clone();
let file_path = file.abs_path.clone();
if let Err(err) = self.filetransfer_recv(
TransferPayload::File(file),
tmpfile.as_path(),
Some(file_name.clone()),
) {
return Err(format!("Could not open file {}: {}", file_name, err));
}
// Get current file modification time
let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e.get_last_change_time(),
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
}
};
// Edit file
if let Err(err) = self.edit_local_file(tmpfile.as_path()) {
return Err(err);
}
// Get local fs entry
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e,
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
}
};
// Check if file has changed
match prev_mtime != tmpfile_entry.get_last_change_time() {
true => {
self.log(
LogLevel::Info,
format!(
"File \"{}\" has changed; writing changes to remote",
file_path.display()
),
);
// Get local fs entry
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e.unwrap_file(),
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
}
};
// Send file
let wrkdir = self.remote().wrkdir.clone();
if let Err(err) = self.filetransfer_send(
TransferPayload::File(tmpfile_entry),
wrkdir.as_path(),
Some(file_name),
) {
return Err(format!(
"Could not write file {}: {}",
file_path.display(),
err
));
}
}
false => {
self.log(
LogLevel::Info,
format!("File \"{}\" hasn't changed", file_path.display()),
);
}
}
Ok(())
}
}

View file

@ -27,7 +27,7 @@
*/
// locals
use super::super::browser::FileExplorerTab;
use super::{FileTransferActivity, FsEntry, SelectedEntry};
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use std::path::PathBuf;
@ -77,10 +77,30 @@ impl FileTransferActivity {
match self.get_found_selected_entries() {
SelectedEntry::One(entry) => match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), save_as);
if let Err(err) = self.filetransfer_send(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
) {
self.log_and_alert(
LogLevel::Error,
format!("Could not upload file: {}", err),
);
return;
}
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
self.filetransfer_recv(&entry.get_realfile(), wrkdir.as_path(), save_as);
if let Err(err) = self.filetransfer_recv(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
) {
self.log_and_alert(
LogLevel::Error,
format!("Could not download file: {}", err),
);
return;
}
}
},
SelectedEntry::Many(entries) => {
@ -90,21 +110,34 @@ impl FileTransferActivity {
dest_path.push(save_as);
}
// Iter files
for entry in entries.iter() {
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.filetransfer_send(
&entry.get_realfile(),
dest_path.as_path(),
None,
);
let entries = entries.iter().map(|x| x.get_realfile()).collect();
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
if let Err(err) = self.filetransfer_send(
TransferPayload::Many(entries),
dest_path.as_path(),
None,
) {
{
self.log_and_alert(
LogLevel::Error,
format!("Could not upload file: {}", err),
);
return;
}
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
self.filetransfer_recv(
&entry.get_realfile(),
dest_path.as_path(),
None,
}
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
if let Err(err) = self.filetransfer_recv(
TransferPayload::Many(entries),
dest_path.as_path(),
None,
) {
self.log_and_alert(
LogLevel::Error,
format!("Could not download file: {}", err),
);
return;
}
}
}

View file

@ -25,7 +25,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
pub(self) use super::{FileTransferActivity, FsEntry, LogLevel};
pub(self) use super::{FileTransferActivity, FsEntry, LogLevel, TransferPayload};
use tuirealm::{Payload, Value};
// actions
@ -82,8 +82,7 @@ impl FileTransferActivity {
let files: Vec<&FsEntry> = files
.iter()
.map(|x| self.local().get(*x)) // Usize to Option<FsEntry>
.filter(|x| x.is_some()) // Get only some values
.map(|x| x.unwrap()) // Option to FsEntry
.flatten()
.collect();
SelectedEntry::from(files)
}
@ -101,8 +100,7 @@ impl FileTransferActivity {
let files: Vec<&FsEntry> = files
.iter()
.map(|x| self.remote().get(*x)) // Usize to Option<FsEntry>
.filter(|x| x.is_some()) // Get only some values
.map(|x| x.unwrap()) // Option to FsEntry
.flatten()
.collect();
SelectedEntry::from(files)
}
@ -122,8 +120,7 @@ impl FileTransferActivity {
let files: Vec<&FsEntry> = files
.iter()
.map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option<FsEntry>
.filter(|x| x.is_some()) // Get only some values
.map(|x| x.unwrap()) // Option to FsEntry
.flatten()
.collect();
SelectedEntry::from(files)
}

View file

@ -28,7 +28,7 @@
// deps
extern crate open;
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
// ext
use std::path::{Path, PathBuf};
@ -90,12 +90,25 @@ impl FileTransferActivity {
}
Some(p) => p.path().to_path_buf(),
};
self.filetransfer_recv(&entry, cache.as_path(), Some(tmpfile.clone()));
// Make file and open if file exists
let mut tmp: PathBuf = cache;
tmp.push(tmpfile.as_str());
if tmp.exists() {
self.open_path_with(tmp.as_path(), open_with);
match self.filetransfer_recv(
TransferPayload::Any(entry),
cache.as_path(),
Some(tmpfile.clone()),
) {
Ok(_) => {
// Make file and open if file exists
let mut tmp: PathBuf = cache;
tmp.push(tmpfile.as_str());
if tmp.exists() {
self.open_path_with(tmp.as_path(), open_with);
}
}
Err(err) => {
self.log(
LogLevel::Error,
format!("Failed to download remote entry: {}", err),
);
}
}
}

View file

@ -26,7 +26,7 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, SelectedEntry};
use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
use std::path::PathBuf;
impl FileTransferActivity {
@ -50,7 +50,19 @@ impl FileTransferActivity {
let wrkdir: PathBuf = self.remote().wrkdir.clone();
match self.get_local_selected_entries() {
SelectedEntry::One(entry) => {
self.filetransfer_send(&entry.get_realfile(), wrkdir.as_path(), save_as);
if let Err(err) = self.filetransfer_send(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
) {
{
self.log_and_alert(
LogLevel::Error,
format!("Could not upload file: {}", err),
);
return;
}
}
}
SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input
@ -59,8 +71,19 @@ impl FileTransferActivity {
dest_path.push(save_as);
}
// Iter files
for entry in entries.iter() {
self.filetransfer_send(&entry.get_realfile(), dest_path.as_path(), None);
let entries = entries.iter().map(|x| x.get_realfile()).collect();
if let Err(err) = self.filetransfer_send(
TransferPayload::Many(entries),
dest_path.as_path(),
None,
) {
{
self.log_and_alert(
LogLevel::Error,
format!("Could not upload file: {}", err),
);
return;
}
}
}
SelectedEntry::None => {}
@ -71,7 +94,19 @@ impl FileTransferActivity {
let wrkdir: PathBuf = self.local().wrkdir.clone();
match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => {
self.filetransfer_recv(&entry.get_realfile(), wrkdir.as_path(), save_as);
if let Err(err) = self.filetransfer_recv(
TransferPayload::Any(entry.get_realfile()),
wrkdir.as_path(),
save_as,
) {
{
self.log_and_alert(
LogLevel::Error,
format!("Could not download file: {}", err),
);
return;
}
}
}
SelectedEntry::Many(entries) => {
// In case of selection: save multiple files in wrkdir/input
@ -80,8 +115,19 @@ impl FileTransferActivity {
dest_path.push(save_as);
}
// Iter files
for entry in entries.iter() {
self.filetransfer_recv(&entry.get_realfile(), dest_path.as_path(), None);
let entries = entries.iter().map(|x| x.get_realfile()).collect();
if let Err(err) = self.filetransfer_recv(
TransferPayload::Many(entries),
dest_path.as_path(),
None,
) {
{
self.log_and_alert(
LogLevel::Error,
format!("Could not download file: {}", err),
);
return;
}
}
}
SelectedEntry::None => {}

View file

@ -49,9 +49,10 @@ use crate::fs::explorer::FileExplorer;
use crate::fs::FsEntry;
use crate::host::Localhost;
use crate::system::config_client::ConfigClient;
pub(crate) use lib::browser;
pub(self) use lib::browser;
use lib::browser::Browser;
use lib::transfer::TransferStates;
pub(self) use session::TransferPayload;
// Includes
use chrono::{DateTime, Local};
@ -89,7 +90,8 @@ const COMPONENT_RADIO_DELETE: &str = "RADIO_DELETE";
const COMPONENT_RADIO_DISCONNECT: &str = "RADIO_DISCONNECT";
const COMPONENT_RADIO_QUIT: &str = "RADIO_QUIT";
const COMPONENT_RADIO_SORTING: &str = "RADIO_SORTING";
const COMPONENT_SPAN_STATUS_BAR: &str = "STATUS_BAR";
const COMPONENT_SPAN_STATUS_BAR_LOCAL: &str = "STATUS_BAR_LOCAL";
const COMPONENT_SPAN_STATUS_BAR_REMOTE: &str = "STATUS_BAR_REMOTE";
const COMPONENT_LIST_FILEINFO: &str = "LIST_FILEINFO";
/// ## LogLevel

View file

@ -40,11 +40,9 @@ use crate::utils::fmt::fmt_millis;
// Ext
use bytesize::ByteSize;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::fs::OpenOptions;
use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use std::time::{Instant, SystemTime};
use std::time::Instant;
use thiserror::Error;
/// ## TransferErrorReason
@ -66,6 +64,19 @@ enum TransferErrorReason {
FileTransferError(FileTransferError),
}
/// ## TransferPayload
///
/// Represents the entity to send or receive during a transfer.
/// - File: describes an individual `FsFile` to send
/// - Any: Can be any kind of `FsEntry`, but just one
/// - Many: a list of `FsEntry`
#[derive(Debug)]
pub(super) enum TransferPayload {
File(FsFile),
Any(FsEntry),
Many(Vec<FsEntry>),
}
impl FileTransferActivity {
/// ### connect
///
@ -106,6 +117,7 @@ impl FileTransferActivity {
}
Err(err) => {
// Set popup fatal error
self.umount_wait();
self.mount_fatal(&err.to_string());
}
}
@ -196,11 +208,66 @@ impl FileTransferActivity {
/// If dst_name is Some, entry will be saved with a different name.
/// If entry is a directory, this applies to directory only
pub(super) fn filetransfer_send(
&mut self,
payload: TransferPayload,
curr_remote_path: &Path,
dst_name: Option<String>,
) -> Result<(), String> {
// Use different method based on payload
match payload {
TransferPayload::Any(entry) => {
self.filetransfer_send_any(&entry, curr_remote_path, dst_name)
}
TransferPayload::File(file) => {
self.filetransfer_send_file(&file, curr_remote_path, dst_name)
}
TransferPayload::Many(entries) => {
self.filetransfer_send_many(entries, curr_remote_path)
}
}
}
/// ### filetransfer_send_file
///
/// Send one file to remote at specified path.
fn filetransfer_send_file(
&mut self,
file: &FsFile,
curr_remote_path: &Path,
dst_name: Option<String>,
) -> Result<(), String> {
// Reset states
self.transfer.reset();
// Calculate total size of transfer
let total_transfer_size: usize = file.size;
self.transfer.full.init(total_transfer_size);
// Mount progress bar
self.mount_progress_bar(format!("Uploading {}...", file.abs_path.display()));
// Get remote path
let file_name: String = file.name.clone();
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
let remote_file_name: PathBuf = match dst_name {
Some(s) => PathBuf::from(s.as_str()),
None => PathBuf::from(file_name.as_str()),
};
remote_path.push(remote_file_name);
// Send
let result = self.filetransfer_send_one(file, remote_path.as_path(), file_name);
// Umount progress bar
self.umount_progress_bar();
// Return result
result.map_err(|x| x.to_string())
}
/// ### filetransfer_send_any
///
/// Send a `TransferPayload` of type `Any`
fn filetransfer_send_any(
&mut self,
entry: &FsEntry,
curr_remote_path: &Path,
dst_name: Option<String>,
) {
) -> Result<(), String> {
// Reset states
self.transfer.reset();
// Calculate total size of transfer
@ -212,6 +279,34 @@ impl FileTransferActivity {
self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
// Umount progress bar
self.umount_progress_bar();
Ok(())
}
/// ### filetransfer_send_many
///
/// Send many entries to remote
fn filetransfer_send_many(
&mut self,
entries: Vec<FsEntry>,
curr_remote_path: &Path,
) -> Result<(), String> {
// Reset states
self.transfer.reset();
// Calculate total size of transfer
let total_transfer_size: usize = entries
.iter()
.map(|x| self.get_total_transfer_size_local(x))
.sum();
self.transfer.full.init(total_transfer_size);
// Mount progress bar
self.mount_progress_bar(format!("Uploading {} entries...", entries.len()));
// Send recurse
entries
.iter()
.for_each(|x| self.filetransfer_send_recurse(x, curr_remote_path, None));
// Umount progress bar
self.umount_progress_bar();
Ok(())
}
fn filetransfer_send_recurse(
@ -235,8 +330,7 @@ impl FileTransferActivity {
// Match entry
match entry {
FsEntry::File(file) => {
if let Err(err) =
self.filetransfer_send_file(file, remote_path.as_path(), file_name)
if let Err(err) = self.filetransfer_send_one(file, remote_path.as_path(), file_name)
{
// Log error
self.log_and_alert(
@ -339,7 +433,7 @@ impl FileTransferActivity {
/// ### filetransfer_send_file
///
/// Send local file and write it to remote path
fn filetransfer_send_file(
fn filetransfer_send_one(
&mut self,
local: &FsFile,
remote: &Path,
@ -448,11 +542,29 @@ impl FileTransferActivity {
/// If dst_name is Some, entry will be saved with a different name.
/// If entry is a directory, this applies to directory only
pub(super) fn filetransfer_recv(
&mut self,
payload: TransferPayload,
local_path: &Path,
dst_name: Option<String>,
) -> Result<(), String> {
match payload {
TransferPayload::Any(entry) => self.filetransfer_recv_any(&entry, local_path, dst_name),
TransferPayload::File(file) => self.filetransfer_recv_file(&file, local_path),
TransferPayload::Many(entries) => self.filetransfer_recv_many(entries, local_path),
}
}
/// ### filetransfer_recv_any
///
/// Recv fs entry from remote.
/// If dst_name is Some, entry will be saved with a different name.
/// If entry is a directory, this applies to directory only
fn filetransfer_recv_any(
&mut self,
entry: &FsEntry,
local_path: &Path,
dst_name: Option<String>,
) {
) -> Result<(), String> {
// Reset states
self.transfer.reset();
// Calculate total transfer size
@ -464,6 +576,53 @@ impl FileTransferActivity {
self.filetransfer_recv_recurse(entry, local_path, dst_name);
// Umount progress bar
self.umount_progress_bar();
Ok(())
}
/// ### filetransfer_recv_file
///
/// Receive a single file from remote.
fn filetransfer_recv_file(&mut self, entry: &FsFile, local_path: &Path) -> Result<(), String> {
// Reset states
self.transfer.reset();
// Calculate total transfer size
let total_transfer_size: usize = entry.size;
self.transfer.full.init(total_transfer_size);
// Mount progress bar
self.mount_progress_bar(format!("Downloading {}...", entry.abs_path.display()));
// Receive
let result = self.filetransfer_recv_one(local_path, entry, entry.name.clone());
// Umount progress bar
self.umount_progress_bar();
// Return result
result.map_err(|x| x.to_string())
}
/// ### filetransfer_send_many
///
/// Send many entries to remote
fn filetransfer_recv_many(
&mut self,
entries: Vec<FsEntry>,
curr_remote_path: &Path,
) -> Result<(), String> {
// Reset states
self.transfer.reset();
// Calculate total size of transfer
let total_transfer_size: usize = entries
.iter()
.map(|x| self.get_total_transfer_size_remote(x))
.sum();
self.transfer.full.init(total_transfer_size);
// Mount progress bar
self.mount_progress_bar(format!("Downloading {} entries...", entries.len()));
// Send recurse
entries
.iter()
.for_each(|x| self.filetransfer_recv_recurse(x, curr_remote_path, None));
// Umount progress bar
self.umount_progress_bar();
Ok(())
}
fn filetransfer_recv_recurse(
@ -489,7 +648,7 @@ impl FileTransferActivity {
local_file_path.push(local_file_name.as_str());
// Download file
if let Err(err) =
self.filetransfer_recv_file(local_file_path.as_path(), file, file_name)
self.filetransfer_recv_one(local_file_path.as_path(), file, file_name)
{
self.log_and_alert(
LogLevel::Error,
@ -537,7 +696,11 @@ impl FileTransferActivity {
match self.host.mkdir_ex(local_dir_path.as_path(), true) {
Ok(_) => {
// Apply file mode to directory
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(any(
target_family = "unix",
target_os = "macos",
target_os = "linux"
))]
if let Some(pex) = dir.unix_pex {
if let Err(err) = self.host.chmod(local_dir_path.as_path(), pex) {
self.log(
@ -613,10 +776,10 @@ impl FileTransferActivity {
}
}
/// ### filetransfer_recv_file
/// ### filetransfer_recv_one
///
/// Receive file from remote and write it to local path
fn filetransfer_recv_file(
fn filetransfer_recv_one(
&mut self,
local: &Path,
remote: &FsFile,
@ -694,7 +857,11 @@ impl FileTransferActivity {
return Err(TransferErrorReason::Abrupted);
}
// Apply file mode to file
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(any(
target_family = "unix",
target_os = "macos",
target_os = "linux"
))]
if let Some(pex) = remote.unix_pex {
if let Err(err) = self.host.chmod(local, pex) {
self.log(
@ -785,251 +952,6 @@ impl FileTransferActivity {
}
}
/// ### edit_local_file
///
/// Edit a file on localhost
pub(super) fn edit_local_file(&mut self, path: &Path) -> Result<(), String> {
// Read first 2048 bytes or less from file to check if it is textual
match OpenOptions::new().read(true).open(path) {
Ok(mut f) => {
// Read
let mut buff: [u8; 2048] = [0; 2048];
match f.read(&mut buff) {
Ok(size) => {
if content_inspector::inspect(&buff[0..size]).is_binary() {
return Err("Could not open file in editor: file is binary".to_string());
}
}
Err(err) => {
return Err(format!("Could not read file: {}", err));
}
}
}
Err(err) => {
return Err(format!("Could not read file: {}", err));
}
}
debug!("Ok, file {} is textual; opening file...", path.display());
// Put input mode back to normal
if let Err(err) = disable_raw_mode() {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
}
// Open editor
match edit::edit_file(path) {
Ok(_) => self.log(
LogLevel::Info,
format!(
"Changes performed through editor saved to \"{}\"!",
path.display()
),
),
Err(err) => return Err(format!("Could not open editor: {}", err)),
}
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();
// Enter alternate mode
ctx.enter_alternate_screen();
}
// Re-enable raw mode
let _ = enable_raw_mode();
Ok(())
}
/// ### edit_remote_file
///
/// Edit file on remote host
pub(super) fn edit_remote_file(&mut self, file: &FsFile) -> Result<(), String> {
// Create temp file
let tmpfile: PathBuf = match self.download_file_as_temp(file) {
Ok(p) => p,
Err(err) => return Err(err),
};
// Get current file modification time
let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e.get_last_change_time(),
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
}
};
// Edit file
if let Err(err) = self.edit_local_file(tmpfile.as_path()) {
return Err(err);
}
// Get local fs entry
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e,
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
}
};
// Check if file has changed
match prev_mtime != tmpfile_entry.get_last_change_time() {
true => {
self.log(
LogLevel::Info,
format!(
"File \"{}\" has changed; writing changes to remote",
file.abs_path.display()
),
);
// Get local fs entry
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e,
Err(err) => {
return Err(format!(
"Could not stat \"{}\": {}",
tmpfile.as_path().display(),
err
))
}
};
// Write file
let tmpfile_entry: &FsFile = match &tmpfile_entry {
FsEntry::Directory(_) => panic!("tempfile is a directory for some reason"),
FsEntry::File(f) => f,
};
// Send file
if let Err(err) = self.filetransfer_send_file(
tmpfile_entry,
file.abs_path.as_path(),
file.name.clone(),
) {
return Err(format!(
"Could not write file {}: {}",
file.abs_path.display(),
err
));
}
}
false => {
self.log(
LogLevel::Info,
format!("File \"{}\" hasn't changed", file.abs_path.display()),
);
}
}
Ok(())
}
/// ### tricky_copy
///
/// Tricky copy will be used whenever copy command is not available on remote host
pub(super) fn tricky_copy(&mut self, entry: &FsEntry, dest: &Path) {
// match entry
match entry {
FsEntry::File(entry) => {
// Create tempfile
let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() {
Ok(f) => f,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: could not create temporary file: {}", err),
);
return;
}
};
// Download file
if let Err(err) =
self.filetransfer_recv_file(tmpfile.path(), entry, entry.name.clone())
{
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: could not download to temporary file: {}", err),
);
return;
}
// Get local fs entry
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.path()) {
Ok(e) => e,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Copy failed: could not stat \"{}\": {}",
tmpfile.path().display(),
err
),
);
return;
}
};
let tmpfile_entry = match &tmpfile_entry {
FsEntry::Directory(_) => panic!("tempfile is a directory for some reason"),
FsEntry::File(f) => f,
};
// Upload file to destination
if let Err(err) = self.filetransfer_send_file(
tmpfile_entry,
dest,
String::from(dest.to_string_lossy()),
) {
self.log_and_alert(
LogLevel::Error,
format!(
"Copy failed: could not write file {}: {}",
entry.abs_path.display(),
err
),
);
return;
}
}
FsEntry::Directory(_) => {
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
Ok(d) => d,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!("Copy failed: could not create temporary directory: {}", err),
);
return;
}
};
// Download file
self.filetransfer_recv(entry, tempdir.path(), None);
// Get path of dest
let mut tempdir_path: PathBuf = tempdir.path().to_path_buf();
tempdir_path.push(entry.get_name());
// Stat dir
let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) {
Ok(e) => e,
Err(err) => {
self.log_and_alert(
LogLevel::Error,
format!(
"Copy failed: could not stat \"{}\": {}",
tempdir.path().display(),
err
),
);
return;
}
};
// Upload to destination
let wrkdir: PathBuf = self.remote().wrkdir.clone();
self.filetransfer_send(
&tempdir_entry,
wrkdir.as_path(),
Some(String::from(dest.to_string_lossy())),
);
}
}
}
/// ### download_file_as_temp
///
/// Download provided file as a temporary file
@ -1047,7 +969,11 @@ impl FileTransferActivity {
}
};
// Download file
match self.filetransfer_recv_file(tmpfile.as_path(), file, file.name.clone()) {
match self.filetransfer_recv(
TransferPayload::File(file.clone()),
tmpfile.as_path(),
Some(file.name.clone()),
) {
Err(err) => Err(format!(
"Could not download {} to temporary file: {}",
file.abs_path.display(),

View file

@ -107,6 +107,8 @@ impl Update for FileTransferActivity {
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_CHAR_A) => {
// Toggle hidden files
self.local_mut().toggle_hidden_files();
// Update status bar
self.refresh_local_status_bar();
// Reload file list component
self.update_local_filelist()
}
@ -179,6 +181,8 @@ impl Update for FileTransferActivity {
(COMPONENT_EXPLORER_REMOTE, &MSG_KEY_CHAR_A) => {
// Toggle hidden files
self.remote_mut().toggle_hidden_files();
// Update status bar
self.refresh_remote_status_bar();
// Reload file list component
self.update_remote_filelist()
}
@ -295,7 +299,7 @@ impl Update for FileTransferActivity {
// Toggle browser sync
self.browser.toggle_sync_browsing();
// Update status bar
self.refresh_status_bar();
self.refresh_remote_status_bar();
None
}
(COMPONENT_EXPLORER_LOCAL, &MSG_KEY_ESC)
@ -652,7 +656,11 @@ impl Update for FileTransferActivity {
_ => panic!("Found result doesn't support SORTING"),
}
// Update status bar
self.refresh_status_bar();
match self.browser.tab() {
FileExplorerTab::Local => self.refresh_local_status_bar(),
FileExplorerTab::Remote => self.refresh_remote_status_bar(),
_ => panic!("Found result doesn't support SORTING"),
};
// Reload files
match self.browser.tab() {
FileExplorerTab::Local => self.update_local_filelist(),

View file

@ -28,7 +28,7 @@
// Deps
extern crate bytesize;
extern crate hostname;
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
extern crate users;
// locals
use super::{browser::FileExplorerTab, Context, FileTransferActivity};
@ -49,6 +49,7 @@ use tuirealm::components::{
input::{Input, InputPropsBuilder},
progress_bar::{ProgressBar, ProgressBarPropsBuilder},
radio::{Radio, RadioPropsBuilder},
scrolltable::{ScrollTablePropsBuilder, Scrolltable},
span::{Span, SpanPropsBuilder},
table::{Table, TablePropsBuilder},
};
@ -58,7 +59,7 @@ use tuirealm::tui::{
style::Color,
widgets::{BorderType, Borders, Clear},
};
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
use users::{get_group_by_gid, get_user_by_uid};
impl FileTransferActivity {
@ -99,13 +100,18 @@ impl FileTransferActivity {
.build(),
)),
);
// Mount status bar
// Mount status bars
self.view.mount(
super::COMPONENT_SPAN_STATUS_BAR,
super::COMPONENT_SPAN_STATUS_BAR_LOCAL,
Box::new(Span::new(SpanPropsBuilder::default().build())),
);
self.view.mount(
super::COMPONENT_SPAN_STATUS_BAR_REMOTE,
Box::new(Span::new(SpanPropsBuilder::default().build())),
);
// Load process bar
self.refresh_status_bar();
self.refresh_local_status_bar();
self.refresh_remote_status_bar();
// Update components
let _ = self.update_local_filelist();
let _ = self.update_remote_filelist();
@ -144,6 +150,12 @@ impl FileTransferActivity {
.constraints([Constraint::Length(1), Constraint::Length(10)].as_ref())
.direction(Direction::Vertical)
.split(chunks[1]);
// Create status bar chunks
let status_bar_chunks = Layout::default()
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.direction(Direction::Horizontal)
.horizontal_margin(1)
.split(bottom_chunks[0]);
// If width is unset in the storage, set width
if !store.isset(super::STORAGE_EXPLORER_WIDTH) {
store.set_unsigned(super::STORAGE_EXPLORER_WIDTH, tabs_chunks[0].width as usize);
@ -169,11 +181,20 @@ impl FileTransferActivity {
.view
.render(super::COMPONENT_EXPLORER_REMOTE, f, tabs_chunks[1]),
}
// Draw log box and status bar
// Draw log box
self.view
.render(super::COMPONENT_LOG_BOX, f, bottom_chunks[1]);
self.view
.render(super::COMPONENT_SPAN_STATUS_BAR, f, bottom_chunks[0]);
// Draw status bar
self.view.render(
super::COMPONENT_SPAN_STATUS_BAR_LOCAL,
f,
status_bar_chunks[0],
);
self.view.render(
super::COMPONENT_SPAN_STATUS_BAR_REMOTE,
f,
status_bar_chunks[1],
);
// @! Draw popups
if let Some(props) = self.view.get_props(super::COMPONENT_INPUT_COPY) {
if props.visible {
@ -817,7 +838,7 @@ impl FileTransferActivity {
.build(),
);
// User
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
let username: String = match file.get_user() {
Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(),
@ -828,7 +849,7 @@ impl FileTransferActivity {
#[cfg(target_os = "windows")]
let username: String = format!("{}", file.get_user().unwrap_or(0));
// Group
#[cfg(any(target_os = "unix", target_os = "macos", target_os = "linux"))]
#[cfg(target_family = "unix")]
let group: String = match file.get_group() {
Some(gid) => match get_group_by_gid(gid) {
Some(group) => group.name().to_string_lossy().to_string(),
@ -864,9 +885,54 @@ impl FileTransferActivity {
self.view.umount(super::COMPONENT_LIST_FILEINFO);
}
pub(super) fn refresh_status_bar(&mut self) {
let bar_spans: Vec<TextSpan> = vec![
TextSpanBuilder::new("Synchronized Browsing: ")
pub(super) fn refresh_local_status_bar(&mut self) {
let local_bar_spans: Vec<TextSpan> = vec![
TextSpanBuilder::new("File sorting: ")
.with_foreground(Color::LightYellow)
.build(),
TextSpanBuilder::new(Self::get_file_sorting_str(self.local().get_file_sorting()))
.with_foreground(Color::LightYellow)
.reversed()
.build(),
TextSpanBuilder::new(" Hidden files: ")
.with_foreground(Color::LightBlue)
.build(),
TextSpanBuilder::new(Self::get_hidden_files_str(
self.local().hidden_files_visible(),
))
.with_foreground(Color::LightBlue)
.reversed()
.build(),
];
if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR_LOCAL) {
self.view.update(
super::COMPONENT_SPAN_STATUS_BAR_LOCAL,
SpanPropsBuilder::from(props)
.with_spans(local_bar_spans)
.build(),
);
}
}
pub(super) fn refresh_remote_status_bar(&mut self) {
let remote_bar_spans: Vec<TextSpan> = vec![
TextSpanBuilder::new("File sorting: ")
.with_foreground(Color::LightYellow)
.build(),
TextSpanBuilder::new(Self::get_file_sorting_str(self.remote().get_file_sorting()))
.with_foreground(Color::LightYellow)
.reversed()
.build(),
TextSpanBuilder::new(" Hidden files: ")
.with_foreground(Color::LightBlue)
.build(),
TextSpanBuilder::new(Self::get_hidden_files_str(
self.remote().hidden_files_visible(),
))
.with_foreground(Color::LightBlue)
.reversed()
.build(),
TextSpanBuilder::new(" Sync Browsing: ")
.with_foreground(Color::LightGreen)
.build(),
TextSpanBuilder::new(match self.browser.sync_browsing {
@ -876,25 +942,13 @@ impl FileTransferActivity {
.with_foreground(Color::LightGreen)
.reversed()
.build(),
TextSpanBuilder::new(" Localhost file sorting: ")
.with_foreground(Color::LightYellow)
.build(),
TextSpanBuilder::new(Self::get_file_sorting_str(self.local().get_file_sorting()))
.with_foreground(Color::LightYellow)
.reversed()
.build(),
TextSpanBuilder::new(" Remote host file sorting: ")
.with_foreground(Color::LightBlue)
.build(),
TextSpanBuilder::new(Self::get_file_sorting_str(self.remote().get_file_sorting()))
.with_foreground(Color::LightBlue)
.reversed()
.build(),
];
if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR) {
if let Some(props) = self.view.get_props(super::COMPONENT_SPAN_STATUS_BAR_REMOTE) {
self.view.update(
super::COMPONENT_SPAN_STATUS_BAR,
SpanPropsBuilder::from(props).with_spans(bar_spans).build(),
super::COMPONENT_SPAN_STATUS_BAR_REMOTE,
SpanPropsBuilder::from(props)
.with_spans(remote_bar_spans)
.build(),
);
}
}
@ -905,9 +959,12 @@ impl FileTransferActivity {
pub(super) fn mount_help(&mut self) {
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(Table::new(
TablePropsBuilder::default()
Box::new(Scrolltable::new(
ScrollTablePropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_highlighted_str(Some("?"))
.with_max_scroll_step(8)
.bold()
.with_table(
Some(String::from("Help")),
TableBuilder::default()
@ -1171,4 +1228,11 @@ impl FileTransferActivity {
FileSorting::BySize => "By size",
}
}
fn get_hidden_files_str(show: bool) -> &'static str {
match show {
true => "Show",
false => "Hide",
}
}
}

View file

@ -115,6 +115,7 @@ impl SetupActivity {
error!("Failed to disable raw mode: {}", err);
}
// Leave alternate mode
#[cfg(not(target_os = "windows"))]
if let Some(ctx) = self.context.as_mut() {
ctx.leave_alternate_screen();
}
@ -149,6 +150,7 @@ impl SetupActivity {
}
}
// Restore terminal
#[cfg(not(target_os = "windows"))]
if let Some(ctx) = self.context.as_mut() {
// Clear screen
ctx.clear_screen();

View file

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

View file

@ -40,8 +40,8 @@ use std::path::PathBuf;
use tuirealm::components::{
input::{Input, InputPropsBuilder},
radio::{Radio, RadioPropsBuilder},
scrolltable::{ScrollTablePropsBuilder, Scrolltable},
span::{Span, SpanPropsBuilder},
table::{Table, TablePropsBuilder},
};
use tuirealm::tui::{
layout::{Constraint, Direction, Layout},
@ -557,9 +557,12 @@ impl SetupActivity {
pub(super) fn mount_help(&mut self) {
self.view.mount(
super::COMPONENT_TEXT_HELP,
Box::new(Table::new(
TablePropsBuilder::default()
Box::new(Scrolltable::new(
ScrollTablePropsBuilder::default()
.with_borders(Borders::ALL, BorderType::Rounded, Color::White)
.with_highlighted_str(Some("?"))
.with_max_scroll_step(8)
.bold()
.with_table(
Some(String::from("Help")),
TableBuilder::default()

View file

@ -103,6 +103,7 @@ impl Context {
/// ### enter_alternate_screen
///
/// Enter alternate screen (gui window)
#[cfg(not(target_os = "windows"))]
pub fn enter_alternate_screen(&mut self) {
match execute!(
self.terminal.backend_mut(),
@ -177,7 +178,7 @@ mod tests {
}
#[test]
#[cfg(not(feature = "githubActions"))]
#[cfg(not(feature = "github-actions"))]
fn test_ui_context() {
// Prepare stuff
let mut ctx: Context = Context::new(None, Some(String::from("alles kaput")));
@ -190,9 +191,12 @@ mod tests {
assert!(ctx.get_error().is_some());
assert!(ctx.get_error().is_none());
// Try other methods
ctx.enter_alternate_screen();
ctx.clear_screen();
ctx.leave_alternate_screen();
#[cfg(not(target_os = "windows"))]
{
ctx.enter_alternate_screen();
ctx.clear_screen();
ctx.leave_alternate_screen();
}
drop(ctx);
}
}

View file

@ -216,7 +216,7 @@ mod tests {
}
#[test]
#[cfg(any(target_os = "unix", target_os = "linux", target_os = "macos"))]
#[cfg(target_family = "unix")]
fn test_utils_fmt_path_elide() {
let p: &Path = &Path::new("/develop/pippo");
// Under max size

View file

@ -80,7 +80,7 @@ mod tests {
use super::*;
#[test]
#[cfg(not(all(target_os = "macos", feature = "githubActions")))]
#[cfg(not(all(target_os = "macos", feature = "github-actions")))]
fn test_utils_git_check_for_updates() {
assert!(check_for_updates("100.0.0").ok().unwrap().is_none());
assert!(check_for_updates("0.0.1").ok().unwrap().is_some());

View file

@ -33,3 +33,7 @@ pub mod git;
pub mod parser;
pub mod random;
pub mod ui;
#[cfg(test)]
#[allow(dead_code)]
pub mod test_helpers;

View file

@ -186,13 +186,10 @@ pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result<SystemT
let this_year: i32 = Utc::now().year();
let date_time_str: String = format!("{} {}", tm, this_year);
// Now parse
match NaiveDateTime::parse_from_str(
NaiveDateTime::parse_from_str(
date_time_str.as_ref(),
format!("{} %Y", fmt_hours).as_ref(),
) {
Ok(dt) => dt,
Err(err) => return Err(err),
}
)?
}
};
// Convert datetime to system time

248
src/utils/test_helpers.rs Normal file
View file

@ -0,0 +1,248 @@
//! ## TestHelpers
//!
//! contains helper functions for tests
/**
* MIT License
*
* termscp - Copyright (c) 2021 Christian Visintin
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use crate::fs::{FsDirectory, FsEntry, FsFile};
// ext
use std::fs::File;
#[cfg(feature = "with-containers")]
use std::fs::OpenOptions;
#[cfg(feature = "with-containers")]
use std::io::Read;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use tempfile::NamedTempFile;
pub fn create_sample_file_entry() -> (FsFile, NamedTempFile) {
// Write
let tmpfile = create_sample_file();
(
FsFile {
name: tmpfile
.path()
.file_name()
.unwrap()
.to_string_lossy()
.to_string(),
abs_path: tmpfile.path().to_path_buf(),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 127,
ftype: None, // File type
readonly: false,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
},
tmpfile,
)
}
pub fn create_sample_file() -> NamedTempFile {
// Write
let mut tmpfile: tempfile::NamedTempFile = tempfile::NamedTempFile::new().unwrap();
writeln!(
tmpfile,
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus."
)
.unwrap();
tmpfile
}
/// ### make_file_at
///
/// Make a file with `name` at specified path
pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> {
let mut p: PathBuf = PathBuf::from(dir);
p.push(filename);
let mut file: File = File::create(p.as_path())?;
writeln!(
file,
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus."
)?;
Ok(())
}
/// ### make_dir_at
///
/// Make a directory in `dir`
pub fn make_dir_at(dir: &Path, dirname: &str) -> std::io::Result<()> {
let mut p: PathBuf = PathBuf::from(dir);
p.push(dirname);
std::fs::create_dir(p.as_path())
}
#[cfg(feature = "with-containers")]
pub fn write_file(file: &NamedTempFile, writable: &mut Box<dyn Write>) {
let mut fhnd = OpenOptions::new()
.create(false)
.read(true)
.write(false)
.open(file.path())
.ok()
.unwrap();
// Read file
let mut buffer: [u8; 65536] = [0; 65536];
assert!(fhnd.read(&mut buffer).is_ok());
// Write file
assert!(writable.write(&buffer).is_ok());
}
#[cfg(feature = "with-containers")]
pub fn write_ssh_key() -> NamedTempFile {
let mut tmpfile: NamedTempFile = NamedTempFile::new().unwrap();
writeln!(
tmpfile,
r"-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAxKyYUMRCNPlb4ZV1VMofrzApu2l3wgP4Ot9wBvHsw/+RMpcHIbQK
9iQqAVp8Z+M1fJyPXTKjoJtIzuCLF6Sjo0KI7/tFTh+yPnA5QYNLZOIRZb8skumL4gwHww
5Z942FDPuUDQ30C2mZR9lr3Cd5pA8S1ZSPTAV9QQHkpgoS8cAL8QC6dp3CJjUC8wzvXh3I
oN3bTKxCpM10KMEVuWO3lM4Nvr71auB9gzo1sFJ3bwebCZIRH01FROyA/GXRiaOtJFG/9N
nWWI/iG5AJzArKpLZNHIP+FxV/NoRH0WBXm9Wq5MrBYrD1NQzm+kInpS/2sXk3m1aZWqLm
HF2NKRXSbQAAA8iI+KSniPikpwAAAAdzc2gtcnNhAAABAQDErJhQxEI0+VvhlXVUyh+vMC
m7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VO
H7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAe
SmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndv
B5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkys
FisPU1DOb6QielL/axeTebVplaouYcXY0pFdJtAAAAAwEAAQAAAP8u3PFuTVV5SfGazwIm
MgNaux82iOsAT/HWFWecQAkqqrruUw5f+YajH/riV61NE9aq2qNOkcJrgpTWtqpt980GGd
SHWlgpRWQzfIooEiDk6Pk8RVFZsEykkDlJQSIu2onZjhi5A5ojHgZoGGabDsztSqoyOjPq
6WPvGYRiDAR3leBMyp1WufBCJqAsC4L8CjPJSmnZhc5a0zXkC9Syz74Fa08tdM7bGhtvP1
GmzuYxkgxHH2IFeoumUSBHRiTZayGuRUDel6jgEiUMxenaDKXe7FpYzMm9tQZA10Mm4LhK
5rP9nd2/KRTFRnfZMnKvtIRC9vtlSLBe14qw+4ZCl60AAACAf1kghlO3+HIWplOmk/lCL0
w75Zz+RdvueL9UuoyNN1QrUEY420LsixgWSeRPby+Rb/hW+XSAZJQHowQ8acFJhU85So7f
4O4wcDuE4f6hpsW9tTfkCEUdLCQJ7EKLCrod6jIV7hvI6rvXiVucRpeAzdOaq4uzj2cwDd
tOdYVsnmQAAACBAOVxBsvO/Sr3rZUbNtA6KewZh/09HNGoKNaCeiD7vaSn2UJbbPRByF/o
Oo5zv8ee8r3882NnmG808XfSn7pPZAzbbTmOaJt0fmyZhivCghSNzV6njW3o0PdnC0fGZQ
ruVXgkd7RJFbsIiD4dDcF4VCjwWHfTK21EOgJUA5pN6TNvAAAAgQDbcJWRx8Uyhkj2+srb
3n2Rt6CR7kEl9cw17ItFjMn+pO81/5U2aGw0iLlX7E06TAMQC+dyW/WaxQRey8RRdtbJ1e
TNKCN34QCWkyuYRHGhcNc0quEDayPw5QWGXlP4BzjfRUcPxY9cCXLe5wDLYsX33HwOAc59
RorU9FCmS/654wAAABFyb290QDhjNTBmZDRjMzQ1YQECAw==
-----END OPENSSH PRIVATE KEY-----"
)
.unwrap();
tmpfile
}
/// ### make_fsentry
///
/// Create a FsEntry at specified path
pub fn make_fsentry(path: PathBuf, is_dir: bool) -> FsEntry {
match is_dir {
true => FsEntry::Directory(FsDirectory {
name: path.file_name().unwrap().to_string_lossy().to_string(),
abs_path: path,
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
readonly: false,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
}),
false => FsEntry::File(FsFile {
name: path.file_name().unwrap().to_string_lossy().to_string(),
abs_path: path,
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 127,
ftype: None, // File type
readonly: false,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((6, 4, 4)), // UNIX only
}),
}
}
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_utils_test_helpers_sample_file() {
let (file, _) = create_sample_file_entry();
assert_eq!(file.readonly, false);
}
#[test]
#[cfg(feature = "with-containers")]
fn test_utils_test_helpers_write_file() {
let (_, temp) = create_sample_file_entry();
let tempdest = NamedTempFile::new().unwrap();
let mut dest: Box<dyn Write> = Box::new(
OpenOptions::new()
.create(true)
.read(false)
.write(true)
.open(tempdest.path())
.ok()
.unwrap(),
);
write_file(&temp, &mut dest);
}
#[test]
#[cfg(feature = "with-containers")]
fn test_utils_test_helpers_write_ssh_key() {
let _ = write_ssh_key();
}
#[test]
fn test_utils_test_helpers_make_fsentry() {
assert_eq!(
make_fsentry(PathBuf::from("/tmp/omar.txt"), false)
.unwrap_file()
.name
.as_str(),
"omar.txt"
);
assert_eq!(
make_fsentry(PathBuf::from("/tmp/cards"), true)
.unwrap_dir()
.name
.as_str(),
"cards"
);
}
#[test]
fn test_utils_test_helpers_make_samples() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
assert!(make_file_at(tmpdir.path(), "omaroni.txt").is_ok());
assert!(make_file_at(PathBuf::from("/aaaaa/bbbbb/cccc").as_path(), "readme.txt").is_err());
assert!(make_dir_at(tmpdir.path(), "docs").is_ok());
assert!(make_dir_at(PathBuf::from("/aaaaa/bbbbb/cccc").as_path(), "docs").is_err());
}
}

35
tests/docker-compose.yml Normal file
View file

@ -0,0 +1,35 @@
version: "3"
services:
openssh-server:
image: ghcr.io/linuxserver/openssh-server
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
- SUDO_ACCESS=false
- PUBLIC_KEY=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a
- PASSWORD_ACCESS=true
- USER_PASSWORD=password
- USER_NAME=sftp
ports:
- "10022:2222"
openssh-server-scp:
image: ghcr.io/linuxserver/openssh-server
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
- SUDO_ACCESS=false
- PASSWORD_ACCESS=true
- PUBLIC_KEY=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a
- USER_PASSWORD=password
- USER_NAME=sftp
ports:
- "10222:2222"
ftp-server:
image: afharo/pure-ftp
ports:
- "10021:21"
- "30000-30009:30000-30009"
environment:
- PUBLICHOST=localhost

29
tests/test.sh Executable file
View file

@ -0,0 +1,29 @@
#!/usr/bin/env sh
if [ ! -f docker-compose.yml ]; then
set -e
cd tests/
set +e
fi
echo "Prepare volume..."
rm -rf /tmp/termscp-test-ftp
mkdir -p /tmp/termscp-test-ftp
echo "Building docker image..."
docker compose build
set -e
docker compose up -d
set +e
# Go back to src root
cd ..
# Run tests
echo "Running tests"
cargo test --features with-containers -- --test-threads 1
TEST_RESULT=$?
# Stop container
cd tests/
echo "Stopping container..."
docker compose stop
exit $TEST_RESULT