diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index a04e7e1..9163c5d 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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: ... diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index b9779c9..b4e895f 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -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" diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml new file mode 100644 index 0000000..a1855cb --- /dev/null +++ b/.github/workflows/freebsd.yml @@ -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 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 79a9775..d500ad0 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -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 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index dbe3b63..18661b2 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -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 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 7e685ec..b162f67 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 961f084..89bc842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5db7290..80083a7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 215daf7..c4d2717 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 8f75ac1..4780b63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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\")"] diff --git a/README.md b/README.md index 3fe81a7..bd6cda0 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,11 @@

Developed by @veeso

-

Current version: 0.6.0 FIXME: (23/05/2021)

+

Current version: 0.6.0 FIXME: (21/06/2021)

-[![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 diff --git a/codecov.yml b/codecov.yml deleted file mode 100644 index a93ff15..0000000 --- a/codecov.yml +++ /dev/null @@ -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 diff --git a/dist/build/x86_64_archlinux/Dockerfile b/dist/build/x86_64_archlinux/Dockerfile index bda246d..8be577a 100644 --- a/dist/build/x86_64_archlinux/Dockerfile +++ b/dist/build/x86_64_archlinux/Dockerfile @@ -1,4 +1,4 @@ -FROM archlinux:base-20210120.0.13969 as builder +FROM archlinux:latest as builder WORKDIR /usr/src/ # Install dependencies diff --git a/dist/pkgs/arch/PKGBUILD b/dist/pkgs/arch/PKGBUILD index a48876d..ce950fb 100644 --- a/dist/pkgs/arch/PKGBUILD +++ b/dist/pkgs/arch/PKGBUILD @@ -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/" diff --git a/dist/pkgs/freebsd/manifest b/dist/pkgs/freebsd/manifest new file mode 100755 index 0000000..a63fcb2 --- /dev/null +++ b/dist/pkgs/freebsd/manifest @@ -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: < 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, - sftp: Option, - 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 = 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, - } - }; - ``` diff --git a/install.sh b/install.sh index 3144d1b..3fce076 100755 --- a/install.sh +++ b/install.sh @@ -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() { diff --git a/src/config/mod.rs b/src/config/mod.rs index 4153bc2..06bcb44 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -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") diff --git a/src/config/serializer.rs b/src/config/serializer.rs index 2375243..5ec89f4 100644 --- a/src/config/serializer.rs +++ b/src/config/serializer.rs @@ -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 = 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 = 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(); diff --git a/src/filetransfer/ftp_transfer.rs b/src/filetransfer/ftp_transfer.rs index 4b69880..9cb6199 100644 --- a/src/filetransfer/ftp_transfer.rs +++ b/src/filetransfer/ftp_transfer.rs @@ -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 = 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 = 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 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 = 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 = 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 = 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 = 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 = + Box::new(open_file(temp.path(), true, true, true).ok().unwrap()); + assert!(ftp.on_sent(writable).is_err()); } } diff --git a/src/filetransfer/mod.rs b/src/filetransfer/mod.rs index fff23c4..0158e10 100644 --- a/src/filetransfer/mod.rs +++ b/src/filetransfer/mod.rs @@ -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()) { diff --git a/src/filetransfer/scp_transfer.rs b/src/filetransfer/scp_transfer.rs index df17b47..34907a0 100644 --- a/src/filetransfer/scp_transfer.rs +++ b/src/filetransfer/scp_transfer.rs @@ -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, 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 = 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 = 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 = 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 = 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 = 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()); diff --git a/src/filetransfer/sftp_transfer.rs b/src/filetransfer/sftp_transfer.rs index d63f1c4..5f353f0 100644 --- a/src/filetransfer/sftp_transfer.rs +++ b/src/filetransfer/sftp_transfer.rs @@ -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 = match self.list_dir(d.abs_path.as_path()) { - Ok(entries) => entries, - Err(err) => return Err(err), - }; + let directory_content: Vec = 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 = 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 = 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 = 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 = 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 = 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()); diff --git a/src/fs/explorer/formatter.rs b/src/fs/explorer/formatter.rs index 68a5c0a..96bdbba 100644 --- a/src/fs/explorer/formatter.rs +++ b/src/fs/explorer/formatter.rs @@ -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!( diff --git a/src/fs/explorer/mod.rs b/src/fs/explorer/mod.rs index ec90def..320867b 100644 --- a/src/fs/explorer/mod.rs +++ b/src/fs/explorer/mod.rs @@ -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!( diff --git a/src/fs/mod.rs b/src/fs/mod.rs index 84ca480..07e06a9 100644 --- a/src/fs/mod.rs +++ b/src/fs/mod.rs @@ -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] diff --git a/src/host/mod.rs b/src/host/mod.rs index f3215bc..f832f6e 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -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 { 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 = 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 = 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 = 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 = 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(), - } - } } diff --git a/src/system/bookmarks_client.rs b/src/system/bookmarks_client.rs index 0d6fb93..b9db7d8 100644 --- a/src/system/bookmarks_client.rs +++ b/src/system/bookmarks_client.rs @@ -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() - } } diff --git a/src/system/config_client.rs b/src/system/config_client.rs index b0919b8..9caaa0b 100644 --- a/src/system/config_client.rs +++ b/src/system/config_client.rs @@ -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-----", diff --git a/src/system/sshkey_storage.rs b/src/system/sshkey_storage.rs index ea6cfb4..fd5418a 100644 --- a/src/system/sshkey_storage.rs +++ b/src/system/sshkey_storage.rs @@ -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() - } } diff --git a/src/ui/activities/auth/misc.rs b/src/ui/activities/auth/misc.rs index acda009..d30b877 100644 --- a/src/ui/activities/auth/misc.rs +++ b/src/ui/activities/auth/misc.rs @@ -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(); + } + } } diff --git a/src/ui/activities/auth/mod.rs b/src/ui/activities/auth/mod.rs index cf400af..76be0e0 100644 --- a/src/ui/activities/auth/mod.rs +++ b/src/ui/activities/auth/mod.rs @@ -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); diff --git a/src/ui/activities/auth/update.rs b/src/ui/activities/auth/update.rs index dd8c1f2..23971bb 100644 --- a/src/ui/activities/auth/update.rs +++ b/src/ui/activities/auth/update.rs @@ -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 key for all other components diff --git a/src/ui/activities/auth/view.rs b/src/ui/activities/auth/view.rs index 363801f..eb55543 100644 --- a/src/ui/activities/auth/view.rs +++ b/src/ui/activities/auth/view.rs @@ -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() diff --git a/src/ui/activities/filetransfer/actions/copy.rs b/src/ui/activities/filetransfer/actions/copy.rs index 5809b8e..27e43a6 100644 --- a/src/ui/activities/filetransfer/actions/copy.rs +++ b/src/ui/activities/filetransfer/actions/copy.rs @@ -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; + } + } + } + } } diff --git a/src/ui/activities/filetransfer/actions/edit.rs b/src/ui/activities/filetransfer/actions/edit.rs index ec88186..e6d3837 100644 --- a/src/ui/activities/filetransfer/actions/edit.rs +++ b/src/ui/activities/filetransfer/actions/edit.rs @@ -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(()) + } } diff --git a/src/ui/activities/filetransfer/actions/find.rs b/src/ui/activities/filetransfer/actions/find.rs index d229d8e..a7b9aed 100644 --- a/src/ui/activities/filetransfer/actions/find.rs +++ b/src/ui/activities/filetransfer/actions/find.rs @@ -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; } } } diff --git a/src/ui/activities/filetransfer/actions/mod.rs b/src/ui/activities/filetransfer/actions/mod.rs index 6df793b..9a9020e 100644 --- a/src/ui/activities/filetransfer/actions/mod.rs +++ b/src/ui/activities/filetransfer/actions/mod.rs @@ -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 - .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 - .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 - .filter(|x| x.is_some()) // Get only some values - .map(|x| x.unwrap()) // Option to FsEntry + .flatten() .collect(); SelectedEntry::from(files) } diff --git a/src/ui/activities/filetransfer/actions/open.rs b/src/ui/activities/filetransfer/actions/open.rs index ecd8735..d2536cc 100644 --- a/src/ui/activities/filetransfer/actions/open.rs +++ b/src/ui/activities/filetransfer/actions/open.rs @@ -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), + ); + } } } diff --git a/src/ui/activities/filetransfer/actions/save.rs b/src/ui/activities/filetransfer/actions/save.rs index 508d99c..4a5c238 100644 --- a/src/ui/activities/filetransfer/actions/save.rs +++ b/src/ui/activities/filetransfer/actions/save.rs @@ -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 => {} diff --git a/src/ui/activities/filetransfer/mod.rs b/src/ui/activities/filetransfer/mod.rs index 9b21a84..da53142 100644 --- a/src/ui/activities/filetransfer/mod.rs +++ b/src/ui/activities/filetransfer/mod.rs @@ -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 diff --git a/src/ui/activities/filetransfer/session.rs b/src/ui/activities/filetransfer/session.rs index f5fb653..8a55b83 100644 --- a/src/ui/activities/filetransfer/session.rs +++ b/src/ui/activities/filetransfer/session.rs @@ -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), +} + 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, + ) -> 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, + ) -> 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, - ) { + ) -> 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, + 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, + ) -> 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, - ) { + ) -> 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, + 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(), diff --git a/src/ui/activities/filetransfer/update.rs b/src/ui/activities/filetransfer/update.rs index 6d9f42d..272163d 100644 --- a/src/ui/activities/filetransfer/update.rs +++ b/src/ui/activities/filetransfer/update.rs @@ -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(), diff --git a/src/ui/activities/filetransfer/view.rs b/src/ui/activities/filetransfer/view.rs index 11b5c83..ffc78a4 100644 --- a/src/ui/activities/filetransfer/view.rs +++ b/src/ui/activities/filetransfer/view.rs @@ -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 = vec![ - TextSpanBuilder::new("Synchronized Browsing: ") + pub(super) fn refresh_local_status_bar(&mut self) { + let local_bar_spans: Vec = 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 = 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", + } + } } diff --git a/src/ui/activities/setup/actions.rs b/src/ui/activities/setup/actions.rs index d965d42..4ca3aeb 100644 --- a/src/ui/activities/setup/actions.rs +++ b/src/ui/activities/setup/actions.rs @@ -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(); diff --git a/src/ui/activities/setup/config.rs b/src/ui/activities/setup/config.rs index 25c1b0d..2373134 100644 --- a/src/ui/activities/setup/config.rs +++ b/src/ui/activities/setup/config.rs @@ -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() { diff --git a/src/ui/activities/setup/view.rs b/src/ui/activities/setup/view.rs index 7c37187..36342e3 100644 --- a/src/ui/activities/setup/view.rs +++ b/src/ui/activities/setup/view.rs @@ -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() diff --git a/src/ui/context.rs b/src/ui/context.rs index e3d44ef..d9affa6 100644 --- a/src/ui/context.rs +++ b/src/ui/context.rs @@ -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); } } diff --git a/src/utils/fmt.rs b/src/utils/fmt.rs index 8f3a210..1957ab1 100644 --- a/src/utils/fmt.rs +++ b/src/utils/fmt.rs @@ -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 diff --git a/src/utils/git.rs b/src/utils/git.rs index c0ef365..cab9070 100644 --- a/src/utils/git.rs +++ b/src/utils/git.rs @@ -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()); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e11bd67..f956857 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -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; diff --git a/src/utils/parser.rs b/src/utils/parser.rs index b5a8f41..9ac603d 100644 --- a/src/utils/parser.rs +++ b/src/utils/parser.rs @@ -186,13 +186,10 @@ pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result dt, - Err(err) => return Err(err), - } + )? } }; // Convert datetime to system time diff --git a/src/utils/test_helpers.rs b/src/utils/test_helpers.rs new file mode 100644 index 0000000..9e6c9fe --- /dev/null +++ b/src/utils/test_helpers.rs @@ -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) { + 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 = 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()); + } +} diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml new file mode 100644 index 0000000..d68fed6 --- /dev/null +++ b/tests/docker-compose.yml @@ -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 diff --git a/tests/test.sh b/tests/test.sh new file mode 100755 index 0000000..daecb00 --- /dev/null +++ b/tests/test.sh @@ -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