Removed filetransfer module; migrated to remotefs crate

This commit is contained in:
veeso
2021-12-09 18:07:36 +01:00
committed by Christian Visintin
parent 25dd1b9b0a
commit df7a4381c4
60 changed files with 1185 additions and 6814 deletions

View File

@@ -8,7 +8,6 @@ ignore:
- "../*"
- src/main.rs
- src/activity_manager.rs
- src/filetransfer/transfer/s3/mod.rs
- src/support.rs
- src/system/notifications.rs
- "src/ui/activities/*"

View File

@@ -13,8 +13,6 @@ jobs:
- uses: actions/checkout@v2
- name: Install dependencies
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
uses: actions-rs/toolchain@v1
with:
@@ -24,7 +22,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
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:
CARGO_INCREMENTAL: "0"
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"

View File

@@ -13,8 +13,6 @@ jobs:
- uses: actions/checkout@v2
- name: Install dependencies
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
with:
toolchain: stable
@@ -24,7 +22,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
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
run: cargo fmt --all -- --check
- name: Clippy

View File

@@ -44,6 +44,7 @@ Released on FIXME:
- Dependencies:
- Updated `tui-realm` to `1.3.0`
- 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)
## 0.7.0

139
Cargo.lock generated
View File

@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "attohttpc"
version = "0.16.3"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247"
checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072"
dependencies = [
"http",
"log",
@@ -134,21 +134,7 @@ dependencies = [
"serde",
"serde_json",
"url",
"wildmatch 1.1.0",
]
[[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",
"wildmatch",
]
[[package]]
@@ -159,13 +145,13 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "aws-creds"
version = "0.26.0"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1331d069460a674d42bd27c12b47ce578f789954c7bd7f239fd030771eca6616"
checksum = "460a75eac8f3cb7683e0a9a588a83c3ff039331ea7bfbfbfcecf1dacab276e11"
dependencies = [
"anyhow",
"attohttpc 0.16.3",
"dirs 3.0.2",
"attohttpc",
"dirs 4.0.0",
"rust-ini",
"serde",
"serde-xml-rs",
@@ -456,6 +442,16 @@ dependencies = [
"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]]
name = "ctor"
version = "0.1.21"
@@ -529,15 +525,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "dirs"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs"
version = "4.0.0"
@@ -799,7 +786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
dependencies = [
"digest",
"hmac",
"hmac 0.10.1",
]
[[package]]
@@ -808,7 +795,17 @@ version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
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",
]
@@ -1133,15 +1130,6 @@ version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "miniz_oxide"
version = "0.4.4"
@@ -1686,6 +1674,26 @@ version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "remove_dir_all"
version = "0.5.3"
@@ -1754,35 +1762,34 @@ dependencies = [
[[package]]
name = "rust-ini"
version = "0.16.1"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55b134767a87e0b086f73a4ce569ac9ce7d202f39c8eab6caa266e2617e73ac6"
checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22"
dependencies = [
"cfg-if 0.1.10",
"cfg-if 1.0.0",
"ordered-multimap",
]
[[package]]
name = "rust-s3"
version = "0.27.0-rc4"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c93272c1d654d492f8ab30b94cd43d98f2700b1db55b2576aff7712ce40e3ef"
checksum = "18c58d4682844a5d6301efbf915dd7a9d3d638006f9bb821527a0bbbf2a4cfc2"
dependencies = [
"anyhow",
"async-trait",
"attohttpc 0.17.0",
"attohttpc",
"aws-creds",
"aws-region",
"base64",
"cfg-if 1.0.0",
"chrono",
"hex",
"hmac",
"hmac 0.11.0",
"http",
"log",
"maybe-async",
"md5",
"minidom",
"percent-encoding",
"serde",
"serde-xml-rs",
@@ -1926,9 +1933,9 @@ dependencies = [
[[package]]
name = "serde-xml-rs"
version = "0.4.1"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa"
checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa"
dependencies = [
"log",
"serde",
@@ -1977,7 +1984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d"
dependencies = [
"lazy_static",
"parking_lot 0.10.2",
"parking_lot 0.11.2",
"serial_test_derive",
]
@@ -2086,6 +2093,16 @@ dependencies = [
"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]]
name = "strum"
version = "0.8.0"
@@ -2110,12 +2127,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "suppaftp"
version = "4.1.2"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29a4d861acfdc117c6d373c3b743c534dbbbb2d782e7646b27439a7c5282ad6a"
checksum = "3c42610de5ed0db28274b24c010aa530ebc391e5a4e9bd44a08e9daa8a245cc5"
dependencies = [
"chrono",
"lazy_static",
"log",
"native-tls",
"regex",
"thiserror",
@@ -2214,18 +2232,15 @@ dependencies = [
"magic-crypt",
"notify-rust",
"open",
"path-slash",
"pretty_assertions",
"rand 0.8.4",
"regex",
"remotefs",
"rpassword",
"rust-s3",
"self_update",
"serde",
"serial_test",
"simplelog",
"ssh2",
"suppaftp",
"tempfile",
"textwrap",
"thiserror",
@@ -2234,7 +2249,7 @@ dependencies = [
"tuirealm",
"users",
"whoami",
"wildmatch 2.1.0",
"wildmatch",
]
[[package]]
@@ -2662,12 +2677,6 @@ dependencies = [
"web-sys",
]
[[package]]
name = "wildmatch"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a"
[[package]]
name = "wildmatch"
version = "2.1.0"

View File

@@ -48,13 +48,11 @@ notify-rust = { version = "4.5.3", default-features = false, features = [ "d" ]
open = "2.0.1"
rand = "0.8.4"
regex = "1.5.4"
remotefs = { version = "0.1.1", features = [ "aws-s3", "ftp", "ssh" ] }
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" ] }
serde = { version = "^1.0.0", features = [ "derive" ] }
simplelog = "0.10.0"
ssh2 = "0.9.0"
suppaftp = { version = "4.1.2", features = [ "secure" ] }
tempfile = "3.1.0"
textwrap = "0.14.2"
thiserror = "^1.0.0"
@@ -71,14 +69,8 @@ serial_test = "^0.5.1"
[features]
default = [ "with-keyring" ]
github-actions = [ ]
with-s3-ci = []
with-containers = []
with-keyring = [ "keyring" ]
[target."cfg(target_family = \"unix\")"]
[target."cfg(target_family = \"unix\")".dependencies]
users = "0.11.0"
[target."cfg(target_os = \"windows\")"]
[target."cfg(target_os = \"windows\")".dependencies]
path-slash = "0.1.4"

View File

@@ -287,11 +287,9 @@ termscp is powered by these awesome projects:
- [edit](https://github.com/milkey-mouse/edit)
- [keyring-rs](https://github.com/hwchen/keyring-rs)
- [open-rs](https://github.com/Byron/open-rs)
- [remotefs](https://github.com/veeso/remotefs-rs)
- [rpassword](https://github.com/conradkleinespel/rpassword)
- [rust-s3](https://github.com/durch/rust-s3)
- [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)
- [tui-rs](https://github.com/fdehau/tui-rs)
- [tui-realm](https://github.com/veeso/tui-realm)

View File

@@ -3,7 +3,6 @@
Document audience: developers
- [Developer Manual](#developer-manual)
- [How to test](#how-to-test)
- [How termscp works](#how-termscp-works)
- [Activities](#activities)
- [The Context](#the-context)
@@ -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>
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

View File

@@ -26,7 +26,7 @@
* SOFTWARE.
*/
// Deps
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
use crate::filetransfer::FileTransferParams;
use crate::host::{HostError, Localhost};
use crate::system::config_client::ConfigClient;
use crate::system::environment;
@@ -192,7 +192,6 @@ impl ActivityManager {
}
};
// Prepare activity
let protocol: FileTransferProtocol = ft_params.protocol;
let host: Localhost = match Localhost::new(self.local_dir.clone()) {
Ok(host) => host,
Err(err) => {
@@ -203,7 +202,7 @@ impl ActivityManager {
}
};
let mut activity: FileTransferActivity =
FileTransferActivity::new(host, protocol, self.ticks);
FileTransferActivity::new(host, ft_params, self.ticks);
// Prepare result
let result: Option<NextActivity>;
// Create activity

View File

@@ -36,7 +36,7 @@ use std::str::FromStr;
///
/// UserHosts contains all the hosts saved by the user in the data storage
/// It contains both `Bookmark`
#[derive(Deserialize, Serialize, Debug)]
#[derive(Deserialize, Serialize, Debug, Default)]
pub struct UserHosts {
pub bookmarks: HashMap<String, Bookmark>,
pub recents: HashMap<String, Bookmark>,
@@ -76,15 +76,6 @@ pub struct S3Params {
// -- impls
impl Default for UserHosts {
fn default() -> Self {
Self {
bookmarks: HashMap::new(),
recents: HashMap::new(),
}
}
}
impl From<FileTransferParams> for Bookmark {
fn from(params: FileTransferParams) -> Self {
let protocol: FileTransferProtocol = params.protocol;

View File

@@ -35,7 +35,7 @@ use std::path::PathBuf;
pub const DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD: u64 = 536870912; // 512MB
#[derive(Deserialize, Serialize, std::fmt::Debug)]
#[derive(Deserialize, Serialize, Debug, Default)]
/// ## UserConfig
///
/// UserConfig contains all the configurations for the user,
@@ -45,7 +45,7 @@ pub struct UserConfig {
pub remote: RemoteConfig,
}
#[derive(Deserialize, Serialize, std::fmt::Debug)]
#[derive(Deserialize, Serialize, Debug)]
/// ## UserInterfaceConfig
///
/// 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
}
#[derive(Deserialize, Serialize, std::fmt::Debug)]
#[derive(Deserialize, Serialize, Debug, Default)]
/// ## RemoteConfig
///
/// 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
}
impl Default for UserConfig {
fn default() -> Self {
UserConfig {
user_interface: UserInterfaceConfig::default(),
remote: RemoteConfig::default(),
}
}
}
impl Default for UserInterfaceConfig {
fn default() -> Self {
UserInterfaceConfig {
@@ -99,14 +90,6 @@ impl Default for UserInterfaceConfig {
}
}
impl Default for RemoteConfig {
fn default() -> Self {
RemoteConfig {
ssh_keys: HashMap::new(),
}
}
}
// Tests
#[cfg(test)]

View File

@@ -26,18 +26,18 @@
* SOFTWARE.
*/
// Locals
use super::FsEntry;
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
use crate::utils::path::diff_paths;
// Ext
use bytesize::ByteSize;
use regex::Regex;
use remotefs::Entry;
use std::path::PathBuf;
#[cfg(target_family = "unix")]
use users::{get_group_by_gid, get_user_by_uid};
// Types
// FmtCallback: Formatter, fsentry: &FsEntry, cur_str, prefix, length, extra
type FmtCallback = fn(&Formatter, &FsEntry, &str, &str, Option<&usize>, Option<&String>) -> String;
// FmtCallback: Formatter, fsentry: &Entry, cur_str, prefix, length, extra
type FmtCallback = fn(&Formatter, &Entry, &str, &str, Option<&usize>, Option<&String>) -> String;
// Keys
const FMT_KEY_ATIME: &str = "ATIME";
@@ -66,7 +66,7 @@ lazy_static! {
/// ## 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 chain of function is made using the Formatters method.
/// This method provides an extremely fast way to format fs entries
@@ -105,7 +105,7 @@ impl CallChainBlock {
/// ### next
///
/// 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
let new_str: String = (self.func)(
fmt,
@@ -177,7 +177,7 @@ impl Formatter {
/// ### fmt
///
/// Format fsentry
pub fn fmt(&self, fsentry: &FsEntry) -> String {
pub fn fmt(&self, fsentry: &Entry) -> String {
// Execute callchain blocks
self.call_chain.next(self, fsentry, "")
}
@@ -189,7 +189,7 @@ impl Formatter {
/// Format last access time
fn fmt_atime(
&self,
fsentry: &FsEntry,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
@@ -197,7 +197,7 @@ impl Formatter {
) -> String {
// Get date (use extra args as format or default "%b %d %Y %H:%M")
let datetime: String = fmt_time(
fsentry.get_last_access_time(),
fsentry.metadata().atime,
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
@@ -218,7 +218,7 @@ impl Formatter {
/// Format creation time
fn fmt_ctime(
&self,
fsentry: &FsEntry,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
@@ -226,7 +226,7 @@ impl Formatter {
) -> String {
// Get date
let datetime: String = fmt_time(
fsentry.get_creation_time(),
fsentry.metadata().ctime,
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
@@ -247,7 +247,7 @@ impl Formatter {
/// Format owner group
fn fmt_group(
&self,
fsentry: &FsEntry,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
@@ -255,7 +255,7 @@ impl Formatter {
) -> String {
// Get username
#[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(user) => user.name().to_string_lossy().to_string(),
None => gid.to_string(),
@@ -263,7 +263,7 @@ impl Formatter {
None => 0.to_string(),
};
#[cfg(target_os = "windows")]
let group: String = match fsentry.get_group() {
let group: String = match fsentry.metadata().gid {
Some(gid) => gid.to_string(),
None => 0.to_string(),
};
@@ -282,7 +282,7 @@ impl Formatter {
/// Format last change time
fn fmt_mtime(
&self,
fsentry: &FsEntry,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
@@ -290,7 +290,7 @@ impl Formatter {
) -> String {
// Get date
let datetime: String = fmt_time(
fsentry.get_last_change_time(),
fsentry.metadata().mtime,
match fmt_extra {
Some(fmt) => fmt.as_ref(),
None => "%b %d %Y %H:%M",
@@ -311,7 +311,7 @@ impl Formatter {
/// Format file name
fn fmt_name(
&self,
fsentry: &FsEntry,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
@@ -322,7 +322,7 @@ impl Formatter {
Some(l) => *l,
None => 24,
};
let name: &str = fsentry.get_name();
let name: &str = fsentry.name();
let last_idx: usize = match fsentry.is_dir() {
// NOTE: For directories is l - 2, since we push '/' to name
true => file_len - 2,
@@ -344,19 +344,16 @@ impl Formatter {
/// Format path
fn fmt_path(
&self,
fsentry: &FsEntry,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
fmt_extra: Option<&String>,
) -> String {
let p = match fmt_extra {
None => fsentry.get_abs_path(),
Some(rel) => diff_paths(
fsentry.get_abs_path().as_path(),
PathBuf::from(rel.as_str()).as_path(),
)
.unwrap_or_else(|| fsentry.get_abs_path()),
None => fsentry.path().to_path_buf(),
Some(rel) => diff_paths(fsentry.path(), PathBuf::from(rel.as_str()).as_path())
.unwrap_or_else(|| fsentry.path().to_path_buf()),
};
format!(
"{}{}{}",
@@ -374,7 +371,7 @@ impl Formatter {
/// Format file permissions
fn fmt_pex(
&self,
fsentry: &FsEntry,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
@@ -382,7 +379,7 @@ impl Formatter {
) -> String {
// Create mode string
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',
false => match fsentry.is_dir() {
true => 'd',
@@ -390,10 +387,16 @@ impl Formatter {
},
};
pex.push(file_type);
match fsentry.get_unix_pex() {
match fsentry.metadata().mode {
None => pex.push_str("?????????"),
Some((owner, group, others)) => pex.push_str(
format!("{}{}{}", fmt_pex(owner), fmt_pex(group), fmt_pex(others)).as_str(),
Some(mode) => pex.push_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
@@ -405,7 +408,7 @@ impl Formatter {
/// Format file size
fn fmt_size(
&self,
fsentry: &FsEntry,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
@@ -413,7 +416,7 @@ impl Formatter {
) -> String {
if fsentry.is_file() {
// 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
format!("{}{}{:10}", cur_str, prefix, size.to_string())
} else {
@@ -427,7 +430,7 @@ impl Formatter {
/// Format file symlink (if any)
fn fmt_symlink(
&self,
fsentry: &FsEntry,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
fmt_len: Option<&usize>,
@@ -439,16 +442,13 @@ impl Formatter {
None => 21,
};
// Replace `FMT_KEY_NAME` with name
match fsentry.is_symlink() {
false => format!("{}{} ", cur_str, prefix),
true => format!(
match fsentry.metadata().symlink.as_deref() {
None => format!("{}{} ", cur_str, prefix),
Some(p) => format!(
"{}{}-> {:0width$}",
cur_str,
prefix,
fmt_path_elide(
fsentry.get_realfile().get_abs_path().as_path(),
file_len - 1
),
fmt_path_elide(p, file_len - 1),
width = file_len
),
}
@@ -459,7 +459,7 @@ impl Formatter {
/// Format owner user
fn fmt_user(
&self,
fsentry: &FsEntry,
fsentry: &Entry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
@@ -467,7 +467,7 @@ impl Formatter {
) -> String {
// Get username
#[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(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
@@ -475,7 +475,7 @@ impl Formatter {
None => 0.to_string(),
};
#[cfg(target_os = "windows")]
let username: String = match fsentry.get_user() {
let username: String = match fsentry.metadata().uid {
Some(uid) => uid.to_string(),
None => 0.to_string(),
};
@@ -489,7 +489,7 @@ impl Formatter {
/// It does nothing, just returns cur_str
fn fmt_fallback(
&self,
_fsentry: &FsEntry,
_fsentry: &Entry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,
@@ -574,9 +574,9 @@ impl Formatter {
mod tests {
use super::*;
use crate::fs::{FsDirectory, FsFile, UnixPex};
use pretty_assertions::assert_eq;
use remotefs::fs::{Directory, File, Metadata, UnixPex};
use std::path::PathBuf;
use std::time::SystemTime;
@@ -585,19 +585,21 @@ mod tests {
// Make a dummy formatter
let dummy_formatter: Formatter = Formatter::new("");
// Make a dummy entry
let t_now: SystemTime = SystemTime::now();
let dummy_entry: FsEntry = FsEntry::File(FsFile {
let t: SystemTime = SystemTime::now();
let dummy_entry: Entry = Entry::File(File {
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
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
});
let prefix: String = String::from("h");
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
@@ -626,18 +628,20 @@ mod tests {
let formatter: Formatter = Formatter::default();
// Experiments :D
let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
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
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
});
#[cfg(target_family = "unix")]
assert_eq!(
@@ -656,18 +660,20 @@ mod tests {
)
);
// Elide name
let entry: FsEntry = FsEntry::File(FsFile {
let entry: Entry = Entry::File(File {
name: String::from("piroparoporoperoperupupu.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
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
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
});
#[cfg(target_family = "unix")]
assert_eq!(
@@ -686,18 +692,20 @@ mod tests {
)
);
// No pex
let entry: FsEntry = FsEntry::File(FsFile {
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: None, // UNIX only
user: Some(0), // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: None,
},
});
#[cfg(target_family = "unix")]
assert_eq!(
@@ -716,18 +724,20 @@ mod tests {
)
);
// No user
let entry: FsEntry = FsEntry::File(FsFile {
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
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: Some(0), // UNIX only
unix_pex: None, // UNIX only
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: None,
gid: Some(0),
mode: None,
},
});
#[cfg(target_family = "unix")]
assert_eq!(
@@ -752,24 +762,27 @@ mod tests {
// Make default
let formatter: Formatter = Formatter::default();
// Experiments :D
let t_now: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::Directory(FsDirectory {
let t: SystemTime = SystemTime::now();
let entry: Entry = Entry::Directory(Directory {
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(5), UnixPex::from(5))), // UNIX only
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 4096,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o755)),
},
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"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")]
@@ -777,27 +790,30 @@ mod tests {
formatter.fmt(&entry),
format!(
"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
let entry: FsEntry = FsEntry::Directory(FsDirectory {
let entry: Entry = Entry::Directory(Directory {
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: None, // UNIX only
group: Some(0), // UNIX only
unix_pex: None, // UNIX only
path: PathBuf::from("/home/cvisintin/projects"),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 4096,
symlink: None,
uid: None,
gid: Some(0),
mode: None,
},
});
#[cfg(target_family = "unix")]
assert_eq!(
formatter.fmt(&entry),
format!(
"projects/ d????????? 0 {}",
fmt_time(t_now, "%b %d %Y %H:%M")
fmt_time(t, "%b %d %Y %H:%M")
)
);
#[cfg(target_os = "windows")]
@@ -805,7 +821,7 @@ mod tests {
formatter.fmt(&entry),
format!(
"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}");
// Directory (with symlink)
let t: SystemTime = SystemTime::now();
let pointer: FsEntry = FsEntry::File(FsFile {
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 {
let entry: Entry = Entry::Directory(Directory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/project"),
last_change_time: t,
last_access_time: t,
creation_time: t,
symlink: Some(Box::new(pointer)), // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
path: PathBuf::from("/home/cvisintin/project"),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 4096,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o755)),
},
});
assert_eq!(formatter.fmt(&entry), format!(
"projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}",
@@ -847,16 +853,19 @@ mod tests {
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// Directory without symlink
let entry: FsEntry = FsEntry::Directory(FsDirectory {
let entry: Entry = Entry::Directory(Directory {
name: String::from("projects"),
abs_path: PathBuf::from("/home/cvisintin/project"),
last_change_time: t,
last_access_time: t,
creation_time: t,
symlink: None, // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
path: PathBuf::from("/home/cvisintin/project"),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 4096,
symlink: None,
uid: None,
gid: None,
mode: Some(UnixPex::from(0o755)),
},
});
assert_eq!(formatter.fmt(&entry), format!(
"projects/ 0 0 drwxr-xr-x {} {} {}",
@@ -865,31 +874,20 @@ mod tests {
fmt_time(t, "%a %b %d %Y %H:%M"),
));
// File with symlink
let pointer: FsEntry = FsEntry::File(FsFile {
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 {
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
size: 8192,
ftype: Some(String::from("txt")),
symlink: Some(Box::new(pointer)), // UNIX only
user: None, // UNIX only
group: None, // UNIX only
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
});
assert_eq!(formatter.fmt(&entry), format!(
"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"),
));
// File without symlink
let entry: FsEntry = FsEntry::File(FsFile {
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
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: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: None,
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
});
assert_eq!(formatter.fmt(&entry), format!(
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
@@ -923,18 +923,20 @@ mod tests {
#[cfg(target_family = "unix")]
fn should_fmt_path() {
let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/tmp/a/b/c/bar.txt"),
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: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
path: PathBuf::from("/tmp/a/b/c/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
mtime: t,
size: 8192,
symlink: Some(PathBuf::from("project.info")),
uid: None,
gid: None,
mode: Some(UnixPex::from(0o644)),
},
});
let formatter: Formatter = Formatter::new("File path: {PATH}");
assert_eq!(
@@ -955,7 +957,7 @@ mod tests {
/// Dummy formatter, just yelds an 'A' at the end of the current string
fn dummy_fmt(
_fmt: &Formatter,
_entry: &FsEntry,
_entry: &Entry,
cur_str: &str,
prefix: &str,
_fmt_len: Option<&usize>,

View File

@@ -29,9 +29,9 @@
pub(crate) mod builder;
mod formatter;
// Locals
use super::FsEntry;
use formatter::Formatter;
// Ext
use remotefs::fs::Entry;
use std::cmp::Reverse;
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
@@ -77,8 +77,8 @@ pub struct FileExplorer {
pub(crate) file_sorting: FileSorting, // File sorting criteria
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
pub(crate) opts: ExplorerOpts, // Explorer options
pub(crate) fmt: Formatter, // FsEntry formatter
files: Vec<FsEntry>, // Files in directory
pub(crate) fmt: Formatter, // Entry formatter
files: Vec<Entry>, // Files in directory
}
impl Default for FileExplorer {
@@ -121,7 +121,7 @@ impl FileExplorer {
/// Set Explorer files
/// This method will also sort entries based on current options
/// 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;
// Sort
self.sort();
@@ -149,7 +149,7 @@ impl FileExplorer {
///
/// Iterate over files
/// 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
let opts: ExplorerOpts = self.opts;
Box::new(self.files.iter().filter(move |x| {
@@ -166,14 +166,14 @@ impl FileExplorer {
/// ### iter_files_all
///
/// 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())
}
/// ### get
///
/// 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 filtered = self
.files
@@ -196,7 +196,7 @@ impl FileExplorer {
/// ### fmt_file
///
/// Format a file entry
pub fn fmt_file(&self, entry: &FsEntry) -> String {
pub fn fmt_file(&self, entry: &Entry) -> String {
self.fmt.fmt(entry)
}
@@ -256,17 +256,15 @@ impl FileExplorer {
///
/// Sort explorer files by their name. All names are converted to lowercase
fn sort_files_by_name(&mut self) {
self.files
.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase());
self.files.sort_by_key(|x: &Entry| x.name().to_lowercase());
}
/// ### sort_files_by_mtime
///
/// Sort files by mtime; the newest comes first
fn sort_files_by_mtime(&mut self) {
self.files.sort_by(|a: &FsEntry, b: &FsEntry| {
b.get_last_change_time().cmp(&a.get_last_change_time())
});
self.files
.sort_by(|a: &Entry, b: &Entry| b.metadata().mtime.cmp(&a.metadata().mtime));
}
/// ### sort_files_by_creation_time
@@ -274,28 +272,29 @@ impl FileExplorer {
/// Sort files by creation time; the newest comes first
fn sort_files_by_creation_time(&mut self) {
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
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 come first
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 come last
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
@@ -363,10 +362,10 @@ impl FromStr for GroupDirs {
mod tests {
use super::*;
use crate::fs::{FsDirectory, FsFile, UnixPex};
use crate::utils::fmt::fmt_time;
use pretty_assertions::assert_eq;
use remotefs::fs::{Directory, File, Metadata, UnixPex};
use std::thread::sleep;
use std::time::{Duration, SystemTime};
@@ -430,10 +429,7 @@ mod tests {
assert!(explorer.get(100).is_none());
//assert_eq!(explorer.count(), 6);
// Verify (files are sorted by name)
assert_eq!(
explorer.files.get(0).unwrap().get_name(),
String::from(".git/")
);
assert_eq!(explorer.files.get(0).unwrap().name(), ".git/");
// Iter files (all)
assert_eq!(explorer.iter_files_all().count(), 6);
// Iter files (hidden excluded) (.git, .gitignore are hidden)
@@ -461,47 +457,41 @@ mod tests {
]);
explorer.sort_by(FileSorting::Name);
// 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/"
assert_eq!(explorer.files.get(8).unwrap().get_name(), "src/");
assert_eq!(explorer.files.get(8).unwrap().name(), "src/");
}
#[test]
fn test_fs_explorer_sort_by_mtime() {
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
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)
explorer.set_files(vec![entry1, entry2]);
explorer.sort_by(FileSorting::ModifyTime);
// First entry should be "CODE_OF_CONDUCT.md"
assert_eq!(
explorer.files.get(0).unwrap().get_name(),
"CODE_OF_CONDUCT.md"
);
assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
// 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]
fn test_fs_explorer_sort_by_creation_time() {
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
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)
explorer.set_files(vec![entry1, entry2]);
explorer.sort_by(FileSorting::CreationTime);
// First entry should be "CODE_OF_CONDUCT.md"
assert_eq!(
explorer.files.get(0).unwrap().get_name(),
"CODE_OF_CONDUCT.md"
);
assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
// 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]
@@ -510,14 +500,14 @@ mod tests {
// Create files (files are then sorted by name)
explorer.set_files(vec![
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),
]);
explorer.sort_by(FileSorting::Size);
// Directory has size 4096
assert_eq!(explorer.files.get(0).unwrap().get_name(), "src/");
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
assert_eq!(explorer.files.get(2).unwrap().get_name(), "CONTRIBUTING.md");
assert_eq!(explorer.files.get(0).unwrap().name(), "src/");
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
assert_eq!(explorer.files.get(2).unwrap().name(), "CONTRIBUTING.md");
}
#[test]
@@ -539,12 +529,12 @@ mod tests {
explorer.sort_by(FileSorting::Name);
explorer.group_dirs_by(Some(GroupDirs::First));
// First entry should be "docs"
assert_eq!(explorer.files.get(0).unwrap().get_name(), "docs/");
assert_eq!(explorer.files.get(1).unwrap().get_name(), "src/");
assert_eq!(explorer.files.get(0).unwrap().name(), "docs/");
assert_eq!(explorer.files.get(1).unwrap().name(), "src/");
// 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)
assert_eq!(explorer.files.get(9).unwrap().get_name(), "README.md");
assert_eq!(explorer.files.get(9).unwrap().name(), "README.md");
}
#[test]
@@ -566,12 +556,12 @@ mod tests {
explorer.sort_by(FileSorting::Name);
explorer.group_dirs_by(Some(GroupDirs::Last));
// Last entry should be "src"
assert_eq!(explorer.files.get(8).unwrap().get_name(), "docs/");
assert_eq!(explorer.files.get(9).unwrap().get_name(), "src/");
assert_eq!(explorer.files.get(8).unwrap().name(), "docs/");
assert_eq!(explorer.files.get(9).unwrap().name(), "src/");
// 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)
assert_eq!(explorer.files.get(7).unwrap().get_name(), "README.md");
assert_eq!(explorer.files.get(7).unwrap().name(), "README.md");
}
#[test]
@@ -579,18 +569,20 @@ mod tests {
let explorer: FileExplorer = FileExplorer::default();
// Create fs entry
let t: SystemTime = SystemTime::now();
let entry: FsEntry = FsEntry::File(FsFile {
let entry: Entry = Entry::File(File {
name: String::from("bar.txt"),
abs_path: PathBuf::from("/bar.txt"),
last_change_time: t,
last_access_time: t,
creation_time: t,
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
path: PathBuf::from("/bar.txt"),
extension: Some(String::from("txt")),
metadata: Metadata {
atime: t,
ctime: t,
size: 8192,
mtime: t,
symlink: None,
uid: Some(0),
gid: Some(0),
mode: Some(UnixPex::from(0o644)),
},
});
#[cfg(target_family = "unix")]
assert_eq!(
@@ -654,67 +646,61 @@ mod tests {
]);
explorer.del_entry(0);
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);
assert_eq!(explorer.files.len(), 3);
}
fn make_fs_entry(name: &str, is_dir: bool) -> FsEntry {
let t_now: SystemTime = SystemTime::now();
fn make_fs_entry(name: &str, is_dir: bool) -> Entry {
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 {
false => FsEntry::File(FsFile {
false => Entry::File(File {
name: name.to_string(),
abs_path: PathBuf::from(name),
last_change_time: t_now,
last_access_time: t_now,
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
path: PathBuf::from(name),
extension: None,
metadata,
}),
true => FsEntry::Directory(FsDirectory {
true => Entry::Directory(Directory {
name: name.to_string(),
abs_path: PathBuf::from(name),
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
path: PathBuf::from(name),
metadata,
}),
}
}
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> FsEntry {
let t_now: SystemTime = SystemTime::now();
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> Entry {
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 {
false => FsEntry::File(FsFile {
false => Entry::File(File {
name: name.to_string(),
abs_path: PathBuf::from(name),
last_change_time: t_now,
last_access_time: t_now,
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
path: PathBuf::from(name),
extension: None,
metadata,
}),
true => FsEntry::Directory(FsDirectory {
true => Entry::Directory(Directory {
name: name.to_string(),
abs_path: PathBuf::from(name),
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
path: PathBuf::from(name),
metadata,
}),
}
}

213
src/filetransfer/builder.rs Normal file
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)
}
}

View File

@@ -1,6 +1,6 @@
//! ## 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
@@ -25,21 +25,12 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// locals
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
mod builder;
pub mod params;
mod transfer;
// -- export types
pub use builder::Builder;
pub use params::{FileTransferParams, ProtocolParams};
pub use transfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
/// ## FileTransferProtocol
///
@@ -53,325 +44,6 @@ pub enum FileTransferProtocol {
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
impl std::string::ToString for FileTransferProtocol {
@@ -479,96 +151,4 @@ mod tests {
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
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);
}
}

View File

@@ -103,8 +103,7 @@ impl Default for ProtocolParams {
}
impl ProtocolParams {
/// ### generic_params
///
#[cfg(test)]
/// Retrieve generic parameters from protocol params if any
pub fn generic_params(&self) -> Option<&GenericProtocolParams> {
match self {
@@ -120,8 +119,7 @@ impl ProtocolParams {
}
}
/// ### s3_params
///
#[cfg(test)]
/// Retrieve AWS S3 parameters if any
pub fn s3_params(&self) -> Option<&AwsS3Params> {
match self {

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());
}
}

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;

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))
}
}

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

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);
}
}

View File

@@ -26,7 +26,10 @@
* SOFTWARE.
*/
// 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::time::SystemTime;
use thiserror::Error;
@@ -38,9 +41,6 @@ use std::fs::set_permissions;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
// Locals
#[cfg(target_family = "unix")]
use crate::fs::UnixPex;
use crate::fs::{FsDirectory, FsEntry, FsFile};
use crate::utils::path;
/// ## HostErrorType
@@ -118,7 +118,7 @@ impl std::fmt::Display for HostError {
/// It provides functions to navigate across the local host file system
pub struct Localhost {
wrkdir: PathBuf,
files: Vec<FsEntry>,
files: Vec<Entry>,
}
impl Localhost {
@@ -169,7 +169,7 @@ impl Localhost {
///
/// List files in current directory
#[allow(dead_code)]
pub fn list_dir(&self) -> Vec<FsEntry> {
pub fn list_dir(&self) -> Vec<Entry> {
self.files.clone()
}
@@ -177,7 +177,7 @@ impl Localhost {
///
/// Change working directory with the new provided directory
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());
// Check whether directory exists
if !self.file_exists(new_dir.as_path()) {
@@ -227,7 +227,7 @@ impl Localhost {
/// Extended option version of makedir.
/// ignex: don't report error if directory already exists
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());
// If dir already exists, return Error
if dir_path.exists() {
@@ -265,25 +265,25 @@ impl Localhost {
/// ### remove
///
/// Remove file entry
pub fn remove(&mut self, entry: &FsEntry) -> Result<(), HostError> {
pub fn remove(&mut self, entry: &Entry) -> Result<(), HostError> {
match entry {
FsEntry::Directory(dir) => {
Entry::Directory(dir) => {
// If file doesn't exist; return error
debug!("Removing directory {}", dir.abs_path.display());
if !dir.abs_path.as_path().exists() {
debug!("Removing directory {}", dir.path.display());
if !dir.path.as_path().exists() {
error!("Directory doesn't exist");
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
dir.abs_path.as_path(),
dir.path.as_path(),
));
}
// Remove
match std::fs::remove_dir_all(dir.abs_path.as_path()) {
match std::fs::remove_dir_all(dir.path.as_path()) {
Ok(_) => {
// Update dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
info!("Removed directory {}", dir.abs_path.display());
info!("Removed directory {}", dir.path.display());
Ok(())
}
Err(err) => {
@@ -291,28 +291,28 @@ impl Localhost {
Err(HostError::new(
HostErrorType::DeleteFailed,
Some(err),
dir.abs_path.as_path(),
dir.path.as_path(),
))
}
}
}
FsEntry::File(file) => {
Entry::File(file) => {
// If file doesn't exist; return error
debug!("Removing file {}", file.abs_path.display());
if !file.abs_path.as_path().exists() {
debug!("Removing file {}", file.path.display());
if !file.path.as_path().exists() {
error!("File doesn't exist");
return Err(HostError::new(
HostErrorType::NoSuchFileOrDirectory,
None,
file.abs_path.as_path(),
file.path.as_path(),
));
}
// Remove
match std::fs::remove_file(file.abs_path.as_path()) {
match std::fs::remove_file(file.path.as_path()) {
Ok(_) => {
// Update dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
info!("Removed file {}", file.abs_path.display());
info!("Removed file {}", file.path.display());
Ok(())
}
Err(err) => {
@@ -320,7 +320,7 @@ impl Localhost {
Err(HostError::new(
HostErrorType::DeleteFailed,
Some(err),
file.abs_path.as_path(),
file.path.as_path(),
))
}
}
@@ -331,15 +331,14 @@ impl Localhost {
/// ### rename
///
/// Rename file or directory to new name
pub fn rename(&mut self, entry: &FsEntry, dst_path: &Path) -> Result<(), HostError> {
let abs_path: PathBuf = entry.get_abs_path();
match std::fs::rename(abs_path.as_path(), dst_path) {
pub fn rename(&mut self, entry: &Entry, dst_path: &Path) -> Result<(), HostError> {
match std::fs::rename(entry.path(), dst_path) {
Ok(_) => {
// Scan dir
self.files = self.scan_dir(self.wrkdir.as_path())?;
debug!(
"Moved file {} to {}",
entry.get_abs_path().display(),
entry.path().display(),
dst_path.display()
);
Ok(())
@@ -347,14 +346,14 @@ impl Localhost {
Err(err) => {
error!(
"Failed to move {} to {}: {}",
entry.get_abs_path().display(),
entry.path().display(),
dst_path.display(),
err
);
Err(HostError::new(
HostErrorType::CouldNotCreateFile,
Some(err),
abs_path.as_path(),
entry.path(),
))
}
}
@@ -363,17 +362,17 @@ impl Localhost {
/// ### copy
///
/// 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
let dst: PathBuf = self.to_abs_path(dst);
let dst: PathBuf = self.to_path(dst);
info!(
"Copying file {} to {}",
entry.get_abs_path().display(),
entry.path().display(),
dst.display()
);
// Match entry
match entry {
FsEntry::File(file) => {
Entry::File(file) => {
// Copy file
// If destination path is a directory, push file name
let dst: PathBuf = match dst.as_path().is_dir() {
@@ -385,29 +384,29 @@ impl Localhost {
false => dst.clone(),
};
// 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);
return Err(HostError::new(
HostErrorType::CouldNotCreateFile,
Some(err),
file.abs_path.as_path(),
file.path.as_path(),
));
}
info!("File copied");
}
FsEntry::Directory(dir) => {
Entry::Directory(dir) => {
// If destination path doesn't exist, create destination
if !dst.exists() {
debug!("Directory {} doesn't exist; creating it", dst.display());
self.mkdir(dst.as_path())?;
}
// 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
for dir_entry in dir_files.iter() {
// Calculate dst
let mut sub_dst: PathBuf = dst.clone();
sub_dst.push(dir_entry.get_name());
sub_dst.push(dir_entry.name());
// Call function recursively
self.copy(dir_entry, sub_dst.as_path())?;
}
@@ -439,12 +438,12 @@ impl Localhost {
/// ### stat
///
/// Stat file and create a FsEntry
/// Stat file and create a Entry
#[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());
let path: PathBuf = self.to_abs_path(path);
let attr: Metadata = match fs::metadata(path.as_path()) {
let path: PathBuf = self.to_path(path);
let attr = match fs::metadata(path.as_path()) {
Ok(metadata) => metadata,
Err(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
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() {
true => FsEntry::Directory(FsDirectory {
name: file_name,
abs_path: path.clone(),
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
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())),
true => Entry::Directory(Directory {
name,
path,
metadata,
}),
false => {
// Is File
let extension: Option<String> = path
let extension = path
.extension()
.map(|s| String::from(s.to_str().unwrap_or("")));
FsEntry::File(FsFile {
name: file_name,
abs_path: path.clone(),
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
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())),
Entry::File(File {
name,
path,
extension,
metadata,
})
}
})
@@ -505,12 +493,12 @@ impl Localhost {
/// ### stat
///
/// Stat file and create a FsEntry
/// Stat file and create a Entry
#[cfg(target_os = "windows")]
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
let path: PathBuf = self.to_abs_path(path);
pub fn stat(&self, path: &Path) -> Result<Entry, HostError> {
let path: PathBuf = self.to_path(path);
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,
Err(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
Ok(match path.is_dir() {
true => FsEntry::Directory(FsDirectory {
name: file_name,
abs_path: path.clone(),
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
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,
true => Entry::Directory(Directory {
name,
path,
metadata,
}),
false => {
// Is File
let extension: Option<String> = path
let extension = path
.extension()
.map(|s| String::from(s.to_str().unwrap_or("")));
FsEntry::File(FsFile {
name: file_name,
abs_path: path.clone(),
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
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,
Entry::File(File {
name,
path,
extension,
metadata,
})
}
})
@@ -601,13 +578,13 @@ impl Localhost {
///
/// Change file mode to file, according to UNIX permissions
#[cfg(target_family = "unix")]
pub fn chmod(&self, path: &Path, pex: (u8, u8, u8)) -> Result<(), HostError> {
let path: PathBuf = self.to_abs_path(path);
pub fn chmod(&self, path: &Path, pex: UnixPex) -> Result<(), HostError> {
let path: PathBuf = self.to_path(path);
// Get metadta
match fs::metadata(path.as_path()) {
Ok(metadata) => {
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) {
Ok(_) => {
info!("Changed mode for {} to {:?}", path.display(), pex);
@@ -641,8 +618,8 @@ impl Localhost {
/// ### open_file_read
///
/// Open file for read
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> {
let file: PathBuf = self.to_abs_path(file);
pub fn open_file_read(&self, file: &Path) -> Result<StdFile, HostError> {
let file: PathBuf = self.to_path(file);
info!("Opening file {} for read", file.display());
if !self.file_exists(file.as_path()) {
error!("File doesn't exist!");
@@ -673,8 +650,8 @@ impl Localhost {
/// ### open_file_write
///
/// Open file for write
pub fn open_file_write(&self, file: &Path) -> Result<File, HostError> {
let file: PathBuf = self.to_abs_path(file);
pub fn open_file_write(&self, file: &Path) -> Result<StdFile, HostError> {
let file: PathBuf = self.to_path(file);
info!("Opening file {} for write", file.display());
match OpenOptions::new()
.create(true)
@@ -711,11 +688,11 @@ impl Localhost {
/// ### scan_dir
///
/// 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());
match std::fs::read_dir(dir) {
Ok(e) => {
let mut fs_entries: Vec<FsEntry> = Vec::new();
let mut fs_entries: Vec<Entry> = Vec::new();
for entry in e.flatten() {
// NOTE: 0.4.1, don't fail if stat for one file fails
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.
/// 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))
}
@@ -748,9 +725,9 @@ impl Localhost {
/// Recursive call for `find` method.
/// 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.
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
let mut drained: Vec<FsEntry> = Vec::new();
let mut drained: Vec<Entry> = Vec::new();
match self.scan_dir(dir) {
Err(err) => Err(err),
Ok(entries) => {
@@ -763,16 +740,16 @@ impl Localhost {
*/
for entry in entries.iter() {
match entry {
FsEntry::Directory(dir) => {
Entry::Directory(dir) => {
// If directory matches; push directory to drained
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()) {
drained.push(FsEntry::File(file.clone()));
drained.push(Entry::File(file.clone()));
}
}
}
@@ -782,29 +759,10 @@ impl Localhost {
}
}
/// ### u32_to_mode
///
/// 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
/// ### to_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)
}
}
@@ -819,7 +777,7 @@ mod tests {
use pretty_assertions::assert_eq;
#[cfg(target_family = "unix")]
use std::fs::File;
use std::fs::File as StdFile;
#[cfg(target_family = "unix")]
use std::io::Write;
@@ -970,7 +928,7 @@ mod tests {
fn test_host_localhost_symlinks() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// 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
assert!(symlink(
format!("{}/foo.txt", tmpdir.path().display()),
@@ -979,33 +937,33 @@ mod tests {
.is_ok());
// Get dir
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
let file_0: &FsEntry = files.get(0).unwrap();
let file_0: &Entry = files.get(0).unwrap();
match file_0 {
FsEntry::File(file_0) => {
Entry::File(file_0) => {
if file_0.name == String::from("foo.txt") {
assert!(file_0.symlink.is_none());
assert!(file_0.metadata.symlink.is_none());
} else {
assert_eq!(
*file_0.symlink.as_ref().unwrap().get_abs_path(),
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
file_0.metadata.symlink.as_ref().unwrap(),
&PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
);
}
}
_ => panic!("expected entry 0 to be file: {:?}", file_0),
};
// Verify simlink
let file_1: &FsEntry = files.get(1).unwrap();
let file_1: &Entry = files.get(1).unwrap();
match file_1 {
FsEntry::File(file_1) => {
Entry::File(file_1) => {
if file_1.name == String::from("bar.txt") {
assert_eq!(
*file_1.symlink.as_ref().unwrap().get_abs_path(),
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
file_1.metadata.symlink.as_ref().unwrap(),
&PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
);
} else {
assert!(file_1.symlink.is_none());
assert!(file_1.metadata.symlink.is_none());
}
}
_ => panic!("expected entry 0 to be file: {:?}", file_1),
@@ -1017,10 +975,10 @@ mod tests {
fn test_host_localhost_mkdir() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
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!(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
// Try to re-create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
@@ -1042,19 +1000,19 @@ mod tests {
fn test_host_localhost_remove() {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
// 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 files: Vec<FsEntry> = host.list_dir();
let files: Vec<Entry> = host.list_dir();
assert_eq!(files.len(), 1); // There should be 1 file now
// Remove file
assert!(host.remove(files.get(0).unwrap()).is_ok());
// 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
// Create directory
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
// 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!(host.remove(files.get(0).unwrap()).is_ok());
// Remove unexisting directory
@@ -1073,11 +1031,11 @@ mod tests {
// Create sample file
let src_path: PathBuf =
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 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.get(0).unwrap().get_name(), "foo.txt");
assert_eq!(files.get(0).unwrap().name(), "foo.txt");
// Rename file
let dst_path: PathBuf =
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())
.is_ok());
// 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.get(0).unwrap().get_name(), "bar.txt");
assert_eq!(files.get(0).unwrap().name(), "bar.txt");
// Fail
let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu");
assert!(host
@@ -1101,16 +1059,16 @@ mod tests {
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
let file: tempfile::NamedTempFile = create_sample_file();
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
assert!(host.chmod(file.path(), (7, 7, 5)).is_ok());
assert!(host.chmod(file.path(), UnixPex::from(0o755)).is_ok());
// Chmod to dir
assert!(host.chmod(tmpdir.path(), (7, 5, 0)).is_ok());
assert!(host.chmod(tmpdir.path(), UnixPex::from(0o750)).is_ok());
// Error
assert!(host
.chmod(Path::new("/tmp/krgiogoiegj/kwrgnoerig"), (7, 7, 7))
.chmod(
Path::new("/tmp/krgiogoiegj/kwrgnoerig"),
UnixPex::from(0o777)
)
.is_err());
}
@@ -1122,15 +1080,15 @@ mod tests {
let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
file1_path.push("foo.txt");
// 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());
// Get file 2 path
let mut file2_path: PathBuf = PathBuf::from(tmpdir.path());
file2_path.push("bar.txt");
// Create host
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let file1_entry: FsEntry = host.files.get(0).unwrap().clone();
assert_eq!(file1_entry.get_name(), String::from("foo.txt"));
let file1_entry: Entry = host.files.get(0).unwrap().clone();
assert_eq!(file1_entry.name(), String::from("foo.txt"));
// Copy
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
// Verify host has two files
@@ -1152,14 +1110,14 @@ mod tests {
let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
file1_path.push("foo.txt");
// 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());
// Get file 2 path
let file2_path: PathBuf = PathBuf::from("bar.txt");
// Create host
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let file1_entry: FsEntry = host.files.get(0).unwrap().clone();
assert_eq!(file1_entry.get_name(), String::from("foo.txt"));
let file1_entry: Entry = host.files.get(0).unwrap().clone();
assert_eq!(file1_entry.name(), String::from("foo.txt"));
// Copy
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
// Verify host has two files
@@ -1178,15 +1136,15 @@ mod tests {
let mut file1_path: PathBuf = dir_src.clone();
file1_path.push("foo.txt");
// 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());
// Copy dir src to dir ddest
let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path());
dir_dest.push("test_dest_dir/");
// Create host
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone();
assert_eq!(dir_src_entry.get_name(), String::from("test_dir"));
let dir_src_entry: Entry = host.files.get(0).unwrap().clone();
assert_eq!(dir_src_entry.name(), String::from("test_dir"));
// Copy
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
// Verify host has two files
@@ -1209,14 +1167,14 @@ mod tests {
let mut file1_path: PathBuf = dir_src.clone();
file1_path.push("foo.txt");
// 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());
// Copy dir src to dir ddest
let dir_dest: PathBuf = PathBuf::from("test_dest_dir/");
// Create host
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone();
assert_eq!(dir_src_entry.get_name(), String::from("test_dir"));
let dir_src_entry: Entry = host.files.get(0).unwrap().clone();
assert_eq!(dir_src_entry.name(), String::from("test_dir"));
// Copy
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
// Verify host has two files
@@ -1255,20 +1213,20 @@ mod tests {
assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok());
let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
// Find txt files
let mut result: Vec<FsEntry> = host.find("*.txt").ok().unwrap();
result.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase());
let mut result: Vec<Entry> = host.find("*.txt").ok().unwrap();
result.sort_by_key(|x: &Entry| x.name().to_lowercase());
// There should be 3 entries
assert_eq!(result.len(), 3);
// 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[1].get_name(), "omar.txt");
assert_eq!(result[2].get_name(), "pippo.txt");
assert_eq!(result[0].name(), "errors.txt");
assert_eq!(result[1].name(), "omar.txt");
assert_eq!(result[2].name(), "pippo.txt");
// Search for directory
let mut result: Vec<FsEntry> = host.find("examples*").ok().unwrap();
result.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase());
let mut result: Vec<Entry> = host.find("examples*").ok().unwrap();
result.sort_by_key(|x: &Entry| x.name().to_lowercase());
assert_eq!(result.len(), 2);
assert_eq!(result[0].get_name(), "examples");
assert_eq!(result[1].get_name(), "examples.csv");
assert_eq!(result[0].name(), "examples");
assert_eq!(result[1].name(), "examples.csv");
}
#[test]

View File

@@ -45,8 +45,8 @@ use std::time::Duration;
// Include
mod activity_manager;
mod config;
mod explorer;
mod filetransfer;
mod fs;
mod host;
mod support;
mod system;

View File

@@ -57,7 +57,7 @@ pub struct Release {
///
/// 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.
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct Update {
ask_confirm: 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 {
fn from(s: Status) -> Self {
match s {

View File

@@ -30,8 +30,8 @@ use crate::config::{
params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD},
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
};
use crate::explorer::GroupDirs;
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
// Ext
use std::fs::{create_dir, remove_file, File, OpenOptions};
use std::io::Write;

View File

@@ -28,8 +28,9 @@
// Locals
use super::config_client::ConfigClient;
// Ext
use remotefs::client::ssh::SshKeyStorage as SshKeyStorageT;
use std::collections::HashMap;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
pub struct SshKeyStorage {
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 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)]
mod tests {

View File

@@ -52,19 +52,11 @@ use tuirealm::{Component, MockComponent};
// -- global listener
#[derive(MockComponent)]
#[derive(Default, MockComponent)]
pub struct GlobalListener {
component: Phantom,
}
impl Default for GlobalListener {
fn default() -> Self {
Self {
component: Phantom::default(),
}
}
}
impl Component<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {

View File

@@ -26,7 +26,9 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry};
use super::FileTransferActivity;
use remotefs::Directory;
use std::path::PathBuf;
impl FileTransferActivity {
@@ -34,70 +36,24 @@ impl FileTransferActivity {
///
/// Enter a directory on local host from entry
/// Return true whether the directory changed
pub(crate) fn action_enter_local_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
match entry {
FsEntry::Directory(dir) => {
self.local_changedir(dir.abs_path.as_path(), 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,
}
}
pub(crate) fn action_enter_local_dir(&mut self, dir: Directory, block_sync: bool) -> bool {
self.local_changedir(dir.path.as_path(), true);
if self.browser.sync_browsing && !block_sync {
self.action_change_remote_dir(dir.name, true);
}
true
}
/// ### action_enter_remote_dir
///
/// Enter a directory on local host from entry
/// Return true whether the directory changed
pub(crate) fn action_enter_remote_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
match entry {
FsEntry::Directory(dir) => {
self.remote_changedir(dir.abs_path.as_path(), 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,
}
}
pub(crate) fn action_enter_remote_dir(&mut self, dir: Directory, block_sync: bool) -> bool {
self.remote_changedir(dir.path.as_path(), true);
if self.browser.sync_browsing && !block_sync {
self.action_change_local_dir(dir.name, true);
}
true
}
/// ### action_change_local_dir

View File

@@ -26,9 +26,9 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use crate::filetransfer::FileTransferErrorType;
use crate::fs::FsFile;
use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
use remotefs::{Entry, RemoteErrorType};
use std::path::{Path, PathBuf};
impl FileTransferActivity {
@@ -49,7 +49,7 @@ impl FileTransferActivity {
// Iter files
for entry in entries.iter() {
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());
}
// Reload entries
@@ -76,7 +76,7 @@ impl FileTransferActivity {
// Iter files
for entry in entries.into_iter() {
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());
}
// 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) {
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Copied \"{}\" to \"{}\"",
entry.get_abs_path().display(),
entry.path().display(),
dest.display()
),
);
@@ -102,7 +102,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!(
"Could not copy \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
entry.path().display(),
dest.display(),
err
),
@@ -110,20 +110,20 @@ impl FileTransferActivity {
}
}
fn remote_copy_file(&mut self, entry: FsEntry, dest: &Path) {
match self.client.as_mut().copy(&entry, dest) {
fn remote_copy_file(&mut self, entry: Entry, dest: &Path) {
match self.client.as_mut().copy(entry.path(), dest) {
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Copied \"{}\" to \"{}\"",
entry.get_abs_path().display(),
entry.path().display(),
dest.display()
),
);
}
Err(err) => match err.kind() {
FileTransferErrorType::UnsupportedFeature => {
Err(err) => match err.kind {
RemoteErrorType::UnsupportedFeature => {
// If copy is not supported, perform the tricky copy
let _ = self.tricky_copy(entry, dest);
}
@@ -131,7 +131,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!(
"Could not copy \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
entry.path().display(),
dest.display(),
err
),
@@ -143,12 +143,12 @@ impl FileTransferActivity {
/// ### tricky_copy
///
/// Tricky copy will be used whenever copy command is not available on remote host
pub(super) fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) -> 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
self.umount_wait();
// match entry
match entry {
FsEntry::File(entry) => {
Entry::File(entry) => {
// Create tempfile
let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() {
Ok(f) => f,
@@ -162,7 +162,7 @@ impl FileTransferActivity {
};
// Download file
let name = entry.name.clone();
let entry_path = entry.abs_path.clone();
let entry_path = entry.path.clone();
if let Err(err) =
self.filetransfer_recv(TransferPayload::File(entry), tmpfile.path(), Some(name))
{
@@ -173,7 +173,7 @@ impl FileTransferActivity {
return Err(err);
}
// 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(),
Err(err) => {
self.log_and_alert(
@@ -206,7 +206,7 @@ impl FileTransferActivity {
}
Ok(())
}
FsEntry::Directory(_) => {
Entry::Directory(_) => {
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
Ok(d) => d,
Err(err) => {
@@ -219,7 +219,7 @@ impl FileTransferActivity {
};
// Get path of dest
let mut tempdir_path: PathBuf = tempdir.path().to_path_buf();
tempdir_path.push(entry.get_name());
tempdir_path.push(entry.name());
// Download file
if let Err(err) =
self.filetransfer_recv(TransferPayload::Any(entry), tempdir.path(), None)
@@ -231,7 +231,7 @@ impl FileTransferActivity {
return Err(err);
}
// 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,
Err(err) => {
self.log_and_alert(

View File

@@ -26,7 +26,9 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use super::{FileTransferActivity, LogLevel, SelectedEntry};
use remotefs::Entry;
impl FileTransferActivity {
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) {
Ok(_) => {
// Log
self.log(
LogLevel::Info,
format!("Removed file \"{}\"", entry.get_abs_path().display()),
format!("Removed file \"{}\"", entry.path().display()),
);
}
Err(err) => {
@@ -85,7 +87,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!(
"Could not delete file \"{}\": {}",
entry.get_abs_path().display(),
entry.path().display(),
err
),
);
@@ -93,12 +95,12 @@ impl FileTransferActivity {
}
}
pub(crate) fn remote_remove_file(&mut self, entry: &FsEntry) {
match self.client.remove(entry) {
pub(crate) fn remote_remove_file(&mut self, entry: &Entry) {
match self.client.remove_dir_all(entry.path()) {
Ok(_) => {
self.log(
LogLevel::Info,
format!("Removed file \"{}\"", entry.get_abs_path().display()),
format!("Removed file \"{}\"", entry.path().display()),
);
}
Err(err) => {
@@ -106,7 +108,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!(
"Could not delete file \"{}\": {}",
entry.get_abs_path().display(),
entry.path().display(),
err
),
);

View File

@@ -26,9 +26,10 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use crate::fs::FsFile;
use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
// ext
use remotefs::{Entry, File};
use std::fs::OpenOptions;
use std::io::Read;
use std::path::{Path, PathBuf};
@@ -36,7 +37,7 @@ use std::time::SystemTime;
impl FileTransferActivity {
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::Many(entries) => entries,
SelectedEntry::None => vec![],
@@ -47,10 +48,10 @@ impl FileTransferActivity {
if entry.is_file() {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"", entry.get_abs_path().display()),
format!("Opening file \"{}\"", entry.path().display()),
);
// 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);
}
}
@@ -60,7 +61,7 @@ impl FileTransferActivity {
}
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::Many(entries) => entries,
SelectedEntry::None => vec![],
@@ -68,10 +69,10 @@ impl FileTransferActivity {
// Edit all entries
for entry in entries.into_iter() {
// Check if file
if let FsEntry::File(file) = entry {
if let Entry::File(file) = entry {
self.log(
LogLevel::Info,
format!("Opening file \"{}\"", file.abs_path.display()),
format!("Opening file \"{}\"", file.path.display()),
);
// Edit file
if let Err(err) = self.edit_remote_file(file) {
@@ -149,7 +150,7 @@ impl FileTransferActivity {
/// ### edit_remote_file
///
/// 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
let tmpfile: PathBuf = match self.download_file_as_temp(&file) {
Ok(p) => p,
@@ -157,7 +158,7 @@ impl FileTransferActivity {
};
// Download file
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(
TransferPayload::File(file),
tmpfile.as_path(),
@@ -167,7 +168,7 @@ impl FileTransferActivity {
}
// Get current file modification time
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) => {
return Err(format!(
"Could not stat \"{}\": {}",
@@ -181,7 +182,7 @@ impl FileTransferActivity {
return Err(err);
}
// 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,
Err(err) => {
return Err(format!(
@@ -192,7 +193,7 @@ impl FileTransferActivity {
}
};
// Check if file has changed
match prev_mtime != tmpfile_entry.get_last_change_time() {
match prev_mtime != tmpfile_entry.metadata().mtime {
true => {
self.log(
LogLevel::Info,
@@ -202,7 +203,7 @@ impl FileTransferActivity {
),
);
// 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(),
Err(err) => {
return Err(format!(

View File

@@ -49,9 +49,12 @@ impl FileTransferActivity {
pub(crate) fn action_remote_exec(&mut self, input: String) {
match self.client.as_mut().exec(input.as_str()) {
Ok(output) => {
Ok((rc, output)) => {
// Reload files
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
self.log(
LogLevel::Info,
format!("\"{}\" (exitcode: {}): {}", input, rc, output),
);
self.reload_remote_dir();
}
Err(err) => {

View File

@@ -27,21 +27,19 @@
*/
// locals
use super::super::browser::FileExplorerTab;
use super::{
FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferOpts, TransferPayload,
};
use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferOpts, TransferPayload};
use std::path::PathBuf;
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()) {
Ok(entries) => Ok(entries),
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()) {
Ok(entries) => Ok(entries),
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() {
// Get path: if a directory, use directory path; if it is a File, get parent path
let path: PathBuf = match entry {
FsEntry::Directory(dir) => dir.abs_path,
FsEntry::File(file) => match file.abs_path.parent() {
Entry::Directory(dir) => dir.path,
Entry::File(file) => match file.path.parent() {
None => PathBuf::from("."),
Some(p) => p.to_path_buf(),
},
@@ -86,10 +84,10 @@ impl FileTransferActivity {
{
// Save 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(
TransferPayload::Any(entry.get_realfile()),
TransferPayload::Any(entry),
wrkdir.as_path(),
opts.save_as,
) {
@@ -107,10 +105,10 @@ impl FileTransferActivity {
{
// Save 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(
TransferPayload::Any(entry.get_realfile()),
TransferPayload::Any(entry),
wrkdir.as_path(),
opts.save_as,
) {
@@ -128,12 +126,11 @@ impl FileTransferActivity {
dest_path.push(save_as);
}
// Iter files
let entries: Vec<FsEntry> = entries.iter().map(|x| x.get_realfile()).collect();
match self.browser.tab() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
if opts.check_replace && self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries
let existing_files: Vec<&Entry> = entries
.iter()
.filter(|x| {
self.remote_file_exists(
@@ -166,7 +163,7 @@ impl FileTransferActivity {
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
if opts.check_replace && self.config().get_prompt_on_file_replace() {
// Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries
let existing_files: Vec<&Entry> = entries
.iter()
.filter(|x| {
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() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
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() {
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
self.action_open_local_file(entry, with);

View File

@@ -27,6 +27,7 @@
*/
// locals
use super::{FileTransferActivity, LogLevel};
use remotefs::fs::UnixPex;
use std::path::PathBuf;
impl FileTransferActivity {
@@ -48,11 +49,10 @@ impl FileTransferActivity {
}
}
pub(crate) fn action_remote_mkdir(&mut self, input: String) {
match self
.client
.as_mut()
.mkdir(PathBuf::from(input.as_str()).as_path())
{
match self.client.as_mut().create_dir(
PathBuf::from(input.as_str()).as_path(),
UnixPex::from(0o755),
) {
Ok(_) => {
// Reload files
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));

View File

@@ -26,9 +26,9 @@
* SOFTWARE.
*/
pub(self) use super::{
browser::FileExplorerTab, FileTransferActivity, FsEntry, Id, LogLevel, TransferOpts,
TransferPayload,
browser::FileExplorerTab, FileTransferActivity, Id, LogLevel, TransferOpts, TransferPayload,
};
pub(self) use remotefs::Entry;
use tuirealm::{State, StateValue};
// actions
@@ -47,8 +47,8 @@ pub(crate) mod submit;
#[derive(Debug)]
pub(crate) enum SelectedEntry {
One(FsEntry),
Many(Vec<FsEntry>),
One(Entry),
Many(Vec<Entry>),
None,
}
@@ -59,8 +59,8 @@ enum SelectedEntryIndex {
None,
}
impl From<Option<&FsEntry>> for SelectedEntry {
fn from(opt: Option<&FsEntry>) -> Self {
impl From<Option<&Entry>> for SelectedEntry {
fn from(opt: Option<&Entry>) -> Self {
match opt {
Some(e) => SelectedEntry::One(e.clone()),
None => SelectedEntry::None,
@@ -68,8 +68,8 @@ impl From<Option<&FsEntry>> for SelectedEntry {
}
}
impl From<Vec<&FsEntry>> for SelectedEntry {
fn from(files: Vec<&FsEntry>) -> Self {
impl From<Vec<&Entry>> for SelectedEntry {
fn from(files: Vec<&Entry>) -> Self {
SelectedEntry::Many(files.into_iter().cloned().collect())
}
}
@@ -82,9 +82,9 @@ impl FileTransferActivity {
match self.get_selected_index(&Id::ExplorerLocal) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)),
SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files
let files: Vec<&Entry> = files
.iter()
.map(|x| self.local().get(*x)) // Usize to Option<FsEntry>
.map(|x| self.local().get(*x)) // Usize to Option<Entry>
.flatten()
.collect();
SelectedEntry::from(files)
@@ -100,9 +100,9 @@ impl FileTransferActivity {
match self.get_selected_index(&Id::ExplorerRemote) {
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)),
SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files
let files: Vec<&Entry> = files
.iter()
.map(|x| self.remote().get(*x)) // Usize to Option<FsEntry>
.map(|x| self.remote().get(*x)) // Usize to Option<Entry>
.flatten()
.collect();
SelectedEntry::from(files)
@@ -120,9 +120,9 @@ impl FileTransferActivity {
SelectedEntry::from(self.found().as_ref().unwrap().get(idx))
}
SelectedEntryIndex::Many(files) => {
let files: Vec<&FsEntry> = files
let files: Vec<&Entry> = files
.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()
.collect();
SelectedEntry::from(files)

View File

@@ -26,7 +26,7 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel};
use super::{Entry, FileTransferActivity, LogLevel};
use std::fs::File;
use std::path::PathBuf;
@@ -35,7 +35,7 @@ impl FileTransferActivity {
// Check if file exists
let mut file_exists: bool = false;
for file in self.local().iter_files_all() {
if input == file.get_name() {
if input == file.name() {
file_exists = true;
}
}
@@ -67,7 +67,7 @@ impl FileTransferActivity {
// Check if file exists
let mut file_exists: bool = false;
for file in self.remote().iter_files_all() {
if input == file.get_name() {
if input == file.name() {
file_exists = true;
}
}
@@ -88,7 +88,7 @@ impl FileTransferActivity {
),
Ok(tfile) => {
// Stat tempfile
let local_file: FsEntry = match self.host.stat(tfile.path()) {
let local_file: Entry = match self.host.stat(tfile.path()) {
Err(err) => {
self.log_and_alert(
LogLevel::Error,
@@ -98,7 +98,7 @@ impl FileTransferActivity {
}
Ok(f) => f,
};
if let FsEntry::File(local_file) = local_file {
if let Entry::File(local_file) = local_file {
// Create file
let reader = Box::new(match File::open(tfile.path()) {
Ok(f) => f,
@@ -112,7 +112,7 @@ impl FileTransferActivity {
});
match self
.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(
LogLevel::Error,

View File

@@ -26,7 +26,7 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
// ext
use std::path::{Path, PathBuf};
@@ -35,7 +35,7 @@ impl FileTransferActivity {
///
/// Open local file
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::Many(entries) => entries,
SelectedEntry::None => vec![],
@@ -49,7 +49,7 @@ impl FileTransferActivity {
///
/// Open local file
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::Many(entries) => entries,
SelectedEntry::None => vec![],
@@ -62,25 +62,22 @@ impl FileTransferActivity {
/// ### action_open_local_file
///
/// Perform open lopcal file
pub(crate) fn action_open_local_file(&mut self, entry: &FsEntry, open_with: Option<&str>) {
let entry: FsEntry = entry.get_realfile();
self.open_path_with(entry.get_abs_path().as_path(), open_with);
pub(crate) fn action_open_local_file(&mut self, entry: &Entry, open_with: Option<&str>) {
self.open_path_with(entry.path(), open_with);
}
/// ### action_open_local
///
/// 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>) {
let entry: FsEntry = entry.get_realfile();
pub(crate) fn action_open_remote_file(&mut self, entry: &Entry, open_with: Option<&str>) {
// Download file
let tmpfile: String =
match self.get_cache_tmp_name(entry.get_name(), entry.get_ftype().as_deref()) {
None => {
self.log(LogLevel::Error, String::from("Could not create tempdir"));
return;
}
Some(p) => p,
};
let tmpfile: String = match self.get_cache_tmp_name(entry.name(), entry.extension()) {
None => {
self.log(LogLevel::Error, String::from("Could not create tempdir"));
return;
}
Some(p) => p,
};
let cache: PathBuf = match self.cache.as_ref() {
None => {
self.log(LogLevel::Error, String::from("Could not create tempdir"));
@@ -89,7 +86,7 @@ impl FileTransferActivity {
Some(p) => p.path().to_path_buf(),
};
match self.filetransfer_recv(
TransferPayload::Any(entry),
TransferPayload::Any(entry.clone()),
cache.as_path(),
Some(tmpfile.clone()),
) {
@@ -114,7 +111,7 @@ impl FileTransferActivity {
///
/// Open selected file with provided application
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::Many(entries) => entries,
SelectedEntry::None => vec![],
@@ -129,7 +126,7 @@ impl FileTransferActivity {
///
/// Open selected file with provided application
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::Many(entries) => entries,
SelectedEntry::None => vec![],

View File

@@ -26,8 +26,9 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
use crate::filetransfer::FileTransferErrorType;
use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry};
use remotefs::RemoteErrorType;
use std::path::{Path, PathBuf};
impl FileTransferActivity {
@@ -45,7 +46,7 @@ impl FileTransferActivity {
// Iter files
for entry in entries.iter() {
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());
}
// Reload entries
@@ -69,7 +70,7 @@ impl FileTransferActivity {
// Iter files
for entry in entries.iter() {
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());
}
// 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) {
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(),
entry.path().display(),
dest.display()
),
);
@@ -95,7 +96,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!(
"Could not move \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
entry.path().display(),
dest.display(),
err
),
@@ -103,26 +104,26 @@ impl FileTransferActivity {
}
}
fn remote_rename_file(&mut self, entry: &FsEntry, dest: &Path) {
match self.client.as_mut().rename(entry, dest) {
fn remote_rename_file(&mut self, entry: &Entry, dest: &Path) {
match self.client.as_mut().mov(entry.path(), dest) {
Ok(_) => {
self.log(
LogLevel::Info,
format!(
"Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(),
entry.path().display(),
dest.display()
),
);
}
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => {
self.tricky_move(entry, dest);
}
Err(err) => self.log_and_alert(
LogLevel::Error,
format!(
"Could not move \"{}\" to \"{}\": {}",
entry.get_abs_path().display(),
entry.path().display(),
dest.display(),
err
),
@@ -134,21 +135,21 @@ impl FileTransferActivity {
///
/// 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`)
fn tricky_move(&mut self, entry: &FsEntry, dest: &Path) {
fn tricky_move(&mut self, entry: &Entry, dest: &Path) {
debug!(
"Using tricky-move to move entry {} to {}",
entry.get_abs_path().display(),
entry.path().display(),
dest.display()
);
if self.tricky_copy(entry.clone(), dest).is_ok() {
// Delete remote existing 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(
LogLevel::Info,
format!(
"Moved \"{}\" to \"{}\"",
entry.get_abs_path().display(),
entry.path().display(),
dest.display()
),
),
@@ -156,7 +157,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!(
"Copied \"{}\" to \"{}\"; but failed to remove src: {}",
entry.get_abs_path().display(),
entry.path().display(),
dest.display(),
err
),

View File

@@ -27,7 +27,7 @@
*/
// locals
use super::{
super::STORAGE_PENDING_TRANSFER, FileExplorerTab, FileTransferActivity, FsEntry, LogLevel,
super::STORAGE_PENDING_TRANSFER, Entry, FileExplorerTab, FileTransferActivity, LogLevel,
SelectedEntry, TransferOpts, TransferPayload,
};
use std::path::{Path, PathBuf};
@@ -101,10 +101,10 @@ impl FileTransferActivity {
{
// Save 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(
TransferPayload::Any(entry.get_realfile()),
TransferPayload::Any(entry.clone()),
wrkdir.as_path(),
opts.save_as,
) {
@@ -123,10 +123,9 @@ impl FileTransferActivity {
dest_path.push(save_as);
}
// 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() {
// Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries
let existing_files: Vec<&Entry> = entries
.iter()
.filter(|x| {
self.remote_file_exists(
@@ -171,10 +170,10 @@ impl FileTransferActivity {
{
// Save 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(
TransferPayload::Any(entry.get_realfile()),
TransferPayload::Any(entry.clone()),
wrkdir.as_path(),
opts.save_as,
) {
@@ -193,10 +192,9 @@ impl FileTransferActivity {
dest_path.push(save_as);
}
// 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() {
// Check which file would be replaced
let existing_files: Vec<&FsEntry> = entries
let existing_files: Vec<&Entry> = entries
.iter()
.filter(|x| {
self.local_file_exists(
@@ -244,8 +242,8 @@ impl FileTransferActivity {
/// ### set_pending_transfer_many
///
/// 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) {
let file_names: Vec<&str> = files.iter().map(|x| x.get_name()).collect();
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.name()).collect();
self.mount_radio_replace_many(file_names.as_slice());
self.context_mut()
.store_mut()
@@ -255,16 +253,16 @@ impl FileTransferActivity {
/// ### file_to_check
///
/// 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 {
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();
p.push(e.get_name());
p.push(e.name());
p
}
}

View File

@@ -26,7 +26,9 @@
* SOFTWARE.
*/
// locals
use super::{FileTransferActivity, FsEntry};
use super::{Entry, FileTransferActivity};
use remotefs::fs::{File, Metadata};
enum SubmitAction {
ChangeDir,
@@ -38,25 +40,41 @@ impl FileTransferActivity {
///
/// Decides which action to perform on submit for local explorer
/// Return true whether the directory changed
pub(crate) fn action_submit_local(&mut self, entry: FsEntry) -> bool {
let action: SubmitAction = match &entry {
FsEntry::Directory(_) => SubmitAction::ChangeDir,
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(_) => SubmitAction::ChangeDir,
_ => SubmitAction::None,
}
pub(crate) fn action_submit_local(&mut self, entry: Entry) -> bool {
let (action, entry) = match &entry {
Entry::Directory(_) => (SubmitAction::ChangeDir, entry),
Entry::File(File {
path,
metadata:
Metadata {
symlink: Some(symlink),
..
},
..
}) => {
// 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 {
SubmitAction::ChangeDir => self.action_enter_local_dir(entry, false),
SubmitAction::None => false,
match (action, entry) {
(SubmitAction::ChangeDir, Entry::Directory(dir)) => {
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
/// Return true whether the directory changed
pub(crate) fn action_submit_remote(&mut self, entry: FsEntry) -> bool {
let action: SubmitAction = match &entry {
FsEntry::Directory(_) => SubmitAction::ChangeDir,
FsEntry::File(file) => {
match &file.symlink {
Some(symlink_entry) => {
// If symlink and is directory, point to symlink
match &**symlink_entry {
FsEntry::Directory(_) => SubmitAction::ChangeDir,
_ => SubmitAction::None,
}
pub(crate) fn action_submit_remote(&mut self, entry: Entry) -> bool {
let (action, entry) = match &entry {
Entry::Directory(_) => (SubmitAction::ChangeDir, entry),
Entry::File(File {
path,
metadata:
Metadata {
symlink: Some(symlink),
..
},
..
}) => {
// 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 {
SubmitAction::ChangeDir => self.action_enter_remote_dir(entry, false),
SubmitAction::None => false,
match (action, entry) {
(SubmitAction::ChangeDir, Entry::Directory(dir)) => {
self.action_enter_remote_dir(dir, false)
}
(SubmitAction::ChangeDir, _) => false,
(SubmitAction::None, _) => false,
}
}
}

View File

@@ -225,21 +225,12 @@ impl Component<Msg, NoUserEvent> for Log {
/// ## OwnStates
///
/// OwnStates contains states for this component
#[derive(Clone)]
#[derive(Clone, Default)]
struct OwnStates {
list_index: usize, // Index of selected element in list
list_len: usize, // Length of file list
}
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
list_index: 0,
list_len: 0,
}
}
}
impl OwnStates {
/// ### set_list_len
///

View File

@@ -27,12 +27,11 @@
*/
use super::super::Browser;
use super::{Msg, TransferMsg, UiMsg};
use crate::fs::explorer::FileSorting;
use crate::fs::FsEntry;
use crate::explorer::FileSorting;
use crate::utils::fmt::fmt_time;
use bytesize::ByteSize;
use std::path::PathBuf;
use remotefs::Entry;
use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
@@ -400,38 +399,32 @@ pub struct FileInfoPopup {
}
impl FileInfoPopup {
pub fn new(file: &FsEntry) -> Self {
pub fn new(file: &Entry) -> Self {
let mut texts: TableBuilder = TableBuilder::default();
// Abs path
let real_path: Option<PathBuf> = {
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 real_path = file.metadata().symlink.as_deref();
let path: String = match real_path {
Some(symlink) => format!("{} -> {}", file.get_abs_path().display(), symlink.display()),
None => format!("{}", file.get_abs_path().display()),
Some(symlink) => format!("{} -> {}", file.path().display(), symlink.display()),
None => format!("{}", file.path().display()),
};
// Make texts
texts
.add_col(TextSpan::from("Path: "))
.add_col(TextSpan::new(path.as_str()).fg(Color::Yellow));
if let Some(filetype) = file.get_ftype() {
if let Some(filetype) = file.extension() {
texts
.add_row()
.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
.add_row()
.add_col(TextSpan::from("Size: "))
.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.get_last_access_time(), "%b %d %Y %H:%M:%S");
let mtime: 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 ctime: String = fmt_time(file.metadata().ctime, "%b %d %Y %H:%M:%S");
let mtime: String = fmt_time(file.metadata().mtime, "%b %d %Y %H:%M:%S");
texts
.add_row()
.add_col(TextSpan::from("Creation time: "))
@@ -446,7 +439,7 @@ impl FileInfoPopup {
.add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed));
// User
#[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(user) => user.name().to_string_lossy().to_string(),
None => uid.to_string(),
@@ -454,10 +447,10 @@ impl FileInfoPopup {
None => String::from("0"),
};
#[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
#[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(group) => group.name().to_string_lossy().to_string(),
None => gid.to_string(),
@@ -465,7 +458,7 @@ impl FileInfoPopup {
None => String::from("0"),
};
#[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
.add_row()
.add_col(TextSpan::from("User: "))
@@ -478,7 +471,7 @@ impl FileInfoPopup {
component: List::default()
.borders(Borders::default().modifiers(BorderType::Rounded))
.scroll(false)
.title(file.get_name(), Alignment::Left)
.title(file.name(), Alignment::Left)
.rows(texts.build()),
}
}

View File

@@ -39,21 +39,12 @@ pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
/// ## OwnStates
///
/// OwnStates contains states for this component
#[derive(Clone)]
#[derive(Clone, Default)]
struct OwnStates {
list_index: usize, // Index of selected element in list
selected: Vec<usize>, // Selected files
}
impl Default for OwnStates {
fn default() -> Self {
OwnStates {
list_index: 0,
selected: Vec::new(),
}
}
}
impl OwnStates {
/// ### init_list_states
///

View File

@@ -25,10 +25,10 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
use crate::fs::FsEntry;
use crate::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
use crate::system::config_client::ConfigClient;
use remotefs::Entry;
use std::path::Path;
/// ## FileExplorerTab
@@ -100,7 +100,7 @@ impl Browser {
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);
explorer.set_files(files);
self.found = Some((tab, explorer));

View File

@@ -29,7 +29,6 @@ use super::{
use crate::filetransfer::ProtocolParams;
use crate::system::environment;
use crate::system::notifications::Notification;
use crate::system::sshkey_storage::SshKeyStorage;
use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex};
use crate::utils::path;
// 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
///
/// Set text editor to use
@@ -227,7 +219,7 @@ impl FileTransferActivity {
TransferPayload::Any(entry) => {
format!(
"\"{}\" has been successfully transferred ({})",
entry.get_name(),
entry.name(),
transfer_stats
)
}

View File

@@ -37,10 +37,8 @@ mod view;
// locals
use super::{Activity, Context, ExitReason};
use crate::config::themes::Theme;
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
use crate::fs::explorer::{FileExplorer, FileSorting};
use crate::fs::FsEntry;
use crate::explorer::{FileExplorer, FileSorting};
use crate::filetransfer::{Builder, FileTransferParams};
use crate::host::Localhost;
use crate::system::config_client::ConfigClient;
pub(self) use lib::browser;
@@ -50,6 +48,7 @@ pub(self) use session::TransferPayload;
// Includes
use chrono::{DateTime, Local};
use remotefs::RemoteFs;
use std::collections::VecDeque;
use std::time::Duration;
use tempfile::TempDir;
@@ -217,8 +216,8 @@ pub struct FileTransferActivity {
redraw: bool,
/// Localhost bridge
host: Localhost,
/// Remote host
client: Box<dyn FileTransfer>,
/// Remote host client
client: Box<dyn RemoteFs>,
/// Browser
browser: Browser,
/// Current log lines
@@ -232,7 +231,7 @@ impl FileTransferActivity {
/// ### new
///
/// 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
let config_client: ConfigClient = Self::init_config_client();
Self {
@@ -245,16 +244,7 @@ impl FileTransferActivity {
),
redraw: true,
host,
client: match protocol {
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()),
},
client: Builder::build(params.protocol, params.params.clone(), &config_client),
browser: Browser::new(&config_client),
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
transfer: TransferStates::default(),

View File

@@ -27,14 +27,14 @@
*/
// Locals
use super::{FileTransferActivity, LogLevel};
use crate::filetransfer::{FileTransferError, FileTransferErrorType};
use crate::fs::{FsEntry, FsFile};
use crate::host::HostError;
use crate::utils::fmt::fmt_millis;
// Ext
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::path::{Path, PathBuf};
use std::time::Instant;
@@ -56,20 +56,20 @@ enum TransferErrorReason {
#[error("I/O error on remote: {0}")]
RemoteIoError(std::io::Error),
#[error("File transfer error: {0}")]
FileTransferError(FileTransferError),
FileTransferError(RemoteError),
}
/// ## TransferPayload
///
/// Represents the entity to send or receive during a transfer.
/// - File: describes an individual `FsFile` to send
/// - Any: Can be any kind of `FsEntry`, but just one
/// - Many: a list of `FsEntry`
/// - File: describes an individual `File` to send
/// - Any: Can be any kind of `Entry`, but just one
/// - Many: a list of `Entry`
#[derive(Debug)]
pub(super) enum TransferPayload {
File(FsFile),
Any(FsEntry),
Many(Vec<FsEntry>),
File(File),
Any(Entry),
Many(Vec<Entry>),
}
impl FileTransferActivity {
@@ -78,11 +78,11 @@ impl FileTransferActivity {
/// Connect to remote
pub(super) fn connect(&mut self) {
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
match self.client.connect(&ft_params.params) {
Ok(welcome) => {
if let Some(banner) = welcome {
match self.client.connect() {
Ok(Welcome { banner, .. }) => {
if let Some(banner) = banner {
// Log welcome
self.log(
LogLevel::Info,
@@ -234,17 +234,17 @@ impl FileTransferActivity {
/// Send one file to remote at specified path.
fn filetransfer_send_file(
&mut self,
file: &FsFile,
file: &File,
curr_remote_path: &Path,
dst_name: Option<String>,
) -> Result<(), String> {
// Reset states
self.transfer.reset();
// Calculate total size of transfer
let total_transfer_size: usize = file.size;
let total_transfer_size: usize = file.metadata.size as usize;
self.transfer.full.init(total_transfer_size);
// 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
let file_name: String = file.name.clone();
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
@@ -266,7 +266,7 @@ impl FileTransferActivity {
/// Send a `TransferPayload` of type `Any`
fn filetransfer_send_any(
&mut self,
entry: &FsEntry,
entry: &Entry,
curr_remote_path: &Path,
dst_name: Option<String>,
) -> Result<(), String> {
@@ -276,7 +276,7 @@ impl FileTransferActivity {
let total_transfer_size: usize = self.get_total_transfer_size_local(entry);
self.transfer.full.init(total_transfer_size);
// 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
let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
// Umount progress bar
@@ -289,7 +289,7 @@ impl FileTransferActivity {
/// Send many entries to remote
fn filetransfer_send_many(
&mut self,
entries: &[FsEntry],
entries: &[Entry],
curr_remote_path: &Path,
) -> Result<(), String> {
// Reset states
@@ -315,14 +315,14 @@ impl FileTransferActivity {
fn filetransfer_send_recurse(
&mut self,
entry: &FsEntry,
entry: &Entry,
curr_remote_path: &Path,
dst_name: Option<String>,
) -> Result<(), String> {
// Write popup
let file_name: String = match entry {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
Entry::Directory(dir) => dir.name.clone(),
Entry::File(file) => file.name.clone(),
};
// Get remote path
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
@@ -333,7 +333,7 @@ impl FileTransferActivity {
remote_path.push(remote_file_name);
// 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) {
Err(err) => {
// If transfer was abrupted or there was an IO error on remote, remove file
@@ -352,7 +352,7 @@ impl FileTransferActivity {
),
),
Ok(entry) => {
if let Err(err) = self.client.remove(&entry) {
if let Err(err) = self.client.remove_file(entry.path()) {
self.log(
LogLevel::Error,
format!(
@@ -370,16 +370,19 @@ impl FileTransferActivity {
Ok(_) => Ok(()),
}
}
FsEntry::Directory(dir) => {
Entry::Directory(dir) => {
// 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(_) => {
self.log(
LogLevel::Info,
format!("Created directory \"{}\"", remote_path.display()),
);
}
Err(err) if err.kind() == FileTransferErrorType::DirectoryAlreadyExists => {
Err(err) if err.kind == RemoteErrorType::DirectoryAlreadyExists => {
self.log(
LogLevel::Info,
format!(
@@ -401,7 +404,7 @@ impl FileTransferActivity {
}
}
// 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) => {
// Iterate over files
for entry in entries.iter() {
@@ -423,7 +426,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!(
"Could not scan directory \"{}\": {}",
dir.abs_path.display(),
dir.path.display(),
err
),
);
@@ -439,7 +442,7 @@ impl FileTransferActivity {
// Log abort
self.log_and_alert(
LogLevel::Warn,
format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()),
format!("Upload aborted for \"{}\"!", entry.path().display()),
);
}
result
@@ -450,18 +453,24 @@ impl FileTransferActivity {
/// Send local file and write it to remote path
fn filetransfer_send_one(
&mut self,
local: &FsFile,
local: &File,
remote: &Path,
file_name: String,
) -> 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
// Try to open local file
match self.host.open_file_read(local.abs_path.as_path()) {
Ok(fhnd) => match self.client.send_file(local, remote) {
match self.host.open_file_read(local.path.as_path()) {
Ok(fhnd) => match self.client.create(remote, &metadata) {
Ok(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)
}
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
@@ -475,10 +484,10 @@ impl FileTransferActivity {
/// Send file to remote using stream
fn filetransfer_send_one_with_stream(
&mut self,
local: &FsFile,
local: &File,
remote: &Path,
file_name: String,
mut reader: File,
mut reader: StdFile,
mut writer: Box<dyn Write>,
) -> Result<(), TransferErrorReason> {
// Write file
@@ -548,7 +557,7 @@ impl FileTransferActivity {
}
}
// Finalize stream
if let Err(err) = self.client.on_sent(writer) {
if let Err(err) = self.client.on_written(writer) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
@@ -562,7 +571,7 @@ impl FileTransferActivity {
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
local.path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
@@ -573,14 +582,20 @@ impl FileTransferActivity {
/// ### 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(
&mut self,
local: &FsFile,
local: &File,
remote: &Path,
file_name: String,
mut reader: File,
mut reader: StdFile,
) -> 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
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
// Init transfer
@@ -593,10 +608,7 @@ impl FileTransferActivity {
self.update_progress_bar(format!("Uploading \"{}\"", file_name));
self.view();
// Send file
if let Err(err) = self
.client
.send_file_wno_stream(local, remote, Box::new(reader))
{
if let Err(err) = self.client.create_file(remote, &metadata, Box::new(reader)) {
return Err(TransferErrorReason::FileTransferError(err));
}
// Set transfer size ok
@@ -610,7 +622,7 @@ impl FileTransferActivity {
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
local.abs_path.display(),
local.path.display(),
remote.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
@@ -656,7 +668,7 @@ impl FileTransferActivity {
/// If entry is a directory, this applies to directory only
fn filetransfer_recv_any(
&mut self,
entry: &FsEntry,
entry: &Entry,
local_path: &Path,
dst_name: Option<String>,
) -> Result<(), String> {
@@ -666,7 +678,7 @@ impl FileTransferActivity {
let total_transfer_size: usize = self.get_total_transfer_size_remote(entry);
self.transfer.full.init(total_transfer_size);
// Mount progress bar
self.mount_progress_bar(format!("Downloading {}", entry.get_abs_path().display()));
self.mount_progress_bar(format!("Downloading {}", entry.path().display()));
// Receive
let result = self.filetransfer_recv_recurse(entry, local_path, dst_name);
// Umount progress bar
@@ -677,14 +689,14 @@ impl FileTransferActivity {
/// ### filetransfer_recv_file
///
/// 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
self.transfer.reset();
// 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);
// Mount progress bar
self.mount_progress_bar(format!("Downloading {}", entry.abs_path.display()));
self.mount_progress_bar(format!("Downloading {}", entry.path.display()));
// Receive
let result = self.filetransfer_recv_one(local_path, entry, entry.name.clone());
// Umount progress bar
@@ -698,7 +710,7 @@ impl FileTransferActivity {
/// Send many entries to remote
fn filetransfer_recv_many(
&mut self,
entries: &[FsEntry],
entries: &[Entry],
curr_remote_path: &Path,
) -> Result<(), String> {
// Reset states
@@ -724,18 +736,18 @@ impl FileTransferActivity {
fn filetransfer_recv_recurse(
&mut self,
entry: &FsEntry,
entry: &Entry,
local_path: &Path,
dst_name: Option<String>,
) -> Result<(), String> {
// Write popup
let file_name: String = match entry {
FsEntry::Directory(dir) => dir.name.clone(),
FsEntry::File(file) => file.name.clone(),
Entry::Directory(dir) => dir.name.clone(),
Entry::File(file) => file.name.clone(),
};
// Match entry
let result: Result<(), String> = match entry {
FsEntry::File(file) => {
Entry::File(file) => {
// Get local file
let mut local_file_path: PathBuf = PathBuf::from(local_path);
let local_file_name: String = match dst_name {
@@ -781,7 +793,7 @@ impl FileTransferActivity {
Ok(())
}
}
FsEntry::Directory(dir) => {
Entry::Directory(dir) => {
// Get dir name
let mut local_dir_path: PathBuf = PathBuf::from(local_path);
match dst_name {
@@ -797,16 +809,13 @@ impl FileTransferActivity {
target_os = "macos",
target_os = "linux"
))]
if let Some((owner, group, others)) = dir.unix_pex {
if let Err(err) = self.host.chmod(
local_dir_path.as_path(),
(owner.as_byte(), group.as_byte(), others.as_byte()),
) {
if let Some(mode) = dir.metadata.mode {
if let Err(err) = self.host.chmod(local_dir_path.as_path(), mode) {
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
"Could not apply file mode {:o} to \"{}\": {}",
u32::from(mode),
local_dir_path.display(),
err
),
@@ -818,7 +827,7 @@ impl FileTransferActivity {
format!("Created directory \"{}\"", local_dir_path.display()),
);
// 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) => {
// Iterate over files
for entry in entries.iter() {
@@ -843,7 +852,7 @@ impl FileTransferActivity {
LogLevel::Error,
format!(
"Could not scan directory \"{}\": {}",
dir.abs_path.display(),
dir.path.display(),
err
),
);
@@ -872,10 +881,7 @@ impl FileTransferActivity {
// Log abort
self.log_and_alert(
LogLevel::Warn,
format!(
"Download aborted for \"{}\"!",
entry.get_abs_path().display()
),
format!("Download aborted for \"{}\"!", entry.path().display()),
);
}
result
@@ -887,18 +893,18 @@ impl FileTransferActivity {
fn filetransfer_recv_one(
&mut self,
local: &Path,
remote: &FsFile,
remote: &File,
file_name: String,
) -> Result<(), TransferErrorReason> {
// Try to open local file
match self.host.open_file_write(local) {
Ok(local_file) => {
// 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(
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)
}
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
@@ -910,24 +916,24 @@ impl FileTransferActivity {
/// ### 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(
&mut self,
local: &Path,
remote: &FsFile,
remote: &File,
file_name: String,
mut reader: Box<dyn Read>,
mut writer: File,
mut writer: StdFile,
) -> Result<(), TransferErrorReason> {
let mut total_bytes_written: usize = 0;
// Init transfer
self.transfer.partial.init(remote.size);
self.transfer.partial.init(remote.metadata.size as usize);
// Write local file
let mut last_progress_val: f64 = 0.0;
let mut last_input_event_fetch: Option<Instant> = None;
// While the entire file hasn't been completely read,
// 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
if last_input_event_fetch.is_none()
|| last_input_event_fetch
@@ -978,7 +984,7 @@ impl FileTransferActivity {
}
}
// Finalize stream
if let Err(err) = self.client.on_recv(reader) {
if let Err(err) = self.client.on_read(reader) {
self.log(
LogLevel::Warn,
format!("Could not finalize remote stream: \"{}\"", err),
@@ -990,16 +996,13 @@ impl FileTransferActivity {
}
// Apply file mode to file
#[cfg(target_family = "unix")]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
if let Some(mode) = remote.metadata.mode {
if let Err(err) = self.host.chmod(local, mode) {
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
"Could not apply file mode {:o} to \"{}\": {}",
u32::from(mode),
local.display(),
err
),
@@ -1011,7 +1014,7 @@ impl FileTransferActivity {
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
remote.path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
@@ -1022,40 +1025,47 @@ impl FileTransferActivity {
/// ### 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(
&mut self,
local: &Path,
remote: &FsFile,
remote: &File,
file_name: String,
) -> Result<(), TransferErrorReason> {
// Open local file
let reader = self
.host
.open_file_write(local)
.map_err(TransferErrorReason::HostError)
.map(Box::new)?;
// Init transfer
self.transfer.partial.init(remote.size);
self.transfer.partial.init(remote.metadata.size as usize);
// Draw before transfer
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
// 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));
}
// Update progress at the end
self.transfer.partial.update_progress(remote.size);
self.transfer.full.update_progress(remote.size);
self.transfer
.partial
.update_progress(remote.metadata.size as usize);
self.transfer
.full
.update_progress(remote.metadata.size as usize);
// Draw after transfer
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
self.view();
// Apply file mode to file
#[cfg(target_family = "unix")]
if let Some((owner, group, others)) = remote.unix_pex {
if let Err(err) = self
.host
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
{
if let Some(mode) = remote.metadata.mode {
if let Err(err) = self.host.chmod(local, mode) {
self.log(
LogLevel::Error,
format!(
"Could not apply file mode {:?} to \"{}\": {}",
(owner.as_byte(), group.as_byte(), others.as_byte()),
"Could not apply file mode {:o} to \"{}\": {}",
u32::from(mode),
local.display(),
err
),
@@ -1067,7 +1077,7 @@ impl FileTransferActivity {
LogLevel::Info,
format!(
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
remote.abs_path.display(),
remote.path.display(),
local.display(),
fmt_millis(self.transfer.partial.started().elapsed()),
ByteSize(self.transfer.partial.calc_bytes_per_second()),
@@ -1136,7 +1146,7 @@ impl FileTransferActivity {
/// ### download_file_as_temp
///
/// 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() {
Some(cache) => {
let mut p: PathBuf = cache.path().to_path_buf();
@@ -1157,7 +1167,7 @@ impl FileTransferActivity {
) {
Err(err) => Err(format!(
"Could not download {} to temporary file: {}",
file.abs_path.display(),
file.path.display(),
err
)),
Ok(()) => Ok(tmpfile),
@@ -1169,12 +1179,12 @@ impl FileTransferActivity {
/// ### get_total_transfer_size_local
///
/// 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 {
FsEntry::File(file) => file.size,
FsEntry::Directory(dir) => {
Entry::File(file) => file.metadata.size as usize,
Entry::Directory(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
.iter()
.map(|x| self.get_total_transfer_size_local(x))
@@ -1182,11 +1192,7 @@ impl FileTransferActivity {
Err(err) => {
self.log(
LogLevel::Error,
format!(
"Could not list directory {}: {}",
dir.abs_path.display(),
err
),
format!("Could not list directory {}: {}", dir.path.display(), err),
);
0
}
@@ -1198,12 +1204,12 @@ impl FileTransferActivity {
/// ### get_total_transfer_size_remote
///
/// 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 {
FsEntry::File(file) => file.size,
FsEntry::Directory(dir) => {
Entry::File(file) => file.metadata.size as usize,
Entry::Directory(dir) => {
// List directory
match self.client.list_dir(dir.abs_path.as_path()) {
match self.client.list_dir(dir.path.as_path()) {
Ok(files) => files
.iter()
.map(|x| self.get_total_transfer_size_remote(x))
@@ -1211,11 +1217,7 @@ impl FileTransferActivity {
Err(err) => {
self.log(
LogLevel::Error,
format!(
"Could not list directory {}: {}",
dir.abs_path.display(),
err
),
format!("Could not list directory {}: {}", dir.path.display(), err),
);
0
}

View File

@@ -31,8 +31,8 @@ use super::{
browser::{FileExplorerTab, FoundExplorerTab},
ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg,
};
use crate::fs::FsEntry;
// externals
use remotefs::fs::Entry;
use tuirealm::{
props::{AttrValue, Attribute},
State, StateValue, Update,
@@ -282,7 +282,7 @@ impl FileTransferActivity {
// Mount wait
self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, search).as_str());
// 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::Remote => self.action_remote_find(search.clone()),
_ => panic!("Trying to search for files, while already in a find result"),

View File

@@ -30,11 +30,11 @@ use super::{
browser::{FileExplorerTab, FoundExplorerTab},
components, Context, FileTransferActivity, Id,
};
use crate::fs::explorer::FileSorting;
use crate::fs::FsEntry;
use crate::explorer::FileSorting;
use crate::ui::store::Store;
use crate::utils::ui::draw_area_in;
// Ext
use remotefs::fs::Entry;
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::tui::layout::{Constraint, Direction, Layout};
use tuirealm::tui::widgets::Clear;
@@ -747,7 +747,7 @@ impl FileTransferActivity {
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
.app
.remount(

View File

@@ -26,8 +26,8 @@
* SOFTWARE.
*/
use super::{ConfigMsg, Msg};
use crate::explorer::GroupDirs as GroupDirsEnum;
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs as GroupDirsEnum;
use crate::utils::parser::parse_bytesize;
use tui_realm_stdlib::{Input, Radio};

View File

@@ -46,19 +46,11 @@ use tuirealm::{Component, MockComponent};
// -- global listener
#[derive(MockComponent)]
#[derive(Default, MockComponent)]
pub struct GlobalListener {
component: Phantom,
}
impl Default for GlobalListener {
fn default() -> Self {
Self {
component: Phantom::default(),
}
}
}
impl Component<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
match ev {

View File

@@ -28,8 +28,8 @@
*/
// Locals
use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout};
use crate::explorer::GroupDirs;
use crate::filetransfer::FileTransferProtocol;
use crate::fs::explorer::GroupDirs;
use crate::utils::fmt::fmt_bytes;
// Ext

View File

@@ -25,7 +25,7 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use crate::fs::UnixPex;
use remotefs::fs::UnixPexClass;
use chrono::prelude::*;
use std::path::{Path, PathBuf};
@@ -35,18 +35,18 @@ use tuirealm::tui::style::Color;
/// ### fmt_pex
///
/// 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!(
"{}{}{}",
match pex.can_read() {
match pex.read() {
true => 'r',
false => '-',
},
match pex.can_write() {
match pex.write() {
true => 'w',
false => '-',
},
match pex.can_execute() {
match pex.execute() {
true => 'x',
false => '-',
}
@@ -315,9 +315,9 @@ mod tests {
#[test]
fn test_utils_fmt_pex() {
assert_eq!(fmt_pex(UnixPex::from(7)), String::from("rwx"));
assert_eq!(fmt_pex(UnixPex::from(5)), String::from("r-x"));
assert_eq!(fmt_pex(UnixPex::from(6)), String::from("rw-"));
assert_eq!(fmt_pex(UnixPexClass::from(7)), String::from("rwx"));
assert_eq!(fmt_pex(UnixPexClass::from(5)), String::from("r-x"));
assert_eq!(fmt_pex(UnixPexClass::from(6)), String::from("rw-"));
}
#[test]

View File

@@ -37,12 +37,9 @@ use crate::system::environment;
// Ext
use bytesize::ByteSize;
use chrono::format::ParseError;
use chrono::prelude::*;
use regex::Regex;
use std::path::PathBuf;
use std::str::FromStr;
use std::time::{Duration, SystemTime};
use tuirealm::tui::style::Color;
// 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 string
@@ -611,7 +560,6 @@ pub fn parse_bytesize<S: AsRef<str>>(bytes: S) -> Option<ByteSize> {
mod tests {
use super::*;
use crate::utils::fmt::fmt_time;
use pretty_assertions::assert_eq;
@@ -800,68 +748,6 @@ mod tests {
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]
fn test_utils_parse_semver() {
assert_eq!(

View File

@@ -25,39 +25,27 @@
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
use remotefs::fs::{Directory, Entry, File, Metadata};
// ext
use std::fs::File;
#[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::fs::File as StdFile;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use tempfile::NamedTempFile;
pub fn create_sample_file_entry() -> (FsFile, NamedTempFile) {
pub fn create_sample_file_entry() -> (File, NamedTempFile) {
// Write
let tmpfile = create_sample_file();
(
FsFile {
File {
name: tmpfile
.path()
.file_name()
.unwrap()
.to_string_lossy()
.to_string(),
abs_path: tmpfile.path().to_path_buf(),
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 127,
ftype: None, // File type
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
path: tmpfile.path().to_path_buf(),
extension: None,
metadata: Metadata::default(),
},
tmpfile,
)
@@ -80,7 +68,7 @@ pub fn create_sample_file() -> NamedTempFile {
pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> {
let mut p: PathBuf = PathBuf::from(dir);
p.push(filename);
let mut file: File = File::create(p.as_path())?;
let mut file = StdFile::create(p.as_path())?;
writeln!(
file,
"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())
}
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
pub fn write_file(file: &NamedTempFile, writable: &mut Box<dyn Write>) {
let mut fhnd = OpenOptions::new()
.create(false)
.read(true)
.write(false)
.open(file.path())
.ok()
.unwrap();
// Read file
let mut buffer: [u8; 65536] = [0; 65536];
assert!(fhnd.read(&mut buffer).is_ok());
// Write file
assert!(writable.write(&buffer).is_ok());
}
#[cfg(feature = "with-containers")]
pub fn write_ssh_key() -> NamedTempFile {
let mut tmpfile: NamedTempFile = NamedTempFile::new().unwrap();
writeln!(
tmpfile,
r"-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
NhAAAAAwEAAQAAAQEAxKyYUMRCNPlb4ZV1VMofrzApu2l3wgP4Ot9wBvHsw/+RMpcHIbQK
9iQqAVp8Z+M1fJyPXTKjoJtIzuCLF6Sjo0KI7/tFTh+yPnA5QYNLZOIRZb8skumL4gwHww
5Z942FDPuUDQ30C2mZR9lr3Cd5pA8S1ZSPTAV9QQHkpgoS8cAL8QC6dp3CJjUC8wzvXh3I
oN3bTKxCpM10KMEVuWO3lM4Nvr71auB9gzo1sFJ3bwebCZIRH01FROyA/GXRiaOtJFG/9N
nWWI/iG5AJzArKpLZNHIP+FxV/NoRH0WBXm9Wq5MrBYrD1NQzm+kInpS/2sXk3m1aZWqLm
HF2NKRXSbQAAA8iI+KSniPikpwAAAAdzc2gtcnNhAAABAQDErJhQxEI0+VvhlXVUyh+vMC
m7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VO
H7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAe
SmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndv
B5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkys
FisPU1DOb6QielL/axeTebVplaouYcXY0pFdJtAAAAAwEAAQAAAP8u3PFuTVV5SfGazwIm
MgNaux82iOsAT/HWFWecQAkqqrruUw5f+YajH/riV61NE9aq2qNOkcJrgpTWtqpt980GGd
SHWlgpRWQzfIooEiDk6Pk8RVFZsEykkDlJQSIu2onZjhi5A5ojHgZoGGabDsztSqoyOjPq
6WPvGYRiDAR3leBMyp1WufBCJqAsC4L8CjPJSmnZhc5a0zXkC9Syz74Fa08tdM7bGhtvP1
GmzuYxkgxHH2IFeoumUSBHRiTZayGuRUDel6jgEiUMxenaDKXe7FpYzMm9tQZA10Mm4LhK
5rP9nd2/KRTFRnfZMnKvtIRC9vtlSLBe14qw+4ZCl60AAACAf1kghlO3+HIWplOmk/lCL0
w75Zz+RdvueL9UuoyNN1QrUEY420LsixgWSeRPby+Rb/hW+XSAZJQHowQ8acFJhU85So7f
4O4wcDuE4f6hpsW9tTfkCEUdLCQJ7EKLCrod6jIV7hvI6rvXiVucRpeAzdOaq4uzj2cwDd
tOdYVsnmQAAACBAOVxBsvO/Sr3rZUbNtA6KewZh/09HNGoKNaCeiD7vaSn2UJbbPRByF/o
Oo5zv8ee8r3882NnmG808XfSn7pPZAzbbTmOaJt0fmyZhivCghSNzV6njW3o0PdnC0fGZQ
ruVXgkd7RJFbsIiD4dDcF4VCjwWHfTK21EOgJUA5pN6TNvAAAAgQDbcJWRx8Uyhkj2+srb
3n2Rt6CR7kEl9cw17ItFjMn+pO81/5U2aGw0iLlX7E06TAMQC+dyW/WaxQRey8RRdtbJ1e
TNKCN34QCWkyuYRHGhcNc0quEDayPw5QWGXlP4BzjfRUcPxY9cCXLe5wDLYsX33HwOAc59
RorU9FCmS/654wAAABFyb290QDhjNTBmZDRjMzQ1YQECAw==
-----END OPENSSH PRIVATE KEY-----"
)
.unwrap();
tmpfile
}
/// ### make_fsentry
///
/// Create a FsEntry at specified path
pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> FsEntry {
/// Create a Entry at specified path
pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> Entry {
let path: PathBuf = path.as_ref().to_path_buf();
match is_dir {
true => FsEntry::Directory(FsDirectory {
true => Entry::Directory(Directory {
name: path.file_name().unwrap().to_string_lossy().to_string(),
abs_path: path,
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
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
path,
metadata: Metadata::default(),
}),
false => FsEntry::File(FsFile {
false => Entry::File(File {
name: path.file_name().unwrap().to_string_lossy().to_string(),
abs_path: path,
last_change_time: SystemTime::UNIX_EPOCH,
last_access_time: SystemTime::UNIX_EPOCH,
creation_time: SystemTime::UNIX_EPOCH,
size: 127,
ftype: None, // File type
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
path,
extension: None,
metadata: Metadata::default(),
}),
}
}
@@ -186,8 +106,11 @@ pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> FsEntry {
/// ### create_file_ioers
///
/// 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) {
(File::open(p).ok().unwrap(), File::create(p).ok().unwrap())
pub fn create_file_ioers(p: &Path) -> (StdFile, StdFile) {
(
StdFile::open(p).ok().unwrap(),
StdFile::create(p).ok().unwrap(),
)
}
mod test {
@@ -197,31 +120,7 @@ mod test {
#[test]
fn test_utils_test_helpers_sample_file() {
let (file, _) = 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();
let _ = create_sample_file_entry();
}
#[test]

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

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