Removed filetransfer module; migrated to remotefs crate

This commit is contained in:
veeso
2022-01-06 10:44:34 +01:00
committed by Christian Visintin
parent 25dd1b9b0a
commit df7a4381c4
60 changed files with 1185 additions and 6814 deletions
-1
View File
@@ -8,7 +8,6 @@ ignore:
- "../*" - "../*"
- src/main.rs - src/main.rs
- src/activity_manager.rs - src/activity_manager.rs
- src/filetransfer/transfer/s3/mod.rs
- src/support.rs - src/support.rs
- src/system/notifications.rs - src/system/notifications.rs
- "src/ui/activities/*" - "src/ui/activities/*"
+1 -3
View File
@@ -13,8 +13,6 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev
- name: Setup containers
run: docker-compose -f "tests/docker-compose.yml" up -d --build
- name: Setup nightly toolchain - name: Setup nightly toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@@ -24,7 +22,7 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: test command: test
args: --no-default-features --features github-actions --features with-containers --no-fail-fast args: --no-default-features --features github-actions --no-fail-fast
env: env:
CARGO_INCREMENTAL: "0" CARGO_INCREMENTAL: "0"
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests" RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
+1 -3
View File
@@ -13,8 +13,6 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Install dependencies - name: Install dependencies
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev
- name: Setup containers
run: docker-compose -f "tests/docker-compose.yml" up -d --build
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: stable toolchain: stable
@@ -24,7 +22,7 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: test command: test
args: --no-default-features --features github-actions --features with-containers --no-fail-fast args: --no-default-features --features github-actions --no-fail-fast
- name: Format - name: Format
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
- name: Clippy - name: Clippy
+1
View File
@@ -44,6 +44,7 @@ Released on FIXME:
- Dependencies: - Dependencies:
- Updated `tui-realm` to `1.3.0` - Updated `tui-realm` to `1.3.0`
- Updated `tui-realm-stdlib` to `1.1.4` - Updated `tui-realm-stdlib` to `1.1.4`
- Removed `rust-s3`, `ssh2`, `suppaftp`; replaced by `remotefs 0.1.1`
- Removed `crossterm` (since bridged by tui-realm) - Removed `crossterm` (since bridged by tui-realm)
## 0.7.0 ## 0.7.0
Generated
+74 -65
View File
@@ -123,9 +123,9 @@ dependencies = [
[[package]] [[package]]
name = "attohttpc" name = "attohttpc"
version = "0.16.3" version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247" checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072"
dependencies = [ dependencies = [
"http", "http",
"log", "log",
@@ -134,21 +134,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"url", "url",
"wildmatch 1.1.0", "wildmatch",
]
[[package]]
name = "attohttpc"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8bda305457262b339322106c776e3fd21df860018e566eb6a5b1aa4b6ae02d"
dependencies = [
"http",
"log",
"native-tls",
"openssl",
"url",
"wildmatch 1.1.0",
] ]
[[package]] [[package]]
@@ -159,13 +145,13 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]] [[package]]
name = "aws-creds" name = "aws-creds"
version = "0.26.0" version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1331d069460a674d42bd27c12b47ce578f789954c7bd7f239fd030771eca6616" checksum = "460a75eac8f3cb7683e0a9a588a83c3ff039331ea7bfbfbfcecf1dacab276e11"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"attohttpc 0.16.3", "attohttpc",
"dirs 3.0.2", "dirs 4.0.0",
"rust-ini", "rust-ini",
"serde", "serde",
"serde-xml-rs", "serde-xml-rs",
@@ -456,6 +442,16 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "crypto-mac"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
dependencies = [
"generic-array",
"subtle",
]
[[package]] [[package]]
name = "ctor" name = "ctor"
version = "0.1.21" version = "0.1.21"
@@ -529,15 +525,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "dirs"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
dependencies = [
"dirs-sys",
]
[[package]] [[package]]
name = "dirs" name = "dirs"
version = "4.0.0" version = "4.0.0"
@@ -799,7 +786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f" checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
dependencies = [ dependencies = [
"digest", "digest",
"hmac", "hmac 0.10.1",
] ]
[[package]] [[package]]
@@ -808,7 +795,17 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15" checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
dependencies = [ dependencies = [
"crypto-mac", "crypto-mac 0.10.1",
"digest",
]
[[package]]
name = "hmac"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
dependencies = [
"crypto-mac 0.11.1",
"digest", "digest",
] ]
@@ -1133,15 +1130,6 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "minidom"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "332592c2149fc7dd40a64fc9ef6f0d65607284b474cef9817d1fc8c7e7b3608e"
dependencies = [
"quick-xml",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.4.4" version = "0.4.4"
@@ -1686,6 +1674,26 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "remotefs"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c72915b01014a11d7e21b3a28141ff32b881bd8103c0f65269cc7932a03ae61c"
dependencies = [
"chrono",
"lazy_static",
"log",
"path-slash",
"regex",
"rust-s3",
"ssh2",
"ssh2-config",
"suppaftp",
"thiserror",
"users",
"wildmatch",
]
[[package]] [[package]]
name = "remove_dir_all" name = "remove_dir_all"
version = "0.5.3" version = "0.5.3"
@@ -1754,35 +1762,34 @@ dependencies = [
[[package]] [[package]]
name = "rust-ini" name = "rust-ini"
version = "0.16.1" version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55b134767a87e0b086f73a4ce569ac9ce7d202f39c8eab6caa266e2617e73ac6" checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22"
dependencies = [ dependencies = [
"cfg-if 0.1.10", "cfg-if 1.0.0",
"ordered-multimap", "ordered-multimap",
] ]
[[package]] [[package]]
name = "rust-s3" name = "rust-s3"
version = "0.27.0-rc4" version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c93272c1d654d492f8ab30b94cd43d98f2700b1db55b2576aff7712ce40e3ef" checksum = "18c58d4682844a5d6301efbf915dd7a9d3d638006f9bb821527a0bbbf2a4cfc2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"attohttpc 0.17.0", "attohttpc",
"aws-creds", "aws-creds",
"aws-region", "aws-region",
"base64", "base64",
"cfg-if 1.0.0", "cfg-if 1.0.0",
"chrono", "chrono",
"hex", "hex",
"hmac", "hmac 0.11.0",
"http", "http",
"log", "log",
"maybe-async", "maybe-async",
"md5", "md5",
"minidom",
"percent-encoding", "percent-encoding",
"serde", "serde",
"serde-xml-rs", "serde-xml-rs",
@@ -1926,9 +1933,9 @@ dependencies = [
[[package]] [[package]]
name = "serde-xml-rs" name = "serde-xml-rs"
version = "0.4.1" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa" checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa"
dependencies = [ dependencies = [
"log", "log",
"serde", "serde",
@@ -1977,7 +1984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d" checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"parking_lot 0.10.2", "parking_lot 0.11.2",
"serial_test_derive", "serial_test_derive",
] ]
@@ -2086,6 +2093,16 @@ dependencies = [
"parking_lot 0.10.2", "parking_lot 0.10.2",
] ]
[[package]]
name = "ssh2-config"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e64d0ea4897c9415c34011a4cdf21a0e0168c200595f1f543be1ca807942d8"
dependencies = [
"thiserror",
"wildmatch",
]
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.8.0" version = "0.8.0"
@@ -2110,12 +2127,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]] [[package]]
name = "suppaftp" name = "suppaftp"
version = "4.1.2" version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29a4d861acfdc117c6d373c3b743c534dbbbb2d782e7646b27439a7c5282ad6a" checksum = "3c42610de5ed0db28274b24c010aa530ebc391e5a4e9bd44a08e9daa8a245cc5"
dependencies = [ dependencies = [
"chrono", "chrono",
"lazy_static", "lazy_static",
"log",
"native-tls", "native-tls",
"regex", "regex",
"thiserror", "thiserror",
@@ -2214,18 +2232,15 @@ dependencies = [
"magic-crypt", "magic-crypt",
"notify-rust", "notify-rust",
"open", "open",
"path-slash",
"pretty_assertions", "pretty_assertions",
"rand 0.8.4", "rand 0.8.4",
"regex", "regex",
"remotefs",
"rpassword", "rpassword",
"rust-s3",
"self_update", "self_update",
"serde", "serde",
"serial_test", "serial_test",
"simplelog", "simplelog",
"ssh2",
"suppaftp",
"tempfile", "tempfile",
"textwrap", "textwrap",
"thiserror", "thiserror",
@@ -2234,7 +2249,7 @@ dependencies = [
"tuirealm", "tuirealm",
"users", "users",
"whoami", "whoami",
"wildmatch 2.1.0", "wildmatch",
] ]
[[package]] [[package]]
@@ -2662,12 +2677,6 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "wildmatch"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a"
[[package]] [[package]]
name = "wildmatch" name = "wildmatch"
version = "2.1.0" version = "2.1.0"
+1 -9
View File
@@ -48,13 +48,11 @@ notify-rust = { version = "4.5.3", default-features = false, features = [ "d" ]
open = "2.0.1" open = "2.0.1"
rand = "0.8.4" rand = "0.8.4"
regex = "1.5.4" regex = "1.5.4"
remotefs = { version = "0.1.1", features = [ "aws-s3", "ftp", "ssh" ] }
rpassword = "5.0.1" rpassword = "5.0.1"
rust-s3 = { version = "0.27.0-rc4", default-features = false, features = [ "sync-native-tls", "sync" ] }
self_update = { version = "0.27.0", features = [ "archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate" ] } self_update = { version = "0.27.0", features = [ "archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate" ] }
serde = { version = "^1.0.0", features = [ "derive" ] } serde = { version = "^1.0.0", features = [ "derive" ] }
simplelog = "0.10.0" simplelog = "0.10.0"
ssh2 = "0.9.0"
suppaftp = { version = "4.1.2", features = [ "secure" ] }
tempfile = "3.1.0" tempfile = "3.1.0"
textwrap = "0.14.2" textwrap = "0.14.2"
thiserror = "^1.0.0" thiserror = "^1.0.0"
@@ -71,14 +69,8 @@ serial_test = "^0.5.1"
[features] [features]
default = [ "with-keyring" ] default = [ "with-keyring" ]
github-actions = [ ] github-actions = [ ]
with-s3-ci = []
with-containers = []
with-keyring = [ "keyring" ] with-keyring = [ "keyring" ]
[target."cfg(target_family = \"unix\")"] [target."cfg(target_family = \"unix\")"]
[target."cfg(target_family = \"unix\")".dependencies] [target."cfg(target_family = \"unix\")".dependencies]
users = "0.11.0" users = "0.11.0"
[target."cfg(target_os = \"windows\")"]
[target."cfg(target_os = \"windows\")".dependencies]
path-slash = "0.1.4"
+1 -3
View File
@@ -287,11 +287,9 @@ termscp is powered by these awesome projects:
- [edit](https://github.com/milkey-mouse/edit) - [edit](https://github.com/milkey-mouse/edit)
- [keyring-rs](https://github.com/hwchen/keyring-rs) - [keyring-rs](https://github.com/hwchen/keyring-rs)
- [open-rs](https://github.com/Byron/open-rs) - [open-rs](https://github.com/Byron/open-rs)
- [remotefs](https://github.com/veeso/remotefs-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword) - [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-s3](https://github.com/durch/rust-s3)
- [self_update](https://github.com/jaemk/self_update) - [self_update](https://github.com/jaemk/self_update)
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
- [suppaftp](https://github.com/veeso/suppaftp)
- [textwrap](https://github.com/mgeisler/textwrap) - [textwrap](https://github.com/mgeisler/textwrap)
- [tui-rs](https://github.com/fdehau/tui-rs) - [tui-rs](https://github.com/fdehau/tui-rs)
- [tui-realm](https://github.com/veeso/tui-realm) - [tui-realm](https://github.com/veeso/tui-realm)
-10
View File
@@ -3,7 +3,6 @@
Document audience: developers Document audience: developers
- [Developer Manual](#developer-manual) - [Developer Manual](#developer-manual)
- [How to test](#how-to-test)
- [How termscp works](#how-termscp-works) - [How termscp works](#how-termscp-works)
- [Activities](#activities) - [Activities](#activities)
- [The Context](#the-context) - [The Context](#the-context)
@@ -11,15 +10,6 @@ Document audience: developers
Welcome to the developer manual for termscp. This chapter DOESN'T contain the documentation for termscp modules, which can instead be found on Rust Docs at <https://docs.rs/termscp> Welcome to the developer manual for termscp. This chapter DOESN'T contain the documentation for termscp modules, which can instead be found on Rust Docs at <https://docs.rs/termscp>
This chapter describes how termscp works and the guide lines to implement stuff such as file transfers and add features to the user interface. 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 ## How termscp works
+2 -3
View File
@@ -26,7 +26,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
// Deps // Deps
use crate::filetransfer::{FileTransferParams, FileTransferProtocol}; use crate::filetransfer::FileTransferParams;
use crate::host::{HostError, Localhost}; use crate::host::{HostError, Localhost};
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
use crate::system::environment; use crate::system::environment;
@@ -192,7 +192,6 @@ impl ActivityManager {
} }
}; };
// Prepare activity // Prepare activity
let protocol: FileTransferProtocol = ft_params.protocol;
let host: Localhost = match Localhost::new(self.local_dir.clone()) { let host: Localhost = match Localhost::new(self.local_dir.clone()) {
Ok(host) => host, Ok(host) => host,
Err(err) => { Err(err) => {
@@ -203,7 +202,7 @@ impl ActivityManager {
} }
}; };
let mut activity: FileTransferActivity = let mut activity: FileTransferActivity =
FileTransferActivity::new(host, protocol, self.ticks); FileTransferActivity::new(host, ft_params, self.ticks);
// Prepare result // Prepare result
let result: Option<NextActivity>; let result: Option<NextActivity>;
// Create activity // Create activity
+1 -10
View File
@@ -36,7 +36,7 @@ use std::str::FromStr;
/// ///
/// UserHosts contains all the hosts saved by the user in the data storage /// UserHosts contains all the hosts saved by the user in the data storage
/// It contains both `Bookmark` /// It contains both `Bookmark`
#[derive(Deserialize, Serialize, Debug)] #[derive(Deserialize, Serialize, Debug, Default)]
pub struct UserHosts { pub struct UserHosts {
pub bookmarks: HashMap<String, Bookmark>, pub bookmarks: HashMap<String, Bookmark>,
pub recents: HashMap<String, Bookmark>, pub recents: HashMap<String, Bookmark>,
@@ -76,15 +76,6 @@ pub struct S3Params {
// -- impls // -- impls
impl Default for UserHosts {
fn default() -> Self {
Self {
bookmarks: HashMap::new(),
recents: HashMap::new(),
}
}
}
impl From<FileTransferParams> for Bookmark { impl From<FileTransferParams> for Bookmark {
fn from(params: FileTransferParams) -> Self { fn from(params: FileTransferParams) -> Self {
let protocol: FileTransferProtocol = params.protocol; let protocol: FileTransferProtocol = params.protocol;
+3 -20
View File
@@ -35,7 +35,7 @@ use std::path::PathBuf;
pub const DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD: u64 = 536870912; // 512MB pub const DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD: u64 = 536870912; // 512MB
#[derive(Deserialize, Serialize, std::fmt::Debug)] #[derive(Deserialize, Serialize, Debug, Default)]
/// ## UserConfig /// ## UserConfig
/// ///
/// UserConfig contains all the configurations for the user, /// UserConfig contains all the configurations for the user,
@@ -45,7 +45,7 @@ pub struct UserConfig {
pub remote: RemoteConfig, pub remote: RemoteConfig,
} }
#[derive(Deserialize, Serialize, std::fmt::Debug)] #[derive(Deserialize, Serialize, Debug)]
/// ## UserInterfaceConfig /// ## UserInterfaceConfig
/// ///
/// UserInterfaceConfig provides all the keys to configure the user interface /// UserInterfaceConfig provides all the keys to configure the user interface
@@ -62,7 +62,7 @@ pub struct UserInterfaceConfig {
pub notification_threshold: Option<u64>, // @! Since 0.7.0; Default 512MB pub notification_threshold: Option<u64>, // @! Since 0.7.0; Default 512MB
} }
#[derive(Deserialize, Serialize, std::fmt::Debug)] #[derive(Deserialize, Serialize, Debug, Default)]
/// ## RemoteConfig /// ## RemoteConfig
/// ///
/// Contains configuratio related to remote hosts /// Contains configuratio related to remote hosts
@@ -70,15 +70,6 @@ pub struct RemoteConfig {
pub ssh_keys: HashMap<String, PathBuf>, // Association between host name and path to private key pub ssh_keys: HashMap<String, PathBuf>, // Association between host name and path to private key
} }
impl Default for UserConfig {
fn default() -> Self {
UserConfig {
user_interface: UserInterfaceConfig::default(),
remote: RemoteConfig::default(),
}
}
}
impl Default for UserInterfaceConfig { impl Default for UserInterfaceConfig {
fn default() -> Self { fn default() -> Self {
UserInterfaceConfig { UserInterfaceConfig {
@@ -99,14 +90,6 @@ impl Default for UserInterfaceConfig {
} }
} }
impl Default for RemoteConfig {
fn default() -> Self {
RemoteConfig {
ssh_keys: HashMap::new(),
}
}
}
// Tests // Tests
#[cfg(test)] #[cfg(test)]
@@ -26,18 +26,18 @@
* SOFTWARE. * SOFTWARE.
*/ */
// Locals // Locals
use super::FsEntry;
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time}; use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
use crate::utils::path::diff_paths; use crate::utils::path::diff_paths;
// Ext // Ext
use bytesize::ByteSize; use bytesize::ByteSize;
use regex::Regex; use regex::Regex;
use remotefs::Entry;
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
use users::{get_group_by_gid, get_user_by_uid}; use users::{get_group_by_gid, get_user_by_uid};
// Types // Types
// FmtCallback: Formatter, fsentry: &FsEntry, cur_str, prefix, length, extra // FmtCallback: Formatter, fsentry: &Entry, cur_str, prefix, length, extra
type FmtCallback = fn(&Formatter, &FsEntry, &str, &str, Option<&usize>, Option<&String>) -> String; type FmtCallback = fn(&Formatter, &Entry, &str, &str, Option<&usize>, Option<&String>) -> String;
// Keys // Keys
const FMT_KEY_ATIME: &str = "ATIME"; const FMT_KEY_ATIME: &str = "ATIME";
@@ -66,7 +66,7 @@ lazy_static! {
/// ## CallChainBlock /// ## CallChainBlock
/// ///
/// Call Chain block is a block in a chain of functions which are called in order to format the FsEntry. /// Call Chain block is a block in a chain of functions which are called in order to format the Entry.
/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found /// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found
/// a chain of function is made using the Formatters method. /// a chain of function is made using the Formatters method.
/// This method provides an extremely fast way to format fs entries /// This method provides an extremely fast way to format fs entries
@@ -105,7 +105,7 @@ impl CallChainBlock {
/// ### next /// ### next
/// ///
/// Call next callback in the CallChain /// Call next callback in the CallChain
pub fn next(&self, fmt: &Formatter, fsentry: &FsEntry, cur_str: &str) -> String { pub fn next(&self, fmt: &Formatter, fsentry: &Entry, cur_str: &str) -> String {
// Call func // Call func
let new_str: String = (self.func)( let new_str: String = (self.func)(
fmt, fmt,
@@ -177,7 +177,7 @@ impl Formatter {
/// ### fmt /// ### fmt
/// ///
/// Format fsentry /// Format fsentry
pub fn fmt(&self, fsentry: &FsEntry) -> String { pub fn fmt(&self, fsentry: &Entry) -> String {
// Execute callchain blocks // Execute callchain blocks
self.call_chain.next(self, fsentry, "") self.call_chain.next(self, fsentry, "")
} }
@@ -189,7 +189,7 @@ impl Formatter {
/// Format last access time /// Format last access time
fn fmt_atime( fn fmt_atime(
&self, &self,
fsentry: &FsEntry, fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
fmt_len: Option<&usize>, fmt_len: Option<&usize>,
@@ -197,7 +197,7 @@ impl Formatter {
) -> String { ) -> String {
// Get date (use extra args as format or default "%b %d %Y %H:%M") // Get date (use extra args as format or default "%b %d %Y %H:%M")
let datetime: String = fmt_time( let datetime: String = fmt_time(
fsentry.get_last_access_time(), fsentry.metadata().atime,
match fmt_extra { match fmt_extra {
Some(fmt) => fmt.as_ref(), Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M", None => "%b %d %Y %H:%M",
@@ -218,7 +218,7 @@ impl Formatter {
/// Format creation time /// Format creation time
fn fmt_ctime( fn fmt_ctime(
&self, &self,
fsentry: &FsEntry, fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
fmt_len: Option<&usize>, fmt_len: Option<&usize>,
@@ -226,7 +226,7 @@ impl Formatter {
) -> String { ) -> String {
// Get date // Get date
let datetime: String = fmt_time( let datetime: String = fmt_time(
fsentry.get_creation_time(), fsentry.metadata().ctime,
match fmt_extra { match fmt_extra {
Some(fmt) => fmt.as_ref(), Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M", None => "%b %d %Y %H:%M",
@@ -247,7 +247,7 @@ impl Formatter {
/// Format owner group /// Format owner group
fn fmt_group( fn fmt_group(
&self, &self,
fsentry: &FsEntry, fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
fmt_len: Option<&usize>, fmt_len: Option<&usize>,
@@ -255,7 +255,7 @@ impl Formatter {
) -> String { ) -> String {
// Get username // Get username
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
let group: String = match fsentry.get_group() { let group: String = match fsentry.metadata().gid {
Some(gid) => match get_group_by_gid(gid) { Some(gid) => match get_group_by_gid(gid) {
Some(user) => user.name().to_string_lossy().to_string(), Some(user) => user.name().to_string_lossy().to_string(),
None => gid.to_string(), None => gid.to_string(),
@@ -263,7 +263,7 @@ impl Formatter {
None => 0.to_string(), None => 0.to_string(),
}; };
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let group: String = match fsentry.get_group() { let group: String = match fsentry.metadata().gid {
Some(gid) => gid.to_string(), Some(gid) => gid.to_string(),
None => 0.to_string(), None => 0.to_string(),
}; };
@@ -282,7 +282,7 @@ impl Formatter {
/// Format last change time /// Format last change time
fn fmt_mtime( fn fmt_mtime(
&self, &self,
fsentry: &FsEntry, fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
fmt_len: Option<&usize>, fmt_len: Option<&usize>,
@@ -290,7 +290,7 @@ impl Formatter {
) -> String { ) -> String {
// Get date // Get date
let datetime: String = fmt_time( let datetime: String = fmt_time(
fsentry.get_last_change_time(), fsentry.metadata().mtime,
match fmt_extra { match fmt_extra {
Some(fmt) => fmt.as_ref(), Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M", None => "%b %d %Y %H:%M",
@@ -311,7 +311,7 @@ impl Formatter {
/// Format file name /// Format file name
fn fmt_name( fn fmt_name(
&self, &self,
fsentry: &FsEntry, fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
fmt_len: Option<&usize>, fmt_len: Option<&usize>,
@@ -322,7 +322,7 @@ impl Formatter {
Some(l) => *l, Some(l) => *l,
None => 24, None => 24,
}; };
let name: &str = fsentry.get_name(); let name: &str = fsentry.name();
let last_idx: usize = match fsentry.is_dir() { let last_idx: usize = match fsentry.is_dir() {
// NOTE: For directories is l - 2, since we push '/' to name // NOTE: For directories is l - 2, since we push '/' to name
true => file_len - 2, true => file_len - 2,
@@ -344,19 +344,16 @@ impl Formatter {
/// Format path /// Format path
fn fmt_path( fn fmt_path(
&self, &self,
fsentry: &FsEntry, fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
fmt_len: Option<&usize>, fmt_len: Option<&usize>,
fmt_extra: Option<&String>, fmt_extra: Option<&String>,
) -> String { ) -> String {
let p = match fmt_extra { let p = match fmt_extra {
None => fsentry.get_abs_path(), None => fsentry.path().to_path_buf(),
Some(rel) => diff_paths( Some(rel) => diff_paths(fsentry.path(), PathBuf::from(rel.as_str()).as_path())
fsentry.get_abs_path().as_path(), .unwrap_or_else(|| fsentry.path().to_path_buf()),
PathBuf::from(rel.as_str()).as_path(),
)
.unwrap_or_else(|| fsentry.get_abs_path()),
}; };
format!( format!(
"{}{}{}", "{}{}{}",
@@ -374,7 +371,7 @@ impl Formatter {
/// Format file permissions /// Format file permissions
fn fmt_pex( fn fmt_pex(
&self, &self,
fsentry: &FsEntry, fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
_fmt_len: Option<&usize>, _fmt_len: Option<&usize>,
@@ -382,7 +379,7 @@ impl Formatter {
) -> String { ) -> String {
// Create mode string // Create mode string
let mut pex: String = String::with_capacity(10); let mut pex: String = String::with_capacity(10);
let file_type: char = match fsentry.is_symlink() { let file_type: char = match fsentry.metadata().symlink.is_some() {
true => 'l', true => 'l',
false => match fsentry.is_dir() { false => match fsentry.is_dir() {
true => 'd', true => 'd',
@@ -390,10 +387,16 @@ impl Formatter {
}, },
}; };
pex.push(file_type); pex.push(file_type);
match fsentry.get_unix_pex() { match fsentry.metadata().mode {
None => pex.push_str("?????????"), None => pex.push_str("?????????"),
Some((owner, group, others)) => pex.push_str( Some(mode) => pex.push_str(
format!("{}{}{}", fmt_pex(owner), fmt_pex(group), fmt_pex(others)).as_str(), format!(
"{}{}{}",
fmt_pex(mode.user()),
fmt_pex(mode.group()),
fmt_pex(mode.others())
)
.as_str(),
), ),
} }
// Add to cur str, prefix and the key value // Add to cur str, prefix and the key value
@@ -405,7 +408,7 @@ impl Formatter {
/// Format file size /// Format file size
fn fmt_size( fn fmt_size(
&self, &self,
fsentry: &FsEntry, fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
_fmt_len: Option<&usize>, _fmt_len: Option<&usize>,
@@ -413,7 +416,7 @@ impl Formatter {
) -> String { ) -> String {
if fsentry.is_file() { if fsentry.is_file() {
// Get byte size // Get byte size
let size: ByteSize = ByteSize(fsentry.get_size() as u64); let size: ByteSize = ByteSize(fsentry.metadata().size);
// Add to cur str, prefix and the key value // Add to cur str, prefix and the key value
format!("{}{}{:10}", cur_str, prefix, size.to_string()) format!("{}{}{:10}", cur_str, prefix, size.to_string())
} else { } else {
@@ -427,7 +430,7 @@ impl Formatter {
/// Format file symlink (if any) /// Format file symlink (if any)
fn fmt_symlink( fn fmt_symlink(
&self, &self,
fsentry: &FsEntry, fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
fmt_len: Option<&usize>, fmt_len: Option<&usize>,
@@ -439,16 +442,13 @@ impl Formatter {
None => 21, None => 21,
}; };
// Replace `FMT_KEY_NAME` with name // Replace `FMT_KEY_NAME` with name
match fsentry.is_symlink() { match fsentry.metadata().symlink.as_deref() {
false => format!("{}{} ", cur_str, prefix), None => format!("{}{} ", cur_str, prefix),
true => format!( Some(p) => format!(
"{}{}-> {:0width$}", "{}{}-> {:0width$}",
cur_str, cur_str,
prefix, prefix,
fmt_path_elide( fmt_path_elide(p, file_len - 1),
fsentry.get_realfile().get_abs_path().as_path(),
file_len - 1
),
width = file_len width = file_len
), ),
} }
@@ -459,7 +459,7 @@ impl Formatter {
/// Format owner user /// Format owner user
fn fmt_user( fn fmt_user(
&self, &self,
fsentry: &FsEntry, fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
_fmt_len: Option<&usize>, _fmt_len: Option<&usize>,
@@ -467,7 +467,7 @@ impl Formatter {
) -> String { ) -> String {
// Get username // Get username
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
let username: String = match fsentry.get_user() { let username: String = match fsentry.metadata().uid {
Some(uid) => match get_user_by_uid(uid) { Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(), Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(), None => uid.to_string(),
@@ -475,7 +475,7 @@ impl Formatter {
None => 0.to_string(), None => 0.to_string(),
}; };
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let username: String = match fsentry.get_user() { let username: String = match fsentry.metadata().uid {
Some(uid) => uid.to_string(), Some(uid) => uid.to_string(),
None => 0.to_string(), None => 0.to_string(),
}; };
@@ -489,7 +489,7 @@ impl Formatter {
/// It does nothing, just returns cur_str /// It does nothing, just returns cur_str
fn fmt_fallback( fn fmt_fallback(
&self, &self,
_fsentry: &FsEntry, _fsentry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
_fmt_len: Option<&usize>, _fmt_len: Option<&usize>,
@@ -574,9 +574,9 @@ impl Formatter {
mod tests { mod tests {
use super::*; use super::*;
use crate::fs::{FsDirectory, FsFile, UnixPex};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use remotefs::fs::{Directory, File, Metadata, UnixPex};
use std::path::PathBuf; use std::path::PathBuf;
use std::time::SystemTime; use std::time::SystemTime;
@@ -585,19 +585,21 @@ mod tests {
// Make a dummy formatter // Make a dummy formatter
let dummy_formatter: Formatter = Formatter::new(""); let dummy_formatter: Formatter = Formatter::new("");
// Make a dummy entry // Make a dummy entry
let t_now: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let dummy_entry: FsEntry = FsEntry::File(FsFile { let dummy_entry: Entry = Entry::File(File {
name: String::from("bar.txt"), name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"), path: PathBuf::from("/bar.txt"),
last_change_time: t_now, extension: Some(String::from("txt")),
last_access_time: t_now, metadata: Metadata {
creation_time: t_now, atime: t,
size: 8192, ctime: t,
ftype: Some(String::from("txt")), mtime: t,
symlink: None, // UNIX only size: 8192,
user: Some(0), // UNIX only symlink: None,
group: Some(0), // UNIX only uid: Some(0),
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
}); });
let prefix: String = String::from("h"); let prefix: String = String::from("h");
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None); let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
@@ -626,18 +628,20 @@ mod tests {
let formatter: Formatter = Formatter::default(); let formatter: Formatter = Formatter::default();
// Experiments :D // Experiments :D
let t: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile { let entry: Entry = Entry::File(File {
name: String::from("bar.txt"), name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"), path: PathBuf::from("/bar.txt"),
last_change_time: t, extension: Some(String::from("txt")),
last_access_time: t, metadata: Metadata {
creation_time: t, atime: t,
size: 8192, ctime: t,
ftype: Some(String::from("txt")), mtime: t,
symlink: None, // UNIX only size: 8192,
user: Some(0), // UNIX only symlink: None,
group: Some(0), // UNIX only uid: Some(0),
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
}); });
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
assert_eq!( assert_eq!(
@@ -656,18 +660,20 @@ mod tests {
) )
); );
// Elide name // Elide name
let entry: FsEntry = FsEntry::File(FsFile { let entry: Entry = Entry::File(File {
name: String::from("piroparoporoperoperupupu.txt"), name: String::from("piroparoporoperoperupupu.txt"),
abs_path: PathBuf::from("/bar.txt"), path: PathBuf::from("/bar.txt"),
last_change_time: t, extension: Some(String::from("txt")),
last_access_time: t, metadata: Metadata {
creation_time: t, atime: t,
size: 8192, ctime: t,
ftype: Some(String::from("txt")), mtime: t,
symlink: None, // UNIX only size: 8192,
user: Some(0), // UNIX only symlink: None,
group: Some(0), // UNIX only uid: Some(0),
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
}); });
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
assert_eq!( assert_eq!(
@@ -686,18 +692,20 @@ mod tests {
) )
); );
// No pex // No pex
let entry: FsEntry = FsEntry::File(FsFile { let entry: Entry = Entry::File(File {
name: String::from("bar.txt"), name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"), path: PathBuf::from("/bar.txt"),
last_change_time: t, extension: Some(String::from("txt")),
last_access_time: t, metadata: Metadata {
creation_time: t, atime: t,
size: 8192, ctime: t,
ftype: Some(String::from("txt")), mtime: t,
symlink: None, // UNIX only size: 8192,
user: Some(0), // UNIX only symlink: None,
group: Some(0), // UNIX only uid: Some(0),
unix_pex: None, // UNIX only gid: Some(0),
mode: None,
},
}); });
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
assert_eq!( assert_eq!(
@@ -716,18 +724,20 @@ mod tests {
) )
); );
// No user // No user
let entry: FsEntry = FsEntry::File(FsFile { let entry: Entry = Entry::File(File {
name: String::from("bar.txt"), name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"), path: PathBuf::from("/bar.txt"),
last_change_time: t, extension: Some(String::from("txt")),
last_access_time: t, metadata: Metadata {
creation_time: t, atime: t,
size: 8192, ctime: t,
ftype: Some(String::from("txt")), mtime: t,
symlink: None, // UNIX only size: 8192,
user: None, // UNIX only symlink: None,
group: Some(0), // UNIX only uid: None,
unix_pex: None, // UNIX only gid: Some(0),
mode: None,
},
}); });
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
assert_eq!( assert_eq!(
@@ -752,24 +762,27 @@ mod tests {
// Make default // Make default
let formatter: Formatter = Formatter::default(); let formatter: Formatter = Formatter::default();
// Experiments :D // Experiments :D
let t_now: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::Directory(FsDirectory { let entry: Entry = Entry::Directory(Directory {
name: String::from("projects"), name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/projects"), path: PathBuf::from("/home/cvisintin/projects"),
last_change_time: t_now, metadata: Metadata {
last_access_time: t_now, atime: t,
creation_time: t_now, ctime: t,
symlink: None, // UNIX only mtime: t,
user: Some(0), // UNIX only size: 4096,
group: Some(0), // UNIX only symlink: None,
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o755)),
},
}); });
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
"projects/ drwxr-xr-x root {}", "projects/ drwxr-xr-x root {}",
fmt_time(t_now, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@@ -777,27 +790,30 @@ mod tests {
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
"projects/ drwxr-xr-x 0 {}", "projects/ drwxr-xr-x 0 {}",
fmt_time(t_now, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
// No pex, no user // No pex, no user
let entry: FsEntry = FsEntry::Directory(FsDirectory { let entry: Entry = Entry::Directory(Directory {
name: String::from("projects"), name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/projects"), path: PathBuf::from("/home/cvisintin/projects"),
last_change_time: t_now, metadata: Metadata {
last_access_time: t_now, atime: t,
creation_time: t_now, ctime: t,
symlink: None, // UNIX only mtime: t,
user: None, // UNIX only size: 4096,
group: Some(0), // UNIX only symlink: None,
unix_pex: None, // UNIX only uid: None,
gid: Some(0),
mode: None,
},
}); });
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
assert_eq!( assert_eq!(
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
"projects/ d????????? 0 {}", "projects/ d????????? 0 {}",
fmt_time(t_now, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@@ -805,7 +821,7 @@ mod tests {
formatter.fmt(&entry), formatter.fmt(&entry),
format!( format!(
"projects/ d????????? 0 {}", "projects/ d????????? 0 {}",
fmt_time(t_now, "%b %d %Y %H:%M") fmt_time(t, "%b %d %Y %H:%M")
) )
); );
} }
@@ -816,29 +832,19 @@ mod tests {
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}"); Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
// Directory (with symlink) // Directory (with symlink)
let t: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let pointer: FsEntry = FsEntry::File(FsFile { let entry: Entry = Entry::Directory(Directory {
name: String::from("project.info"),
abs_path: PathBuf::from("/project.info"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: None, // UNIX only
});
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"), name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/project"), path: PathBuf::from("/home/cvisintin/project"),
last_change_time: t, metadata: Metadata {
last_access_time: t, atime: t,
creation_time: t, ctime: t,
symlink: Some(Box::new(pointer)), // UNIX only mtime: t,
user: None, // UNIX only size: 4096,
group: None, // UNIX only symlink: Some(PathBuf::from("project.info")),
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only uid: None,
gid: None,
mode: Some(UnixPex::from(0o755)),
},
}); });
assert_eq!(formatter.fmt(&entry), format!( assert_eq!(formatter.fmt(&entry), format!(
"projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}", "projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}",
@@ -847,16 +853,19 @@ mod tests {
fmt_time(t, "%a %b %d %Y %H:%M"), fmt_time(t, "%a %b %d %Y %H:%M"),
)); ));
// Directory without symlink // Directory without symlink
let entry: FsEntry = FsEntry::Directory(FsDirectory { let entry: Entry = Entry::Directory(Directory {
name: String::from("projects"), name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/project"), path: PathBuf::from("/home/cvisintin/project"),
last_change_time: t, metadata: Metadata {
last_access_time: t, atime: t,
creation_time: t, ctime: t,
symlink: None, // UNIX only mtime: t,
user: None, // UNIX only size: 4096,
group: None, // UNIX only symlink: None,
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only uid: None,
gid: None,
mode: Some(UnixPex::from(0o755)),
},
}); });
assert_eq!(formatter.fmt(&entry), format!( assert_eq!(formatter.fmt(&entry), format!(
"projects/ 0 0 drwxr-xr-x {} {} {}", "projects/ 0 0 drwxr-xr-x {} {} {}",
@@ -865,31 +874,20 @@ mod tests {
fmt_time(t, "%a %b %d %Y %H:%M"), fmt_time(t, "%a %b %d %Y %H:%M"),
)); ));
// File with symlink // File with symlink
let pointer: FsEntry = FsEntry::File(FsFile { let entry: Entry = Entry::File(File {
name: String::from("project.info"),
abs_path: PathBuf::from("/project.info"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: None, // UNIX only
});
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from("bar.txt"), name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"), path: PathBuf::from("/bar.txt"),
last_change_time: t, extension: Some(String::from("txt")),
last_access_time: t, metadata: Metadata {
creation_time: t, atime: t,
size: 8192, ctime: t,
ftype: Some(String::from("txt")), mtime: t,
symlink: Some(Box::new(pointer)), // UNIX only size: 8192,
user: None, // UNIX only symlink: Some(PathBuf::from("project.info")),
group: None, // UNIX only uid: None,
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only gid: None,
mode: Some(UnixPex::from(0o644)),
},
}); });
assert_eq!(formatter.fmt(&entry), format!( assert_eq!(formatter.fmt(&entry), format!(
"bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}", "bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
@@ -898,18 +896,20 @@ mod tests {
fmt_time(t, "%a %b %d %Y %H:%M"), fmt_time(t, "%a %b %d %Y %H:%M"),
)); ));
// File without symlink // File without symlink
let entry: FsEntry = FsEntry::File(FsFile { let entry: Entry = Entry::File(File {
name: String::from("bar.txt"), name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"), path: PathBuf::from("/bar.txt"),
last_change_time: t, extension: Some(String::from("txt")),
last_access_time: t, metadata: Metadata {
creation_time: t, atime: t,
size: 8192, ctime: t,
ftype: Some(String::from("txt")), mtime: t,
symlink: None, // UNIX only size: 8192,
user: None, // UNIX only symlink: None,
group: None, // UNIX only uid: None,
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only gid: None,
mode: Some(UnixPex::from(0o644)),
},
}); });
assert_eq!(formatter.fmt(&entry), format!( assert_eq!(formatter.fmt(&entry), format!(
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}", "bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
@@ -923,18 +923,20 @@ mod tests {
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
fn should_fmt_path() { fn should_fmt_path() {
let t: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile { let entry: Entry = Entry::File(File {
name: String::from("bar.txt"), name: String::from("bar.txt"),
abs_path: PathBuf::from("/tmp/a/b/c/bar.txt"), path: PathBuf::from("/tmp/a/b/c/bar.txt"),
last_change_time: t, extension: Some(String::from("txt")),
last_access_time: t, metadata: Metadata {
creation_time: t, atime: t,
size: 8192, ctime: t,
ftype: Some(String::from("txt")), mtime: t,
symlink: None, // UNIX only size: 8192,
user: None, // UNIX only symlink: Some(PathBuf::from("project.info")),
group: None, // UNIX only uid: None,
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only gid: None,
mode: Some(UnixPex::from(0o644)),
},
}); });
let formatter: Formatter = Formatter::new("File path: {PATH}"); let formatter: Formatter = Formatter::new("File path: {PATH}");
assert_eq!( assert_eq!(
@@ -955,7 +957,7 @@ mod tests {
/// Dummy formatter, just yelds an 'A' at the end of the current string /// Dummy formatter, just yelds an 'A' at the end of the current string
fn dummy_fmt( fn dummy_fmt(
_fmt: &Formatter, _fmt: &Formatter,
_entry: &FsEntry, _entry: &Entry,
cur_str: &str, cur_str: &str,
prefix: &str, prefix: &str,
_fmt_len: Option<&usize>, _fmt_len: Option<&usize>,
+92 -106
View File
@@ -29,9 +29,9 @@
pub(crate) mod builder; pub(crate) mod builder;
mod formatter; mod formatter;
// Locals // Locals
use super::FsEntry;
use formatter::Formatter; use formatter::Formatter;
// Ext // Ext
use remotefs::fs::Entry;
use std::cmp::Reverse; use std::cmp::Reverse;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -77,8 +77,8 @@ pub struct FileExplorer {
pub(crate) file_sorting: FileSorting, // File sorting criteria pub(crate) file_sorting: FileSorting, // File sorting criteria
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
pub(crate) opts: ExplorerOpts, // Explorer options pub(crate) opts: ExplorerOpts, // Explorer options
pub(crate) fmt: Formatter, // FsEntry formatter pub(crate) fmt: Formatter, // Entry formatter
files: Vec<FsEntry>, // Files in directory files: Vec<Entry>, // Files in directory
} }
impl Default for FileExplorer { impl Default for FileExplorer {
@@ -121,7 +121,7 @@ impl FileExplorer {
/// Set Explorer files /// Set Explorer files
/// This method will also sort entries based on current options /// This method will also sort entries based on current options
/// Once all sorting have been performed, index is moved to first valid entry. /// Once all sorting have been performed, index is moved to first valid entry.
pub fn set_files(&mut self, files: Vec<FsEntry>) { pub fn set_files(&mut self, files: Vec<Entry>) {
self.files = files; self.files = files;
// Sort // Sort
self.sort(); self.sort();
@@ -149,7 +149,7 @@ impl FileExplorer {
/// ///
/// Iterate over files /// Iterate over files
/// Filters are applied based on current options (e.g. hidden files not returned) /// Filters are applied based on current options (e.g. hidden files not returned)
pub fn iter_files(&self) -> impl Iterator<Item = &FsEntry> + '_ { pub fn iter_files(&self) -> impl Iterator<Item = &Entry> + '_ {
// Filter // Filter
let opts: ExplorerOpts = self.opts; let opts: ExplorerOpts = self.opts;
Box::new(self.files.iter().filter(move |x| { Box::new(self.files.iter().filter(move |x| {
@@ -166,14 +166,14 @@ impl FileExplorer {
/// ### iter_files_all /// ### iter_files_all
/// ///
/// Iterate all files; doesn't care about options /// Iterate all files; doesn't care about options
pub fn iter_files_all(&self) -> impl Iterator<Item = &FsEntry> + '_ { pub fn iter_files_all(&self) -> impl Iterator<Item = &Entry> + '_ {
Box::new(self.files.iter()) Box::new(self.files.iter())
} }
/// ### get /// ### get
/// ///
/// Get file at relative index /// Get file at relative index
pub fn get(&self, idx: usize) -> Option<&FsEntry> { pub fn get(&self, idx: usize) -> Option<&Entry> {
let opts: ExplorerOpts = self.opts; let opts: ExplorerOpts = self.opts;
let filtered = self let filtered = self
.files .files
@@ -196,7 +196,7 @@ impl FileExplorer {
/// ### fmt_file /// ### fmt_file
/// ///
/// Format a file entry /// Format a file entry
pub fn fmt_file(&self, entry: &FsEntry) -> String { pub fn fmt_file(&self, entry: &Entry) -> String {
self.fmt.fmt(entry) self.fmt.fmt(entry)
} }
@@ -256,17 +256,15 @@ impl FileExplorer {
/// ///
/// Sort explorer files by their name. All names are converted to lowercase /// Sort explorer files by their name. All names are converted to lowercase
fn sort_files_by_name(&mut self) { fn sort_files_by_name(&mut self) {
self.files self.files.sort_by_key(|x: &Entry| x.name().to_lowercase());
.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase());
} }
/// ### sort_files_by_mtime /// ### sort_files_by_mtime
/// ///
/// Sort files by mtime; the newest comes first /// Sort files by mtime; the newest comes first
fn sort_files_by_mtime(&mut self) { fn sort_files_by_mtime(&mut self) {
self.files.sort_by(|a: &FsEntry, b: &FsEntry| { self.files
b.get_last_change_time().cmp(&a.get_last_change_time()) .sort_by(|a: &Entry, b: &Entry| b.metadata().mtime.cmp(&a.metadata().mtime));
});
} }
/// ### sort_files_by_creation_time /// ### sort_files_by_creation_time
@@ -274,28 +272,29 @@ impl FileExplorer {
/// Sort files by creation time; the newest comes first /// Sort files by creation time; the newest comes first
fn sort_files_by_creation_time(&mut self) { fn sort_files_by_creation_time(&mut self) {
self.files self.files
.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time())); .sort_by_key(|b: &Entry| Reverse(b.metadata().ctime));
} }
/// ### sort_files_by_size /// ### sort_files_by_size
/// ///
/// Sort files by size /// Sort files by size
fn sort_files_by_size(&mut self) { fn sort_files_by_size(&mut self) {
self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_size())); self.files
.sort_by_key(|b: &Entry| Reverse(b.metadata().size));
} }
/// ### sort_files_directories_first /// ### sort_files_directories_first
/// ///
/// Sort files; directories come first /// Sort files; directories come first
fn sort_files_directories_first(&mut self) { fn sort_files_directories_first(&mut self) {
self.files.sort_by_key(|x: &FsEntry| x.is_file()); self.files.sort_by_key(|x: &Entry| x.is_file());
} }
/// ### sort_files_directories_last /// ### sort_files_directories_last
/// ///
/// Sort files; directories come last /// Sort files; directories come last
fn sort_files_directories_last(&mut self) { fn sort_files_directories_last(&mut self) {
self.files.sort_by_key(|x: &FsEntry| x.is_dir()); self.files.sort_by_key(|x: &Entry| x.is_dir());
} }
/// ### toggle_hidden_files /// ### toggle_hidden_files
@@ -363,10 +362,10 @@ impl FromStr for GroupDirs {
mod tests { mod tests {
use super::*; use super::*;
use crate::fs::{FsDirectory, FsFile, UnixPex};
use crate::utils::fmt::fmt_time; use crate::utils::fmt::fmt_time;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use remotefs::fs::{Directory, File, Metadata, UnixPex};
use std::thread::sleep; use std::thread::sleep;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
@@ -430,10 +429,7 @@ mod tests {
assert!(explorer.get(100).is_none()); assert!(explorer.get(100).is_none());
//assert_eq!(explorer.count(), 6); //assert_eq!(explorer.count(), 6);
// Verify (files are sorted by name) // Verify (files are sorted by name)
assert_eq!( assert_eq!(explorer.files.get(0).unwrap().name(), ".git/");
explorer.files.get(0).unwrap().get_name(),
String::from(".git/")
);
// Iter files (all) // Iter files (all)
assert_eq!(explorer.iter_files_all().count(), 6); assert_eq!(explorer.iter_files_all().count(), 6);
// Iter files (hidden excluded) (.git, .gitignore are hidden) // Iter files (hidden excluded) (.git, .gitignore are hidden)
@@ -461,47 +457,41 @@ mod tests {
]); ]);
explorer.sort_by(FileSorting::Name); explorer.sort_by(FileSorting::Name);
// First entry should be "Cargo.lock" // First entry should be "Cargo.lock"
assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock"); assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock");
// Last should be "src/" // Last should be "src/"
assert_eq!(explorer.files.get(8).unwrap().get_name(), "src/"); assert_eq!(explorer.files.get(8).unwrap().name(), "src/");
} }
#[test] #[test]
fn test_fs_explorer_sort_by_mtime() { fn test_fs_explorer_sort_by_mtime() {
let mut explorer: FileExplorer = FileExplorer::default(); let mut explorer: FileExplorer = FileExplorer::default();
let entry1: FsEntry = make_fs_entry("README.md", false); let entry1: Entry = make_fs_entry("README.md", false);
// Wait 1 sec // Wait 1 sec
sleep(Duration::from_secs(1)); sleep(Duration::from_secs(1));
let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false); let entry2: Entry = make_fs_entry("CODE_OF_CONDUCT.md", false);
// Create files (files are then sorted by name) // Create files (files are then sorted by name)
explorer.set_files(vec![entry1, entry2]); explorer.set_files(vec![entry1, entry2]);
explorer.sort_by(FileSorting::ModifyTime); explorer.sort_by(FileSorting::ModifyTime);
// First entry should be "CODE_OF_CONDUCT.md" // First entry should be "CODE_OF_CONDUCT.md"
assert_eq!( assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
explorer.files.get(0).unwrap().get_name(),
"CODE_OF_CONDUCT.md"
);
// Last should be "src/" // Last should be "src/"
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
} }
#[test] #[test]
fn test_fs_explorer_sort_by_creation_time() { fn test_fs_explorer_sort_by_creation_time() {
let mut explorer: FileExplorer = FileExplorer::default(); let mut explorer: FileExplorer = FileExplorer::default();
let entry1: FsEntry = make_fs_entry("README.md", false); let entry1: Entry = make_fs_entry("README.md", false);
// Wait 1 sec // Wait 1 sec
sleep(Duration::from_secs(1)); sleep(Duration::from_secs(1));
let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false); let entry2: Entry = make_fs_entry("CODE_OF_CONDUCT.md", false);
// Create files (files are then sorted by name) // Create files (files are then sorted by name)
explorer.set_files(vec![entry1, entry2]); explorer.set_files(vec![entry1, entry2]);
explorer.sort_by(FileSorting::CreationTime); explorer.sort_by(FileSorting::CreationTime);
// First entry should be "CODE_OF_CONDUCT.md" // First entry should be "CODE_OF_CONDUCT.md"
assert_eq!( assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
explorer.files.get(0).unwrap().get_name(),
"CODE_OF_CONDUCT.md"
);
// Last should be "src/" // Last should be "src/"
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
} }
#[test] #[test]
@@ -510,14 +500,14 @@ mod tests {
// Create files (files are then sorted by name) // Create files (files are then sorted by name)
explorer.set_files(vec![ explorer.set_files(vec![
make_fs_entry_with_size("README.md", false, 1024), make_fs_entry_with_size("README.md", false, 1024),
make_fs_entry("src/", true), make_fs_entry_with_size("src/", true, 4096),
make_fs_entry_with_size("CONTRIBUTING.md", false, 256), make_fs_entry_with_size("CONTRIBUTING.md", false, 256),
]); ]);
explorer.sort_by(FileSorting::Size); explorer.sort_by(FileSorting::Size);
// Directory has size 4096 // Directory has size 4096
assert_eq!(explorer.files.get(0).unwrap().get_name(), "src/"); assert_eq!(explorer.files.get(0).unwrap().name(), "src/");
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md"); assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
assert_eq!(explorer.files.get(2).unwrap().get_name(), "CONTRIBUTING.md"); assert_eq!(explorer.files.get(2).unwrap().name(), "CONTRIBUTING.md");
} }
#[test] #[test]
@@ -539,12 +529,12 @@ mod tests {
explorer.sort_by(FileSorting::Name); explorer.sort_by(FileSorting::Name);
explorer.group_dirs_by(Some(GroupDirs::First)); explorer.group_dirs_by(Some(GroupDirs::First));
// First entry should be "docs" // First entry should be "docs"
assert_eq!(explorer.files.get(0).unwrap().get_name(), "docs/"); assert_eq!(explorer.files.get(0).unwrap().name(), "docs/");
assert_eq!(explorer.files.get(1).unwrap().get_name(), "src/"); assert_eq!(explorer.files.get(1).unwrap().name(), "src/");
// 3rd is file first for alphabetical order // 3rd is file first for alphabetical order
assert_eq!(explorer.files.get(2).unwrap().get_name(), "Cargo.lock"); assert_eq!(explorer.files.get(2).unwrap().name(), "Cargo.lock");
// Last should be "README.md" (last file for alphabetical ordening) // Last should be "README.md" (last file for alphabetical ordening)
assert_eq!(explorer.files.get(9).unwrap().get_name(), "README.md"); assert_eq!(explorer.files.get(9).unwrap().name(), "README.md");
} }
#[test] #[test]
@@ -566,12 +556,12 @@ mod tests {
explorer.sort_by(FileSorting::Name); explorer.sort_by(FileSorting::Name);
explorer.group_dirs_by(Some(GroupDirs::Last)); explorer.group_dirs_by(Some(GroupDirs::Last));
// Last entry should be "src" // Last entry should be "src"
assert_eq!(explorer.files.get(8).unwrap().get_name(), "docs/"); assert_eq!(explorer.files.get(8).unwrap().name(), "docs/");
assert_eq!(explorer.files.get(9).unwrap().get_name(), "src/"); assert_eq!(explorer.files.get(9).unwrap().name(), "src/");
// first is file for alphabetical order // first is file for alphabetical order
assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock"); assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock");
// Last in files should be "README.md" (last file for alphabetical ordening) // Last in files should be "README.md" (last file for alphabetical ordening)
assert_eq!(explorer.files.get(7).unwrap().get_name(), "README.md"); assert_eq!(explorer.files.get(7).unwrap().name(), "README.md");
} }
#[test] #[test]
@@ -579,18 +569,20 @@ mod tests {
let explorer: FileExplorer = FileExplorer::default(); let explorer: FileExplorer = FileExplorer::default();
// Create fs entry // Create fs entry
let t: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile { let entry: Entry = Entry::File(File {
name: String::from("bar.txt"), name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"), path: PathBuf::from("/bar.txt"),
last_change_time: t, extension: Some(String::from("txt")),
last_access_time: t, metadata: Metadata {
creation_time: t, atime: t,
size: 8192, ctime: t,
ftype: Some(String::from("txt")), size: 8192,
symlink: None, // UNIX only mtime: t,
user: Some(0), // UNIX only symlink: None,
group: Some(0), // UNIX only uid: Some(0),
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
}); });
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
assert_eq!( assert_eq!(
@@ -654,67 +646,61 @@ mod tests {
]); ]);
explorer.del_entry(0); explorer.del_entry(0);
assert_eq!(explorer.files.len(), 3); assert_eq!(explorer.files.len(), 3);
assert_eq!(explorer.files[0].get_name(), "docs/"); assert_eq!(explorer.files[0].name(), "docs/");
explorer.del_entry(5); explorer.del_entry(5);
assert_eq!(explorer.files.len(), 3); assert_eq!(explorer.files.len(), 3);
} }
fn make_fs_entry(name: &str, is_dir: bool) -> FsEntry { fn make_fs_entry(name: &str, is_dir: bool) -> Entry {
let t_now: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let metadata = Metadata {
atime: t,
ctime: t,
mtime: t,
symlink: None,
gid: Some(0),
uid: Some(0),
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
size: 64,
};
match is_dir { match is_dir {
false => FsEntry::File(FsFile { false => Entry::File(File {
name: name.to_string(), name: name.to_string(),
abs_path: PathBuf::from(name), path: PathBuf::from(name),
last_change_time: t_now, extension: None,
last_access_time: t_now, metadata,
creation_time: t_now,
size: 64,
ftype: None, // File type
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
}), }),
true => FsEntry::Directory(FsDirectory { true => Entry::Directory(Directory {
name: name.to_string(), name: name.to_string(),
abs_path: PathBuf::from(name), path: PathBuf::from(name),
last_change_time: t_now, metadata,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
}), }),
} }
} }
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> FsEntry { fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> Entry {
let t_now: SystemTime = SystemTime::now(); let t: SystemTime = SystemTime::now();
let metadata = Metadata {
atime: t,
ctime: t,
mtime: t,
symlink: None,
gid: Some(0),
uid: Some(0),
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
size: size as u64,
};
match is_dir { match is_dir {
false => FsEntry::File(FsFile { false => Entry::File(File {
name: name.to_string(), name: name.to_string(),
abs_path: PathBuf::from(name), path: PathBuf::from(name),
last_change_time: t_now, extension: None,
last_access_time: t_now, metadata,
creation_time: t_now,
size: size,
ftype: None, // File type
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
}), }),
true => FsEntry::Directory(FsDirectory { true => Entry::Directory(Directory {
name: name.to_string(), name: name.to_string(),
abs_path: PathBuf::from(name), path: PathBuf::from(name),
last_change_time: t_now, metadata,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
}), }),
} }
} }
+213
View File
@@ -0,0 +1,213 @@
//! ## builder
//!
//! Remotefs client builder
/**
* 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 super::params::{AwsS3Params, GenericProtocolParams};
use super::{FileTransferProtocol, ProtocolParams};
use crate::system::config_client::ConfigClient;
use crate::system::sshkey_storage::SshKeyStorage;
use remotefs::client::{
aws_s3::AwsS3Fs,
ftp::FtpFs,
ssh::{ScpFs, SftpFs, SshOpts},
};
use remotefs::RemoteFs;
/// Remotefs builder
pub struct Builder;
impl Builder {
/// Build RemoteFs client from protocol and params.
///
/// if protocol and parameters are inconsistent, the function will panic.
pub fn build(
protocol: FileTransferProtocol,
params: ProtocolParams,
config_client: &ConfigClient,
) -> Box<dyn RemoteFs> {
match (protocol, params) {
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => {
Box::new(Self::aws_s3_client(params))
}
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
Box::new(Self::ftp_client(params, secure))
}
(FileTransferProtocol::Scp, ProtocolParams::Generic(params)) => {
Box::new(Self::scp_client(params, config_client))
}
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
Box::new(Self::sftp_client(params, config_client))
}
(protocol, params) => {
error!("Invalid params for protocol '{:?}'", protocol);
panic!(
"Invalid protocol '{:?}' with parameters of type {:?}",
protocol, params
)
}
}
}
/// Build aws s3 client from parameters
fn aws_s3_client(params: AwsS3Params) -> AwsS3Fs {
let mut client = AwsS3Fs::new(params.bucket_name, params.region);
if let Some(profile) = params.profile {
client = client.profile(profile);
}
client
}
/// Build ftp client from parameters
fn ftp_client(params: GenericProtocolParams, secure: bool) -> FtpFs {
let mut client = FtpFs::new(params.address, params.port).passive_mode();
if let Some(username) = params.username {
client = client.username(username);
}
if let Some(password) = params.password {
client = client.password(password);
}
if secure {
client = client.secure(true, true);
}
client
}
/// Build scp client
fn scp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> ScpFs {
Self::build_ssh_opts(params, config_client).into()
}
/// Build sftp client
fn sftp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> SftpFs {
Self::build_ssh_opts(params, config_client).into()
}
/// Build ssh options from generic protocol params and client configuration
fn build_ssh_opts(params: GenericProtocolParams, config_client: &ConfigClient) -> SshOpts {
let mut opts = SshOpts::new(params.address)
.key_storage(Box::new(Self::make_ssh_storage(config_client)))
.port(params.port);
if let Some(username) = params.username {
opts = opts.username(username);
}
if let Some(password) = params.password {
opts = opts.password(password);
}
opts
}
/// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded)
fn make_ssh_storage(config_client: &ConfigClient) -> SshKeyStorage {
SshKeyStorage::storage_from_config(config_client)
}
}
#[cfg(test)]
mod test {
use super::*;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
#[test]
fn should_build_aws_s3_fs() {
let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test")));
let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
}
#[test]
fn should_build_ftp_fs() {
let params = ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(21)
.username(Some("omar"))
.password(Some("qwerty123")),
);
let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::Ftp(true), params, &config_client);
}
#[test]
fn should_build_scp_fs() {
let params = ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(22)
.username(Some("omar"))
.password(Some("qwerty123")),
);
let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::Scp, params, &config_client);
}
#[test]
fn should_build_sftp_fs() {
let params = ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(22)
.username(Some("omar"))
.password(Some("qwerty123")),
);
let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client);
}
#[test]
#[should_panic]
fn should_not_build_fs() {
let params = ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(22)
.username(Some("omar"))
.password(Some("qwerty123")),
);
let config_client = get_config_client();
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
}
fn get_config_client() -> ConfigClient {
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
let (cfg_path, ssh_keys_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
ConfigClient::new(cfg_path.as_path(), ssh_keys_path.as_path())
.ok()
.unwrap()
}
/// Get paths for configuration and keys directory
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
let mut k: PathBuf = PathBuf::from(dir);
let mut c: PathBuf = k.clone();
k.push("ssh-keys/");
c.push("config.toml");
(c, k)
}
}
+3 -423
View File
@@ -1,6 +1,6 @@
//! ## FileTransfer //! ## FileTransfer
//! //!
//! `filetransfer` is the module which provides the trait file transfers must implement and the different file transfers //! `filetransfer` is the module which provides the file transfer protocols and remotefs builders
/** /**
* MIT License * MIT License
@@ -25,21 +25,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
// locals mod builder;
use crate::fs::{FsEntry, FsFile};
// ext
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use thiserror::Error;
use wildmatch::WildMatch;
// exports
pub mod params; pub mod params;
mod transfer;
// -- export types // -- export types
pub use builder::Builder;
pub use params::{FileTransferParams, ProtocolParams}; pub use params::{FileTransferParams, ProtocolParams};
pub use transfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
/// ## FileTransferProtocol /// ## FileTransferProtocol
/// ///
@@ -53,325 +44,6 @@ pub enum FileTransferProtocol {
AwsS3, AwsS3,
} }
/// ## FileTransferError
///
/// FileTransferError defines the possible errors available for a file transfer
#[derive(Debug)]
pub struct FileTransferError {
code: FileTransferErrorType,
msg: Option<String>,
}
/// ## FileTransferErrorType
///
/// FileTransferErrorType defines the possible errors available for a file transfer
#[derive(Error, Debug, Clone, Copy, PartialEq)]
pub enum FileTransferErrorType {
#[error("Authentication failed")]
AuthenticationFailed,
#[error("Bad address syntax")]
BadAddress,
#[error("Connection error")]
ConnectionError,
#[error("SSL error")]
SslError,
#[error("Could not stat directory")]
DirStatFailed,
#[error("Directory already exists")]
DirectoryAlreadyExists,
#[error("Failed to create file")]
FileCreateDenied,
#[error("No such file or directory")]
NoSuchFileOrDirectory,
#[error("Not enough permissions")]
PexError,
#[error("Protocol error")]
ProtocolError,
#[error("Uninitialized session")]
UninitializedSession,
#[error("Unsupported feature")]
UnsupportedFeature,
}
impl FileTransferError {
/// ### new
///
/// Instantiates a new FileTransferError
pub fn new(code: FileTransferErrorType) -> FileTransferError {
FileTransferError { code, msg: None }
}
/// ### new_ex
///
/// Instantiates a new FileTransferError with message
pub fn new_ex(code: FileTransferErrorType, msg: String) -> FileTransferError {
let mut err: FileTransferError = FileTransferError::new(code);
err.msg = Some(msg);
err
}
/// ### kind
///
/// Returns the error kind
pub fn kind(&self) -> FileTransferErrorType {
self.code
}
}
impl std::fmt::Display for FileTransferError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match &self.msg {
Some(msg) => write!(f, "{} ({})", self.code, msg),
None => write!(f, "{}", self.code),
}
}
}
/// ## FileTransferResult
///
/// Result type returned by a `FileTransfer` implementation
pub type FileTransferResult<T> = Result<T, FileTransferError>;
/// ## FileTransfer
///
/// File transfer trait must be implemented by all the file transfers and defines the method used by a generic file transfer
pub trait FileTransfer {
/// ### connect
///
/// Connect to the remote server
/// Can return banner / welcome message on success
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>>;
/// ### disconnect
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> FileTransferResult<()>;
/// ### is_connected
///
/// Indicates whether the client is connected to remote
fn is_connected(&self) -> bool;
/// ### pwd
///
/// Print working directory
fn pwd(&mut self) -> FileTransferResult<PathBuf>;
/// ### change_dir
///
/// Change working directory
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf>;
/// ### copy
///
/// Copy file to destination
fn copy(&mut self, src: &FsEntry, dst: &Path) -> FileTransferResult<()>;
/// ### list_dir
///
/// List directory entries
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>>;
/// ### mkdir
///
/// Make directory
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()>;
/// ### remove
///
/// Remove a file or a directory
fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()>;
/// ### rename
///
/// Rename file or a directory
fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()>;
/// ### stat
///
/// Stat file and return FsEntry
fn stat(&mut self, path: &Path) -> FileTransferResult<FsEntry>;
/// ### exec
///
/// Execute a command on remote host
fn exec(&mut self, cmd: &str) -> FileTransferResult<String>;
/// ### send_file
///
/// Send file to remote
/// File name is referred to the name of the file as it will be saved
/// Data contains the file data
/// Returns file and its size.
/// By default returns unsupported feature
fn send_file(
&mut self,
_local: &FsFile,
_file_name: &Path,
) -> FileTransferResult<Box<dyn Write>> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### recv_file
///
/// Receive file from remote with provided name
/// Returns file and its size
/// By default returns unsupported feature
fn recv_file(&mut self, _file: &FsFile) -> FileTransferResult<Box<dyn Read>> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### on_sent
///
/// Finalize send method.
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
/// The purpose of this method is to finalize the connection with the peer when writing data.
/// This is necessary for some protocols such as FTP.
/// You must call this method each time you want to finalize the write of the remote file.
/// By default this function returns already `Ok(())`
fn on_sent(&mut self, _writable: Box<dyn Write>) -> FileTransferResult<()> {
Ok(())
}
/// ### on_recv
///
/// Finalize recv method.
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
/// The purpose of this method is to finalize the connection with the peer when reading data.
/// This mighe be necessary for some protocols.
/// You must call this method each time you want to finalize the read of the remote file.
/// By default this function returns already `Ok(())`
fn on_recv(&mut self, _readable: Box<dyn Read>) -> FileTransferResult<()> {
Ok(())
}
/// ### send_file_wno_stream
///
/// Send a file to remote WITHOUT using streams.
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
/// By default this function uses the streams function to copy content from reader to writer
fn send_file_wno_stream(
&mut self,
src: &FsFile,
dest: &Path,
mut reader: Box<dyn Read>,
) -> FileTransferResult<()> {
match self.is_connected() {
true => {
let mut stream = self.send_file(src, dest)?;
io::copy(&mut reader, &mut stream).map_err(|e| {
FileTransferError::new_ex(FileTransferErrorType::ProtocolError, e.to_string())
})?;
self.on_sent(stream)
}
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### recv_file_wno_stream
///
/// Receive a file from remote WITHOUT using streams.
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
/// For safety reasons this function doesn't accept the `Write` trait, but the destination path.
/// By default this function uses the streams function to copy content from reader to writer
fn recv_file_wno_stream(&mut self, src: &FsFile, dest: &Path) -> FileTransferResult<()> {
match self.is_connected() {
true => {
let mut writer = File::create(dest).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("Could not open local file: {}", e),
)
})?;
let mut stream = self.recv_file(src)?;
io::copy(&mut stream, &mut writer)
.map(|_| ())
.map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
e.to_string(),
)
})?;
self.on_recv(stream)
}
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### find
///
/// Find files from current directory (in all subdirectories) whose name matches the provided search
/// Search supports wildcards ('?', '*')
fn find(&mut self, search: &str) -> FileTransferResult<Vec<FsEntry>> {
match self.is_connected() {
true => {
// Starting from current directory, iter dir
match self.pwd() {
Ok(p) => self.iter_search(p.as_path(), &WildMatch::new(search)),
Err(err) => Err(err),
}
}
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### iter_search
///
/// Search recursively in `dir` for file matching the wildcard.
/// NOTE: DON'T RE-IMPLEMENT THIS FUNCTION, unless the file transfer provides a faster way to do so
/// NOTE: don't call this method from outside; consider it as private
fn iter_search(&mut self, dir: &Path, filter: &WildMatch) -> FileTransferResult<Vec<FsEntry>> {
let mut drained: Vec<FsEntry> = Vec::new();
// Scan directory
match self.list_dir(dir) {
Ok(entries) => {
/* For each entry:
- if is dir: call iter_search with `dir`
- push `iter_search` result to `drained`
- if is file: check if it matches `filter`
- if it matches `filter`: push to to filter
*/
for entry in entries.iter() {
match entry {
FsEntry::Directory(dir) => {
// If directory name, matches wildcard, push it to drained
if filter.matches(dir.name.as_str()) {
drained.push(FsEntry::Directory(dir.clone()));
}
drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?);
}
FsEntry::File(file) => {
if filter.matches(file.name.as_str()) {
drained.push(FsEntry::File(file.clone()));
}
}
}
}
Ok(drained)
}
Err(err) => Err(err),
}
}
}
// Traits // Traits
impl std::string::ToString for FileTransferProtocol { impl std::string::ToString for FileTransferProtocol {
@@ -479,96 +151,4 @@ mod tests {
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP")); assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3")); assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
} }
#[test]
fn test_filetransfer_mod_error() {
let err: FileTransferError = FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
String::from("non va una mazza"),
);
assert_eq!(*err.msg.as_ref().unwrap(), String::from("non va una mazza"));
assert_eq!(
format!("{}", err),
String::from("No such file or directory (non va una mazza)")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::AuthenticationFailed)
),
String::from("Authentication failed")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::BadAddress)
),
String::from("Bad address syntax")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::ConnectionError)
),
String::from("Connection error")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::DirStatFailed)
),
String::from("Could not stat directory")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::FileCreateDenied)
),
String::from("Failed to create file")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::NoSuchFileOrDirectory)
),
String::from("No such file or directory")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::PexError)
),
String::from("Not enough permissions")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::ProtocolError)
),
String::from("Protocol error")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::SslError)
),
String::from("SSL error")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::UninitializedSession)
),
String::from("Uninitialized session")
);
assert_eq!(
format!(
"{}",
FileTransferError::new(FileTransferErrorType::UnsupportedFeature)
),
String::from("Unsupported feature")
);
let err = FileTransferError::new(FileTransferErrorType::UnsupportedFeature);
assert_eq!(err.kind(), FileTransferErrorType::UnsupportedFeature);
}
} }
+2 -4
View File
@@ -103,8 +103,7 @@ impl Default for ProtocolParams {
} }
impl ProtocolParams { impl ProtocolParams {
/// ### generic_params #[cfg(test)]
///
/// Retrieve generic parameters from protocol params if any /// Retrieve generic parameters from protocol params if any
pub fn generic_params(&self) -> Option<&GenericProtocolParams> { pub fn generic_params(&self) -> Option<&GenericProtocolParams> {
match self { match self {
@@ -120,8 +119,7 @@ impl ProtocolParams {
} }
} }
/// ### s3_params #[cfg(test)]
///
/// Retrieve AWS S3 parameters if any /// Retrieve AWS S3 parameters if any
pub fn s3_params(&self) -> Option<&AwsS3Params> { pub fn s3_params(&self) -> Option<&AwsS3Params> {
match self { match self {
-960
View File
@@ -1,960 +0,0 @@
//! ## FTP transfer
//!
//! `ftp_transfer` is the module which provides the implementation for the FTP/FTPS file transfer
/**
* 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 super::{
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
};
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use crate::utils::fmt::shadow_password;
use crate::utils::path;
// Includes
use std::convert::TryFrom;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::UNIX_EPOCH;
use suppaftp::native_tls::TlsConnector;
use suppaftp::{
list::{File, PosixPexQuery},
status::FILE_UNAVAILABLE,
types::{FileType, Response},
FtpError, FtpStream,
};
/// ## FtpFileTransfer
///
/// Ftp file transfer struct
pub struct FtpFileTransfer {
stream: Option<FtpStream>,
ftps: bool,
}
impl FtpFileTransfer {
/// ### new
///
/// Instantiates a new `FtpFileTransfer`
pub fn new(ftps: bool) -> FtpFileTransfer {
FtpFileTransfer { stream: None, ftps }
}
/// ### resolve
///
/// Fix provided path; on Windows fixes the backslashes, converting them to slashes
/// While on POSIX does nothing
#[cfg(target_os = "windows")]
fn resolve(p: &Path) -> PathBuf {
PathBuf::from(path_slash::PathExt::to_slash_lossy(p).as_str())
}
#[cfg(target_family = "unix")]
fn resolve(p: &Path) -> PathBuf {
p.to_path_buf()
}
/// ### parse_list_lines
///
/// Parse all lines of LIST command output and instantiates a vector of FsEntry from it.
/// This function also converts from `suppaftp::list::File` to `FsEntry`
fn parse_list_lines(&mut self, path: &Path, lines: Vec<String>) -> Vec<FsEntry> {
// Iter and collect
lines
.into_iter()
.map(File::try_from) // Try to convert to file
.flatten() // Remove errors
.map(|x| {
let mut abs_path: PathBuf = path.to_path_buf();
abs_path.push(x.name());
match x.is_directory() {
true => FsEntry::Directory(FsDirectory {
name: x.name().to_string(),
abs_path,
last_access_time: x.modified(),
last_change_time: x.modified(),
creation_time: x.modified(),
symlink: None,
user: x.uid(),
group: x.gid(),
unix_pex: Some(Self::query_unix_pex(&x)),
}),
false => FsEntry::File(FsFile {
name: x.name().to_string(),
size: x.size(),
ftype: abs_path
.extension()
.map(|ext| String::from(ext.to_str().unwrap_or(""))),
last_access_time: x.modified(),
last_change_time: x.modified(),
creation_time: x.modified(),
user: x.uid(),
group: x.gid(),
symlink: Self::get_symlink_entry(path, x.symlink()),
abs_path,
unix_pex: Some(Self::query_unix_pex(&x)),
}),
}
})
.collect()
}
/// ### get_symlink_entry
///
/// Get FsEntry from symlink
fn get_symlink_entry(wrkdir: &Path, link: Option<&Path>) -> Option<Box<FsEntry>> {
match link {
None => None,
Some(p) => {
// Make abs path
let abs_path: PathBuf = path::absolutize(wrkdir, p);
Some(Box::new(FsEntry::File(FsFile {
name: p
.file_name()
.map(|x| x.to_str().unwrap_or("").to_string())
.unwrap_or_default(),
ftype: abs_path
.extension()
.map(|ext| String::from(ext.to_str().unwrap_or(""))),
size: 0,
last_access_time: UNIX_EPOCH,
last_change_time: UNIX_EPOCH,
creation_time: UNIX_EPOCH,
user: None,
group: None,
symlink: None,
unix_pex: None,
abs_path,
})))
}
}
}
/// ### query_unix_pex
///
/// Returns unix pex in tuple of values
fn query_unix_pex(f: &File) -> (UnixPex, UnixPex, UnixPex) {
(
UnixPex::new(
f.can_read(PosixPexQuery::Owner),
f.can_write(PosixPexQuery::Owner),
f.can_execute(PosixPexQuery::Owner),
),
UnixPex::new(
f.can_read(PosixPexQuery::Group),
f.can_write(PosixPexQuery::Group),
f.can_execute(PosixPexQuery::Group),
),
UnixPex::new(
f.can_read(PosixPexQuery::Others),
f.can_write(PosixPexQuery::Others),
f.can_execute(PosixPexQuery::Others),
),
)
}
}
impl FileTransfer for FtpFileTransfer {
/// ### connect
///
/// Connect to the remote server
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>> {
let params = match params.generic_params() {
Some(params) => params,
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
};
// Get stream
info!("Connecting to {}:{}", params.address, params.port);
let mut stream: FtpStream =
match FtpStream::connect(format!("{}:{}", params.address, params.port)) {
Ok(stream) => stream,
Err(err) => {
error!("Failed to connect: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
));
}
};
// If SSL, open secure session
if self.ftps {
info!("Setting up TLS stream...");
let ctx = match TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()
{
Ok(tls) => tls,
Err(err) => {
error!("Failed to setup TLS stream: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::SslError,
err.to_string(),
));
}
};
stream = match stream.into_secure(ctx, params.address.as_str()) {
Ok(s) => s,
Err(err) => {
error!("Failed to setup TLS stream: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::SslError,
err.to_string(),
));
}
};
}
// Login (use anonymous if credentials are unspecified)
let username: String = match &params.username {
Some(u) => u.to_string(),
None => String::from("anonymous"),
};
let password: String = match &params.password {
Some(pwd) => pwd.to_string(),
None => String::new(),
};
info!(
"Signin in with username: {}, password: {}",
username,
shadow_password(password.as_str())
);
if let Err(err) = stream.login(username.as_str(), password.as_str()) {
error!("Login failed: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
err.to_string(),
));
}
debug!("Setting transfer type to Binary");
// Initialize file type
if let Err(err) = stream.transfer_type(FileType::Binary) {
error!("Failed to set transfer type to binary: {}", err);
return Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
err.to_string(),
));
}
// Set stream
self.stream = Some(stream);
info!("Connection successfully established");
// Return OK
Ok(self
.stream
.as_ref()
.unwrap()
.get_welcome_msg()
.map(|x| x.to_string()))
}
/// ### disconnect
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> FileTransferResult<()> {
info!("Disconnecting from FTP server...");
match &mut self.stream {
Some(stream) => match stream.quit() {
Ok(_) => {
self.stream = None;
Ok(())
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
)),
},
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### is_connected
///
/// Indicates whether the client is connected to remote
fn is_connected(&self) -> bool {
self.stream.is_some()
}
/// ### pwd
///
/// Print working directory
fn pwd(&mut self) -> FileTransferResult<PathBuf> {
info!("PWD");
match &mut self.stream {
Some(stream) => match stream.pwd() {
Ok(path) => Ok(PathBuf::from(path.as_str())),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
)),
},
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### change_dir
///
/// Change working directory
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf> {
let dir: PathBuf = Self::resolve(dir);
info!("Changing directory to {}", dir.display());
match &mut self.stream {
Some(stream) => match stream.cwd(&dir.as_path().to_string_lossy()) {
Ok(_) => Ok(dir),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ConnectionError,
err.to_string(),
)),
},
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### copy
///
/// Copy file to destination
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> FileTransferResult<()> {
// FTP doesn't support file copy
debug!("COPY issues (will fail, since unsupported)");
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### list_dir
///
/// List directory entries
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>> {
let dir: PathBuf = Self::resolve(path);
info!("LIST dir {}", dir.display());
match &mut self.stream {
Some(stream) => match stream.list(Some(&dir.as_path().to_string_lossy())) {
Ok(lines) => {
debug!("Got {} lines in LIST result", lines.len());
// Iterate over entries
Ok(self.parse_list_lines(path, lines))
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::DirStatFailed,
err.to_string(),
)),
},
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### mkdir
///
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> {
let dir: PathBuf = Self::resolve(dir);
info!("MKDIR {}", dir.display());
match &mut self.stream {
Some(stream) => match stream.mkdir(&dir.as_path().to_string_lossy()) {
Ok(_) => Ok(()),
Err(FtpError::UnexpectedResponse(Response {
// Directory already exists
code: FILE_UNAVAILABLE,
body: _,
})) => {
error!("Directory {} already exists", dir.display());
Err(FileTransferError::new(
FileTransferErrorType::DirectoryAlreadyExists,
))
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
err.to_string(),
)),
},
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### remove
///
/// Remove a file or a directory
fn remove(&mut self, fsentry: &FsEntry) -> FileTransferResult<()> {
if self.stream.is_none() {
return Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
));
}
info!("Removing entry {}", fsentry.get_abs_path().display());
let wrkdir: PathBuf = self.pwd()?;
match fsentry {
// Match fs entry...
FsEntry::File(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
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");
let result = match self.list_dir(dir.abs_path.as_path()) {
Ok(files) => {
// Remove recursively files
debug!("Removing {} entries from directory...", files.len());
for file in files.iter() {
if let Err(err) = self.remove(file) {
return Err(FileTransferError::new_ex(
FileTransferErrorType::PexError,
err.to_string(),
));
}
}
// Once all files in directory have been deleted, remove directory
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(_) => {
debug!("Removed {}", dir.abs_path.display());
Ok(())
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::PexError,
err.to_string(),
)),
}
}
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::DirStatFailed,
err.to_string(),
)),
};
// Restore directory
match self.change_dir(wrkdir.as_path()) {
Err(err) => Err(err),
Ok(_) => result,
}
}
}
}
/// ### rename
///
/// Rename file or a directory
fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()> {
let dst: PathBuf = Self::resolve(dst);
info!(
"Renaming {} to {}",
file.get_abs_path().display(),
dst.display()
);
match &mut self.stream {
Some(stream) => {
// Get name
let src_name: String = match file {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
};
// Only names are supported
match stream.rename(src_name.as_str(), &dst.as_path().to_string_lossy()) {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
err.to_string(),
)),
}
}
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### stat
///
/// Stat file and return FsEntry
fn stat(&mut self, _path: &Path) -> FileTransferResult<FsEntry> {
match &mut self.stream {
Some(_) => Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
)),
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### exec
///
/// Execute a command on remote host
fn exec(&mut self, _cmd: &str) -> FileTransferResult<String> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### send_file
///
/// Send file to remote
/// File name is referred to the name of the file as it will be saved
/// Data contains the file data
/// Returns file and its size
fn send_file(
&mut self,
_local: &FsFile,
file_name: &Path,
) -> FileTransferResult<Box<dyn Write>> {
let file_name: PathBuf = Self::resolve(file_name);
info!("Sending file {}", file_name.display());
match &mut self.stream {
Some(stream) => match stream.put_with_stream(&file_name.as_path().to_string_lossy()) {
Ok(writer) => Ok(Box::new(writer)), // NOTE: don't use BufWriter here, since already returned by the library
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
err.to_string(),
)),
},
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### recv_file
///
/// Receive file from remote with provided name
/// Returns file and its size
fn recv_file(&mut self, file: &FsFile) -> FileTransferResult<Box<dyn Read>> {
info!("Receiving file {}", file.abs_path.display());
match &mut self.stream {
Some(stream) => match stream.retr_as_stream(&file.abs_path.as_path().to_string_lossy())
{
Ok(reader) => Ok(Box::new(reader)), // NOTE: don't use BufReader here, since already returned by the library
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
err.to_string(),
)),
},
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### on_sent
///
/// Finalize send method.
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
/// The purpose of this method is to finalize the connection with the peer when writing data.
/// This is necessary for some protocols such as FTP.
/// You must call this method each time you want to finalize the write of the remote file.
fn on_sent(&mut self, writable: Box<dyn Write>) -> FileTransferResult<()> {
info!("Finalizing put stream");
match &mut self.stream {
Some(stream) => match stream.finalize_put_stream(writable) {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
err.to_string(),
)),
},
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### on_recv
///
/// Finalize recv method.
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
/// The purpose of this method is to finalize the connection with the peer when reading data.
/// This mighe be necessary for some protocols.
/// You must call this method each time you want to finalize the read of the remote file.
fn on_recv(&mut self, readable: Box<dyn Read>) -> FileTransferResult<()> {
info!("Finalizing get");
match &mut self.stream {
Some(stream) => match stream.finalize_retr_stream(readable) {
Ok(_) => Ok(()),
Err(err) => Err(FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
err.to_string(),
)),
},
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filetransfer::params::GenericProtocolParams;
use crate::utils::file::open_file;
#[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]
fn test_filetransfer_ftp_new() {
let ftp: FtpFileTransfer = FtpFileTransfer::new(false);
assert_eq!(ftp.ftps, false);
assert!(ftp.stream.is_none());
// FTPS
let ftp: FtpFileTransfer = FtpFileTransfer::new(true);
assert_eq!(ftp.ftps, true);
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
let hostname: String = String::from("127.0.0.1");
assert!(ftp
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address(hostname)
.port(10021)
.username(Some("test"))
.password(Some("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());
// Remake directory (should report already exists)
assert_eq!(
ftp.mkdir(PathBuf::from("/home").as_path())
.err()
.unwrap()
.kind(),
FileTransferErrorType::DirectoryAlreadyExists
);
// Make directory (err)
assert!(ftp.mkdir(PathBuf::from("/root/pommlar").as_path()).is_err());
// Change directory
assert!(ftp.change_dir(PathBuf::from("/home").as_path()).is_ok());
// Change directory (err)
assert!(ftp
.change_dir(PathBuf::from("/tmp/oooo/aaaa/eee").as_path())
.is_err());
// Copy (not supported)
assert!(ftp
.copy(&FsEntry::File(entry.clone()), PathBuf::from("/").as_path())
.is_err());
// Exec (not supported)
assert!(ftp.exec("echo 1;").is_err());
// Upload 2 files
let mut writable = ftp
.send_file(&entry, PathBuf::from("omar.txt").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(ftp.on_sent(writable).is_ok());
let mut writable = ftp
.send_file(&entry, PathBuf::from("README.md").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(ftp.on_sent(writable).is_ok());
// Upload file (err)
assert!(ftp
.send_file(&entry, PathBuf::from("/ommlar/omarone").as_path())
.is_err());
// List dir
let list: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/home").as_path()).ok().unwrap();
assert_eq!(list.len(), 2);
// Find
assert!(ftp.change_dir(PathBuf::from("/").as_path()).is_ok());
assert_eq!(ftp.find("*.txt").ok().unwrap().len(), 1);
assert_eq!(ftp.find("*.md").ok().unwrap().len(), 1);
assert_eq!(ftp.find("*.jpeg").ok().unwrap().len(), 0);
assert!(ftp.change_dir(PathBuf::from("/home").as_path()).is_ok());
// Rename
assert!(ftp.mkdir(PathBuf::from("/uploads").as_path()).is_ok());
assert!(ftp
.rename(
list.get(0).unwrap(),
PathBuf::from("/uploads/README.txt").as_path()
)
.is_ok());
// Rename (err)
assert!(ftp
.rename(list.get(0).unwrap(), PathBuf::from("OMARONE").as_path())
.is_err());
let dummy: FsEntry = FsEntry::File(FsFile {
name: String::from("cucumber.txt"),
abs_path: PathBuf::from("/cucumber.txt"),
last_change_time: UNIX_EPOCH,
last_access_time: UNIX_EPOCH,
creation_time: UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
assert!(ftp
.rename(&dummy, PathBuf::from("/a/b/c").as_path())
.is_err());
// Remove
assert!(ftp.remove(list.get(1).unwrap()).is_ok());
assert!(ftp.remove(list.get(1).unwrap()).is_err());
// Receive file
let mut writable = ftp
.send_file(&entry, PathBuf::from("/uploads/README.txt").as_path())
.ok()
.unwrap();
write_file(&file, &mut writable);
assert!(ftp.on_sent(writable).is_ok());
let file: FsFile = ftp
.list_dir(PathBuf::from("/uploads").as_path())
.ok()
.unwrap()
.get(0)
.unwrap()
.clone()
.unwrap_file();
let mut readable = ftp.recv_file(&file).ok().unwrap();
let mut data: Vec<u8> = vec![0; 1024];
assert!(readable.read(&mut data).is_ok());
assert!(ftp.on_recv(readable).is_ok());
// Receive file (err)
assert!(ftp.recv_file(&entry).is_err());
// Cleanup
assert!(ftp.change_dir(PathBuf::from("/").as_path()).is_ok());
assert!(ftp
.remove(&make_fsentry(PathBuf::from("/home"), true))
.is_ok());
assert!(ftp
.remove(&make_fsentry(PathBuf::from("/uploads"), true))
.is_ok());
// Disconnect
assert!(ftp.disconnect().is_ok());
assert_eq!(ftp.is_connected(), false);
}
#[test]
#[cfg(feature = "with-containers")]
fn test_filetransfer_ftp_server_bad_auth() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10021)
.username(Some("omar"))
.password(Some("ommlar"))
))
.is_err());
}
#[test]
#[cfg(feature = "with-containers")]
fn test_filetransfer_ftp_no_credentials() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
assert!(ftp
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("127.0.0.1")
.port(10021)
.username::<&str>(None)
.password::<&str>(None)
))
.is_err());
}
#[test]
fn test_filetransfer_ftp_server_bad_server() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("mybad.veribad.server")
.port(21)
.username::<&str>(None)
.password::<&str>(None)
))
.is_err());
}
#[test]
fn test_filetransfer_ftp_parse_list_line_unix() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Simple file
let file: FsFile = ftp
.parse_list_lines(
PathBuf::from("/tmp").as_path(),
vec!["-rw-rw-r-- 1 root dialout 8192 Nov 5 2018 omar.txt".to_string()],
)
.get(0)
.unwrap()
.clone()
.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(),
(UnixPex::from(6), UnixPex::from(6), UnixPex::from(4))
);
assert_eq!(
file.last_access_time
.duration_since(UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
file.last_change_time
.duration_since(UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
file.creation_time.duration_since(UNIX_EPOCH).ok().unwrap(),
Duration::from_secs(1541376000)
);
}
#[test]
fn test_filetransfer_ftp_list_dir_dos_syntax() {
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
// Connect
assert!(ftp
.connect(&ProtocolParams::Generic(
GenericProtocolParams::default()
.address("test.rebex.net")
.port(21)
.username(Some("demo"))
.password(Some("password"))
))
.is_ok());
// Pwd
assert_eq!(ftp.pwd().ok().unwrap(), PathBuf::from("/"));
// List dir
let files: Vec<FsEntry> = ftp.list_dir(PathBuf::from("/").as_path()).ok().unwrap();
// There should be at least 1 file
assert!(files.len() > 0);
// Disconnect
assert!(ftp.disconnect().is_ok());
}
#[test]
fn test_filetransfer_ftp_uninitialized() {
let file: FsFile = FsFile {
name: String::from("omar.txt"),
abs_path: PathBuf::from("/omar.txt"),
last_change_time: UNIX_EPOCH,
last_access_time: UNIX_EPOCH,
creation_time: UNIX_EPOCH,
size: 0,
ftype: Some(String::from("txt")), // File type
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
};
let mut ftp: FtpFileTransfer = FtpFileTransfer::new(false);
assert!(ftp.change_dir(Path::new("/tmp")).is_err());
assert!(ftp.disconnect().is_err());
assert!(ftp.list_dir(Path::new("/tmp")).is_err());
assert!(ftp.mkdir(Path::new("/tmp")).is_err());
assert!(ftp
.remove(&make_fsentry(PathBuf::from("/nowhere"), false))
.is_err());
assert!(ftp
.rename(
&make_fsentry(PathBuf::from("/nowhere"), false),
PathBuf::from("/culonia").as_path()
)
.is_err());
assert!(ftp.pwd().is_err());
assert!(ftp.stat(Path::new("/tmp")).is_err());
assert!(ftp.recv_file(&file).is_err());
assert!(ftp.send_file(&file, Path::new("/tmp/omar.txt")).is_err());
let (_, temp): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
let readable: Box<dyn Read> = Box::new(std::fs::File::open(temp.path()).unwrap());
assert!(ftp.on_recv(readable).is_err());
let (_, temp): (FsFile, tempfile::NamedTempFile) = create_sample_file_entry();
let writable: Box<dyn Write> =
Box::new(open_file(temp.path(), true, true, true).ok().unwrap());
assert!(ftp.on_sent(writable).is_err());
}
}
-20
View File
@@ -1,20 +0,0 @@
//! # transfer
//!
//! This module exposes all the file transfers supported by termscp
// -- import
use super::{
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
};
// -- modules
mod ftp;
mod s3;
mod scp;
mod sftp;
// -- export
pub use self::s3::S3FileTransfer;
pub use ftp::FtpFileTransfer;
pub use scp::ScpFileTransfer;
pub use sftp::SftpFileTransfer;
-699
View File
@@ -1,699 +0,0 @@
//! ## S3 transfer
//!
//! S3 file transfer module
/**
* 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.
*/
// -- mod
mod object;
// Locals
use super::{
FileTransfer, FileTransferError, FileTransferErrorType, FileTransferResult, ProtocolParams,
};
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::utils::path;
use object::S3Object;
// ext
use s3::creds::Credentials;
use s3::serde_types::Object;
use s3::{Bucket, Region};
use std::fs::File;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::str::FromStr;
/// ## S3FileTransfer
///
/// Aws s3 file transfer
pub struct S3FileTransfer {
bucket: Option<Bucket>,
wrkdir: PathBuf,
}
impl Default for S3FileTransfer {
fn default() -> Self {
Self {
bucket: None,
wrkdir: PathBuf::from("/"),
}
}
}
impl S3FileTransfer {
/// ### list_objects
///
/// List objects contained in `p` path
fn list_objects(&self, p: &Path, list_dir: bool) -> FileTransferResult<Vec<S3Object>> {
// Make path relative
let key: String = Self::fmt_path(p, list_dir);
debug!("Query list directory {}; key: {}", p.display(), key);
self.query_objects(key, true)
}
/// ### stat_object
///
/// Stat an s3 object
fn stat_object(&self, p: &Path) -> FileTransferResult<S3Object> {
let key: String = Self::fmt_path(p, false);
debug!("Query stat object {}; key: {}", p.display(), key);
let objects = self.query_objects(key, false)?;
// Absolutize path
let absol: PathBuf = path::absolutize(Path::new("/"), p);
// Find associated object
match objects
.into_iter()
.find(|x| x.path.as_path() == absol.as_path())
{
Some(obj) => Ok(obj),
None => Err(FileTransferError::new_ex(
FileTransferErrorType::NoSuchFileOrDirectory,
format!("{}: No such file or directory", p.display()),
)),
}
}
/// ### query_objects
///
/// Query objects at key
fn query_objects(
&self,
key: String,
only_direct_children: bool,
) -> FileTransferResult<Vec<S3Object>> {
let results = self.bucket.as_ref().unwrap().list(key.clone(), None);
match results {
Ok(entries) => {
let mut objects: Vec<S3Object> = Vec::new();
entries.iter().for_each(|x| {
x.contents
.iter()
.filter(|x| {
if only_direct_children {
Self::list_object_should_be_kept(x, key.as_str())
} else {
true
}
})
.for_each(|x| objects.push(S3Object::from(x)))
});
debug!("Found objects: {:?}", objects);
Ok(objects)
}
Err(e) => Err(FileTransferError::new_ex(
FileTransferErrorType::DirStatFailed,
e.to_string(),
)),
}
}
/// ### list_object_should_be_kept
///
/// Returns whether object should be kept after list command.
/// The object won't be kept if:
///
/// 1. is not a direct child of provided dir
fn list_object_should_be_kept(obj: &Object, dir: &str) -> bool {
Self::is_direct_child(obj.key.as_str(), dir)
}
/// ### is_direct_child
///
/// Checks whether Object's key is direct child of `parent` path.
fn is_direct_child(key: &str, parent: &str) -> bool {
key == format!("{}{}", parent, S3Object::object_name(key))
|| key == format!("{}{}/", parent, S3Object::object_name(key))
}
/// ### resolve
///
/// Make s3 absolute path from a given path
fn resolve(&self, p: &Path) -> PathBuf {
path::diff_paths(path::absolutize(self.wrkdir.as_path(), p), &Path::new("/"))
.unwrap_or_default()
}
/// ### fmt_fs_entry_path
///
/// fmt path for fsentry according to format expected by s3
fn fmt_fs_file_path(f: &FsFile) -> String {
Self::fmt_path(f.abs_path.as_path(), false)
}
/// ### fmt_path
///
/// fmt path for fsentry according to format expected by s3
fn fmt_path(p: &Path, is_dir: bool) -> String {
// prevent root as slash
if p == Path::new("/") {
return "".to_string();
}
// Remove root only if absolute
#[cfg(target_family = "unix")]
let is_absolute: bool = p.is_absolute();
// NOTE: don't use is_absolute: on windows won't work
#[cfg(target_family = "windows")]
let is_absolute: bool = p.display().to_string().starts_with('/');
let p: PathBuf = match is_absolute {
true => path::diff_paths(p, &Path::new("/")).unwrap_or_default(),
false => p.to_path_buf(),
};
// NOTE: windows only: resolve paths
#[cfg(target_family = "windows")]
let p: PathBuf = PathBuf::from(path_slash::PathExt::to_slash_lossy(p.as_path()).as_str());
// Fmt
match is_dir {
true => {
let mut p: String = p.display().to_string();
if !p.ends_with('/') {
p.push('/');
}
p
}
false => p.to_string_lossy().to_string(),
}
}
}
impl FileTransfer for S3FileTransfer {
/// ### connect
///
/// Connect to the remote server
/// Can return banner / welcome message on success
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>> {
// Verify parameters are S3
let params = match params.s3_params() {
Some(params) => params,
None => return Err(FileTransferError::new(FileTransferErrorType::BadAddress)),
};
// Load credentials
debug!("Loading credentials... (profile {:?})", params.profile);
let credentials: Credentials =
Credentials::new(None, None, None, None, params.profile.as_deref()).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("Could not load s3 credentials: {}", e),
)
})?;
// Parse region
debug!("Parsing region {}", params.region);
let region: Region = Region::from_str(params.region.as_str()).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("Could not parse s3 region: {}", e),
)
})?;
debug!(
"Credentials loaded! Connecting to bucket {}...",
params.bucket_name
);
self.bucket = Some(
Bucket::new(params.bucket_name.as_str(), region, credentials).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::AuthenticationFailed,
format!("Could not connect to bucket {}: {}", params.bucket_name, e),
)
})?,
);
info!("Connection successfully established");
Ok(None)
}
/// ### disconnect
///
/// Disconnect from the remote server
fn disconnect(&mut self) -> FileTransferResult<()> {
info!("Disconnecting from S3 bucket...");
match self.bucket.take() {
Some(bucket) => {
drop(bucket);
Ok(())
}
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### is_connected
///
/// Indicates whether the client is connected to remote
fn is_connected(&self) -> bool {
self.bucket.is_some()
}
/// ### pwd
///
/// Print working directory
fn pwd(&mut self) -> FileTransferResult<PathBuf> {
info!("PWD");
match self.is_connected() {
true => Ok(self.wrkdir.clone()),
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### change_dir
///
/// Change working directory
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf> {
match &self.bucket.is_some() {
true => {
// Always allow entering root
if dir == Path::new("/") {
self.wrkdir = dir.to_path_buf();
info!("New working directory: {}", self.wrkdir.display());
return Ok(self.wrkdir.clone());
}
// Check if directory exists
debug!("Entering directory {}...", dir.display());
let dir_p: PathBuf = self.resolve(dir);
let dir_s: String = Self::fmt_path(dir_p.as_path(), true);
debug!("Searching for key {} (path: {})...", dir_s, dir_p.display());
// Check if directory already exists
if self
.stat_object(PathBuf::from(dir_s.as_str()).as_path())
.is_ok()
{
self.wrkdir = path::absolutize(Path::new("/"), dir_p.as_path());
info!("New working directory: {}", self.wrkdir.display());
Ok(self.wrkdir.clone())
} else {
Err(FileTransferError::new(
FileTransferErrorType::NoSuchFileOrDirectory,
))
}
}
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### copy
///
/// Copy file to destination
fn copy(&mut self, _src: &FsEntry, _dst: &Path) -> FileTransferResult<()> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### list_dir
///
/// List directory entries
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>> {
match self.is_connected() {
true => self
.list_objects(path, true)
.map(|x| x.into_iter().map(|x| x.into()).collect()),
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### mkdir
///
/// Make directory
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()> {
match &self.bucket {
Some(bucket) => {
let dir: String = Self::fmt_path(self.resolve(dir).as_path(), true);
debug!("Making directory {}...", dir);
// Check if directory already exists
if self
.stat_object(PathBuf::from(dir.as_str()).as_path())
.is_ok()
{
error!("Directory {} already exists", dir);
return Err(FileTransferError::new(
FileTransferErrorType::DirectoryAlreadyExists,
));
}
bucket
.put_object(dir.as_str(), &[])
.map(|_| ())
.map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("Could not make directory: {}", e),
)
})
}
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### remove
///
/// Remove a file or a directory
fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()> {
let path = Self::fmt_path(
path::diff_paths(file.get_abs_path(), &Path::new("/"))
.unwrap_or_default()
.as_path(),
file.is_dir(),
);
info!("Removing object {}...", path);
match &self.bucket {
Some(bucket) => bucket.delete_object(path).map(|_| ()).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("Could not remove file: {}", e),
)
}),
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### rename
///
/// Rename file or a directory
fn rename(&mut self, _file: &FsEntry, _dst: &Path) -> FileTransferResult<()> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### stat
///
/// Stat file and return FsEntry
fn stat(&mut self, p: &Path) -> FileTransferResult<FsEntry> {
match self.is_connected() {
true => {
// First try as a "file"
let path: PathBuf = self.resolve(p);
if let Ok(obj) = self.stat_object(path.as_path()) {
return Ok(obj.into());
}
// Try as a "directory"
debug!("Failed to stat object as file; trying as a directory...");
let path: PathBuf = PathBuf::from(Self::fmt_path(path.as_path(), true));
self.stat_object(path.as_path()).map(|x| x.into())
}
false => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### exec
///
/// Execute a command on remote host
fn exec(&mut self, _cmd: &str) -> FileTransferResult<String> {
Err(FileTransferError::new(
FileTransferErrorType::UnsupportedFeature,
))
}
/// ### send_file_wno_stream
///
/// Send a file to remote WITHOUT using streams.
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
/// By default this function uses the streams function to copy content from reader to writer
fn send_file_wno_stream(
&mut self,
_src: &FsFile,
dest: &Path,
mut reader: Box<dyn Read>,
) -> FileTransferResult<()> {
match &mut self.bucket {
Some(bucket) => {
let key = Self::fmt_path(dest, false);
info!("Query PUT for key '{}'", key);
bucket
.put_object_stream(&mut reader, key.as_str())
.map(|_| ())
.map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("Could not put file: {}", e),
)
})
}
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
/// ### recv_file_wno_stream
///
/// Receive a file from remote WITHOUT using streams.
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
/// By default this function uses the streams function to copy content from reader to writer
fn recv_file_wno_stream(&mut self, src: &FsFile, dest: &Path) -> FileTransferResult<()> {
match &mut self.bucket {
Some(bucket) => {
let mut writer = File::create(dest).map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::FileCreateDenied,
format!("Could not open local file: {}", e),
)
})?;
let key = Self::fmt_fs_file_path(src);
info!("Query GET for key '{}'", key);
bucket
.get_object_stream(key.as_str(), &mut writer)
.map(|_| ())
.map_err(|e| {
FileTransferError::new_ex(
FileTransferErrorType::ProtocolError,
format!("Could not get file: {}", e),
)
})
}
None => Err(FileTransferError::new(
FileTransferErrorType::UninitializedSession,
)),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[cfg(feature = "with-s3-ci")]
use crate::filetransfer::params::AwsS3Params;
#[cfg(feature = "with-s3-ci")]
use crate::utils::random;
use crate::utils::test_helpers;
use pretty_assertions::assert_eq;
#[cfg(feature = "with-s3-ci")]
use std::env;
#[cfg(feature = "with-s3-ci")]
use tempfile::NamedTempFile;
#[test]
fn s3_new() {
let s3: S3FileTransfer = S3FileTransfer::default();
assert_eq!(s3.wrkdir.as_path(), Path::new("/"));
assert!(s3.bucket.is_none());
}
#[test]
fn s3_is_direct_child() {
assert_eq!(S3FileTransfer::is_direct_child("pippo/", ""), true);
assert_eq!(
S3FileTransfer::is_direct_child("pippo/sottocartella/", ""),
false
);
assert_eq!(
S3FileTransfer::is_direct_child("pippo/sottocartella/", "pippo/"),
true
);
assert_eq!(
S3FileTransfer::is_direct_child("pippo/sottocartella/", "pippo"), // This case must be handled indeed
false
);
assert_eq!(
S3FileTransfer::is_direct_child(
"pippo/sottocartella/readme.md",
"pippo/sottocartella/"
),
true
);
assert_eq!(
S3FileTransfer::is_direct_child(
"pippo/sottocartella/readme.md",
"pippo/sottocartella/"
),
true
);
}
#[test]
fn s3_resolve() {
let mut s3: S3FileTransfer = S3FileTransfer::default();
s3.wrkdir = PathBuf::from("/tmp");
// Absolute
assert_eq!(
s3.resolve(&Path::new("/tmp/sottocartella/")).as_path(),
Path::new("tmp/sottocartella")
);
// Relative
assert_eq!(
s3.resolve(&Path::new("subfolder/")).as_path(),
Path::new("tmp/subfolder")
);
}
#[test]
fn s3_fmt_fs_file_path() {
let f: FsFile =
test_helpers::make_fsentry(&Path::new("/tmp/omar.txt"), false).unwrap_file();
assert_eq!(
S3FileTransfer::fmt_fs_file_path(&f).as_str(),
"tmp/omar.txt"
);
}
#[test]
fn s3_fmt_path() {
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("/tmp/omar.txt"), false).as_str(),
"tmp/omar.txt"
);
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("omar.txt"), false).as_str(),
"omar.txt"
);
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("/tmp/subfolder"), true).as_str(),
"tmp/subfolder/"
);
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("tmp/subfolder"), true).as_str(),
"tmp/subfolder/"
);
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("tmp"), true).as_str(),
"tmp/"
);
assert_eq!(
S3FileTransfer::fmt_path(&Path::new("tmp/"), true).as_str(),
"tmp/"
);
assert_eq!(S3FileTransfer::fmt_path(&Path::new("/"), true).as_str(), "");
}
// -- test transfer
#[cfg(feature = "with-s3-ci")]
#[test]
fn s3_filetransfer() {
// Gather s3 environment args
let bucket: String = env::var("AWS_S3_BUCKET").ok().unwrap();
let region: String = env::var("AWS_S3_REGION").ok().unwrap();
let params = get_ftparams(bucket, region);
// Get transfer
let mut s3 = S3FileTransfer::default();
// Connect
assert!(s3.connect(&params).is_ok());
// Check is connected
assert_eq!(s3.is_connected(), true);
// Pwd
assert_eq!(s3.pwd().ok().unwrap(), PathBuf::from("/"));
// Go to github-ci directory
assert!(s3.change_dir(&Path::new("/github-ci")).is_ok());
assert_eq!(s3.pwd().ok().unwrap(), PathBuf::from("/github-ci"));
// Find
assert_eq!(s3.find("*.jpg").ok().unwrap().len(), 1);
// List directory (3 entries)
assert_eq!(s3.list_dir(&Path::new("/github-ci")).ok().unwrap().len(), 3);
// Go to playground
assert!(s3.change_dir(&Path::new("/github-ci/playground")).is_ok());
assert_eq!(
s3.pwd().ok().unwrap(),
PathBuf::from("/github-ci/playground")
);
// Create directory
let dir_name: String = format!("{}/", random::random_alphanumeric_with_len(8));
let mut dir_path: PathBuf = PathBuf::from("/github-ci/playground");
dir_path.push(dir_name.as_str());
let dir_entry = test_helpers::make_fsentry(dir_path.as_path(), true);
assert!(s3.mkdir(dir_path.as_path()).is_ok());
assert!(s3.change_dir(dir_path.as_path()).is_ok());
// Copy/rename file is unsupported
assert!(s3.copy(&dir_entry, &Path::new("/copia")).is_err());
assert!(s3.rename(&dir_entry, &Path::new("/copia")).is_err());
// Exec is unsupported
assert!(s3.exec("omar!").is_err());
// Stat file
let entry = s3
.stat(&Path::new("/github-ci/avril_lavigne.jpg"))
.ok()
.unwrap()
.unwrap_file();
assert_eq!(entry.name.as_str(), "avril_lavigne.jpg");
assert_eq!(
entry.abs_path.as_path(),
Path::new("/github-ci/avril_lavigne.jpg")
);
assert_eq!(entry.ftype.as_deref().unwrap(), "jpg");
assert_eq!(entry.size, 101738);
assert_eq!(entry.user, None);
assert_eq!(entry.group, None);
assert_eq!(entry.unix_pex, None);
// Download file
let (local_file_entry, local_file): (FsFile, NamedTempFile) =
test_helpers::create_sample_file_entry();
let remote_entry =
test_helpers::make_fsentry(&Path::new("/github-ci/avril_lavigne.jpg"), false)
.unwrap_file();
assert!(s3
.recv_file_wno_stream(&remote_entry, local_file.path())
.is_ok());
// Upload file
let mut dest_path = dir_path.clone();
dest_path.push("aurellia_lavagna.jpg");
let reader = Box::new(File::open(local_file.path()).ok().unwrap());
assert!(s3
.send_file_wno_stream(&local_file_entry, dest_path.as_path(), reader)
.is_ok());
// Remove temp dir
assert!(s3.remove(&dir_entry).is_ok());
// Disconnect
assert!(s3.disconnect().is_ok());
}
#[cfg(feature = "with-s3-ci")]
fn get_ftparams(bucket: String, region: String) -> ProtocolParams {
ProtocolParams::AwsS3(AwsS3Params::new(bucket, region, None))
}
}
-247
View File
@@ -1,247 +0,0 @@
//! ## S3 object
//!
//! This module exposes the S3Object structure, which is an intermediate structure to work with
//! S3 objects. Easy to be converted into a FsEntry.
/**
* 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 super::{FsDirectory, FsEntry, FsFile, Object};
use crate::utils::parser::parse_datetime;
use crate::utils::path;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
/// ## S3Object
///
/// An intermediate struct to work with s3 `Object`.
/// Really easy to be converted into a `FsEntry`
#[derive(Debug)]
pub struct S3Object {
pub name: String,
pub path: PathBuf,
pub size: usize,
pub last_modified: SystemTime,
/// Whether or not represents a directory. I already know directories don't exist in s3!
pub is_dir: bool,
}
impl From<&Object> for S3Object {
fn from(obj: &Object) -> Self {
let is_dir: bool = obj.key.ends_with('/');
let abs_path: PathBuf = path::absolutize(
PathBuf::from("/").as_path(),
PathBuf::from(obj.key.as_str()).as_path(),
);
let last_modified: SystemTime =
match parse_datetime(obj.last_modified.as_str(), "%Y-%m-%dT%H:%M:%S%Z") {
Ok(dt) => dt,
Err(_) => UNIX_EPOCH,
};
Self {
name: Self::object_name(obj.key.as_str()),
path: abs_path,
size: obj.size as usize,
last_modified,
is_dir,
}
}
}
impl From<S3Object> for FsEntry {
fn from(obj: S3Object) -> Self {
let abs_path: PathBuf = path::absolutize(Path::new("/"), obj.path.as_path());
match obj.is_dir {
true => FsEntry::Directory(FsDirectory {
name: obj.name,
abs_path,
last_change_time: obj.last_modified,
last_access_time: obj.last_modified,
creation_time: obj.last_modified,
symlink: None,
user: None,
group: None,
unix_pex: None,
}),
false => FsEntry::File(FsFile {
name: obj.name,
ftype: obj
.path
.extension()
.map(|x| x.to_string_lossy().to_string()),
abs_path,
size: obj.size,
last_change_time: obj.last_modified,
last_access_time: obj.last_modified,
creation_time: obj.last_modified,
symlink: None,
user: None,
group: None,
unix_pex: None,
}),
}
}
}
impl S3Object {
/// ### object_name
///
/// Get object name from key
pub fn object_name(key: &str) -> String {
let mut tokens = key.split('/');
let count = tokens.clone().count();
let demi_last: String = match count > 1 {
true => tokens.nth(count - 2).unwrap().to_string(),
false => String::new(),
};
if let Some(last) = tokens.last() {
// If last is not empty, return last one
if !last.is_empty() {
return last.to_string();
}
}
// Return demi last
demi_last
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
use std::time::Duration;
#[test]
fn object_to_s3object_file() {
let obj: Object = Object {
key: String::from("pippo/sottocartella/chiedo.gif"),
e_tag: String::default(),
size: 1516966,
owner: None,
storage_class: String::default(),
last_modified: String::from("2021-08-28T10:20:37.000Z"),
};
let s3_obj: S3Object = S3Object::from(&obj);
assert_eq!(s3_obj.name.as_str(), "chiedo.gif");
assert_eq!(
s3_obj.path.as_path(),
Path::new("/pippo/sottocartella/chiedo.gif")
);
assert_eq!(s3_obj.size, 1516966);
assert_eq!(s3_obj.is_dir, false);
assert_eq!(
s3_obj
.last_modified
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1630146037)
);
}
#[test]
fn object_to_s3object_dir() {
let obj: Object = Object {
key: String::from("temp/"),
e_tag: String::default(),
size: 0,
owner: None,
storage_class: String::default(),
last_modified: String::from("2021-08-28T10:20:37.000Z"),
};
let s3_obj: S3Object = S3Object::from(&obj);
assert_eq!(s3_obj.name.as_str(), "temp");
assert_eq!(s3_obj.path.as_path(), Path::new("/temp"));
assert_eq!(s3_obj.size, 0);
assert_eq!(s3_obj.is_dir, true);
assert_eq!(
s3_obj
.last_modified
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1630146037)
);
}
#[test]
fn fsentry_from_s3obj_file() {
let obj: S3Object = S3Object {
name: String::from("chiedo.gif"),
path: PathBuf::from("/pippo/sottocartella/chiedo.gif"),
size: 1516966,
is_dir: false,
last_modified: UNIX_EPOCH,
};
let entry: FsFile = FsEntry::from(obj).unwrap_file();
assert_eq!(entry.name.as_str(), "chiedo.gif");
assert_eq!(
entry.abs_path.as_path(),
Path::new("/pippo/sottocartella/chiedo.gif")
);
assert_eq!(entry.creation_time, UNIX_EPOCH);
assert_eq!(entry.last_change_time, UNIX_EPOCH);
assert_eq!(entry.last_access_time, UNIX_EPOCH);
assert_eq!(entry.size, 1516966);
assert_eq!(entry.ftype.unwrap().as_str(), "gif");
assert_eq!(entry.user, None);
assert_eq!(entry.group, None);
assert_eq!(entry.unix_pex, None);
}
#[test]
fn fsentry_from_s3obj_directory() {
let obj: S3Object = S3Object {
name: String::from("temp"),
path: PathBuf::from("/temp"),
size: 0,
is_dir: true,
last_modified: UNIX_EPOCH,
};
let entry: FsDirectory = FsEntry::from(obj).unwrap_dir();
assert_eq!(entry.name.as_str(), "temp");
assert_eq!(entry.abs_path.as_path(), Path::new("/temp"));
assert_eq!(entry.creation_time, UNIX_EPOCH);
assert_eq!(entry.last_change_time, UNIX_EPOCH);
assert_eq!(entry.last_access_time, UNIX_EPOCH);
assert_eq!(entry.user, None);
assert_eq!(entry.group, None);
assert_eq!(entry.unix_pex, None);
}
#[test]
fn object_name() {
assert_eq!(
S3Object::object_name("pippo/sottocartella/chiedo.gif").as_str(),
"chiedo.gif"
);
assert_eq!(
S3Object::object_name("pippo/sottocartella/").as_str(),
"sottocartella"
);
assert_eq!(S3Object::object_name("pippo/").as_str(), "pippo");
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-578
View File
@@ -1,578 +0,0 @@
//! ## Fs
//!
//! `fs` is the module which provides file system entities
/**
* 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.
*/
// Mod
pub mod explorer;
// Ext
use std::path::PathBuf;
use std::time::SystemTime;
/// ## FsEntry
///
/// FsEntry represents a generic entry in a directory
#[derive(Clone, std::fmt::Debug)]
pub enum FsEntry {
Directory(FsDirectory),
File(FsFile),
}
/// ## FsDirectory
///
/// Directory provides an interface to file system directories
#[derive(Clone, std::fmt::Debug)]
pub struct FsDirectory {
pub name: String,
pub abs_path: PathBuf,
pub last_change_time: SystemTime,
pub last_access_time: SystemTime,
pub creation_time: SystemTime,
pub symlink: Option<Box<FsEntry>>, // UNIX only
pub user: Option<u32>, // UNIX only
pub group: Option<u32>, // UNIX only
pub unix_pex: Option<(UnixPex, UnixPex, UnixPex)>, // UNIX only
}
/// ### FsFile
///
/// FsFile provides an interface to file system files
#[derive(Clone, std::fmt::Debug)]
pub struct FsFile {
pub name: String,
pub abs_path: PathBuf,
pub last_change_time: SystemTime,
pub last_access_time: SystemTime,
pub creation_time: SystemTime,
pub size: usize,
pub ftype: Option<String>, // File type
pub symlink: Option<Box<FsEntry>>, // UNIX only
pub user: Option<u32>, // UNIX only
pub group: Option<u32>, // UNIX only
pub unix_pex: Option<(UnixPex, UnixPex, UnixPex)>, // UNIX only
}
/// ## UnixPex
///
/// Describes the permissions on POSIX system.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct UnixPex {
read: bool,
write: bool,
execute: bool,
}
impl UnixPex {
/// ### new
///
/// Instantiates a new `UnixPex`
pub fn new(read: bool, write: bool, execute: bool) -> Self {
Self {
read,
write,
execute,
}
}
/// ### can_read
///
/// Returns whether user can read
pub fn can_read(&self) -> bool {
self.read
}
/// ### can_write
///
/// Returns whether user can write
pub fn can_write(&self) -> bool {
self.write
}
/// ### can_execute
///
/// Returns whether user can execute
pub fn can_execute(&self) -> bool {
self.execute
}
/// ### as_byte
///
/// Convert permission to byte as on POSIX systems
pub fn as_byte(&self) -> u8 {
((self.read as u8) << 2) + ((self.write as u8) << 1) + (self.execute as u8)
}
}
impl From<u8> for UnixPex {
fn from(bits: u8) -> Self {
Self {
read: ((bits >> 2) & 0x01) != 0,
write: ((bits >> 1) & 0x01) != 0,
execute: (bits & 0x01) != 0,
}
}
}
impl FsEntry {
/// ### get_abs_path
///
/// Get absolute path from `FsEntry`
pub fn get_abs_path(&self) -> PathBuf {
match self {
FsEntry::Directory(dir) => dir.abs_path.clone(),
FsEntry::File(file) => file.abs_path.clone(),
}
}
/// ### get_name
///
/// Get file name from `FsEntry`
pub fn get_name(&self) -> &'_ str {
match self {
FsEntry::Directory(dir) => dir.name.as_ref(),
FsEntry::File(file) => file.name.as_ref(),
}
}
/// ### get_last_change_time
///
/// Get last change time from `FsEntry`
pub fn get_last_change_time(&self) -> SystemTime {
match self {
FsEntry::Directory(dir) => dir.last_change_time,
FsEntry::File(file) => file.last_change_time,
}
}
/// ### get_last_access_time
///
/// Get access time from `FsEntry`
pub fn get_last_access_time(&self) -> SystemTime {
match self {
FsEntry::Directory(dir) => dir.last_access_time,
FsEntry::File(file) => file.last_access_time,
}
}
/// ### get_creation_time
///
/// Get creation time from `FsEntry`
pub fn get_creation_time(&self) -> SystemTime {
match self {
FsEntry::Directory(dir) => dir.creation_time,
FsEntry::File(file) => file.creation_time,
}
}
/// ### get_size
///
/// Get size from `FsEntry`. For directories is always 4096
pub fn get_size(&self) -> usize {
match self {
FsEntry::Directory(_) => 4096,
FsEntry::File(file) => file.size,
}
}
/// ### get_ftype
///
/// Get file type from `FsEntry`. For directories is always None
pub fn get_ftype(&self) -> Option<String> {
match self {
FsEntry::Directory(_) => None,
FsEntry::File(file) => file.ftype.clone(),
}
}
/// ### get_user
///
/// Get uid from `FsEntry`
pub fn get_user(&self) -> Option<u32> {
match self {
FsEntry::Directory(dir) => dir.user,
FsEntry::File(file) => file.user,
}
}
/// ### get_group
///
/// Get gid from `FsEntry`
pub fn get_group(&self) -> Option<u32> {
match self {
FsEntry::Directory(dir) => dir.group,
FsEntry::File(file) => file.group,
}
}
/// ### get_unix_pex
///
/// Get unix pex from `FsEntry`
pub fn get_unix_pex(&self) -> Option<(UnixPex, UnixPex, UnixPex)> {
match self {
FsEntry::Directory(dir) => dir.unix_pex,
FsEntry::File(file) => file.unix_pex,
}
}
/// ### is_symlink
///
/// Returns whether the `FsEntry` is a symlink
pub fn is_symlink(&self) -> bool {
match self {
FsEntry::Directory(dir) => dir.symlink.is_some(),
FsEntry::File(file) => file.symlink.is_some(),
}
}
/// ### is_dir
///
/// Returns whether a FsEntry is a directory
pub fn is_dir(&self) -> bool {
matches!(self, FsEntry::Directory(_))
}
/// ### is_file
///
/// Returns whether a FsEntry is a File
pub fn is_file(&self) -> bool {
matches!(self, FsEntry::File(_))
}
/// ### is_hidden
///
/// Returns whether FsEntry is hidden
pub fn is_hidden(&self) -> bool {
self.get_name().starts_with('.')
}
/// ### get_realfile
///
/// Return the real file pointed by a `FsEntry`
pub fn get_realfile(&self) -> FsEntry {
match self {
FsEntry::Directory(dir) => match &dir.symlink {
Some(symlink) => symlink.get_realfile(),
None => self.clone(),
},
FsEntry::File(file) => match &file.symlink {
Some(symlink) => symlink.get_realfile(),
None => self.clone(),
},
}
}
/// ### 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)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_fs_fsentry_dir() {
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,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
assert_eq!(entry.get_abs_path(), PathBuf::from("/foo"));
assert_eq!(entry.get_name(), String::from("foo"));
assert_eq!(entry.get_last_access_time(), t_now);
assert_eq!(entry.get_last_change_time(), t_now);
assert_eq!(entry.get_creation_time(), t_now);
assert_eq!(entry.get_size(), 4096);
assert_eq!(entry.get_ftype(), None);
assert_eq!(entry.get_user(), Some(0));
assert_eq!(entry.get_group(), Some(0));
assert_eq!(entry.is_symlink(), false);
assert_eq!(entry.is_dir(), true);
assert_eq!(entry.is_file(), false);
assert_eq!(
entry.get_unix_pex(),
Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5)))
);
assert_eq!(entry.unwrap_dir().abs_path, PathBuf::from("/foo"));
}
#[test]
fn test_fs_fsentry_file() {
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,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
assert_eq!(entry.get_abs_path(), PathBuf::from("/bar.txt"));
assert_eq!(entry.get_name(), String::from("bar.txt"));
assert_eq!(entry.get_last_access_time(), t_now);
assert_eq!(entry.get_last_change_time(), t_now);
assert_eq!(entry.get_creation_time(), t_now);
assert_eq!(entry.get_size(), 8192);
assert_eq!(entry.get_ftype(), Some(String::from("txt")));
assert_eq!(entry.get_user(), Some(0));
assert_eq!(entry.get_group(), Some(0));
assert_eq!(
entry.get_unix_pex(),
Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4)))
);
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,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(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,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
entry.unwrap_file();
}
#[test]
fn test_fs_fsentry_hidden_files() {
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,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
assert_eq!(entry.is_hidden(), false);
let entry: FsEntry = FsEntry::File(FsFile {
name: String::from(".gitignore"),
abs_path: PathBuf::from("/.gitignore"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
assert_eq!(entry.is_hidden(), true);
let entry: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from(".git"),
abs_path: PathBuf::from("/.git"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
assert_eq!(entry.is_hidden(), true);
}
#[test]
fn test_fs_fsentry_realfile_none() {
let t_now: SystemTime = SystemTime::now();
// With file...
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,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
});
// Symlink is None...
assert_eq!(
entry.get_realfile().get_abs_path(),
PathBuf::from("/bar.txt")
);
// With directory...
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,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
});
assert_eq!(entry.get_realfile().get_abs_path(), PathBuf::from("/foo"));
}
#[test]
fn test_fs_fsentry_realfile_some() {
let t_now: SystemTime = SystemTime::now();
// Prepare entries
// root -> child -> target
let entry_target: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))), // UNIX only
});
let entry_child: FsEntry = FsEntry::Directory(FsDirectory {
name: String::from("projects"),
abs_path: PathBuf::from("/develop/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
symlink: Some(Box::new(entry_target)),
user: Some(0),
group: Some(0),
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))),
});
let entry_root: FsEntry = FsEntry::File(FsFile {
name: String::from("projects"),
abs_path: PathBuf::from("/projects"),
last_change_time: t_now,
last_access_time: t_now,
creation_time: t_now,
size: 8,
ftype: None,
symlink: Some(Box::new(entry_child)),
user: Some(0),
group: Some(0),
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))),
});
assert_eq!(entry_root.is_symlink(), true);
// get real file
let real_file: FsEntry = entry_root.get_realfile();
// real file must be projects in /home/cvisintin
assert_eq!(
real_file.get_abs_path(),
PathBuf::from("/home/cvisintin/projects")
);
}
#[test]
fn unix_pex() {
let pex: UnixPex = UnixPex::from(4);
assert_eq!(pex.can_read(), true);
assert_eq!(pex.can_write(), false);
assert_eq!(pex.can_execute(), false);
let pex: UnixPex = UnixPex::from(0);
assert_eq!(pex.can_read(), false);
assert_eq!(pex.can_write(), false);
assert_eq!(pex.can_execute(), false);
let pex: UnixPex = UnixPex::from(3);
assert_eq!(pex.can_read(), false);
assert_eq!(pex.can_write(), true);
assert_eq!(pex.can_execute(), true);
let pex: UnixPex = UnixPex::from(7);
assert_eq!(pex.can_read(), true);
assert_eq!(pex.can_write(), true);
assert_eq!(pex.can_execute(), true);
let pex: UnixPex = UnixPex::from(3);
assert_eq!(pex.as_byte(), 3);
let pex: UnixPex = UnixPex::from(7);
assert_eq!(pex.as_byte(), 7);
}
}
+165 -207
View File
@@ -26,7 +26,10 @@
* SOFTWARE. * SOFTWARE.
*/ */
// ext // ext
use std::fs::{self, File, Metadata, OpenOptions}; #[cfg(target_family = "unix")]
use remotefs::fs::UnixPex;
use remotefs::fs::{Directory, Entry, File, Metadata};
use std::fs::{self, File as StdFile, OpenOptions};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::SystemTime; use std::time::SystemTime;
use thiserror::Error; use thiserror::Error;
@@ -38,9 +41,6 @@ use std::fs::set_permissions;
use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::fs::{MetadataExt, PermissionsExt};
// Locals // Locals
#[cfg(target_family = "unix")]
use crate::fs::UnixPex;
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::utils::path; use crate::utils::path;
/// ## HostErrorType /// ## HostErrorType
@@ -118,7 +118,7 @@ impl std::fmt::Display for HostError {
/// It provides functions to navigate across the local host file system /// It provides functions to navigate across the local host file system
pub struct Localhost { pub struct Localhost {
wrkdir: PathBuf, wrkdir: PathBuf,
files: Vec<FsEntry>, files: Vec<Entry>,
} }
impl Localhost { impl Localhost {
@@ -169,7 +169,7 @@ impl Localhost {
/// ///
/// List files in current directory /// List files in current directory
#[allow(dead_code)] #[allow(dead_code)]
pub fn list_dir(&self) -> Vec<FsEntry> { pub fn list_dir(&self) -> Vec<Entry> {
self.files.clone() self.files.clone()
} }
@@ -177,7 +177,7 @@ impl Localhost {
/// ///
/// Change working directory with the new provided directory /// Change working directory with the new provided directory
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> { pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
let new_dir: PathBuf = self.to_abs_path(new_dir); let new_dir: PathBuf = self.to_path(new_dir);
info!("Changing localhost directory to {}...", new_dir.display()); info!("Changing localhost directory to {}...", new_dir.display());
// Check whether directory exists // Check whether directory exists
if !self.file_exists(new_dir.as_path()) { if !self.file_exists(new_dir.as_path()) {
@@ -227,7 +227,7 @@ impl Localhost {
/// Extended option version of makedir. /// Extended option version of makedir.
/// ignex: don't report error if directory already exists /// ignex: don't report error if directory already exists
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> { pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
let dir_path: PathBuf = self.to_abs_path(dir_name); let dir_path: PathBuf = self.to_path(dir_name);
info!("Making directory {}", dir_path.display()); info!("Making directory {}", dir_path.display());
// If dir already exists, return Error // If dir already exists, return Error
if dir_path.exists() { if dir_path.exists() {
@@ -265,25 +265,25 @@ impl Localhost {
/// ### remove /// ### remove
/// ///
/// Remove file entry /// Remove file entry
pub fn remove(&mut self, entry: &FsEntry) -> Result<(), HostError> { pub fn remove(&mut self, entry: &Entry) -> Result<(), HostError> {
match entry { match entry {
FsEntry::Directory(dir) => { Entry::Directory(dir) => {
// If file doesn't exist; return error // If file doesn't exist; return error
debug!("Removing directory {}", dir.abs_path.display()); debug!("Removing directory {}", dir.path.display());
if !dir.abs_path.as_path().exists() { if !dir.path.as_path().exists() {
error!("Directory doesn't exist"); error!("Directory doesn't exist");
return Err(HostError::new( return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory, HostErrorType::NoSuchFileOrDirectory,
None, None,
dir.abs_path.as_path(), dir.path.as_path(),
)); ));
} }
// Remove // Remove
match std::fs::remove_dir_all(dir.abs_path.as_path()) { match std::fs::remove_dir_all(dir.path.as_path()) {
Ok(_) => { Ok(_) => {
// Update dir // Update dir
self.files = self.scan_dir(self.wrkdir.as_path())?; self.files = self.scan_dir(self.wrkdir.as_path())?;
info!("Removed directory {}", dir.abs_path.display()); info!("Removed directory {}", dir.path.display());
Ok(()) Ok(())
} }
Err(err) => { Err(err) => {
@@ -291,28 +291,28 @@ impl Localhost {
Err(HostError::new( Err(HostError::new(
HostErrorType::DeleteFailed, HostErrorType::DeleteFailed,
Some(err), Some(err),
dir.abs_path.as_path(), dir.path.as_path(),
)) ))
} }
} }
} }
FsEntry::File(file) => { Entry::File(file) => {
// If file doesn't exist; return error // If file doesn't exist; return error
debug!("Removing file {}", file.abs_path.display()); debug!("Removing file {}", file.path.display());
if !file.abs_path.as_path().exists() { if !file.path.as_path().exists() {
error!("File doesn't exist"); error!("File doesn't exist");
return Err(HostError::new( return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory, HostErrorType::NoSuchFileOrDirectory,
None, None,
file.abs_path.as_path(), file.path.as_path(),
)); ));
} }
// Remove // Remove
match std::fs::remove_file(file.abs_path.as_path()) { match std::fs::remove_file(file.path.as_path()) {
Ok(_) => { Ok(_) => {
// Update dir // Update dir
self.files = self.scan_dir(self.wrkdir.as_path())?; self.files = self.scan_dir(self.wrkdir.as_path())?;
info!("Removed file {}", file.abs_path.display()); info!("Removed file {}", file.path.display());
Ok(()) Ok(())
} }
Err(err) => { Err(err) => {
@@ -320,7 +320,7 @@ impl Localhost {
Err(HostError::new( Err(HostError::new(
HostErrorType::DeleteFailed, HostErrorType::DeleteFailed,
Some(err), Some(err),
file.abs_path.as_path(), file.path.as_path(),
)) ))
} }
} }
@@ -331,15 +331,14 @@ impl Localhost {
/// ### rename /// ### rename
/// ///
/// Rename file or directory to new name /// Rename file or directory to new name
pub fn rename(&mut self, entry: &FsEntry, dst_path: &Path) -> Result<(), HostError> { pub fn rename(&mut self, entry: &Entry, dst_path: &Path) -> Result<(), HostError> {
let abs_path: PathBuf = entry.get_abs_path(); match std::fs::rename(entry.path(), dst_path) {
match std::fs::rename(abs_path.as_path(), dst_path) {
Ok(_) => { Ok(_) => {
// Scan dir // Scan dir
self.files = self.scan_dir(self.wrkdir.as_path())?; self.files = self.scan_dir(self.wrkdir.as_path())?;
debug!( debug!(
"Moved file {} to {}", "Moved file {} to {}",
entry.get_abs_path().display(), entry.path().display(),
dst_path.display() dst_path.display()
); );
Ok(()) Ok(())
@@ -347,14 +346,14 @@ impl Localhost {
Err(err) => { Err(err) => {
error!( error!(
"Failed to move {} to {}: {}", "Failed to move {} to {}: {}",
entry.get_abs_path().display(), entry.path().display(),
dst_path.display(), dst_path.display(),
err err
); );
Err(HostError::new( Err(HostError::new(
HostErrorType::CouldNotCreateFile, HostErrorType::CouldNotCreateFile,
Some(err), Some(err),
abs_path.as_path(), entry.path(),
)) ))
} }
} }
@@ -363,17 +362,17 @@ impl Localhost {
/// ### copy /// ### copy
/// ///
/// Copy file to destination path /// Copy file to destination path
pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> { pub fn copy(&mut self, entry: &Entry, dst: &Path) -> Result<(), HostError> {
// Get absolute path of dest // Get absolute path of dest
let dst: PathBuf = self.to_abs_path(dst); let dst: PathBuf = self.to_path(dst);
info!( info!(
"Copying file {} to {}", "Copying file {} to {}",
entry.get_abs_path().display(), entry.path().display(),
dst.display() dst.display()
); );
// Match entry // Match entry
match entry { match entry {
FsEntry::File(file) => { Entry::File(file) => {
// Copy file // Copy file
// If destination path is a directory, push file name // If destination path is a directory, push file name
let dst: PathBuf = match dst.as_path().is_dir() { let dst: PathBuf = match dst.as_path().is_dir() {
@@ -385,29 +384,29 @@ impl Localhost {
false => dst.clone(), false => dst.clone(),
}; };
// Copy entry path to dst path // Copy entry path to dst path
if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) { if let Err(err) = std::fs::copy(file.path.as_path(), dst.as_path()) {
error!("Failed to copy file: {}", err); error!("Failed to copy file: {}", err);
return Err(HostError::new( return Err(HostError::new(
HostErrorType::CouldNotCreateFile, HostErrorType::CouldNotCreateFile,
Some(err), Some(err),
file.abs_path.as_path(), file.path.as_path(),
)); ));
} }
info!("File copied"); info!("File copied");
} }
FsEntry::Directory(dir) => { Entry::Directory(dir) => {
// If destination path doesn't exist, create destination // If destination path doesn't exist, create destination
if !dst.exists() { if !dst.exists() {
debug!("Directory {} doesn't exist; creating it", dst.display()); debug!("Directory {} doesn't exist; creating it", dst.display());
self.mkdir(dst.as_path())?; self.mkdir(dst.as_path())?;
} }
// Scan dir // Scan dir
let dir_files: Vec<FsEntry> = self.scan_dir(dir.abs_path.as_path())?; let dir_files: Vec<Entry> = self.scan_dir(dir.path.as_path())?;
// Iterate files // Iterate files
for dir_entry in dir_files.iter() { for dir_entry in dir_files.iter() {
// Calculate dst // Calculate dst
let mut sub_dst: PathBuf = dst.clone(); let mut sub_dst: PathBuf = dst.clone();
sub_dst.push(dir_entry.get_name()); sub_dst.push(dir_entry.name());
// Call function recursively // Call function recursively
self.copy(dir_entry, sub_dst.as_path())?; self.copy(dir_entry, sub_dst.as_path())?;
} }
@@ -439,12 +438,12 @@ impl Localhost {
/// ### stat /// ### stat
/// ///
/// Stat file and create a FsEntry /// Stat file and create a Entry
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> { pub fn stat(&self, path: &Path) -> Result<Entry, HostError> {
info!("Stating file {}", path.display()); info!("Stating file {}", path.display());
let path: PathBuf = self.to_abs_path(path); let path: PathBuf = self.to_path(path);
let attr: Metadata = match fs::metadata(path.as_path()) { let attr = match fs::metadata(path.as_path()) {
Ok(metadata) => metadata, Ok(metadata) => metadata,
Err(err) => { Err(err) => {
error!("Could not read file metadata: {}", err); error!("Could not read file metadata: {}", err);
@@ -455,49 +454,38 @@ impl Localhost {
)); ));
} }
}; };
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or("")); let name = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
// Match dir / file // Match dir / file
let metadata = Metadata {
atime: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
ctime: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
gid: Some(attr.gid()),
mode: Some(UnixPex::from(attr.mode())),
mtime: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
size: if path.is_dir() {
attr.blksize()
} else {
attr.len()
},
symlink: fs::read_link(path.as_path()).ok(),
uid: Some(attr.uid()),
};
Ok(match path.is_dir() { Ok(match path.is_dir() {
true => FsEntry::Directory(FsDirectory { true => Entry::Directory(Directory {
name: file_name, name,
abs_path: path.clone(), path,
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), metadata,
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
symlink: match fs::read_link(path.as_path()) {
Ok(p) => match self.stat(p.as_path()) {
Ok(entry) => Some(Box::new(entry)),
Err(_) => None,
},
Err(_) => None,
},
user: Some(attr.uid()),
group: Some(attr.gid()),
unix_pex: Some(self.u32_to_mode(attr.mode())),
}), }),
false => { false => {
// Is File // Is File
let extension: Option<String> = path let extension = path
.extension() .extension()
.map(|s| String::from(s.to_str().unwrap_or(""))); .map(|s| String::from(s.to_str().unwrap_or("")));
FsEntry::File(FsFile { Entry::File(File {
name: file_name, name,
abs_path: path.clone(), path,
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), extension,
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), metadata,
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
size: attr.len() as usize,
ftype: extension,
symlink: match fs::read_link(path.as_path()) {
Ok(p) => match self.stat(p.as_path()) {
Ok(entry) => Some(Box::new(entry)),
Err(_) => None,
},
Err(_) => None, // Ignore errors
},
user: Some(attr.uid()),
group: Some(attr.gid()),
unix_pex: Some(self.u32_to_mode(attr.mode())),
}) })
} }
}) })
@@ -505,12 +493,12 @@ impl Localhost {
/// ### stat /// ### stat
/// ///
/// Stat file and create a FsEntry /// Stat file and create a Entry
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> { pub fn stat(&self, path: &Path) -> Result<Entry, HostError> {
let path: PathBuf = self.to_abs_path(path); let path: PathBuf = self.to_path(path);
info!("Stating file {}", path.display()); info!("Stating file {}", path.display());
let attr: Metadata = match fs::metadata(path.as_path()) { let attr = match fs::metadata(path.as_path()) {
Ok(metadata) => metadata, Ok(metadata) => metadata,
Err(err) => { Err(err) => {
error!("Could not read file metadata: {}", err); error!("Could not read file metadata: {}", err);
@@ -521,49 +509,38 @@ impl Localhost {
)); ));
} }
}; };
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or("")); let name = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
let metadata = Metadata {
atime: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
ctime: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
mtime: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
size: if path.is_dir() {
attr.blksize()
} else {
attr.len()
},
symlink: fs::read_link(path.as_path()).ok(),
uid: None,
gid: None,
mode: None,
};
// Match dir / file // Match dir / file
Ok(match path.is_dir() { Ok(match path.is_dir() {
true => FsEntry::Directory(FsDirectory { true => Entry::Directory(Directory {
name: file_name, name,
abs_path: path.clone(), path,
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), metadata,
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
symlink: match fs::read_link(path.as_path()) {
Ok(p) => match self.stat(p.as_path()) {
Ok(entry) => Some(Box::new(entry)),
Err(_) => None, // Ignore errors
},
Err(_) => None,
},
user: None,
group: None,
unix_pex: None,
}), }),
false => { false => {
// Is File // Is File
let extension: Option<String> = path let extension = path
.extension() .extension()
.map(|s| String::from(s.to_str().unwrap_or(""))); .map(|s| String::from(s.to_str().unwrap_or("")));
FsEntry::File(FsFile { Entry::File(File {
name: file_name, name,
abs_path: path.clone(), path,
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH), extension,
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH), metadata,
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
size: attr.len() as usize,
ftype: extension,
symlink: match fs::read_link(path.as_path()) {
Ok(p) => match self.stat(p.as_path()) {
Ok(entry) => Some(Box::new(entry)),
Err(_) => None,
},
Err(_) => None,
},
user: None,
group: None,
unix_pex: None,
}) })
} }
}) })
@@ -601,13 +578,13 @@ impl Localhost {
/// ///
/// Change file mode to file, according to UNIX permissions /// Change file mode to file, according to UNIX permissions
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
pub fn chmod(&self, path: &Path, pex: (u8, u8, u8)) -> Result<(), HostError> { pub fn chmod(&self, path: &Path, pex: UnixPex) -> Result<(), HostError> {
let path: PathBuf = self.to_abs_path(path); let path: PathBuf = self.to_path(path);
// Get metadta // Get metadta
match fs::metadata(path.as_path()) { match fs::metadata(path.as_path()) {
Ok(metadata) => { Ok(metadata) => {
let mut mpex = metadata.permissions(); let mut mpex = metadata.permissions();
mpex.set_mode(self.mode_to_u32(pex)); mpex.set_mode(pex.into());
match set_permissions(path.as_path(), mpex) { match set_permissions(path.as_path(), mpex) {
Ok(_) => { Ok(_) => {
info!("Changed mode for {} to {:?}", path.display(), pex); info!("Changed mode for {} to {:?}", path.display(), pex);
@@ -641,8 +618,8 @@ impl Localhost {
/// ### open_file_read /// ### open_file_read
/// ///
/// Open file for read /// Open file for read
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> { pub fn open_file_read(&self, file: &Path) -> Result<StdFile, HostError> {
let file: PathBuf = self.to_abs_path(file); let file: PathBuf = self.to_path(file);
info!("Opening file {} for read", file.display()); info!("Opening file {} for read", file.display());
if !self.file_exists(file.as_path()) { if !self.file_exists(file.as_path()) {
error!("File doesn't exist!"); error!("File doesn't exist!");
@@ -673,8 +650,8 @@ impl Localhost {
/// ### open_file_write /// ### open_file_write
/// ///
/// Open file for write /// Open file for write
pub fn open_file_write(&self, file: &Path) -> Result<File, HostError> { pub fn open_file_write(&self, file: &Path) -> Result<StdFile, HostError> {
let file: PathBuf = self.to_abs_path(file); let file: PathBuf = self.to_path(file);
info!("Opening file {} for write", file.display()); info!("Opening file {} for write", file.display());
match OpenOptions::new() match OpenOptions::new()
.create(true) .create(true)
@@ -711,11 +688,11 @@ impl Localhost {
/// ### scan_dir /// ### scan_dir
/// ///
/// Get content of the current directory as a list of fs entry /// Get content of the current directory as a list of fs entry
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<FsEntry>, HostError> { pub fn scan_dir(&self, dir: &Path) -> Result<Vec<Entry>, HostError> {
info!("Reading directory {}", dir.display()); info!("Reading directory {}", dir.display());
match std::fs::read_dir(dir) { match std::fs::read_dir(dir) {
Ok(e) => { Ok(e) => {
let mut fs_entries: Vec<FsEntry> = Vec::new(); let mut fs_entries: Vec<Entry> = Vec::new();
for entry in e.flatten() { for entry in e.flatten() {
// NOTE: 0.4.1, don't fail if stat for one file fails // NOTE: 0.4.1, don't fail if stat for one file fails
match self.stat(entry.path().as_path()) { match self.stat(entry.path().as_path()) {
@@ -737,7 +714,7 @@ impl Localhost {
/// ///
/// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course. /// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course.
/// The `search` argument supports wilcards ('*', '?') /// The `search` argument supports wilcards ('*', '?')
pub fn find(&self, search: &str) -> Result<Vec<FsEntry>, HostError> { pub fn find(&self, search: &str) -> Result<Vec<Entry>, HostError> {
self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search)) self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search))
} }
@@ -748,9 +725,9 @@ impl Localhost {
/// Recursive call for `find` method. /// Recursive call for `find` method.
/// Search in current directory for files which match `filter`. /// Search in current directory for files which match `filter`.
/// If a directory is found in current directory, `iter_search` will be called using that dir as argument. /// If a directory is found in current directory, `iter_search` will be called using that dir as argument.
fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result<Vec<FsEntry>, HostError> { fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result<Vec<Entry>, HostError> {
// Scan directory // Scan directory
let mut drained: Vec<FsEntry> = Vec::new(); let mut drained: Vec<Entry> = Vec::new();
match self.scan_dir(dir) { match self.scan_dir(dir) {
Err(err) => Err(err), Err(err) => Err(err),
Ok(entries) => { Ok(entries) => {
@@ -763,16 +740,16 @@ impl Localhost {
*/ */
for entry in entries.iter() { for entry in entries.iter() {
match entry { match entry {
FsEntry::Directory(dir) => { Entry::Directory(dir) => {
// If directory matches; push directory to drained // If directory matches; push directory to drained
if filter.matches(dir.name.as_str()) { if filter.matches(dir.name.as_str()) {
drained.push(FsEntry::Directory(dir.clone())); drained.push(Entry::Directory(dir.clone()));
} }
drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?); drained.append(&mut self.iter_search(dir.path.as_path(), filter)?);
} }
FsEntry::File(file) => { Entry::File(file) => {
if filter.matches(file.name.as_str()) { if filter.matches(file.name.as_str()) {
drained.push(FsEntry::File(file.clone())); drained.push(Entry::File(file.clone()));
} }
} }
} }
@@ -782,29 +759,10 @@ impl Localhost {
} }
} }
/// ### u32_to_mode /// ### to_path
///
/// Return string with format xxxxxx to tuple of permissions (user, group, others)
#[cfg(target_family = "unix")]
fn u32_to_mode(&self, mode: u32) -> (UnixPex, UnixPex, UnixPex) {
let user: UnixPex = UnixPex::from(((mode >> 6) & 0x7) as u8);
let group: UnixPex = UnixPex::from(((mode >> 3) & 0x7) as u8);
let others: UnixPex = UnixPex::from((mode & 0x7) as u8);
(user, group, others)
}
/// mode_to_u32
///
/// Convert owner,group,others to u32
#[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
}
/// ### to_abs_path
/// ///
/// Convert path to absolute path /// Convert path to absolute path
fn to_abs_path(&self, p: &Path) -> PathBuf { fn to_path(&self, p: &Path) -> PathBuf {
path::absolutize(self.wrkdir.as_path(), p) path::absolutize(self.wrkdir.as_path(), p)
} }
} }
@@ -819,7 +777,7 @@ mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
use std::fs::File; use std::fs::File as StdFile;
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
use std::io::Write; use std::io::Write;
@@ -970,7 +928,7 @@ mod tests {
fn test_host_localhost_symlinks() { fn test_host_localhost_symlinks() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// Create sample file // Create sample file
assert!(File::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
// Create symlink // Create symlink
assert!(symlink( assert!(symlink(
format!("{}/foo.txt", tmpdir.path().display()), format!("{}/foo.txt", tmpdir.path().display()),
@@ -979,33 +937,33 @@ mod tests {
.is_ok()); .is_ok());
// Get dir // Get dir
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<FsEntry> = host.list_dir(); let files: Vec<Entry> = host.list_dir();
// Verify files // Verify files
let file_0: &FsEntry = files.get(0).unwrap(); let file_0: &Entry = files.get(0).unwrap();
match file_0 { match file_0 {
FsEntry::File(file_0) => { Entry::File(file_0) => {
if file_0.name == String::from("foo.txt") { if file_0.name == String::from("foo.txt") {
assert!(file_0.symlink.is_none()); assert!(file_0.metadata.symlink.is_none());
} else { } else {
assert_eq!( assert_eq!(
*file_0.symlink.as_ref().unwrap().get_abs_path(), file_0.metadata.symlink.as_ref().unwrap(),
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
); );
} }
} }
_ => panic!("expected entry 0 to be file: {:?}", file_0), _ => panic!("expected entry 0 to be file: {:?}", file_0),
}; };
// Verify simlink // Verify simlink
let file_1: &FsEntry = files.get(1).unwrap(); let file_1: &Entry = files.get(1).unwrap();
match file_1 { match file_1 {
FsEntry::File(file_1) => { Entry::File(file_1) => {
if file_1.name == String::from("bar.txt") { if file_1.name == String::from("bar.txt") {
assert_eq!( assert_eq!(
*file_1.symlink.as_ref().unwrap().get_abs_path(), file_1.metadata.symlink.as_ref().unwrap(),
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display())) &PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
); );
} else { } else {
assert!(file_1.symlink.is_none()); assert!(file_1.metadata.symlink.is_none());
} }
} }
_ => panic!("expected entry 0 to be file: {:?}", file_1), _ => panic!("expected entry 0 to be file: {:?}", file_1),
@@ -1017,10 +975,10 @@ mod tests {
fn test_host_localhost_mkdir() { fn test_host_localhost_mkdir() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<FsEntry> = host.list_dir(); let files: Vec<Entry> = host.list_dir();
assert_eq!(files.len(), 0); // There should be 0 files now assert_eq!(files.len(), 0); // There should be 0 files now
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
let files: Vec<FsEntry> = host.list_dir(); let files: Vec<Entry> = host.list_dir();
assert_eq!(files.len(), 1); // There should be 1 file now assert_eq!(files.len(), 1); // There should be 1 file now
// Try to re-create directory // Try to re-create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err()); assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
@@ -1042,19 +1000,19 @@ mod tests {
fn test_host_localhost_remove() { fn test_host_localhost_remove() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// Create sample file // Create sample file
assert!(File::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok()); assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<FsEntry> = host.list_dir(); let files: Vec<Entry> = host.list_dir();
assert_eq!(files.len(), 1); // There should be 1 file now assert_eq!(files.len(), 1); // There should be 1 file now
// Remove file // Remove file
assert!(host.remove(files.get(0).unwrap()).is_ok()); assert!(host.remove(files.get(0).unwrap()).is_ok());
// There should be 0 files now // There should be 0 files now
let files: Vec<FsEntry> = host.list_dir(); let files: Vec<Entry> = host.list_dir();
assert_eq!(files.len(), 0); // There should be 0 files now assert_eq!(files.len(), 0); // There should be 0 files now
// Create directory // Create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok()); assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
// Delete directory // Delete directory
let files: Vec<FsEntry> = host.list_dir(); let files: Vec<Entry> = host.list_dir();
assert_eq!(files.len(), 1); // There should be 1 file now assert_eq!(files.len(), 1); // There should be 1 file now
assert!(host.remove(files.get(0).unwrap()).is_ok()); assert!(host.remove(files.get(0).unwrap()).is_ok());
// Remove unexisting directory // Remove unexisting directory
@@ -1073,11 +1031,11 @@ mod tests {
// Create sample file // Create sample file
let src_path: PathBuf = let src_path: PathBuf =
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str()); PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str());
assert!(File::create(src_path.as_path()).is_ok()); assert!(StdFile::create(src_path.as_path()).is_ok());
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let files: Vec<FsEntry> = host.list_dir(); let files: Vec<Entry> = host.list_dir();
assert_eq!(files.len(), 1); // There should be 1 file now assert_eq!(files.len(), 1); // There should be 1 file now
assert_eq!(files.get(0).unwrap().get_name(), "foo.txt"); assert_eq!(files.get(0).unwrap().name(), "foo.txt");
// Rename file // Rename file
let dst_path: PathBuf = let dst_path: PathBuf =
PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str()); PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str());
@@ -1085,9 +1043,9 @@ mod tests {
.rename(files.get(0).unwrap(), dst_path.as_path()) .rename(files.get(0).unwrap(), dst_path.as_path())
.is_ok()); .is_ok());
// There should be still 1 file now, but named bar.txt // There should be still 1 file now, but named bar.txt
let files: Vec<FsEntry> = host.list_dir(); let files: Vec<Entry> = host.list_dir();
assert_eq!(files.len(), 1); // There should be 0 files now assert_eq!(files.len(), 1); // There should be 0 files now
assert_eq!(files.get(0).unwrap().get_name(), "bar.txt"); assert_eq!(files.get(0).unwrap().name(), "bar.txt");
// Fail // Fail
let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu"); let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu");
assert!(host assert!(host
@@ -1101,16 +1059,16 @@ mod tests {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap(); let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let file: tempfile::NamedTempFile = create_sample_file(); let file: tempfile::NamedTempFile = create_sample_file();
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
// mode_to_u32
assert_eq!(host.mode_to_u32((6, 4, 4)), 0o644);
assert_eq!(host.mode_to_u32((7, 7, 5)), 0o775);
// Chmod to file // Chmod to file
assert!(host.chmod(file.path(), (7, 7, 5)).is_ok()); assert!(host.chmod(file.path(), UnixPex::from(0o755)).is_ok());
// Chmod to dir // Chmod to dir
assert!(host.chmod(tmpdir.path(), (7, 5, 0)).is_ok()); assert!(host.chmod(tmpdir.path(), UnixPex::from(0o750)).is_ok());
// Error // Error
assert!(host assert!(host
.chmod(Path::new("/tmp/krgiogoiegj/kwrgnoerig"), (7, 7, 7)) .chmod(
Path::new("/tmp/krgiogoiegj/kwrgnoerig"),
UnixPex::from(0o777)
)
.is_err()); .is_err());
} }
@@ -1122,15 +1080,15 @@ mod tests {
let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
file1_path.push("foo.txt"); file1_path.push("foo.txt");
// Write file 1 // Write file 1
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
assert!(file1.write_all(b"Hello world!\n").is_ok()); assert!(file1.write_all(b"Hello world!\n").is_ok());
// Get file 2 path // Get file 2 path
let mut file2_path: PathBuf = PathBuf::from(tmpdir.path()); let mut file2_path: PathBuf = PathBuf::from(tmpdir.path());
file2_path.push("bar.txt"); file2_path.push("bar.txt");
// Create host // Create host
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let file1_entry: FsEntry = host.files.get(0).unwrap().clone(); let file1_entry: Entry = host.files.get(0).unwrap().clone();
assert_eq!(file1_entry.get_name(), String::from("foo.txt")); assert_eq!(file1_entry.name(), String::from("foo.txt"));
// Copy // Copy
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
// Verify host has two files // Verify host has two files
@@ -1152,14 +1110,14 @@ mod tests {
let mut file1_path: PathBuf = PathBuf::from(tmpdir.path()); let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
file1_path.push("foo.txt"); file1_path.push("foo.txt");
// Write file 1 // Write file 1
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
assert!(file1.write_all(b"Hello world!\n").is_ok()); assert!(file1.write_all(b"Hello world!\n").is_ok());
// Get file 2 path // Get file 2 path
let file2_path: PathBuf = PathBuf::from("bar.txt"); let file2_path: PathBuf = PathBuf::from("bar.txt");
// Create host // Create host
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let file1_entry: FsEntry = host.files.get(0).unwrap().clone(); let file1_entry: Entry = host.files.get(0).unwrap().clone();
assert_eq!(file1_entry.get_name(), String::from("foo.txt")); assert_eq!(file1_entry.name(), String::from("foo.txt"));
// Copy // Copy
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok()); assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
// Verify host has two files // Verify host has two files
@@ -1178,15 +1136,15 @@ mod tests {
let mut file1_path: PathBuf = dir_src.clone(); let mut file1_path: PathBuf = dir_src.clone();
file1_path.push("foo.txt"); file1_path.push("foo.txt");
// Write file 1 // Write file 1
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
assert!(file1.write_all(b"Hello world!\n").is_ok()); assert!(file1.write_all(b"Hello world!\n").is_ok());
// Copy dir src to dir ddest // Copy dir src to dir ddest
let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path()); let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path());
dir_dest.push("test_dest_dir/"); dir_dest.push("test_dest_dir/");
// Create host // Create host
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone(); let dir_src_entry: Entry = host.files.get(0).unwrap().clone();
assert_eq!(dir_src_entry.get_name(), String::from("test_dir")); assert_eq!(dir_src_entry.name(), String::from("test_dir"));
// Copy // Copy
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
// Verify host has two files // Verify host has two files
@@ -1209,14 +1167,14 @@ mod tests {
let mut file1_path: PathBuf = dir_src.clone(); let mut file1_path: PathBuf = dir_src.clone();
file1_path.push("foo.txt"); file1_path.push("foo.txt");
// Write file 1 // Write file 1
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap(); let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
assert!(file1.write_all(b"Hello world!\n").is_ok()); assert!(file1.write_all(b"Hello world!\n").is_ok());
// Copy dir src to dir ddest // Copy dir src to dir ddest
let dir_dest: PathBuf = PathBuf::from("test_dest_dir/"); let dir_dest: PathBuf = PathBuf::from("test_dest_dir/");
// Create host // Create host
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap(); let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone(); let dir_src_entry: Entry = host.files.get(0).unwrap().clone();
assert_eq!(dir_src_entry.get_name(), String::from("test_dir")); assert_eq!(dir_src_entry.name(), String::from("test_dir"));
// Copy // Copy
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok()); assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
// Verify host has two files // Verify host has two files
@@ -1255,20 +1213,20 @@ mod tests {
assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok()); assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok());
let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap(); let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
// Find txt files // Find txt files
let mut result: Vec<FsEntry> = host.find("*.txt").ok().unwrap(); let mut result: Vec<Entry> = host.find("*.txt").ok().unwrap();
result.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase()); result.sort_by_key(|x: &Entry| x.name().to_lowercase());
// There should be 3 entries // There should be 3 entries
assert_eq!(result.len(), 3); assert_eq!(result.len(), 3);
// Check names (they should be sorted alphabetically already; NOTE: examples/ comes before pippo.txt) // Check names (they should be sorted alphabetically already; NOTE: examples/ comes before pippo.txt)
assert_eq!(result[0].get_name(), "errors.txt"); assert_eq!(result[0].name(), "errors.txt");
assert_eq!(result[1].get_name(), "omar.txt"); assert_eq!(result[1].name(), "omar.txt");
assert_eq!(result[2].get_name(), "pippo.txt"); assert_eq!(result[2].name(), "pippo.txt");
// Search for directory // Search for directory
let mut result: Vec<FsEntry> = host.find("examples*").ok().unwrap(); let mut result: Vec<Entry> = host.find("examples*").ok().unwrap();
result.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase()); result.sort_by_key(|x: &Entry| x.name().to_lowercase());
assert_eq!(result.len(), 2); assert_eq!(result.len(), 2);
assert_eq!(result[0].get_name(), "examples"); assert_eq!(result[0].name(), "examples");
assert_eq!(result[1].get_name(), "examples.csv"); assert_eq!(result[1].name(), "examples.csv");
} }
#[test] #[test]
+1 -1
View File
@@ -45,8 +45,8 @@ use std::time::Duration;
// Include // Include
mod activity_manager; mod activity_manager;
mod config; mod config;
mod explorer;
mod filetransfer; mod filetransfer;
mod fs;
mod host; mod host;
mod support; mod support;
mod system; mod system;
+1 -11
View File
@@ -57,7 +57,7 @@ pub struct Release {
/// ///
/// The update structure defines the options used to install the update. /// The update structure defines the options used to install the update.
/// Once you're fine with the options, just call the `upgrade()` method to upgrade termscp. /// Once you're fine with the options, just call the `upgrade()` method to upgrade termscp.
#[derive(Debug)] #[derive(Debug, Default)]
pub struct Update { pub struct Update {
ask_confirm: bool, ask_confirm: bool,
progress: bool, progress: bool,
@@ -141,16 +141,6 @@ impl Update {
} }
} }
} }
impl Default for Update {
fn default() -> Self {
Self {
progress: false,
ask_confirm: false,
}
}
}
impl From<Status> for UpdateStatus { impl From<Status> for UpdateStatus {
fn from(s: Status) -> Self { fn from(s: Status) -> Self {
match s { match s {
+1 -1
View File
@@ -30,8 +30,8 @@ use crate::config::{
params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD}, params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD},
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind}, serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
}; };
use crate::explorer::GroupDirs;
use crate::filetransfer::FileTransferProtocol; use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
// Ext // Ext
use std::fs::{create_dir, remove_file, File, OpenOptions}; use std::fs::{create_dir, remove_file, File, OpenOptions};
use std::io::Write; use std::io::Write;
+9 -9
View File
@@ -28,8 +28,9 @@
// Locals // Locals
use super::config_client::ConfigClient; use super::config_client::ConfigClient;
// Ext // Ext
use remotefs::client::ssh::SshKeyStorage as SshKeyStorageT;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::{Path, PathBuf};
pub struct SshKeyStorage { pub struct SshKeyStorage {
hosts: HashMap<String, PathBuf>, // Association between {user}@{host} and RSA key path hosts: HashMap<String, PathBuf>, // Association between {user}@{host} and RSA key path
@@ -74,14 +75,6 @@ impl SshKeyStorage {
} }
} }
/// ### resolve
///
/// Return RSA key path from host and username
pub fn resolve(&self, host: &str, username: &str) -> Option<&PathBuf> {
let key: String = Self::make_mapkey(host, username);
self.hosts.get(&key)
}
/// ### make_mapkey /// ### make_mapkey
/// ///
/// Make mapkey from host and username /// Make mapkey from host and username
@@ -100,6 +93,13 @@ impl SshKeyStorage {
} }
} }
impl SshKeyStorageT for SshKeyStorage {
fn resolve(&self, host: &str, username: &str) -> Option<&Path> {
let key: String = Self::make_mapkey(host, username);
self.hosts.get(&key).map(|x| x.as_path())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
+1 -9
View File
@@ -52,19 +52,11 @@ use tuirealm::{Component, MockComponent};
// -- global listener // -- global listener
#[derive(MockComponent)] #[derive(Default, MockComponent)]
pub struct GlobalListener { pub struct GlobalListener {
component: Phantom, component: Phantom,
} }
impl Default for GlobalListener {
fn default() -> Self {
Self {
component: Phantom::default(),
}
}
}
impl Component<Msg, NoUserEvent> for GlobalListener { impl Component<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
@@ -26,7 +26,9 @@
* SOFTWARE. * SOFTWARE.
*/ */
// locals // locals
use super::{FileTransferActivity, FsEntry}; use super::FileTransferActivity;
use remotefs::Directory;
use std::path::PathBuf; use std::path::PathBuf;
impl FileTransferActivity { impl FileTransferActivity {
@@ -34,70 +36,24 @@ impl FileTransferActivity {
/// ///
/// Enter a directory on local host from entry /// Enter a directory on local host from entry
/// Return true whether the directory changed /// Return true whether the directory changed
pub(crate) fn action_enter_local_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool { pub(crate) fn action_enter_local_dir(&mut self, dir: Directory, block_sync: bool) -> bool {
match entry { self.local_changedir(dir.path.as_path(), true);
FsEntry::Directory(dir) => { if self.browser.sync_browsing && !block_sync {
self.local_changedir(dir.abs_path.as_path(), true); self.action_change_remote_dir(dir.name, true);
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(dir.name, true);
}
true
}
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(dir) => {
self.local_changedir(dir.abs_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(dir.name.clone(), true);
}
true
}
_ => false,
}
}
None => false,
}
}
} }
true
} }
/// ### action_enter_remote_dir /// ### action_enter_remote_dir
/// ///
/// Enter a directory on local host from entry /// Enter a directory on local host from entry
/// Return true whether the directory changed /// Return true whether the directory changed
pub(crate) fn action_enter_remote_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool { pub(crate) fn action_enter_remote_dir(&mut self, dir: Directory, block_sync: bool) -> bool {
match entry { self.remote_changedir(dir.path.as_path(), true);
FsEntry::Directory(dir) => { if self.browser.sync_browsing && !block_sync {
self.remote_changedir(dir.abs_path.as_path(), true); self.action_change_local_dir(dir.name, true);
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(dir.name, true);
}
true
}
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(dir) => {
self.remote_changedir(dir.abs_path.as_path(), true);
// Check whether to sync
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(dir.name.clone(), true);
}
true
}
_ => false,
}
}
None => false,
}
}
} }
true
} }
/// ### action_change_local_dir /// ### action_change_local_dir
+21 -21
View File
@@ -26,9 +26,9 @@
* SOFTWARE. * SOFTWARE.
*/ */
// locals // locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload}; use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
use crate::filetransfer::FileTransferErrorType;
use crate::fs::FsFile; use remotefs::{Entry, RemoteErrorType};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
impl FileTransferActivity { impl FileTransferActivity {
@@ -49,7 +49,7 @@ impl FileTransferActivity {
// Iter files // Iter files
for entry in entries.iter() { for entry in entries.iter() {
let mut dest_path: PathBuf = base_path.clone(); let mut dest_path: PathBuf = base_path.clone();
dest_path.push(entry.get_name()); dest_path.push(entry.name());
self.local_copy_file(entry, dest_path.as_path()); self.local_copy_file(entry, dest_path.as_path());
} }
// Reload entries // Reload entries
@@ -76,7 +76,7 @@ impl FileTransferActivity {
// Iter files // Iter files
for entry in entries.into_iter() { for entry in entries.into_iter() {
let mut dest_path: PathBuf = base_path.clone(); let mut dest_path: PathBuf = base_path.clone();
dest_path.push(entry.get_name()); dest_path.push(entry.name());
self.remote_copy_file(entry, dest_path.as_path()); self.remote_copy_file(entry, dest_path.as_path());
} }
// Reload entries // Reload entries
@@ -86,14 +86,14 @@ impl FileTransferActivity {
} }
} }
fn local_copy_file(&mut self, entry: &FsEntry, dest: &Path) { fn local_copy_file(&mut self, entry: &Entry, dest: &Path) {
match self.host.copy(entry, dest) { match self.host.copy(entry, dest) {
Ok(_) => { Ok(_) => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!( format!(
"Copied \"{}\" to \"{}\"", "Copied \"{}\" to \"{}\"",
entry.get_abs_path().display(), entry.path().display(),
dest.display() dest.display()
), ),
); );
@@ -102,7 +102,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not copy \"{}\" to \"{}\": {}", "Could not copy \"{}\" to \"{}\": {}",
entry.get_abs_path().display(), entry.path().display(),
dest.display(), dest.display(),
err err
), ),
@@ -110,20 +110,20 @@ impl FileTransferActivity {
} }
} }
fn remote_copy_file(&mut self, entry: FsEntry, dest: &Path) { fn remote_copy_file(&mut self, entry: Entry, dest: &Path) {
match self.client.as_mut().copy(&entry, dest) { match self.client.as_mut().copy(entry.path(), dest) {
Ok(_) => { Ok(_) => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!( format!(
"Copied \"{}\" to \"{}\"", "Copied \"{}\" to \"{}\"",
entry.get_abs_path().display(), entry.path().display(),
dest.display() dest.display()
), ),
); );
} }
Err(err) => match err.kind() { Err(err) => match err.kind {
FileTransferErrorType::UnsupportedFeature => { RemoteErrorType::UnsupportedFeature => {
// If copy is not supported, perform the tricky copy // If copy is not supported, perform the tricky copy
let _ = self.tricky_copy(entry, dest); let _ = self.tricky_copy(entry, dest);
} }
@@ -131,7 +131,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not copy \"{}\" to \"{}\": {}", "Could not copy \"{}\" to \"{}\": {}",
entry.get_abs_path().display(), entry.path().display(),
dest.display(), dest.display(),
err err
), ),
@@ -143,12 +143,12 @@ impl FileTransferActivity {
/// ### tricky_copy /// ### tricky_copy
/// ///
/// Tricky copy will be used whenever copy command is not available on remote host /// 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) -> Result<(), String> { pub(super) fn tricky_copy(&mut self, entry: Entry, dest: &Path) -> Result<(), String> {
// NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen // NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen
self.umount_wait(); self.umount_wait();
// match entry // match entry
match entry { match entry {
FsEntry::File(entry) => { Entry::File(entry) => {
// Create tempfile // Create tempfile
let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() { let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() {
Ok(f) => f, Ok(f) => f,
@@ -162,7 +162,7 @@ impl FileTransferActivity {
}; };
// Download file // Download file
let name = entry.name.clone(); let name = entry.name.clone();
let entry_path = entry.abs_path.clone(); let entry_path = entry.path.clone();
if let Err(err) = if let Err(err) =
self.filetransfer_recv(TransferPayload::File(entry), tmpfile.path(), Some(name)) self.filetransfer_recv(TransferPayload::File(entry), tmpfile.path(), Some(name))
{ {
@@ -173,7 +173,7 @@ impl FileTransferActivity {
return Err(err); return Err(err);
} }
// Get local fs entry // Get local fs entry
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) { let tmpfile_entry = match self.host.stat(tmpfile.path()) {
Ok(e) => e.unwrap_file(), Ok(e) => e.unwrap_file(),
Err(err) => { Err(err) => {
self.log_and_alert( self.log_and_alert(
@@ -206,7 +206,7 @@ impl FileTransferActivity {
} }
Ok(()) Ok(())
} }
FsEntry::Directory(_) => { Entry::Directory(_) => {
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() { let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
Ok(d) => d, Ok(d) => d,
Err(err) => { Err(err) => {
@@ -219,7 +219,7 @@ impl FileTransferActivity {
}; };
// Get path of dest // Get path of dest
let mut tempdir_path: PathBuf = tempdir.path().to_path_buf(); let mut tempdir_path: PathBuf = tempdir.path().to_path_buf();
tempdir_path.push(entry.get_name()); tempdir_path.push(entry.name());
// Download file // Download file
if let Err(err) = if let Err(err) =
self.filetransfer_recv(TransferPayload::Any(entry), tempdir.path(), None) self.filetransfer_recv(TransferPayload::Any(entry), tempdir.path(), None)
@@ -231,7 +231,7 @@ impl FileTransferActivity {
return Err(err); return Err(err);
} }
// Stat dir // Stat dir
let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) { let tempdir_entry = match self.host.stat(tempdir_path.as_path()) {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(err) => {
self.log_and_alert( self.log_and_alert(
@@ -26,7 +26,9 @@
* SOFTWARE. * SOFTWARE.
*/ */
// locals // locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry}; use super::{FileTransferActivity, LogLevel, SelectedEntry};
use remotefs::Entry;
impl FileTransferActivity { impl FileTransferActivity {
pub(crate) fn action_local_delete(&mut self) { pub(crate) fn action_local_delete(&mut self) {
@@ -71,13 +73,13 @@ impl FileTransferActivity {
} }
} }
pub(crate) fn local_remove_file(&mut self, entry: &FsEntry) { pub(crate) fn local_remove_file(&mut self, entry: &Entry) {
match self.host.remove(entry) { match self.host.remove(entry) {
Ok(_) => { Ok(_) => {
// Log // Log
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!("Removed file \"{}\"", entry.get_abs_path().display()), format!("Removed file \"{}\"", entry.path().display()),
); );
} }
Err(err) => { Err(err) => {
@@ -85,7 +87,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not delete file \"{}\": {}", "Could not delete file \"{}\": {}",
entry.get_abs_path().display(), entry.path().display(),
err err
), ),
); );
@@ -93,12 +95,12 @@ impl FileTransferActivity {
} }
} }
pub(crate) fn remote_remove_file(&mut self, entry: &FsEntry) { pub(crate) fn remote_remove_file(&mut self, entry: &Entry) {
match self.client.remove(entry) { match self.client.remove_dir_all(entry.path()) {
Ok(_) => { Ok(_) => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!("Removed file \"{}\"", entry.get_abs_path().display()), format!("Removed file \"{}\"", entry.path().display()),
); );
} }
Err(err) => { Err(err) => {
@@ -106,7 +108,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not delete file \"{}\": {}", "Could not delete file \"{}\": {}",
entry.get_abs_path().display(), entry.path().display(),
err err
), ),
); );
+15 -14
View File
@@ -26,9 +26,10 @@
* SOFTWARE. * SOFTWARE.
*/ */
// locals // locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload}; use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
use crate::fs::FsFile;
// ext // ext
use remotefs::{Entry, File};
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::Read; use std::io::Read;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -36,7 +37,7 @@ use std::time::SystemTime;
impl FileTransferActivity { impl FileTransferActivity {
pub(crate) fn action_edit_local_file(&mut self) { pub(crate) fn action_edit_local_file(&mut self) {
let entries: Vec<FsEntry> = match self.get_local_selected_entries() { let entries: Vec<Entry> = match self.get_local_selected_entries() {
SelectedEntry::One(entry) => vec![entry], SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries, SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![], SelectedEntry::None => vec![],
@@ -47,10 +48,10 @@ impl FileTransferActivity {
if entry.is_file() { if entry.is_file() {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!("Opening file \"{}\"…", entry.get_abs_path().display()), format!("Opening file \"{}\"…", entry.path().display()),
); );
// Edit file // Edit file
if let Err(err) = self.edit_local_file(entry.get_abs_path().as_path()) { if let Err(err) = self.edit_local_file(entry.path()) {
self.log_and_alert(LogLevel::Error, err); self.log_and_alert(LogLevel::Error, err);
} }
} }
@@ -60,7 +61,7 @@ impl FileTransferActivity {
} }
pub(crate) fn action_edit_remote_file(&mut self) { pub(crate) fn action_edit_remote_file(&mut self) {
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() { let entries: Vec<Entry> = match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => vec![entry], SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries, SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![], SelectedEntry::None => vec![],
@@ -68,10 +69,10 @@ impl FileTransferActivity {
// Edit all entries // Edit all entries
for entry in entries.into_iter() { for entry in entries.into_iter() {
// Check if file // Check if file
if let FsEntry::File(file) = entry { if let Entry::File(file) = entry {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!("Opening file \"{}\"…", file.abs_path.display()), format!("Opening file \"{}\"…", file.path.display()),
); );
// Edit file // Edit file
if let Err(err) = self.edit_remote_file(file) { if let Err(err) = self.edit_remote_file(file) {
@@ -149,7 +150,7 @@ impl FileTransferActivity {
/// ### edit_remote_file /// ### edit_remote_file
/// ///
/// Edit file on remote host /// Edit file on remote host
fn edit_remote_file(&mut self, file: FsFile) -> Result<(), String> { fn edit_remote_file(&mut self, file: File) -> Result<(), String> {
// Create temp file // Create temp file
let tmpfile: PathBuf = match self.download_file_as_temp(&file) { let tmpfile: PathBuf = match self.download_file_as_temp(&file) {
Ok(p) => p, Ok(p) => p,
@@ -157,7 +158,7 @@ impl FileTransferActivity {
}; };
// Download file // Download file
let file_name = file.name.clone(); let file_name = file.name.clone();
let file_path = file.abs_path.clone(); let file_path = file.path.clone();
if let Err(err) = self.filetransfer_recv( if let Err(err) = self.filetransfer_recv(
TransferPayload::File(file), TransferPayload::File(file),
tmpfile.as_path(), tmpfile.as_path(),
@@ -167,7 +168,7 @@ impl FileTransferActivity {
} }
// Get current file modification time // Get current file modification time
let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) { let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e.get_last_change_time(), Ok(e) => e.metadata().mtime,
Err(err) => { Err(err) => {
return Err(format!( return Err(format!(
"Could not stat \"{}\": {}", "Could not stat \"{}\": {}",
@@ -181,7 +182,7 @@ impl FileTransferActivity {
return Err(err); return Err(err);
} }
// Get local fs entry // Get local fs entry
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.as_path()) { let tmpfile_entry: Entry = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e, Ok(e) => e,
Err(err) => { Err(err) => {
return Err(format!( return Err(format!(
@@ -192,7 +193,7 @@ impl FileTransferActivity {
} }
}; };
// Check if file has changed // Check if file has changed
match prev_mtime != tmpfile_entry.get_last_change_time() { match prev_mtime != tmpfile_entry.metadata().mtime {
true => { true => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
@@ -202,7 +203,7 @@ impl FileTransferActivity {
), ),
); );
// Get local fs entry // Get local fs entry
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.as_path()) { let tmpfile_entry = match self.host.stat(tmpfile.as_path()) {
Ok(e) => e.unwrap_file(), Ok(e) => e.unwrap_file(),
Err(err) => { Err(err) => {
return Err(format!( return Err(format!(
@@ -49,9 +49,12 @@ impl FileTransferActivity {
pub(crate) fn action_remote_exec(&mut self, input: String) { pub(crate) fn action_remote_exec(&mut self, input: String) {
match self.client.as_mut().exec(input.as_str()) { match self.client.as_mut().exec(input.as_str()) {
Ok(output) => { Ok((rc, output)) => {
// Reload files // Reload files
self.log(LogLevel::Info, format!("\"{}\": {}", input, output)); self.log(
LogLevel::Info,
format!("\"{}\" (exitcode: {}): {}", input, rc, output),
);
self.reload_remote_dir(); self.reload_remote_dir();
} }
Err(err) => { Err(err) => {
+13 -16
View File
@@ -27,21 +27,19 @@
*/ */
// locals // locals
use super::super::browser::FileExplorerTab; use super::super::browser::FileExplorerTab;
use super::{ use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferOpts, TransferPayload};
FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferOpts, TransferPayload,
};
use std::path::PathBuf; use std::path::PathBuf;
impl FileTransferActivity { impl FileTransferActivity {
pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> { pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<Entry>, String> {
match self.host.find(input.as_str()) { match self.host.find(input.as_str()) {
Ok(entries) => Ok(entries), Ok(entries) => Ok(entries),
Err(err) => Err(format!("Could not search for files: {}", err)), Err(err) => Err(format!("Could not search for files: {}", err)),
} }
} }
pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> { pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<Entry>, String> {
match self.client.as_mut().find(input.as_str()) { match self.client.as_mut().find(input.as_str()) {
Ok(entries) => Ok(entries), Ok(entries) => Ok(entries),
Err(err) => Err(format!("Could not search for files: {}", err)), Err(err) => Err(format!("Could not search for files: {}", err)),
@@ -53,8 +51,8 @@ impl FileTransferActivity {
if let SelectedEntry::One(entry) = self.get_found_selected_entries() { if let SelectedEntry::One(entry) = self.get_found_selected_entries() {
// Get path: if a directory, use directory path; if it is a File, get parent path // Get path: if a directory, use directory path; if it is a File, get parent path
let path: PathBuf = match entry { let path: PathBuf = match entry {
FsEntry::Directory(dir) => dir.abs_path, Entry::Directory(dir) => dir.path,
FsEntry::File(file) => match file.abs_path.parent() { Entry::File(file) => match file.path.parent() {
None => PathBuf::from("."), None => PathBuf::from("."),
Some(p) => p.to_path_buf(), Some(p) => p.to_path_buf(),
}, },
@@ -86,10 +84,10 @@ impl FileTransferActivity {
{ {
// Save pending transfer // Save pending transfer
self.set_pending_transfer( self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), opts.save_as.as_deref().unwrap_or_else(|| entry.name()),
); );
} else if let Err(err) = self.filetransfer_send( } else if let Err(err) = self.filetransfer_send(
TransferPayload::Any(entry.get_realfile()), TransferPayload::Any(entry),
wrkdir.as_path(), wrkdir.as_path(),
opts.save_as, opts.save_as,
) { ) {
@@ -107,10 +105,10 @@ impl FileTransferActivity {
{ {
// Save pending transfer // Save pending transfer
self.set_pending_transfer( self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), opts.save_as.as_deref().unwrap_or_else(|| entry.name()),
); );
} else if let Err(err) = self.filetransfer_recv( } else if let Err(err) = self.filetransfer_recv(
TransferPayload::Any(entry.get_realfile()), TransferPayload::Any(entry),
wrkdir.as_path(), wrkdir.as_path(),
opts.save_as, opts.save_as,
) { ) {
@@ -128,12 +126,11 @@ impl FileTransferActivity {
dest_path.push(save_as); dest_path.push(save_as);
} }
// Iter files // Iter files
let entries: Vec<FsEntry> = entries.iter().map(|x| x.get_realfile()).collect();
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => { FileExplorerTab::FindLocal | FileExplorerTab::Local => {
if opts.check_replace && self.config().get_prompt_on_file_replace() { if opts.check_replace && self.config().get_prompt_on_file_replace() {
// Check which file would be replaced // Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries let existing_files: Vec<&Entry> = entries
.iter() .iter()
.filter(|x| { .filter(|x| {
self.remote_file_exists( self.remote_file_exists(
@@ -166,7 +163,7 @@ impl FileTransferActivity {
FileExplorerTab::FindRemote | FileExplorerTab::Remote => { FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
if opts.check_replace && self.config().get_prompt_on_file_replace() { if opts.check_replace && self.config().get_prompt_on_file_replace() {
// Check which file would be replaced // Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries let existing_files: Vec<&Entry> = entries
.iter() .iter()
.filter(|x| { .filter(|x| {
self.local_file_exists( self.local_file_exists(
@@ -218,7 +215,7 @@ impl FileTransferActivity {
} }
} }
fn remove_found_file(&mut self, entry: &FsEntry) { fn remove_found_file(&mut self, entry: &Entry) {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => { FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.local_remove_file(entry); self.local_remove_file(entry);
@@ -263,7 +260,7 @@ impl FileTransferActivity {
} }
} }
fn open_found_file(&mut self, entry: &FsEntry, with: Option<&str>) { fn open_found_file(&mut self, entry: &Entry, with: Option<&str>) {
match self.browser.tab() { match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => { FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.action_open_local_file(entry, with); self.action_open_local_file(entry, with);
@@ -27,6 +27,7 @@
*/ */
// locals // locals
use super::{FileTransferActivity, LogLevel}; use super::{FileTransferActivity, LogLevel};
use remotefs::fs::UnixPex;
use std::path::PathBuf; use std::path::PathBuf;
impl FileTransferActivity { impl FileTransferActivity {
@@ -48,11 +49,10 @@ impl FileTransferActivity {
} }
} }
pub(crate) fn action_remote_mkdir(&mut self, input: String) { pub(crate) fn action_remote_mkdir(&mut self, input: String) {
match self match self.client.as_mut().create_dir(
.client PathBuf::from(input.as_str()).as_path(),
.as_mut() UnixPex::from(0o755),
.mkdir(PathBuf::from(input.as_str()).as_path()) ) {
{
Ok(_) => { Ok(_) => {
// Reload files // Reload files
self.log(LogLevel::Info, format!("Created directory \"{}\"", input)); self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
+14 -14
View File
@@ -26,9 +26,9 @@
* SOFTWARE. * SOFTWARE.
*/ */
pub(self) use super::{ pub(self) use super::{
browser::FileExplorerTab, FileTransferActivity, FsEntry, Id, LogLevel, TransferOpts, browser::FileExplorerTab, FileTransferActivity, Id, LogLevel, TransferOpts, TransferPayload,
TransferPayload,
}; };
pub(self) use remotefs::Entry;
use tuirealm::{State, StateValue}; use tuirealm::{State, StateValue};
// actions // actions
@@ -47,8 +47,8 @@ pub(crate) mod submit;
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum SelectedEntry { pub(crate) enum SelectedEntry {
One(FsEntry), One(Entry),
Many(Vec<FsEntry>), Many(Vec<Entry>),
None, None,
} }
@@ -59,8 +59,8 @@ enum SelectedEntryIndex {
None, None,
} }
impl From<Option<&FsEntry>> for SelectedEntry { impl From<Option<&Entry>> for SelectedEntry {
fn from(opt: Option<&FsEntry>) -> Self { fn from(opt: Option<&Entry>) -> Self {
match opt { match opt {
Some(e) => SelectedEntry::One(e.clone()), Some(e) => SelectedEntry::One(e.clone()),
None => SelectedEntry::None, None => SelectedEntry::None,
@@ -68,8 +68,8 @@ impl From<Option<&FsEntry>> for SelectedEntry {
} }
} }
impl From<Vec<&FsEntry>> for SelectedEntry { impl From<Vec<&Entry>> for SelectedEntry {
fn from(files: Vec<&FsEntry>) -> Self { fn from(files: Vec<&Entry>) -> Self {
SelectedEntry::Many(files.into_iter().cloned().collect()) SelectedEntry::Many(files.into_iter().cloned().collect())
} }
} }
@@ -82,9 +82,9 @@ impl FileTransferActivity {
match self.get_selected_index(&Id::ExplorerLocal) { match self.get_selected_index(&Id::ExplorerLocal) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)), SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)),
SelectedEntryIndex::Many(files) => { SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files let files: Vec<&Entry> = files
.iter() .iter()
.map(|x| self.local().get(*x)) // Usize to Option<FsEntry> .map(|x| self.local().get(*x)) // Usize to Option<Entry>
.flatten() .flatten()
.collect(); .collect();
SelectedEntry::from(files) SelectedEntry::from(files)
@@ -100,9 +100,9 @@ impl FileTransferActivity {
match self.get_selected_index(&Id::ExplorerRemote) { match self.get_selected_index(&Id::ExplorerRemote) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)), SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)),
SelectedEntryIndex::Many(files) => { SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files let files: Vec<&Entry> = files
.iter() .iter()
.map(|x| self.remote().get(*x)) // Usize to Option<FsEntry> .map(|x| self.remote().get(*x)) // Usize to Option<Entry>
.flatten() .flatten()
.collect(); .collect();
SelectedEntry::from(files) SelectedEntry::from(files)
@@ -120,9 +120,9 @@ impl FileTransferActivity {
SelectedEntry::from(self.found().as_ref().unwrap().get(idx)) SelectedEntry::from(self.found().as_ref().unwrap().get(idx))
} }
SelectedEntryIndex::Many(files) => { SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files let files: Vec<&Entry> = files
.iter() .iter()
.map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option<FsEntry> .map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option<Entry>
.flatten() .flatten()
.collect(); .collect();
SelectedEntry::from(files) SelectedEntry::from(files)
@@ -26,7 +26,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
// locals // locals
use super::{FileTransferActivity, FsEntry, LogLevel}; use super::{Entry, FileTransferActivity, LogLevel};
use std::fs::File; use std::fs::File;
use std::path::PathBuf; use std::path::PathBuf;
@@ -35,7 +35,7 @@ impl FileTransferActivity {
// Check if file exists // Check if file exists
let mut file_exists: bool = false; let mut file_exists: bool = false;
for file in self.local().iter_files_all() { for file in self.local().iter_files_all() {
if input == file.get_name() { if input == file.name() {
file_exists = true; file_exists = true;
} }
} }
@@ -67,7 +67,7 @@ impl FileTransferActivity {
// Check if file exists // Check if file exists
let mut file_exists: bool = false; let mut file_exists: bool = false;
for file in self.remote().iter_files_all() { for file in self.remote().iter_files_all() {
if input == file.get_name() { if input == file.name() {
file_exists = true; file_exists = true;
} }
} }
@@ -88,7 +88,7 @@ impl FileTransferActivity {
), ),
Ok(tfile) => { Ok(tfile) => {
// Stat tempfile // Stat tempfile
let local_file: FsEntry = match self.host.stat(tfile.path()) { let local_file: Entry = match self.host.stat(tfile.path()) {
Err(err) => { Err(err) => {
self.log_and_alert( self.log_and_alert(
LogLevel::Error, LogLevel::Error,
@@ -98,7 +98,7 @@ impl FileTransferActivity {
} }
Ok(f) => f, Ok(f) => f,
}; };
if let FsEntry::File(local_file) = local_file { if let Entry::File(local_file) = local_file {
// Create file // Create file
let reader = Box::new(match File::open(tfile.path()) { let reader = Box::new(match File::open(tfile.path()) {
Ok(f) => f, Ok(f) => f,
@@ -112,7 +112,7 @@ impl FileTransferActivity {
}); });
match self match self
.client .client
.send_file_wno_stream(&local_file, file_path.as_path(), reader) .create_file(file_path.as_path(), &local_file.metadata, reader)
{ {
Err(err) => self.log_and_alert( Err(err) => self.log_and_alert(
LogLevel::Error, LogLevel::Error,
+16 -19
View File
@@ -26,7 +26,7 @@
* SOFTWARE. * SOFTWARE.
*/ */
// locals // locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload}; use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
// ext // ext
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -35,7 +35,7 @@ impl FileTransferActivity {
/// ///
/// Open local file /// Open local file
pub(crate) fn action_open_local(&mut self) { pub(crate) fn action_open_local(&mut self) {
let entries: Vec<FsEntry> = match self.get_local_selected_entries() { let entries: Vec<Entry> = match self.get_local_selected_entries() {
SelectedEntry::One(entry) => vec![entry], SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries, SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![], SelectedEntry::None => vec![],
@@ -49,7 +49,7 @@ impl FileTransferActivity {
/// ///
/// Open local file /// Open local file
pub(crate) fn action_open_remote(&mut self) { pub(crate) fn action_open_remote(&mut self) {
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() { let entries: Vec<Entry> = match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => vec![entry], SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries, SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![], SelectedEntry::None => vec![],
@@ -62,25 +62,22 @@ impl FileTransferActivity {
/// ### action_open_local_file /// ### action_open_local_file
/// ///
/// Perform open lopcal file /// Perform open lopcal file
pub(crate) fn action_open_local_file(&mut self, entry: &FsEntry, open_with: Option<&str>) { pub(crate) fn action_open_local_file(&mut self, entry: &Entry, open_with: Option<&str>) {
let entry: FsEntry = entry.get_realfile(); self.open_path_with(entry.path(), open_with);
self.open_path_with(entry.get_abs_path().as_path(), open_with);
} }
/// ### action_open_local /// ### action_open_local
/// ///
/// Open remote file. The file is first downloaded to a temporary directory on localhost /// Open remote file. The file is first downloaded to a temporary directory on localhost
pub(crate) fn action_open_remote_file(&mut self, entry: &FsEntry, open_with: Option<&str>) { pub(crate) fn action_open_remote_file(&mut self, entry: &Entry, open_with: Option<&str>) {
let entry: FsEntry = entry.get_realfile();
// Download file // Download file
let tmpfile: String = let tmpfile: String = match self.get_cache_tmp_name(entry.name(), entry.extension()) {
match self.get_cache_tmp_name(entry.get_name(), entry.get_ftype().as_deref()) { None => {
None => { self.log(LogLevel::Error, String::from("Could not create tempdir"));
self.log(LogLevel::Error, String::from("Could not create tempdir")); return;
return; }
} Some(p) => p,
Some(p) => p, };
};
let cache: PathBuf = match self.cache.as_ref() { let cache: PathBuf = match self.cache.as_ref() {
None => { None => {
self.log(LogLevel::Error, String::from("Could not create tempdir")); self.log(LogLevel::Error, String::from("Could not create tempdir"));
@@ -89,7 +86,7 @@ impl FileTransferActivity {
Some(p) => p.path().to_path_buf(), Some(p) => p.path().to_path_buf(),
}; };
match self.filetransfer_recv( match self.filetransfer_recv(
TransferPayload::Any(entry), TransferPayload::Any(entry.clone()),
cache.as_path(), cache.as_path(),
Some(tmpfile.clone()), Some(tmpfile.clone()),
) { ) {
@@ -114,7 +111,7 @@ impl FileTransferActivity {
/// ///
/// Open selected file with provided application /// Open selected file with provided application
pub(crate) fn action_local_open_with(&mut self, with: &str) { pub(crate) fn action_local_open_with(&mut self, with: &str) {
let entries: Vec<FsEntry> = match self.get_local_selected_entries() { let entries: Vec<Entry> = match self.get_local_selected_entries() {
SelectedEntry::One(entry) => vec![entry], SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries, SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![], SelectedEntry::None => vec![],
@@ -129,7 +126,7 @@ impl FileTransferActivity {
/// ///
/// Open selected file with provided application /// Open selected file with provided application
pub(crate) fn action_remote_open_with(&mut self, with: &str) { pub(crate) fn action_remote_open_with(&mut self, with: &str) {
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() { let entries: Vec<Entry> = match self.get_remote_selected_entries() {
SelectedEntry::One(entry) => vec![entry], SelectedEntry::One(entry) => vec![entry],
SelectedEntry::Many(entries) => entries, SelectedEntry::Many(entries) => entries,
SelectedEntry::None => vec![], SelectedEntry::None => vec![],
@@ -26,8 +26,9 @@
* SOFTWARE. * SOFTWARE.
*/ */
// locals // locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry}; use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry};
use crate::filetransfer::FileTransferErrorType;
use remotefs::RemoteErrorType;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
impl FileTransferActivity { impl FileTransferActivity {
@@ -45,7 +46,7 @@ impl FileTransferActivity {
// Iter files // Iter files
for entry in entries.iter() { for entry in entries.iter() {
let mut dest_path: PathBuf = base_path.clone(); let mut dest_path: PathBuf = base_path.clone();
dest_path.push(entry.get_name()); dest_path.push(entry.name());
self.local_rename_file(entry, dest_path.as_path()); self.local_rename_file(entry, dest_path.as_path());
} }
// Reload entries // Reload entries
@@ -69,7 +70,7 @@ impl FileTransferActivity {
// Iter files // Iter files
for entry in entries.iter() { for entry in entries.iter() {
let mut dest_path: PathBuf = base_path.clone(); let mut dest_path: PathBuf = base_path.clone();
dest_path.push(entry.get_name()); dest_path.push(entry.name());
self.remote_rename_file(entry, dest_path.as_path()); self.remote_rename_file(entry, dest_path.as_path());
} }
// Reload entries // Reload entries
@@ -79,14 +80,14 @@ impl FileTransferActivity {
} }
} }
fn local_rename_file(&mut self, entry: &FsEntry, dest: &Path) { fn local_rename_file(&mut self, entry: &Entry, dest: &Path) {
match self.host.rename(entry, dest) { match self.host.rename(entry, dest) {
Ok(_) => { Ok(_) => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!( format!(
"Moved \"{}\" to \"{}\"", "Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(), entry.path().display(),
dest.display() dest.display()
), ),
); );
@@ -95,7 +96,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not move \"{}\" to \"{}\": {}", "Could not move \"{}\" to \"{}\": {}",
entry.get_abs_path().display(), entry.path().display(),
dest.display(), dest.display(),
err err
), ),
@@ -103,26 +104,26 @@ impl FileTransferActivity {
} }
} }
fn remote_rename_file(&mut self, entry: &FsEntry, dest: &Path) { fn remote_rename_file(&mut self, entry: &Entry, dest: &Path) {
match self.client.as_mut().rename(entry, dest) { match self.client.as_mut().mov(entry.path(), dest) {
Ok(_) => { Ok(_) => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!( format!(
"Moved \"{}\" to \"{}\"", "Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(), entry.path().display(),
dest.display() dest.display()
), ),
); );
} }
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => { Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => {
self.tricky_move(entry, dest); self.tricky_move(entry, dest);
} }
Err(err) => self.log_and_alert( Err(err) => self.log_and_alert(
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not move \"{}\" to \"{}\": {}", "Could not move \"{}\" to \"{}\": {}",
entry.get_abs_path().display(), entry.path().display(),
dest.display(), dest.display(),
err err
), ),
@@ -134,21 +135,21 @@ impl FileTransferActivity {
/// ///
/// Tricky move will be used whenever copy command is not available on remote host. /// Tricky move will be used whenever copy command is not available on remote host.
/// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`) /// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`)
fn tricky_move(&mut self, entry: &FsEntry, dest: &Path) { fn tricky_move(&mut self, entry: &Entry, dest: &Path) {
debug!( debug!(
"Using tricky-move to move entry {} to {}", "Using tricky-move to move entry {} to {}",
entry.get_abs_path().display(), entry.path().display(),
dest.display() dest.display()
); );
if self.tricky_copy(entry.clone(), dest).is_ok() { if self.tricky_copy(entry.clone(), dest).is_ok() {
// Delete remote existing entry // Delete remote existing entry
debug!("Tricky-copy worked; removing existing remote entry"); debug!("Tricky-copy worked; removing existing remote entry");
match self.client.remove(entry) { match self.client.remove_dir_all(entry.path()) {
Ok(_) => self.log( Ok(_) => self.log(
LogLevel::Info, LogLevel::Info,
format!( format!(
"Moved \"{}\" to \"{}\"", "Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(), entry.path().display(),
dest.display() dest.display()
), ),
), ),
@@ -156,7 +157,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!( format!(
"Copied \"{}\" to \"{}\"; but failed to remove src: {}", "Copied \"{}\" to \"{}\"; but failed to remove src: {}",
entry.get_abs_path().display(), entry.path().display(),
dest.display(), dest.display(),
err err
), ),
+13 -15
View File
@@ -27,7 +27,7 @@
*/ */
// locals // locals
use super::{ use super::{
super::STORAGE_PENDING_TRANSFER, FileExplorerTab, FileTransferActivity, FsEntry, LogLevel, super::STORAGE_PENDING_TRANSFER, Entry, FileExplorerTab, FileTransferActivity, LogLevel,
SelectedEntry, TransferOpts, TransferPayload, SelectedEntry, TransferOpts, TransferPayload,
}; };
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -101,10 +101,10 @@ impl FileTransferActivity {
{ {
// Save pending transfer // Save pending transfer
self.set_pending_transfer( self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), opts.save_as.as_deref().unwrap_or_else(|| entry.name()),
); );
} else if let Err(err) = self.filetransfer_send( } else if let Err(err) = self.filetransfer_send(
TransferPayload::Any(entry.get_realfile()), TransferPayload::Any(entry.clone()),
wrkdir.as_path(), wrkdir.as_path(),
opts.save_as, opts.save_as,
) { ) {
@@ -123,10 +123,9 @@ impl FileTransferActivity {
dest_path.push(save_as); dest_path.push(save_as);
} }
// Iter files // Iter files
let entries: Vec<FsEntry> = entries.iter().map(|x| x.get_realfile()).collect();
if opts.check_replace && self.config().get_prompt_on_file_replace() { if opts.check_replace && self.config().get_prompt_on_file_replace() {
// Check which file would be replaced // Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries let existing_files: Vec<&Entry> = entries
.iter() .iter()
.filter(|x| { .filter(|x| {
self.remote_file_exists( self.remote_file_exists(
@@ -171,10 +170,10 @@ impl FileTransferActivity {
{ {
// Save pending transfer // Save pending transfer
self.set_pending_transfer( self.set_pending_transfer(
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()), opts.save_as.as_deref().unwrap_or_else(|| entry.name()),
); );
} else if let Err(err) = self.filetransfer_recv( } else if let Err(err) = self.filetransfer_recv(
TransferPayload::Any(entry.get_realfile()), TransferPayload::Any(entry.clone()),
wrkdir.as_path(), wrkdir.as_path(),
opts.save_as, opts.save_as,
) { ) {
@@ -193,10 +192,9 @@ impl FileTransferActivity {
dest_path.push(save_as); dest_path.push(save_as);
} }
// Iter files // Iter files
let entries: Vec<FsEntry> = entries.iter().map(|x| x.get_realfile()).collect();
if opts.check_replace && self.config().get_prompt_on_file_replace() { if opts.check_replace && self.config().get_prompt_on_file_replace() {
// Check which file would be replaced // Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries let existing_files: Vec<&Entry> = entries
.iter() .iter()
.filter(|x| { .filter(|x| {
self.local_file_exists( self.local_file_exists(
@@ -244,8 +242,8 @@ impl FileTransferActivity {
/// ### set_pending_transfer_many /// ### set_pending_transfer_many
/// ///
/// Set pending transfer for many files into storage and mount radio /// Set pending transfer for many files into storage and mount radio
pub(crate) fn set_pending_transfer_many(&mut self, files: Vec<&FsEntry>, dest_path: &str) { pub(crate) fn set_pending_transfer_many(&mut self, files: Vec<&Entry>, dest_path: &str) {
let file_names: Vec<&str> = files.iter().map(|x| x.get_name()).collect(); let file_names: Vec<&str> = files.iter().map(|x| x.name()).collect();
self.mount_radio_replace_many(file_names.as_slice()); self.mount_radio_replace_many(file_names.as_slice());
self.context_mut() self.context_mut()
.store_mut() .store_mut()
@@ -255,16 +253,16 @@ impl FileTransferActivity {
/// ### file_to_check /// ### file_to_check
/// ///
/// Get file to check for path /// Get file to check for path
pub(crate) fn file_to_check(e: &FsEntry, alt: Option<&String>) -> PathBuf { pub(crate) fn file_to_check(e: &Entry, alt: Option<&String>) -> PathBuf {
match alt { match alt {
Some(s) => PathBuf::from(s), Some(s) => PathBuf::from(s),
None => PathBuf::from(e.get_name()), None => PathBuf::from(e.name()),
} }
} }
pub(crate) fn file_to_check_many(e: &FsEntry, wrkdir: &Path) -> PathBuf { pub(crate) fn file_to_check_many(e: &Entry, wrkdir: &Path) -> PathBuf {
let mut p = wrkdir.to_path_buf(); let mut p = wrkdir.to_path_buf();
p.push(e.get_name()); p.push(e.name());
p p
} }
} }
@@ -26,7 +26,9 @@
* SOFTWARE. * SOFTWARE.
*/ */
// locals // locals
use super::{FileTransferActivity, FsEntry}; use super::{Entry, FileTransferActivity};
use remotefs::fs::{File, Metadata};
enum SubmitAction { enum SubmitAction {
ChangeDir, ChangeDir,
@@ -38,25 +40,41 @@ impl FileTransferActivity {
/// ///
/// Decides which action to perform on submit for local explorer /// Decides which action to perform on submit for local explorer
/// Return true whether the directory changed /// Return true whether the directory changed
pub(crate) fn action_submit_local(&mut self, entry: FsEntry) -> bool { pub(crate) fn action_submit_local(&mut self, entry: Entry) -> bool {
let action: SubmitAction = match &entry { let (action, entry) = match &entry {
FsEntry::Directory(_) => SubmitAction::ChangeDir, Entry::Directory(_) => (SubmitAction::ChangeDir, entry),
FsEntry::File(file) => { Entry::File(File {
match &file.symlink { path,
Some(symlink_entry) => { metadata:
// If symlink and is directory, point to symlink Metadata {
match &**symlink_entry { symlink: Some(symlink),
FsEntry::Directory(_) => SubmitAction::ChangeDir, ..
_ => SubmitAction::None, },
} ..
}) => {
// Stat file
let stat_file = match self.host.stat(symlink.as_path()) {
Ok(e) => e,
Err(err) => {
warn!(
"Could not stat file pointed by {} ({}): {}",
path.display(),
symlink.display(),
err
);
entry
} }
None => SubmitAction::None, };
} (SubmitAction::ChangeDir, stat_file)
} }
Entry::File(_) => (SubmitAction::None, entry),
}; };
match action { match (action, entry) {
SubmitAction::ChangeDir => self.action_enter_local_dir(entry, false), (SubmitAction::ChangeDir, Entry::Directory(dir)) => {
SubmitAction::None => false, self.action_enter_local_dir(dir, false)
}
(SubmitAction::ChangeDir, _) => false,
(SubmitAction::None, _) => false,
} }
} }
@@ -64,25 +82,41 @@ impl FileTransferActivity {
/// ///
/// Decides which action to perform on submit for remote explorer /// Decides which action to perform on submit for remote explorer
/// Return true whether the directory changed /// Return true whether the directory changed
pub(crate) fn action_submit_remote(&mut self, entry: FsEntry) -> bool { pub(crate) fn action_submit_remote(&mut self, entry: Entry) -> bool {
let action: SubmitAction = match &entry { let (action, entry) = match &entry {
FsEntry::Directory(_) => SubmitAction::ChangeDir, Entry::Directory(_) => (SubmitAction::ChangeDir, entry),
FsEntry::File(file) => { Entry::File(File {
match &file.symlink { path,
Some(symlink_entry) => { metadata:
// If symlink and is directory, point to symlink Metadata {
match &**symlink_entry { symlink: Some(symlink),
FsEntry::Directory(_) => SubmitAction::ChangeDir, ..
_ => SubmitAction::None, },
} ..
}) => {
// Stat file
let stat_file = match self.client.stat(symlink.as_path()) {
Ok(e) => e,
Err(err) => {
warn!(
"Could not stat file pointed by {} ({}): {}",
path.display(),
symlink.display(),
err
);
entry
} }
None => SubmitAction::None, };
} (SubmitAction::ChangeDir, stat_file)
} }
Entry::File(_) => (SubmitAction::None, entry),
}; };
match action { match (action, entry) {
SubmitAction::ChangeDir => self.action_enter_remote_dir(entry, false), (SubmitAction::ChangeDir, Entry::Directory(dir)) => {
SubmitAction::None => false, self.action_enter_remote_dir(dir, false)
}
(SubmitAction::ChangeDir, _) => false,
(SubmitAction::None, _) => false,
} }
} }
} }
@@ -225,21 +225,12 @@ impl Component<Msg, NoUserEvent> for Log {
/// ## OwnStates /// ## OwnStates
/// ///
/// OwnStates contains states for this component /// OwnStates contains states for this component
#[derive(Clone)] #[derive(Clone, Default)]
struct OwnStates { struct OwnStates {
list_index: usize, // Index of selected element in list list_index: usize, // Index of selected element in list
list_len: usize, // Length of file list list_len: usize, // Length of file list
} }
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
list_index: 0,
list_len: 0,
}
}
}
impl OwnStates { impl OwnStates {
/// ### set_list_len /// ### set_list_len
/// ///
@@ -27,12 +27,11 @@
*/ */
use super::super::Browser; use super::super::Browser;
use super::{Msg, TransferMsg, UiMsg}; use super::{Msg, TransferMsg, UiMsg};
use crate::fs::explorer::FileSorting; use crate::explorer::FileSorting;
use crate::fs::FsEntry;
use crate::utils::fmt::fmt_time; use crate::utils::fmt::fmt_time;
use bytesize::ByteSize; use bytesize::ByteSize;
use std::path::PathBuf; use remotefs::Entry;
use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span}; use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span};
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
@@ -400,38 +399,32 @@ pub struct FileInfoPopup {
} }
impl FileInfoPopup { impl FileInfoPopup {
pub fn new(file: &FsEntry) -> Self { pub fn new(file: &Entry) -> Self {
let mut texts: TableBuilder = TableBuilder::default(); let mut texts: TableBuilder = TableBuilder::default();
// Abs path // Abs path
let real_path: Option<PathBuf> = { let real_path = file.metadata().symlink.as_deref();
let real_file: FsEntry = file.get_realfile();
match real_file.get_abs_path() != file.get_abs_path() {
true => Some(real_file.get_abs_path()),
false => None,
}
};
let path: String = match real_path { let path: String = match real_path {
Some(symlink) => format!("{} -> {}", file.get_abs_path().display(), symlink.display()), Some(symlink) => format!("{} -> {}", file.path().display(), symlink.display()),
None => format!("{}", file.get_abs_path().display()), None => format!("{}", file.path().display()),
}; };
// Make texts // Make texts
texts texts
.add_col(TextSpan::from("Path: ")) .add_col(TextSpan::from("Path: "))
.add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); .add_col(TextSpan::new(path.as_str()).fg(Color::Yellow));
if let Some(filetype) = file.get_ftype() { if let Some(filetype) = file.extension() {
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("File type: ")) .add_col(TextSpan::from("File type: "))
.add_col(TextSpan::new(filetype.as_str()).fg(Color::LightGreen)); .add_col(TextSpan::new(filetype).fg(Color::LightGreen));
} }
let (bsize, size): (ByteSize, usize) = (ByteSize(file.get_size() as u64), file.get_size()); let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size);
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("Size: ")) .add_col(TextSpan::from("Size: "))
.add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan)); .add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan));
let ctime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S"); let atime: String = fmt_time(file.metadata().atime, "%b %d %Y %H:%M:%S");
let atime: String = fmt_time(file.get_last_access_time(), "%b %d %Y %H:%M:%S"); let ctime: String = fmt_time(file.metadata().ctime, "%b %d %Y %H:%M:%S");
let mtime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S"); let mtime: String = fmt_time(file.metadata().mtime, "%b %d %Y %H:%M:%S");
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("Creation time: ")) .add_col(TextSpan::from("Creation time: "))
@@ -446,7 +439,7 @@ impl FileInfoPopup {
.add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); .add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed));
// User // User
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
let username: String = match file.get_user() { let username: String = match file.metadata().uid {
Some(uid) => match get_user_by_uid(uid) { Some(uid) => match get_user_by_uid(uid) {
Some(user) => user.name().to_string_lossy().to_string(), Some(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(), None => uid.to_string(),
@@ -454,10 +447,10 @@ impl FileInfoPopup {
None => String::from("0"), None => String::from("0"),
}; };
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let username: String = format!("{}", file.get_user().unwrap_or(0)); let username: String = format!("{}", file.metadata().uid.unwrap_or(0));
// Group // Group
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
let group: String = match file.get_group() { let group: String = match file.metadata().gid {
Some(gid) => match get_group_by_gid(gid) { Some(gid) => match get_group_by_gid(gid) {
Some(group) => group.name().to_string_lossy().to_string(), Some(group) => group.name().to_string_lossy().to_string(),
None => gid.to_string(), None => gid.to_string(),
@@ -465,7 +458,7 @@ impl FileInfoPopup {
None => String::from("0"), None => String::from("0"),
}; };
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let group: String = format!("{}", file.get_group().unwrap_or(0)); let group: String = format!("{}", file.metadata().gid.unwrap_or(0));
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("User: ")) .add_col(TextSpan::from("User: "))
@@ -478,7 +471,7 @@ impl FileInfoPopup {
component: List::default() component: List::default()
.borders(Borders::default().modifiers(BorderType::Rounded)) .borders(Borders::default().modifiers(BorderType::Rounded))
.scroll(false) .scroll(false)
.title(file.get_name(), Alignment::Left) .title(file.name(), Alignment::Left)
.rows(texts.build()), .rows(texts.build()),
} }
} }
@@ -39,21 +39,12 @@ pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
/// ## OwnStates /// ## OwnStates
/// ///
/// OwnStates contains states for this component /// OwnStates contains states for this component
#[derive(Clone)] #[derive(Clone, Default)]
struct OwnStates { struct OwnStates {
list_index: usize, // Index of selected element in list list_index: usize, // Index of selected element in list
selected: Vec<usize>, // Selected files selected: Vec<usize>, // Selected files
} }
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
list_index: 0,
selected: Vec::new(),
}
}
}
impl OwnStates { impl OwnStates {
/// ### init_list_states /// ### init_list_states
/// ///
@@ -25,10 +25,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs}; use crate::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
use crate::fs::FsEntry;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
use remotefs::Entry;
use std::path::Path; use std::path::Path;
/// ## FileExplorerTab /// ## FileExplorerTab
@@ -100,7 +100,7 @@ impl Browser {
self.found.as_mut().map(|x| &mut x.1) self.found.as_mut().map(|x| &mut x.1)
} }
pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec<FsEntry>, wrkdir: &Path) { pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec<Entry>, wrkdir: &Path) {
let mut explorer = Self::build_found_explorer(wrkdir); let mut explorer = Self::build_found_explorer(wrkdir);
explorer.set_files(files); explorer.set_files(files);
self.found = Some((tab, explorer)); self.found = Some((tab, explorer));
+1 -9
View File
@@ -29,7 +29,6 @@ use super::{
use crate::filetransfer::ProtocolParams; use crate::filetransfer::ProtocolParams;
use crate::system::environment; use crate::system::environment;
use crate::system::notifications::Notification; use crate::system::notifications::Notification;
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex}; use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex};
use crate::utils::path; use crate::utils::path;
// Ext // Ext
@@ -120,13 +119,6 @@ impl FileTransferActivity {
} }
} }
/// ### make_ssh_storage
///
/// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded)
pub(super) fn make_ssh_storage(cli: &ConfigClient) -> SshKeyStorage {
SshKeyStorage::storage_from_config(cli)
}
/// ### setup_text_editor /// ### setup_text_editor
/// ///
/// Set text editor to use /// Set text editor to use
@@ -227,7 +219,7 @@ impl FileTransferActivity {
TransferPayload::Any(entry) => { TransferPayload::Any(entry) => {
format!( format!(
"\"{}\" has been successfully transferred ({})", "\"{}\" has been successfully transferred ({})",
entry.get_name(), entry.name(),
transfer_stats transfer_stats
) )
} }
+7 -17
View File
@@ -37,10 +37,8 @@ mod view;
// locals // locals
use super::{Activity, Context, ExitReason}; use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme; use crate::config::themes::Theme;
use crate::filetransfer::{FileTransfer, FileTransferProtocol}; use crate::explorer::{FileExplorer, FileSorting};
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer}; use crate::filetransfer::{Builder, FileTransferParams};
use crate::fs::explorer::{FileExplorer, FileSorting};
use crate::fs::FsEntry;
use crate::host::Localhost; use crate::host::Localhost;
use crate::system::config_client::ConfigClient; use crate::system::config_client::ConfigClient;
pub(self) use lib::browser; pub(self) use lib::browser;
@@ -50,6 +48,7 @@ pub(self) use session::TransferPayload;
// Includes // Includes
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
use remotefs::RemoteFs;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::time::Duration; use std::time::Duration;
use tempfile::TempDir; use tempfile::TempDir;
@@ -217,8 +216,8 @@ pub struct FileTransferActivity {
redraw: bool, redraw: bool,
/// Localhost bridge /// Localhost bridge
host: Localhost, host: Localhost,
/// Remote host /// Remote host client
client: Box<dyn FileTransfer>, client: Box<dyn RemoteFs>,
/// Browser /// Browser
browser: Browser, browser: Browser,
/// Current log lines /// Current log lines
@@ -232,7 +231,7 @@ impl FileTransferActivity {
/// ### new /// ### new
/// ///
/// Instantiates a new FileTransferActivity /// Instantiates a new FileTransferActivity
pub fn new(host: Localhost, protocol: FileTransferProtocol, ticks: Duration) -> Self { pub fn new(host: Localhost, params: &FileTransferParams, ticks: Duration) -> Self {
// Get config client // Get config client
let config_client: ConfigClient = Self::init_config_client(); let config_client: ConfigClient = Self::init_config_client();
Self { Self {
@@ -245,16 +244,7 @@ impl FileTransferActivity {
), ),
redraw: true, redraw: true,
host, host,
client: match protocol { client: Builder::build(params.protocol, params.params.clone(), &config_client),
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
Self::make_ssh_storage(&config_client),
)),
FileTransferProtocol::Ftp(ftps) => Box::new(FtpFileTransfer::new(ftps)),
FileTransferProtocol::Scp => {
Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client)))
}
FileTransferProtocol::AwsS3 => Box::new(S3FileTransfer::default()),
},
browser: Browser::new(&config_client), browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
transfer: TransferStates::default(), transfer: TransferStates::default(),
+124 -122
View File
@@ -27,14 +27,14 @@
*/ */
// Locals // Locals
use super::{FileTransferActivity, LogLevel}; use super::{FileTransferActivity, LogLevel};
use crate::filetransfer::{FileTransferError, FileTransferErrorType};
use crate::fs::{FsEntry, FsFile};
use crate::host::HostError; use crate::host::HostError;
use crate::utils::fmt::fmt_millis; use crate::utils::fmt::fmt_millis;
// Ext // Ext
use bytesize::ByteSize; use bytesize::ByteSize;
use std::fs::File; use remotefs::fs::{Entry, File, UnixPex, Welcome};
use remotefs::{RemoteError, RemoteErrorType};
use std::fs::File as StdFile;
use std::io::{Read, Seek, Write}; use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Instant; use std::time::Instant;
@@ -56,20 +56,20 @@ enum TransferErrorReason {
#[error("I/O error on remote: {0}")] #[error("I/O error on remote: {0}")]
RemoteIoError(std::io::Error), RemoteIoError(std::io::Error),
#[error("File transfer error: {0}")] #[error("File transfer error: {0}")]
FileTransferError(FileTransferError), FileTransferError(RemoteError),
} }
/// ## TransferPayload /// ## TransferPayload
/// ///
/// Represents the entity to send or receive during a transfer. /// Represents the entity to send or receive during a transfer.
/// - File: describes an individual `FsFile` to send /// - File: describes an individual `File` to send
/// - Any: Can be any kind of `FsEntry`, but just one /// - Any: Can be any kind of `Entry`, but just one
/// - Many: a list of `FsEntry` /// - Many: a list of `Entry`
#[derive(Debug)] #[derive(Debug)]
pub(super) enum TransferPayload { pub(super) enum TransferPayload {
File(FsFile), File(File),
Any(FsEntry), Any(Entry),
Many(Vec<FsEntry>), Many(Vec<Entry>),
} }
impl FileTransferActivity { impl FileTransferActivity {
@@ -78,11 +78,11 @@ impl FileTransferActivity {
/// Connect to remote /// Connect to remote
pub(super) fn connect(&mut self) { pub(super) fn connect(&mut self) {
let ft_params = self.context().ft_params().unwrap().clone(); let ft_params = self.context().ft_params().unwrap().clone();
let entry_dir: Option<PathBuf> = ft_params.entry_directory.clone(); let entry_dir: Option<PathBuf> = ft_params.entry_directory;
// Connect to remote // Connect to remote
match self.client.connect(&ft_params.params) { match self.client.connect() {
Ok(welcome) => { Ok(Welcome { banner, .. }) => {
if let Some(banner) = welcome { if let Some(banner) = banner {
// Log welcome // Log welcome
self.log( self.log(
LogLevel::Info, LogLevel::Info,
@@ -234,17 +234,17 @@ impl FileTransferActivity {
/// Send one file to remote at specified path. /// Send one file to remote at specified path.
fn filetransfer_send_file( fn filetransfer_send_file(
&mut self, &mut self,
file: &FsFile, file: &File,
curr_remote_path: &Path, curr_remote_path: &Path,
dst_name: Option<String>, dst_name: Option<String>,
) -> Result<(), String> { ) -> Result<(), String> {
// Reset states // Reset states
self.transfer.reset(); self.transfer.reset();
// Calculate total size of transfer // Calculate total size of transfer
let total_transfer_size: usize = file.size; let total_transfer_size: usize = file.metadata.size as usize;
self.transfer.full.init(total_transfer_size); self.transfer.full.init(total_transfer_size);
// Mount progress bar // Mount progress bar
self.mount_progress_bar(format!("Uploading {}…", file.abs_path.display())); self.mount_progress_bar(format!("Uploading {}…", file.path.display()));
// Get remote path // Get remote path
let file_name: String = file.name.clone(); let file_name: String = file.name.clone();
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path); let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
@@ -266,7 +266,7 @@ impl FileTransferActivity {
/// Send a `TransferPayload` of type `Any` /// Send a `TransferPayload` of type `Any`
fn filetransfer_send_any( fn filetransfer_send_any(
&mut self, &mut self,
entry: &FsEntry, entry: &Entry,
curr_remote_path: &Path, curr_remote_path: &Path,
dst_name: Option<String>, dst_name: Option<String>,
) -> Result<(), String> { ) -> Result<(), String> {
@@ -276,7 +276,7 @@ impl FileTransferActivity {
let total_transfer_size: usize = self.get_total_transfer_size_local(entry); let total_transfer_size: usize = self.get_total_transfer_size_local(entry);
self.transfer.full.init(total_transfer_size); self.transfer.full.init(total_transfer_size);
// Mount progress bar // Mount progress bar
self.mount_progress_bar(format!("Uploading {}…", entry.get_abs_path().display())); self.mount_progress_bar(format!("Uploading {}…", entry.path().display()));
// Send recurse // Send recurse
let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name); let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
// Umount progress bar // Umount progress bar
@@ -289,7 +289,7 @@ impl FileTransferActivity {
/// Send many entries to remote /// Send many entries to remote
fn filetransfer_send_many( fn filetransfer_send_many(
&mut self, &mut self,
entries: &[FsEntry], entries: &[Entry],
curr_remote_path: &Path, curr_remote_path: &Path,
) -> Result<(), String> { ) -> Result<(), String> {
// Reset states // Reset states
@@ -315,14 +315,14 @@ impl FileTransferActivity {
fn filetransfer_send_recurse( fn filetransfer_send_recurse(
&mut self, &mut self,
entry: &FsEntry, entry: &Entry,
curr_remote_path: &Path, curr_remote_path: &Path,
dst_name: Option<String>, dst_name: Option<String>,
) -> Result<(), String> { ) -> Result<(), String> {
// Write popup // Write popup
let file_name: String = match entry { let file_name: String = match entry {
FsEntry::Directory(dir) => dir.name.clone(), Entry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(), Entry::File(file) => file.name.clone(),
}; };
// Get remote path // Get remote path
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path); let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
@@ -333,7 +333,7 @@ impl FileTransferActivity {
remote_path.push(remote_file_name); remote_path.push(remote_file_name);
// Match entry // Match entry
let result: Result<(), String> = match entry { let result: Result<(), String> = match entry {
FsEntry::File(file) => { Entry::File(file) => {
match self.filetransfer_send_one(file, remote_path.as_path(), file_name) { match self.filetransfer_send_one(file, remote_path.as_path(), file_name) {
Err(err) => { Err(err) => {
// If transfer was abrupted or there was an IO error on remote, remove file // If transfer was abrupted or there was an IO error on remote, remove file
@@ -352,7 +352,7 @@ impl FileTransferActivity {
), ),
), ),
Ok(entry) => { Ok(entry) => {
if let Err(err) = self.client.remove(&entry) { if let Err(err) = self.client.remove_file(entry.path()) {
self.log( self.log(
LogLevel::Error, LogLevel::Error,
format!( format!(
@@ -370,16 +370,19 @@ impl FileTransferActivity {
Ok(_) => Ok(()), Ok(_) => Ok(()),
} }
} }
FsEntry::Directory(dir) => { Entry::Directory(dir) => {
// Create directory on remote first // Create directory on remote first
match self.client.mkdir(remote_path.as_path()) { match self
.client
.create_dir(remote_path.as_path(), UnixPex::from(0o755))
{
Ok(_) => { Ok(_) => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!("Created directory \"{}\"", remote_path.display()), format!("Created directory \"{}\"", remote_path.display()),
); );
} }
Err(err) if err.kind() == FileTransferErrorType::DirectoryAlreadyExists => { Err(err) if err.kind == RemoteErrorType::DirectoryAlreadyExists => {
self.log( self.log(
LogLevel::Info, LogLevel::Info,
format!( format!(
@@ -401,7 +404,7 @@ impl FileTransferActivity {
} }
} }
// Get files in dir // Get files in dir
match self.host.scan_dir(dir.abs_path.as_path()) { match self.host.scan_dir(dir.path.as_path()) {
Ok(entries) => { Ok(entries) => {
// Iterate over files // Iterate over files
for entry in entries.iter() { for entry in entries.iter() {
@@ -423,7 +426,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not scan directory \"{}\": {}", "Could not scan directory \"{}\": {}",
dir.abs_path.display(), dir.path.display(),
err err
), ),
); );
@@ -439,7 +442,7 @@ impl FileTransferActivity {
// Log abort // Log abort
self.log_and_alert( self.log_and_alert(
LogLevel::Warn, LogLevel::Warn,
format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()), format!("Upload aborted for \"{}\"!", entry.path().display()),
); );
} }
result result
@@ -450,18 +453,24 @@ impl FileTransferActivity {
/// Send local file and write it to remote path /// Send local file and write it to remote path
fn filetransfer_send_one( fn filetransfer_send_one(
&mut self, &mut self,
local: &FsFile, local: &File,
remote: &Path, remote: &Path,
file_name: String, file_name: String,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// Sync file size and attributes before transfer
let metadata = self
.host
.stat(local.path.as_path())
.map_err(TransferErrorReason::HostError)
.map(|x| x.metadata().clone())?;
// Upload file // Upload file
// Try to open local file // Try to open local file
match self.host.open_file_read(local.abs_path.as_path()) { match self.host.open_file_read(local.path.as_path()) {
Ok(fhnd) => match self.client.send_file(local, remote) { Ok(fhnd) => match self.client.create(remote, &metadata) {
Ok(rhnd) => { Ok(rhnd) => {
self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd) self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd)
} }
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => { Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => {
self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd) self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd)
} }
Err(err) => Err(TransferErrorReason::FileTransferError(err)), Err(err) => Err(TransferErrorReason::FileTransferError(err)),
@@ -475,10 +484,10 @@ impl FileTransferActivity {
/// Send file to remote using stream /// Send file to remote using stream
fn filetransfer_send_one_with_stream( fn filetransfer_send_one_with_stream(
&mut self, &mut self,
local: &FsFile, local: &File,
remote: &Path, remote: &Path,
file_name: String, file_name: String,
mut reader: File, mut reader: StdFile,
mut writer: Box<dyn Write>, mut writer: Box<dyn Write>,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// Write file // Write file
@@ -548,7 +557,7 @@ impl FileTransferActivity {
} }
} }
// Finalize stream // Finalize stream
if let Err(err) = self.client.on_sent(writer) { if let Err(err) = self.client.on_written(writer) {
self.log( self.log(
LogLevel::Warn, LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err), format!("Could not finalize remote stream: \"{}\"", err),
@@ -562,7 +571,7 @@ impl FileTransferActivity {
LogLevel::Info, LogLevel::Info,
format!( format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(), local.path.display(),
remote.display(), remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()), fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()), ByteSize(self.transfer.partial.calc_bytes_per_second()),
@@ -573,14 +582,20 @@ impl FileTransferActivity {
/// ### filetransfer_send_one_wno_stream /// ### filetransfer_send_one_wno_stream
/// ///
/// Send an `FsFile` to remote without using streams. /// Send an `File` to remote without using streams.
fn filetransfer_send_one_wno_stream( fn filetransfer_send_one_wno_stream(
&mut self, &mut self,
local: &FsFile, local: &File,
remote: &Path, remote: &Path,
file_name: String, file_name: String,
mut reader: File, mut reader: StdFile,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// Sync file size and attributes before transfer
let metadata = self
.host
.stat(local.path.as_path())
.map_err(TransferErrorReason::HostError)
.map(|x| x.metadata().clone())?;
// Write file // Write file
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize; let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer // Init transfer
@@ -593,10 +608,7 @@ impl FileTransferActivity {
self.update_progress_bar(format!("Uploading \"{}\"…", file_name)); self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
self.view(); self.view();
// Send file // Send file
if let Err(err) = self if let Err(err) = self.client.create_file(remote, &metadata, Box::new(reader)) {
.client
.send_file_wno_stream(local, remote, Box::new(reader))
{
return Err(TransferErrorReason::FileTransferError(err)); return Err(TransferErrorReason::FileTransferError(err));
} }
// Set transfer size ok // Set transfer size ok
@@ -610,7 +622,7 @@ impl FileTransferActivity {
LogLevel::Info, LogLevel::Info,
format!( format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(), local.path.display(),
remote.display(), remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()), fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()), ByteSize(self.transfer.partial.calc_bytes_per_second()),
@@ -656,7 +668,7 @@ impl FileTransferActivity {
/// If entry is a directory, this applies to directory only /// If entry is a directory, this applies to directory only
fn filetransfer_recv_any( fn filetransfer_recv_any(
&mut self, &mut self,
entry: &FsEntry, entry: &Entry,
local_path: &Path, local_path: &Path,
dst_name: Option<String>, dst_name: Option<String>,
) -> Result<(), String> { ) -> Result<(), String> {
@@ -666,7 +678,7 @@ impl FileTransferActivity {
let total_transfer_size: usize = self.get_total_transfer_size_remote(entry); let total_transfer_size: usize = self.get_total_transfer_size_remote(entry);
self.transfer.full.init(total_transfer_size); self.transfer.full.init(total_transfer_size);
// Mount progress bar // Mount progress bar
self.mount_progress_bar(format!("Downloading {}…", entry.get_abs_path().display())); self.mount_progress_bar(format!("Downloading {}…", entry.path().display()));
// Receive // Receive
let result = self.filetransfer_recv_recurse(entry, local_path, dst_name); let result = self.filetransfer_recv_recurse(entry, local_path, dst_name);
// Umount progress bar // Umount progress bar
@@ -677,14 +689,14 @@ impl FileTransferActivity {
/// ### filetransfer_recv_file /// ### filetransfer_recv_file
/// ///
/// Receive a single file from remote. /// Receive a single file from remote.
fn filetransfer_recv_file(&mut self, entry: &FsFile, local_path: &Path) -> Result<(), String> { fn filetransfer_recv_file(&mut self, entry: &File, local_path: &Path) -> Result<(), String> {
// Reset states // Reset states
self.transfer.reset(); self.transfer.reset();
// Calculate total transfer size // Calculate total transfer size
let total_transfer_size: usize = entry.size; let total_transfer_size: usize = entry.metadata.size as usize;
self.transfer.full.init(total_transfer_size); self.transfer.full.init(total_transfer_size);
// Mount progress bar // Mount progress bar
self.mount_progress_bar(format!("Downloading {}…", entry.abs_path.display())); self.mount_progress_bar(format!("Downloading {}…", entry.path.display()));
// Receive // Receive
let result = self.filetransfer_recv_one(local_path, entry, entry.name.clone()); let result = self.filetransfer_recv_one(local_path, entry, entry.name.clone());
// Umount progress bar // Umount progress bar
@@ -698,7 +710,7 @@ impl FileTransferActivity {
/// Send many entries to remote /// Send many entries to remote
fn filetransfer_recv_many( fn filetransfer_recv_many(
&mut self, &mut self,
entries: &[FsEntry], entries: &[Entry],
curr_remote_path: &Path, curr_remote_path: &Path,
) -> Result<(), String> { ) -> Result<(), String> {
// Reset states // Reset states
@@ -724,18 +736,18 @@ impl FileTransferActivity {
fn filetransfer_recv_recurse( fn filetransfer_recv_recurse(
&mut self, &mut self,
entry: &FsEntry, entry: &Entry,
local_path: &Path, local_path: &Path,
dst_name: Option<String>, dst_name: Option<String>,
) -> Result<(), String> { ) -> Result<(), String> {
// Write popup // Write popup
let file_name: String = match entry { let file_name: String = match entry {
FsEntry::Directory(dir) => dir.name.clone(), Entry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(), Entry::File(file) => file.name.clone(),
}; };
// Match entry // Match entry
let result: Result<(), String> = match entry { let result: Result<(), String> = match entry {
FsEntry::File(file) => { Entry::File(file) => {
// Get local file // Get local file
let mut local_file_path: PathBuf = PathBuf::from(local_path); let mut local_file_path: PathBuf = PathBuf::from(local_path);
let local_file_name: String = match dst_name { let local_file_name: String = match dst_name {
@@ -781,7 +793,7 @@ impl FileTransferActivity {
Ok(()) Ok(())
} }
} }
FsEntry::Directory(dir) => { Entry::Directory(dir) => {
// Get dir name // Get dir name
let mut local_dir_path: PathBuf = PathBuf::from(local_path); let mut local_dir_path: PathBuf = PathBuf::from(local_path);
match dst_name { match dst_name {
@@ -797,16 +809,13 @@ impl FileTransferActivity {
target_os = "macos", target_os = "macos",
target_os = "linux" target_os = "linux"
))] ))]
if let Some((owner, group, others)) = dir.unix_pex { if let Some(mode) = dir.metadata.mode {
if let Err(err) = self.host.chmod( if let Err(err) = self.host.chmod(local_dir_path.as_path(), mode) {
local_dir_path.as_path(),
(owner.as_byte(), group.as_byte(), others.as_byte()),
) {
self.log( self.log(
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not apply file mode {:?} to \"{}\": {}", "Could not apply file mode {:o} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()), u32::from(mode),
local_dir_path.display(), local_dir_path.display(),
err err
), ),
@@ -818,7 +827,7 @@ impl FileTransferActivity {
format!("Created directory \"{}\"", local_dir_path.display()), format!("Created directory \"{}\"", local_dir_path.display()),
); );
// Get files in dir // Get files in dir
match self.client.list_dir(dir.abs_path.as_path()) { match self.client.list_dir(dir.path.as_path()) {
Ok(entries) => { Ok(entries) => {
// Iterate over files // Iterate over files
for entry in entries.iter() { for entry in entries.iter() {
@@ -843,7 +852,7 @@ impl FileTransferActivity {
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not scan directory \"{}\": {}", "Could not scan directory \"{}\": {}",
dir.abs_path.display(), dir.path.display(),
err err
), ),
); );
@@ -872,10 +881,7 @@ impl FileTransferActivity {
// Log abort // Log abort
self.log_and_alert( self.log_and_alert(
LogLevel::Warn, LogLevel::Warn,
format!( format!("Download aborted for \"{}\"!", entry.path().display()),
"Download aborted for \"{}\"!",
entry.get_abs_path().display()
),
); );
} }
result result
@@ -887,18 +893,18 @@ impl FileTransferActivity {
fn filetransfer_recv_one( fn filetransfer_recv_one(
&mut self, &mut self,
local: &Path, local: &Path,
remote: &FsFile, remote: &File,
file_name: String, file_name: String,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// Try to open local file // Try to open local file
match self.host.open_file_write(local) { match self.host.open_file_write(local) {
Ok(local_file) => { Ok(local_file) => {
// Download file from remote // Download file from remote
match self.client.recv_file(remote) { match self.client.open(remote.path.as_path()) {
Ok(rhnd) => self.filetransfer_recv_one_with_stream( Ok(rhnd) => self.filetransfer_recv_one_with_stream(
local, remote, file_name, rhnd, local_file, local, remote, file_name, rhnd, local_file,
), ),
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => { Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => {
self.filetransfer_recv_one_wno_stream(local, remote, file_name) self.filetransfer_recv_one_wno_stream(local, remote, file_name)
} }
Err(err) => Err(TransferErrorReason::FileTransferError(err)), Err(err) => Err(TransferErrorReason::FileTransferError(err)),
@@ -910,24 +916,24 @@ impl FileTransferActivity {
/// ### filetransfer_recv_one_with_stream /// ### filetransfer_recv_one_with_stream
/// ///
/// Receive an `FsEntry` from remote using stream /// Receive an `Entry` from remote using stream
fn filetransfer_recv_one_with_stream( fn filetransfer_recv_one_with_stream(
&mut self, &mut self,
local: &Path, local: &Path,
remote: &FsFile, remote: &File,
file_name: String, file_name: String,
mut reader: Box<dyn Read>, mut reader: Box<dyn Read>,
mut writer: File, mut writer: StdFile,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
let mut total_bytes_written: usize = 0; let mut total_bytes_written: usize = 0;
// Init transfer // Init transfer
self.transfer.partial.init(remote.size); self.transfer.partial.init(remote.metadata.size as usize);
// Write local file // Write local file
let mut last_progress_val: f64 = 0.0; let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None; let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely read, // While the entire file hasn't been completely read,
// Or filetransfer has been aborted // Or filetransfer has been aborted
while total_bytes_written < remote.size && !self.transfer.aborted() { while total_bytes_written < remote.metadata.size as usize && !self.transfer.aborted() {
// Handle input events (each 500 ms) or is None // Handle input events (each 500 ms) or is None
if last_input_event_fetch.is_none() if last_input_event_fetch.is_none()
|| last_input_event_fetch || last_input_event_fetch
@@ -978,7 +984,7 @@ impl FileTransferActivity {
} }
} }
// Finalize stream // Finalize stream
if let Err(err) = self.client.on_recv(reader) { if let Err(err) = self.client.on_read(reader) {
self.log( self.log(
LogLevel::Warn, LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err), format!("Could not finalize remote stream: \"{}\"", err),
@@ -990,16 +996,13 @@ impl FileTransferActivity {
} }
// Apply file mode to file // Apply file mode to file
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
if let Some((owner, group, others)) = remote.unix_pex { if let Some(mode) = remote.metadata.mode {
if let Err(err) = self if let Err(err) = self.host.chmod(local, mode) {
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log( self.log(
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not apply file mode {:?} to \"{}\": {}", "Could not apply file mode {:o} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()), u32::from(mode),
local.display(), local.display(),
err err
), ),
@@ -1011,7 +1014,7 @@ impl FileTransferActivity {
LogLevel::Info, LogLevel::Info,
format!( format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(), remote.path.display(),
local.display(), local.display(),
fmt_millis(self.transfer.partial.started().elapsed()), fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()), ByteSize(self.transfer.partial.calc_bytes_per_second()),
@@ -1022,40 +1025,47 @@ impl FileTransferActivity {
/// ### filetransfer_recv_one_with_stream /// ### filetransfer_recv_one_with_stream
/// ///
/// Receive an `FsEntry` from remote without using stream /// Receive an `Entry` from remote without using stream
fn filetransfer_recv_one_wno_stream( fn filetransfer_recv_one_wno_stream(
&mut self, &mut self,
local: &Path, local: &Path,
remote: &FsFile, remote: &File,
file_name: String, file_name: String,
) -> Result<(), TransferErrorReason> { ) -> Result<(), TransferErrorReason> {
// Open local file
let reader = self
.host
.open_file_write(local)
.map_err(TransferErrorReason::HostError)
.map(Box::new)?;
// Init transfer // Init transfer
self.transfer.partial.init(remote.size); self.transfer.partial.init(remote.metadata.size as usize);
// Draw before transfer // Draw before transfer
self.update_progress_bar(format!("Downloading \"{}\"", file_name)); self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view(); self.view();
// recv wno stream // recv wno stream
if let Err(err) = self.client.recv_file_wno_stream(remote, local) { if let Err(err) = self.client.open_file(remote.path.as_path(), reader) {
return Err(TransferErrorReason::FileTransferError(err)); return Err(TransferErrorReason::FileTransferError(err));
} }
// Update progress at the end // Update progress at the end
self.transfer.partial.update_progress(remote.size); self.transfer
self.transfer.full.update_progress(remote.size); .partial
.update_progress(remote.metadata.size as usize);
self.transfer
.full
.update_progress(remote.metadata.size as usize);
// Draw after transfer // Draw after transfer
self.update_progress_bar(format!("Downloading \"{}\"", file_name)); self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view(); self.view();
// Apply file mode to file // Apply file mode to file
#[cfg(target_family = "unix")] #[cfg(target_family = "unix")]
if let Some((owner, group, others)) = remote.unix_pex { if let Some(mode) = remote.metadata.mode {
if let Err(err) = self if let Err(err) = self.host.chmod(local, mode) {
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
self.log( self.log(
LogLevel::Error, LogLevel::Error,
format!( format!(
"Could not apply file mode {:?} to \"{}\": {}", "Could not apply file mode {:o} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()), u32::from(mode),
local.display(), local.display(),
err err
), ),
@@ -1067,7 +1077,7 @@ impl FileTransferActivity {
LogLevel::Info, LogLevel::Info,
format!( format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)", "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(), remote.path.display(),
local.display(), local.display(),
fmt_millis(self.transfer.partial.started().elapsed()), fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()), ByteSize(self.transfer.partial.calc_bytes_per_second()),
@@ -1136,7 +1146,7 @@ impl FileTransferActivity {
/// ### download_file_as_temp /// ### download_file_as_temp
/// ///
/// Download provided file as a temporary file /// Download provided file as a temporary file
pub(super) fn download_file_as_temp(&mut self, file: &FsFile) -> Result<PathBuf, String> { pub(super) fn download_file_as_temp(&mut self, file: &File) -> Result<PathBuf, String> {
let tmpfile: PathBuf = match self.cache.as_ref() { let tmpfile: PathBuf = match self.cache.as_ref() {
Some(cache) => { Some(cache) => {
let mut p: PathBuf = cache.path().to_path_buf(); let mut p: PathBuf = cache.path().to_path_buf();
@@ -1157,7 +1167,7 @@ impl FileTransferActivity {
) { ) {
Err(err) => Err(format!( Err(err) => Err(format!(
"Could not download {} to temporary file: {}", "Could not download {} to temporary file: {}",
file.abs_path.display(), file.path.display(),
err err
)), )),
Ok(()) => Ok(tmpfile), Ok(()) => Ok(tmpfile),
@@ -1169,12 +1179,12 @@ impl FileTransferActivity {
/// ### get_total_transfer_size_local /// ### get_total_transfer_size_local
/// ///
/// Get total size of transfer for localhost /// Get total size of transfer for localhost
fn get_total_transfer_size_local(&mut self, entry: &FsEntry) -> usize { fn get_total_transfer_size_local(&mut self, entry: &Entry) -> usize {
match entry { match entry {
FsEntry::File(file) => file.size, Entry::File(file) => file.metadata.size as usize,
FsEntry::Directory(dir) => { Entry::Directory(dir) => {
// List dir // List dir
match self.host.scan_dir(dir.abs_path.as_path()) { match self.host.scan_dir(dir.path.as_path()) {
Ok(files) => files Ok(files) => files
.iter() .iter()
.map(|x| self.get_total_transfer_size_local(x)) .map(|x| self.get_total_transfer_size_local(x))
@@ -1182,11 +1192,7 @@ impl FileTransferActivity {
Err(err) => { Err(err) => {
self.log( self.log(
LogLevel::Error, LogLevel::Error,
format!( format!("Could not list directory {}: {}", dir.path.display(), err),
"Could not list directory {}: {}",
dir.abs_path.display(),
err
),
); );
0 0
} }
@@ -1198,12 +1204,12 @@ impl FileTransferActivity {
/// ### get_total_transfer_size_remote /// ### get_total_transfer_size_remote
/// ///
/// Get total size of transfer for remote host /// Get total size of transfer for remote host
fn get_total_transfer_size_remote(&mut self, entry: &FsEntry) -> usize { fn get_total_transfer_size_remote(&mut self, entry: &Entry) -> usize {
match entry { match entry {
FsEntry::File(file) => file.size, Entry::File(file) => file.metadata.size as usize,
FsEntry::Directory(dir) => { Entry::Directory(dir) => {
// List directory // List directory
match self.client.list_dir(dir.abs_path.as_path()) { match self.client.list_dir(dir.path.as_path()) {
Ok(files) => files Ok(files) => files
.iter() .iter()
.map(|x| self.get_total_transfer_size_remote(x)) .map(|x| self.get_total_transfer_size_remote(x))
@@ -1211,11 +1217,7 @@ impl FileTransferActivity {
Err(err) => { Err(err) => {
self.log( self.log(
LogLevel::Error, LogLevel::Error,
format!( format!("Could not list directory {}: {}", dir.path.display(), err),
"Could not list directory {}: {}",
dir.abs_path.display(),
err
),
); );
0 0
} }
+2 -2
View File
@@ -31,8 +31,8 @@ use super::{
browser::{FileExplorerTab, FoundExplorerTab}, browser::{FileExplorerTab, FoundExplorerTab},
ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg, ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg,
}; };
use crate::fs::FsEntry;
// externals // externals
use remotefs::fs::Entry;
use tuirealm::{ use tuirealm::{
props::{AttrValue, Attribute}, props::{AttrValue, Attribute},
State, StateValue, Update, State, StateValue, Update,
@@ -282,7 +282,7 @@ impl FileTransferActivity {
// Mount wait // Mount wait
self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, search).as_str()); self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, search).as_str());
// Find // Find
let res: Result<Vec<FsEntry>, String> = match self.browser.tab() { let res: Result<Vec<Entry>, String> = match self.browser.tab() {
FileExplorerTab::Local => self.action_local_find(search.clone()), FileExplorerTab::Local => self.action_local_find(search.clone()),
FileExplorerTab::Remote => self.action_remote_find(search.clone()), FileExplorerTab::Remote => self.action_remote_find(search.clone()),
_ => panic!("Trying to search for files, while already in a find result"), _ => panic!("Trying to search for files, while already in a find result"),
+3 -3
View File
@@ -30,11 +30,11 @@ use super::{
browser::{FileExplorerTab, FoundExplorerTab}, browser::{FileExplorerTab, FoundExplorerTab},
components, Context, FileTransferActivity, Id, components, Context, FileTransferActivity, Id,
}; };
use crate::fs::explorer::FileSorting; use crate::explorer::FileSorting;
use crate::fs::FsEntry;
use crate::ui::store::Store; use crate::ui::store::Store;
use crate::utils::ui::draw_area_in; use crate::utils::ui::draw_area_in;
// Ext // Ext
use remotefs::fs::Entry;
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::tui::layout::{Constraint, Direction, Layout}; use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::tui::widgets::Clear; use tuirealm::tui::widgets::Clear;
@@ -747,7 +747,7 @@ impl FileTransferActivity {
let _ = self.app.umount(&Id::ReplacingFilesListPopup); // NOTE: replace anyway let _ = self.app.umount(&Id::ReplacingFilesListPopup); // NOTE: replace anyway
} }
pub(super) fn mount_file_info(&mut self, file: &FsEntry) { pub(super) fn mount_file_info(&mut self, file: &Entry) {
assert!(self assert!(self
.app .app
.remount( .remount(
+1 -1
View File
@@ -26,8 +26,8 @@
* SOFTWARE. * SOFTWARE.
*/ */
use super::{ConfigMsg, Msg}; use super::{ConfigMsg, Msg};
use crate::explorer::GroupDirs as GroupDirsEnum;
use crate::filetransfer::FileTransferProtocol; use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs as GroupDirsEnum;
use crate::utils::parser::parse_bytesize; use crate::utils::parser::parse_bytesize;
use tui_realm_stdlib::{Input, Radio}; use tui_realm_stdlib::{Input, Radio};
+1 -9
View File
@@ -46,19 +46,11 @@ use tuirealm::{Component, MockComponent};
// -- global listener // -- global listener
#[derive(MockComponent)] #[derive(Default, MockComponent)]
pub struct GlobalListener { pub struct GlobalListener {
component: Phantom, component: Phantom,
} }
impl Default for GlobalListener {
fn default() -> Self {
Self {
component: Phantom::default(),
}
}
}
impl Component<Msg, NoUserEvent> for GlobalListener { impl Component<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
+1 -1
View File
@@ -28,8 +28,8 @@
*/ */
// Locals // Locals
use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout}; use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout};
use crate::explorer::GroupDirs;
use crate::filetransfer::FileTransferProtocol; use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
use crate::utils::fmt::fmt_bytes; use crate::utils::fmt::fmt_bytes;
// Ext // Ext
+8 -8
View File
@@ -25,7 +25,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
use crate::fs::UnixPex; use remotefs::fs::UnixPexClass;
use chrono::prelude::*; use chrono::prelude::*;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@@ -35,18 +35,18 @@ use tuirealm::tui::style::Color;
/// ### fmt_pex /// ### fmt_pex
/// ///
/// Convert permissions bytes of permissions value into ls notation (e.g. rwx,-wx,--x) /// Convert permissions bytes of permissions value into ls notation (e.g. rwx,-wx,--x)
pub fn fmt_pex(pex: UnixPex) -> String { pub fn fmt_pex(pex: UnixPexClass) -> String {
format!( format!(
"{}{}{}", "{}{}{}",
match pex.can_read() { match pex.read() {
true => 'r', true => 'r',
false => '-', false => '-',
}, },
match pex.can_write() { match pex.write() {
true => 'w', true => 'w',
false => '-', false => '-',
}, },
match pex.can_execute() { match pex.execute() {
true => 'x', true => 'x',
false => '-', false => '-',
} }
@@ -315,9 +315,9 @@ mod tests {
#[test] #[test]
fn test_utils_fmt_pex() { fn test_utils_fmt_pex() {
assert_eq!(fmt_pex(UnixPex::from(7)), String::from("rwx")); assert_eq!(fmt_pex(UnixPexClass::from(7)), String::from("rwx"));
assert_eq!(fmt_pex(UnixPex::from(5)), String::from("r-x")); assert_eq!(fmt_pex(UnixPexClass::from(5)), String::from("r-x"));
assert_eq!(fmt_pex(UnixPex::from(6)), String::from("rw-")); assert_eq!(fmt_pex(UnixPexClass::from(6)), String::from("rw-"));
} }
#[test] #[test]
-114
View File
@@ -37,12 +37,9 @@ use crate::system::environment;
// Ext // Ext
use bytesize::ByteSize; use bytesize::ByteSize;
use chrono::format::ParseError;
use chrono::prelude::*;
use regex::Regex; use regex::Regex;
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use std::time::{Duration, SystemTime};
use tuirealm::tui::style::Color; use tuirealm::tui::style::Color;
// Regex // Regex
@@ -267,54 +264,6 @@ fn parse_s3_remote_opt(s: &str) -> Result<FileTransferParams, String> {
} }
} }
/// ### parse_lstime
///
/// Convert ls syntax time to System Time
/// ls time has two possible syntax:
/// 1. if year is current: %b %d %H:%M (e.g. Nov 5 13:46)
/// 2. else: %b %d %Y (e.g. Nov 5 2019)
pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result<SystemTime, ParseError> {
let datetime: NaiveDateTime = match NaiveDate::parse_from_str(tm, fmt_year) {
Ok(date) => {
// Case 2.
// Return NaiveDateTime from NaiveDate with time 00:00:00
date.and_hms(0, 0, 0)
}
Err(_) => {
// Might be case 1.
// We need to add Current Year at the end of the string
let this_year: i32 = Utc::now().year();
let date_time_str: String = format!("{} {}", tm, this_year);
// Now parse
NaiveDateTime::parse_from_str(
date_time_str.as_ref(),
format!("{} %Y", fmt_hours).as_ref(),
)?
}
};
// Convert datetime to system time
let sys_time: SystemTime = SystemTime::UNIX_EPOCH;
Ok(sys_time
.checked_add(Duration::from_secs(datetime.timestamp() as u64))
.unwrap_or(SystemTime::UNIX_EPOCH))
}
/// ### parse_datetime
///
/// Parse date time string representation and transform it into `SystemTime`
#[allow(dead_code)]
pub fn parse_datetime(tm: &str, fmt: &str) -> Result<SystemTime, ParseError> {
match NaiveDateTime::parse_from_str(tm, fmt) {
Ok(dt) => {
let sys_time: SystemTime = SystemTime::UNIX_EPOCH;
Ok(sys_time
.checked_add(Duration::from_secs(dt.timestamp() as u64))
.unwrap_or(SystemTime::UNIX_EPOCH))
}
Err(err) => Err(err),
}
}
/// ### parse_semver /// ### parse_semver
/// ///
/// Parse semver string /// Parse semver string
@@ -611,7 +560,6 @@ pub fn parse_bytesize<S: AsRef<str>>(bytes: S) -> Option<ByteSize> {
mod tests { mod tests {
use super::*; use super::*;
use crate::utils::fmt::fmt_time;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -800,68 +748,6 @@ mod tests {
assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err()); assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err());
} }
#[test]
fn test_utils_parse_lstime() {
// Good cases
assert_eq!(
fmt_time(
parse_lstime("Nov 5 16:32", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap(),
"%m %d %M"
)
.as_str(),
"11 05 32"
);
assert_eq!(
fmt_time(
parse_lstime("Dec 2 21:32", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap(),
"%m %d %M"
)
.as_str(),
"12 02 32"
);
assert_eq!(
parse_lstime("Nov 5 2018", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1541376000)
);
assert_eq!(
parse_lstime("Mar 18 2018", "%b %d %Y", "%b %d %H:%M")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1521331200)
);
// bad cases
assert!(parse_lstime("Oma 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
assert!(parse_lstime("Feb 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
assert!(parse_lstime("Feb 15 25:32", "%b %d %Y", "%b %d %H:%M").is_err());
}
#[test]
fn test_utils_parse_datetime() {
assert_eq!(
parse_datetime("04-08-14 03:09PM", "%d-%m-%y %I:%M%p")
.ok()
.unwrap()
.duration_since(SystemTime::UNIX_EPOCH)
.ok()
.unwrap(),
Duration::from_secs(1407164940)
);
// Not enough argument for datetime
assert!(parse_datetime("04-08-14", "%d-%m-%y").is_err());
}
#[test] #[test]
fn test_utils_parse_semver() { fn test_utils_parse_semver() {
assert_eq!( assert_eq!(
+23 -124
View File
@@ -25,39 +25,27 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE. * SOFTWARE.
*/ */
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex}; use remotefs::fs::{Directory, Entry, File, Metadata};
// ext // ext
use std::fs::File; use std::fs::File as StdFile;
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
use std::fs::OpenOptions;
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
use std::io::Read;
use std::io::Write; use std::io::Write;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::SystemTime;
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
pub fn create_sample_file_entry() -> (FsFile, NamedTempFile) { pub fn create_sample_file_entry() -> (File, NamedTempFile) {
// Write // Write
let tmpfile = create_sample_file(); let tmpfile = create_sample_file();
( (
FsFile { File {
name: tmpfile name: tmpfile
.path() .path()
.file_name() .file_name()
.unwrap() .unwrap()
.to_string_lossy() .to_string_lossy()
.to_string(), .to_string(),
abs_path: tmpfile.path().to_path_buf(), path: tmpfile.path().to_path_buf(),
last_change_time: SystemTime::UNIX_EPOCH, extension: None,
last_access_time: SystemTime::UNIX_EPOCH, metadata: Metadata::default(),
creation_time: SystemTime::UNIX_EPOCH,
size: 127,
ftype: None, // File type
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
}, },
tmpfile, tmpfile,
) )
@@ -80,7 +68,7 @@ pub fn create_sample_file() -> NamedTempFile {
pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> { pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> {
let mut p: PathBuf = PathBuf::from(dir); let mut p: PathBuf = PathBuf::from(dir);
p.push(filename); p.push(filename);
let mut file: File = File::create(p.as_path())?; let mut file = StdFile::create(p.as_path())?;
writeln!( writeln!(
file, file,
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus." "Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus."
@@ -97,88 +85,20 @@ pub fn make_dir_at(dir: &Path, dirname: &str) -> std::io::Result<()> {
std::fs::create_dir(p.as_path()) std::fs::create_dir(p.as_path())
} }
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))] /// Create a Entry at specified path
pub fn write_file(file: &NamedTempFile, writable: &mut Box<dyn Write>) { pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> Entry {
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<P: AsRef<Path>>(path: P, is_dir: bool) -> FsEntry {
let path: PathBuf = path.as_ref().to_path_buf(); let path: PathBuf = path.as_ref().to_path_buf();
match is_dir { match is_dir {
true => FsEntry::Directory(FsDirectory { true => Entry::Directory(Directory {
name: path.file_name().unwrap().to_string_lossy().to_string(), name: path.file_name().unwrap().to_string_lossy().to_string(),
abs_path: path, path,
last_change_time: SystemTime::UNIX_EPOCH, metadata: Metadata::default(),
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
}), }),
false => FsEntry::File(FsFile { false => Entry::File(File {
name: path.file_name().unwrap().to_string_lossy().to_string(), name: path.file_name().unwrap().to_string_lossy().to_string(),
abs_path: path, path,
last_change_time: SystemTime::UNIX_EPOCH, extension: None,
last_access_time: SystemTime::UNIX_EPOCH, metadata: Metadata::default(),
creation_time: SystemTime::UNIX_EPOCH,
size: 127,
ftype: None, // File type
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
}), }),
} }
} }
@@ -186,8 +106,11 @@ pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> FsEntry {
/// ### create_file_ioers /// ### create_file_ioers
/// ///
/// Open a file with two handlers, the first is to read, the second is to write /// Open a file with two handlers, the first is to read, the second is to write
pub fn create_file_ioers(p: &Path) -> (File, File) { pub fn create_file_ioers(p: &Path) -> (StdFile, StdFile) {
(File::open(p).ok().unwrap(), File::create(p).ok().unwrap()) (
StdFile::open(p).ok().unwrap(),
StdFile::create(p).ok().unwrap(),
)
} }
mod test { mod test {
@@ -197,31 +120,7 @@ mod test {
#[test] #[test]
fn test_utils_test_helpers_sample_file() { fn test_utils_test_helpers_sample_file() {
let (file, _) = create_sample_file_entry(); let _ = create_sample_file_entry();
assert!(file.symlink.is_none());
}
#[test]
#[cfg(feature = "with-containers")]
fn test_utils_test_helpers_write_file() {
let (_, temp) = create_sample_file_entry();
let tempdest = NamedTempFile::new().unwrap();
let mut dest: Box<dyn Write> = Box::new(
OpenOptions::new()
.create(true)
.read(false)
.write(true)
.open(tempdest.path())
.ok()
.unwrap(),
);
write_file(&temp, &mut dest);
}
#[test]
#[cfg(feature = "with-containers")]
fn test_utils_test_helpers_write_ssh_key() {
let _ = write_ssh_key();
} }
#[test] #[test]
-35
View File
@@ -1,35 +0,0 @@
version: "3"
services:
openssh-server:
image: ghcr.io/linuxserver/openssh-server
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
- SUDO_ACCESS=false
- PUBLIC_KEY=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a
- PASSWORD_ACCESS=true
- USER_PASSWORD=password
- USER_NAME=sftp
ports:
- "10022:2222"
openssh-server-scp:
image: ghcr.io/linuxserver/openssh-server
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
- SUDO_ACCESS=false
- PASSWORD_ACCESS=true
- PUBLIC_KEY=ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDErJhQxEI0+VvhlXVUyh+vMCm7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VOH7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAeSmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndvB5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkysFisPU1DOb6QielL/axeTebVplaouYcXY0pFdJt root@8c50fd4c345a
- USER_PASSWORD=password
- USER_NAME=sftp
ports:
- "10222:2222"
ftp-server:
image: afharo/pure-ftp
ports:
- "10021:21"
- "30000-30009:30000-30009"
environment:
- PUBLICHOST=localhost
-29
View File
@@ -1,29 +0,0 @@
#!/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