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)
-[](https://opensource.org/licenses/MIT) [](https://github.com/veeso/termscp) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
+[](https://opensource.org/licenses/MIT) [](https://github.com/veeso/termscp) [](https://crates.io/crates/termscp) [](https://crates.io/crates/termscp) [](https://docs.rs/termscp)
-[](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://coveralls.io/github/veeso/termscp)
+[](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](https://github.com/veeso/termscp/actions) [](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