mirror of
https://github.com/veeso/termscp.git
synced 2026-09-26 05:51:20 -07:00
Removed filetransfer module; migrated to remotefs crate
This commit is contained in:
committed by
Christian Visintin
parent
25dd1b9b0a
commit
df7a4381c4
@@ -8,7 +8,6 @@ ignore:
|
|||||||
- "../*"
|
- "../*"
|
||||||
- src/main.rs
|
- src/main.rs
|
||||||
- src/activity_manager.rs
|
- src/activity_manager.rs
|
||||||
- src/filetransfer/transfer/s3/mod.rs
|
|
||||||
- src/support.rs
|
- src/support.rs
|
||||||
- src/system/notifications.rs
|
- src/system/notifications.rs
|
||||||
- "src/ui/activities/*"
|
- "src/ui/activities/*"
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev
|
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev
|
||||||
- name: Setup containers
|
|
||||||
run: docker-compose -f "tests/docker-compose.yml" up -d --build
|
|
||||||
- name: Setup nightly toolchain
|
- name: Setup nightly toolchain
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -24,7 +22,7 @@ jobs:
|
|||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
args: --no-default-features --features github-actions --features with-containers --no-fail-fast
|
args: --no-default-features --features github-actions --no-fail-fast
|
||||||
env:
|
env:
|
||||||
CARGO_INCREMENTAL: "0"
|
CARGO_INCREMENTAL: "0"
|
||||||
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
|
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests"
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ jobs:
|
|||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev
|
run: sudo apt update && sudo apt install -y libdbus-1-dev libssh2-1-dev libssl-dev
|
||||||
- name: Setup containers
|
|
||||||
run: docker-compose -f "tests/docker-compose.yml" up -d --build
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
@@ -24,7 +22,7 @@ jobs:
|
|||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
args: --no-default-features --features github-actions --features with-containers --no-fail-fast
|
args: --no-default-features --features github-actions --no-fail-fast
|
||||||
- name: Format
|
- name: Format
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
- name: Clippy
|
- name: Clippy
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ Released on FIXME:
|
|||||||
- Dependencies:
|
- Dependencies:
|
||||||
- Updated `tui-realm` to `1.3.0`
|
- Updated `tui-realm` to `1.3.0`
|
||||||
- Updated `tui-realm-stdlib` to `1.1.4`
|
- Updated `tui-realm-stdlib` to `1.1.4`
|
||||||
|
- Removed `rust-s3`, `ssh2`, `suppaftp`; replaced by `remotefs 0.1.1`
|
||||||
- Removed `crossterm` (since bridged by tui-realm)
|
- Removed `crossterm` (since bridged by tui-realm)
|
||||||
|
|
||||||
## 0.7.0
|
## 0.7.0
|
||||||
|
|||||||
Generated
+74
-65
@@ -123,9 +123,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attohttpc"
|
name = "attohttpc"
|
||||||
version = "0.16.3"
|
version = "0.18.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fdb8867f378f33f78a811a8eb9bf108ad99430d7aad43315dd9319c827ef6247"
|
checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"log",
|
"log",
|
||||||
@@ -134,21 +134,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"url",
|
"url",
|
||||||
"wildmatch 1.1.0",
|
"wildmatch",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "attohttpc"
|
|
||||||
version = "0.17.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9a8bda305457262b339322106c776e3fd21df860018e566eb6a5b1aa4b6ae02d"
|
|
||||||
dependencies = [
|
|
||||||
"http",
|
|
||||||
"log",
|
|
||||||
"native-tls",
|
|
||||||
"openssl",
|
|
||||||
"url",
|
|
||||||
"wildmatch 1.1.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -159,13 +145,13 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aws-creds"
|
name = "aws-creds"
|
||||||
version = "0.26.0"
|
version = "0.27.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1331d069460a674d42bd27c12b47ce578f789954c7bd7f239fd030771eca6616"
|
checksum = "460a75eac8f3cb7683e0a9a588a83c3ff039331ea7bfbfbfcecf1dacab276e11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"attohttpc 0.16.3",
|
"attohttpc",
|
||||||
"dirs 3.0.2",
|
"dirs 4.0.0",
|
||||||
"rust-ini",
|
"rust-ini",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-xml-rs",
|
"serde-xml-rs",
|
||||||
@@ -456,6 +442,16 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crypto-mac"
|
||||||
|
version = "0.11.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714"
|
||||||
|
dependencies = [
|
||||||
|
"generic-array",
|
||||||
|
"subtle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ctor"
|
name = "ctor"
|
||||||
version = "0.1.21"
|
version = "0.1.21"
|
||||||
@@ -529,15 +525,6 @@ dependencies = [
|
|||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs"
|
|
||||||
version = "3.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309"
|
|
||||||
dependencies = [
|
|
||||||
"dirs-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dirs"
|
name = "dirs"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
@@ -799,7 +786,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
|
checksum = "51ab2f639c231793c5f6114bdb9bbe50a7dbbfcd7c7c6bd8475dec2d991e964f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
"hmac",
|
"hmac 0.10.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -808,7 +795,17 @@ version = "0.10.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
|
checksum = "c1441c6b1e930e2817404b5046f1f989899143a12bf92de603b69f4e0aee1e15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crypto-mac",
|
"crypto-mac 0.10.1",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hmac"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b"
|
||||||
|
dependencies = [
|
||||||
|
"crypto-mac 0.11.1",
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1133,15 +1130,6 @@ version = "0.3.16"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minidom"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "332592c2149fc7dd40a64fc9ef6f0d65607284b474cef9817d1fc8c7e7b3608e"
|
|
||||||
dependencies = [
|
|
||||||
"quick-xml",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.4.4"
|
version = "0.4.4"
|
||||||
@@ -1686,6 +1674,26 @@ version = "0.6.25"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "remotefs"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c72915b01014a11d7e21b3a28141ff32b881bd8103c0f65269cc7932a03ae61c"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"path-slash",
|
||||||
|
"regex",
|
||||||
|
"rust-s3",
|
||||||
|
"ssh2",
|
||||||
|
"ssh2-config",
|
||||||
|
"suppaftp",
|
||||||
|
"thiserror",
|
||||||
|
"users",
|
||||||
|
"wildmatch",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "remove_dir_all"
|
name = "remove_dir_all"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -1754,35 +1762,34 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.16.1"
|
version = "0.17.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55b134767a87e0b086f73a4ce569ac9ce7d202f39c8eab6caa266e2617e73ac6"
|
checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 0.1.10",
|
"cfg-if 1.0.0",
|
||||||
"ordered-multimap",
|
"ordered-multimap",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-s3"
|
name = "rust-s3"
|
||||||
version = "0.27.0-rc4"
|
version = "0.28.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5c93272c1d654d492f8ab30b94cd43d98f2700b1db55b2576aff7712ce40e3ef"
|
checksum = "18c58d4682844a5d6301efbf915dd7a9d3d638006f9bb821527a0bbbf2a4cfc2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"attohttpc 0.17.0",
|
"attohttpc",
|
||||||
"aws-creds",
|
"aws-creds",
|
||||||
"aws-region",
|
"aws-region",
|
||||||
"base64",
|
"base64",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"chrono",
|
"chrono",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"hmac 0.11.0",
|
||||||
"http",
|
"http",
|
||||||
"log",
|
"log",
|
||||||
"maybe-async",
|
"maybe-async",
|
||||||
"md5",
|
"md5",
|
||||||
"minidom",
|
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-xml-rs",
|
"serde-xml-rs",
|
||||||
@@ -1926,9 +1933,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde-xml-rs"
|
name = "serde-xml-rs"
|
||||||
version = "0.4.1"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0bf1ba0696ccf0872866277143ff1fd14d22eec235d2b23702f95e6660f7dfa"
|
checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -1977,7 +1984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d"
|
checksum = "e0bccbcf40c8938196944a3da0e133e031a33f4d6b72db3bda3cc556e361905d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"parking_lot 0.10.2",
|
"parking_lot 0.11.2",
|
||||||
"serial_test_derive",
|
"serial_test_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2086,6 +2093,16 @@ dependencies = [
|
|||||||
"parking_lot 0.10.2",
|
"parking_lot 0.10.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssh2-config"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31e64d0ea4897c9415c34011a4cdf21a0e0168c200595f1f543be1ca807942d8"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror",
|
||||||
|
"wildmatch",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -2110,12 +2127,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "suppaftp"
|
name = "suppaftp"
|
||||||
version = "4.1.2"
|
version = "4.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "29a4d861acfdc117c6d373c3b743c534dbbbb2d782e7646b27439a7c5282ad6a"
|
checksum = "3c42610de5ed0db28274b24c010aa530ebc391e5a4e9bd44a08e9daa8a245cc5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"log",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"regex",
|
"regex",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -2214,18 +2232,15 @@ dependencies = [
|
|||||||
"magic-crypt",
|
"magic-crypt",
|
||||||
"notify-rust",
|
"notify-rust",
|
||||||
"open",
|
"open",
|
||||||
"path-slash",
|
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"rand 0.8.4",
|
"rand 0.8.4",
|
||||||
"regex",
|
"regex",
|
||||||
|
"remotefs",
|
||||||
"rpassword",
|
"rpassword",
|
||||||
"rust-s3",
|
|
||||||
"self_update",
|
"self_update",
|
||||||
"serde",
|
"serde",
|
||||||
"serial_test",
|
"serial_test",
|
||||||
"simplelog",
|
"simplelog",
|
||||||
"ssh2",
|
|
||||||
"suppaftp",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
@@ -2234,7 +2249,7 @@ dependencies = [
|
|||||||
"tuirealm",
|
"tuirealm",
|
||||||
"users",
|
"users",
|
||||||
"whoami",
|
"whoami",
|
||||||
"wildmatch 2.1.0",
|
"wildmatch",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2662,12 +2677,6 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wildmatch"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7f44b95f62d34113cf558c93511ac93027e03e9c29a60dd0fd70e6e025c7270a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wildmatch"
|
name = "wildmatch"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
|||||||
+1
-9
@@ -48,13 +48,11 @@ notify-rust = { version = "4.5.3", default-features = false, features = [ "d" ]
|
|||||||
open = "2.0.1"
|
open = "2.0.1"
|
||||||
rand = "0.8.4"
|
rand = "0.8.4"
|
||||||
regex = "1.5.4"
|
regex = "1.5.4"
|
||||||
|
remotefs = { version = "0.1.1", features = [ "aws-s3", "ftp", "ssh" ] }
|
||||||
rpassword = "5.0.1"
|
rpassword = "5.0.1"
|
||||||
rust-s3 = { version = "0.27.0-rc4", default-features = false, features = [ "sync-native-tls", "sync" ] }
|
|
||||||
self_update = { version = "0.27.0", features = [ "archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate" ] }
|
self_update = { version = "0.27.0", features = [ "archive-tar", "archive-zip", "compression-flate2", "compression-zip-deflate" ] }
|
||||||
serde = { version = "^1.0.0", features = [ "derive" ] }
|
serde = { version = "^1.0.0", features = [ "derive" ] }
|
||||||
simplelog = "0.10.0"
|
simplelog = "0.10.0"
|
||||||
ssh2 = "0.9.0"
|
|
||||||
suppaftp = { version = "4.1.2", features = [ "secure" ] }
|
|
||||||
tempfile = "3.1.0"
|
tempfile = "3.1.0"
|
||||||
textwrap = "0.14.2"
|
textwrap = "0.14.2"
|
||||||
thiserror = "^1.0.0"
|
thiserror = "^1.0.0"
|
||||||
@@ -71,14 +69,8 @@ serial_test = "^0.5.1"
|
|||||||
[features]
|
[features]
|
||||||
default = [ "with-keyring" ]
|
default = [ "with-keyring" ]
|
||||||
github-actions = [ ]
|
github-actions = [ ]
|
||||||
with-s3-ci = []
|
|
||||||
with-containers = []
|
|
||||||
with-keyring = [ "keyring" ]
|
with-keyring = [ "keyring" ]
|
||||||
|
|
||||||
[target."cfg(target_family = \"unix\")"]
|
[target."cfg(target_family = \"unix\")"]
|
||||||
[target."cfg(target_family = \"unix\")".dependencies]
|
[target."cfg(target_family = \"unix\")".dependencies]
|
||||||
users = "0.11.0"
|
users = "0.11.0"
|
||||||
|
|
||||||
[target."cfg(target_os = \"windows\")"]
|
|
||||||
[target."cfg(target_os = \"windows\")".dependencies]
|
|
||||||
path-slash = "0.1.4"
|
|
||||||
|
|||||||
@@ -287,11 +287,9 @@ termscp is powered by these awesome projects:
|
|||||||
- [edit](https://github.com/milkey-mouse/edit)
|
- [edit](https://github.com/milkey-mouse/edit)
|
||||||
- [keyring-rs](https://github.com/hwchen/keyring-rs)
|
- [keyring-rs](https://github.com/hwchen/keyring-rs)
|
||||||
- [open-rs](https://github.com/Byron/open-rs)
|
- [open-rs](https://github.com/Byron/open-rs)
|
||||||
|
- [remotefs](https://github.com/veeso/remotefs-rs)
|
||||||
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
- [rpassword](https://github.com/conradkleinespel/rpassword)
|
||||||
- [rust-s3](https://github.com/durch/rust-s3)
|
|
||||||
- [self_update](https://github.com/jaemk/self_update)
|
- [self_update](https://github.com/jaemk/self_update)
|
||||||
- [ssh2-rs](https://github.com/alexcrichton/ssh2-rs)
|
|
||||||
- [suppaftp](https://github.com/veeso/suppaftp)
|
|
||||||
- [textwrap](https://github.com/mgeisler/textwrap)
|
- [textwrap](https://github.com/mgeisler/textwrap)
|
||||||
- [tui-rs](https://github.com/fdehau/tui-rs)
|
- [tui-rs](https://github.com/fdehau/tui-rs)
|
||||||
- [tui-realm](https://github.com/veeso/tui-realm)
|
- [tui-realm](https://github.com/veeso/tui-realm)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
Document audience: developers
|
Document audience: developers
|
||||||
|
|
||||||
- [Developer Manual](#developer-manual)
|
- [Developer Manual](#developer-manual)
|
||||||
- [How to test](#how-to-test)
|
|
||||||
- [How termscp works](#how-termscp-works)
|
- [How termscp works](#how-termscp-works)
|
||||||
- [Activities](#activities)
|
- [Activities](#activities)
|
||||||
- [The Context](#the-context)
|
- [The Context](#the-context)
|
||||||
@@ -11,15 +10,6 @@ Document audience: developers
|
|||||||
Welcome to the developer manual for termscp. This chapter DOESN'T contain the documentation for termscp modules, which can instead be found on Rust Docs at <https://docs.rs/termscp>
|
Welcome to the developer manual for termscp. This chapter DOESN'T contain the documentation for termscp modules, which can instead be found on Rust Docs at <https://docs.rs/termscp>
|
||||||
This chapter describes how termscp works and the guide lines to implement stuff such as file transfers and add features to the user interface.
|
This chapter describes how termscp works and the guide lines to implement stuff such as file transfers and add features to the user interface.
|
||||||
|
|
||||||
## How to test
|
|
||||||
|
|
||||||
First an introduction to tests.
|
|
||||||
|
|
||||||
Usually it's enough to run `cargo test`, but please note that whenever you're working on file transfer you'll need one more step.
|
|
||||||
In order to run tests with file transfers, you need to start the file transfer server containers, which can be started via `docker`.
|
|
||||||
|
|
||||||
To run all tests with file transfers just run: `./tests/test.sh`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## How termscp works
|
## How termscp works
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// Deps
|
// Deps
|
||||||
use crate::filetransfer::{FileTransferParams, FileTransferProtocol};
|
use crate::filetransfer::FileTransferParams;
|
||||||
use crate::host::{HostError, Localhost};
|
use crate::host::{HostError, Localhost};
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
@@ -192,7 +192,6 @@ impl ActivityManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Prepare activity
|
// Prepare activity
|
||||||
let protocol: FileTransferProtocol = ft_params.protocol;
|
|
||||||
let host: Localhost = match Localhost::new(self.local_dir.clone()) {
|
let host: Localhost = match Localhost::new(self.local_dir.clone()) {
|
||||||
Ok(host) => host,
|
Ok(host) => host,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -203,7 +202,7 @@ impl ActivityManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut activity: FileTransferActivity =
|
let mut activity: FileTransferActivity =
|
||||||
FileTransferActivity::new(host, protocol, self.ticks);
|
FileTransferActivity::new(host, ft_params, self.ticks);
|
||||||
// Prepare result
|
// Prepare result
|
||||||
let result: Option<NextActivity>;
|
let result: Option<NextActivity>;
|
||||||
// Create activity
|
// Create activity
|
||||||
|
|||||||
+1
-10
@@ -36,7 +36,7 @@ use std::str::FromStr;
|
|||||||
///
|
///
|
||||||
/// UserHosts contains all the hosts saved by the user in the data storage
|
/// UserHosts contains all the hosts saved by the user in the data storage
|
||||||
/// It contains both `Bookmark`
|
/// It contains both `Bookmark`
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||||
pub struct UserHosts {
|
pub struct UserHosts {
|
||||||
pub bookmarks: HashMap<String, Bookmark>,
|
pub bookmarks: HashMap<String, Bookmark>,
|
||||||
pub recents: HashMap<String, Bookmark>,
|
pub recents: HashMap<String, Bookmark>,
|
||||||
@@ -76,15 +76,6 @@ pub struct S3Params {
|
|||||||
|
|
||||||
// -- impls
|
// -- impls
|
||||||
|
|
||||||
impl Default for UserHosts {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
bookmarks: HashMap::new(),
|
|
||||||
recents: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<FileTransferParams> for Bookmark {
|
impl From<FileTransferParams> for Bookmark {
|
||||||
fn from(params: FileTransferParams) -> Self {
|
fn from(params: FileTransferParams) -> Self {
|
||||||
let protocol: FileTransferProtocol = params.protocol;
|
let protocol: FileTransferProtocol = params.protocol;
|
||||||
|
|||||||
+3
-20
@@ -35,7 +35,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
pub const DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD: u64 = 536870912; // 512MB
|
pub const DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD: u64 = 536870912; // 512MB
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||||
/// ## UserConfig
|
/// ## UserConfig
|
||||||
///
|
///
|
||||||
/// UserConfig contains all the configurations for the user,
|
/// UserConfig contains all the configurations for the user,
|
||||||
@@ -45,7 +45,7 @@ pub struct UserConfig {
|
|||||||
pub remote: RemoteConfig,
|
pub remote: RemoteConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
/// ## UserInterfaceConfig
|
/// ## UserInterfaceConfig
|
||||||
///
|
///
|
||||||
/// UserInterfaceConfig provides all the keys to configure the user interface
|
/// UserInterfaceConfig provides all the keys to configure the user interface
|
||||||
@@ -62,7 +62,7 @@ pub struct UserInterfaceConfig {
|
|||||||
pub notification_threshold: Option<u64>, // @! Since 0.7.0; Default 512MB
|
pub notification_threshold: Option<u64>, // @! Since 0.7.0; Default 512MB
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, std::fmt::Debug)]
|
#[derive(Deserialize, Serialize, Debug, Default)]
|
||||||
/// ## RemoteConfig
|
/// ## RemoteConfig
|
||||||
///
|
///
|
||||||
/// Contains configuratio related to remote hosts
|
/// Contains configuratio related to remote hosts
|
||||||
@@ -70,15 +70,6 @@ pub struct RemoteConfig {
|
|||||||
pub ssh_keys: HashMap<String, PathBuf>, // Association between host name and path to private key
|
pub ssh_keys: HashMap<String, PathBuf>, // Association between host name and path to private key
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UserConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
UserConfig {
|
|
||||||
user_interface: UserInterfaceConfig::default(),
|
|
||||||
remote: RemoteConfig::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for UserInterfaceConfig {
|
impl Default for UserInterfaceConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
UserInterfaceConfig {
|
UserInterfaceConfig {
|
||||||
@@ -99,14 +90,6 @@ impl Default for UserInterfaceConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RemoteConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
RemoteConfig {
|
|
||||||
ssh_keys: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests
|
// Tests
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -26,18 +26,18 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// Locals
|
// Locals
|
||||||
use super::FsEntry;
|
|
||||||
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
|
use crate::utils::fmt::{fmt_path_elide, fmt_pex, fmt_time};
|
||||||
use crate::utils::path::diff_paths;
|
use crate::utils::path::diff_paths;
|
||||||
// Ext
|
// Ext
|
||||||
use bytesize::ByteSize;
|
use bytesize::ByteSize;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use remotefs::Entry;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
use users::{get_group_by_gid, get_user_by_uid};
|
use users::{get_group_by_gid, get_user_by_uid};
|
||||||
// Types
|
// Types
|
||||||
// FmtCallback: Formatter, fsentry: &FsEntry, cur_str, prefix, length, extra
|
// FmtCallback: Formatter, fsentry: &Entry, cur_str, prefix, length, extra
|
||||||
type FmtCallback = fn(&Formatter, &FsEntry, &str, &str, Option<&usize>, Option<&String>) -> String;
|
type FmtCallback = fn(&Formatter, &Entry, &str, &str, Option<&usize>, Option<&String>) -> String;
|
||||||
|
|
||||||
// Keys
|
// Keys
|
||||||
const FMT_KEY_ATIME: &str = "ATIME";
|
const FMT_KEY_ATIME: &str = "ATIME";
|
||||||
@@ -66,7 +66,7 @@ lazy_static! {
|
|||||||
|
|
||||||
/// ## CallChainBlock
|
/// ## CallChainBlock
|
||||||
///
|
///
|
||||||
/// Call Chain block is a block in a chain of functions which are called in order to format the FsEntry.
|
/// Call Chain block is a block in a chain of functions which are called in order to format the Entry.
|
||||||
/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found
|
/// A callChain is instantiated starting from the Formatter syntax and the regex, once the groups are found
|
||||||
/// a chain of function is made using the Formatters method.
|
/// a chain of function is made using the Formatters method.
|
||||||
/// This method provides an extremely fast way to format fs entries
|
/// This method provides an extremely fast way to format fs entries
|
||||||
@@ -105,7 +105,7 @@ impl CallChainBlock {
|
|||||||
/// ### next
|
/// ### next
|
||||||
///
|
///
|
||||||
/// Call next callback in the CallChain
|
/// Call next callback in the CallChain
|
||||||
pub fn next(&self, fmt: &Formatter, fsentry: &FsEntry, cur_str: &str) -> String {
|
pub fn next(&self, fmt: &Formatter, fsentry: &Entry, cur_str: &str) -> String {
|
||||||
// Call func
|
// Call func
|
||||||
let new_str: String = (self.func)(
|
let new_str: String = (self.func)(
|
||||||
fmt,
|
fmt,
|
||||||
@@ -177,7 +177,7 @@ impl Formatter {
|
|||||||
/// ### fmt
|
/// ### fmt
|
||||||
///
|
///
|
||||||
/// Format fsentry
|
/// Format fsentry
|
||||||
pub fn fmt(&self, fsentry: &FsEntry) -> String {
|
pub fn fmt(&self, fsentry: &Entry) -> String {
|
||||||
// Execute callchain blocks
|
// Execute callchain blocks
|
||||||
self.call_chain.next(self, fsentry, "")
|
self.call_chain.next(self, fsentry, "")
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ impl Formatter {
|
|||||||
/// Format last access time
|
/// Format last access time
|
||||||
fn fmt_atime(
|
fn fmt_atime(
|
||||||
&self,
|
&self,
|
||||||
fsentry: &FsEntry,
|
fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
fmt_len: Option<&usize>,
|
fmt_len: Option<&usize>,
|
||||||
@@ -197,7 +197,7 @@ impl Formatter {
|
|||||||
) -> String {
|
) -> String {
|
||||||
// Get date (use extra args as format or default "%b %d %Y %H:%M")
|
// Get date (use extra args as format or default "%b %d %Y %H:%M")
|
||||||
let datetime: String = fmt_time(
|
let datetime: String = fmt_time(
|
||||||
fsentry.get_last_access_time(),
|
fsentry.metadata().atime,
|
||||||
match fmt_extra {
|
match fmt_extra {
|
||||||
Some(fmt) => fmt.as_ref(),
|
Some(fmt) => fmt.as_ref(),
|
||||||
None => "%b %d %Y %H:%M",
|
None => "%b %d %Y %H:%M",
|
||||||
@@ -218,7 +218,7 @@ impl Formatter {
|
|||||||
/// Format creation time
|
/// Format creation time
|
||||||
fn fmt_ctime(
|
fn fmt_ctime(
|
||||||
&self,
|
&self,
|
||||||
fsentry: &FsEntry,
|
fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
fmt_len: Option<&usize>,
|
fmt_len: Option<&usize>,
|
||||||
@@ -226,7 +226,7 @@ impl Formatter {
|
|||||||
) -> String {
|
) -> String {
|
||||||
// Get date
|
// Get date
|
||||||
let datetime: String = fmt_time(
|
let datetime: String = fmt_time(
|
||||||
fsentry.get_creation_time(),
|
fsentry.metadata().ctime,
|
||||||
match fmt_extra {
|
match fmt_extra {
|
||||||
Some(fmt) => fmt.as_ref(),
|
Some(fmt) => fmt.as_ref(),
|
||||||
None => "%b %d %Y %H:%M",
|
None => "%b %d %Y %H:%M",
|
||||||
@@ -247,7 +247,7 @@ impl Formatter {
|
|||||||
/// Format owner group
|
/// Format owner group
|
||||||
fn fmt_group(
|
fn fmt_group(
|
||||||
&self,
|
&self,
|
||||||
fsentry: &FsEntry,
|
fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
fmt_len: Option<&usize>,
|
fmt_len: Option<&usize>,
|
||||||
@@ -255,7 +255,7 @@ impl Formatter {
|
|||||||
) -> String {
|
) -> String {
|
||||||
// Get username
|
// Get username
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
let group: String = match fsentry.get_group() {
|
let group: String = match fsentry.metadata().gid {
|
||||||
Some(gid) => match get_group_by_gid(gid) {
|
Some(gid) => match get_group_by_gid(gid) {
|
||||||
Some(user) => user.name().to_string_lossy().to_string(),
|
Some(user) => user.name().to_string_lossy().to_string(),
|
||||||
None => gid.to_string(),
|
None => gid.to_string(),
|
||||||
@@ -263,7 +263,7 @@ impl Formatter {
|
|||||||
None => 0.to_string(),
|
None => 0.to_string(),
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let group: String = match fsentry.get_group() {
|
let group: String = match fsentry.metadata().gid {
|
||||||
Some(gid) => gid.to_string(),
|
Some(gid) => gid.to_string(),
|
||||||
None => 0.to_string(),
|
None => 0.to_string(),
|
||||||
};
|
};
|
||||||
@@ -282,7 +282,7 @@ impl Formatter {
|
|||||||
/// Format last change time
|
/// Format last change time
|
||||||
fn fmt_mtime(
|
fn fmt_mtime(
|
||||||
&self,
|
&self,
|
||||||
fsentry: &FsEntry,
|
fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
fmt_len: Option<&usize>,
|
fmt_len: Option<&usize>,
|
||||||
@@ -290,7 +290,7 @@ impl Formatter {
|
|||||||
) -> String {
|
) -> String {
|
||||||
// Get date
|
// Get date
|
||||||
let datetime: String = fmt_time(
|
let datetime: String = fmt_time(
|
||||||
fsentry.get_last_change_time(),
|
fsentry.metadata().mtime,
|
||||||
match fmt_extra {
|
match fmt_extra {
|
||||||
Some(fmt) => fmt.as_ref(),
|
Some(fmt) => fmt.as_ref(),
|
||||||
None => "%b %d %Y %H:%M",
|
None => "%b %d %Y %H:%M",
|
||||||
@@ -311,7 +311,7 @@ impl Formatter {
|
|||||||
/// Format file name
|
/// Format file name
|
||||||
fn fmt_name(
|
fn fmt_name(
|
||||||
&self,
|
&self,
|
||||||
fsentry: &FsEntry,
|
fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
fmt_len: Option<&usize>,
|
fmt_len: Option<&usize>,
|
||||||
@@ -322,7 +322,7 @@ impl Formatter {
|
|||||||
Some(l) => *l,
|
Some(l) => *l,
|
||||||
None => 24,
|
None => 24,
|
||||||
};
|
};
|
||||||
let name: &str = fsentry.get_name();
|
let name: &str = fsentry.name();
|
||||||
let last_idx: usize = match fsentry.is_dir() {
|
let last_idx: usize = match fsentry.is_dir() {
|
||||||
// NOTE: For directories is l - 2, since we push '/' to name
|
// NOTE: For directories is l - 2, since we push '/' to name
|
||||||
true => file_len - 2,
|
true => file_len - 2,
|
||||||
@@ -344,19 +344,16 @@ impl Formatter {
|
|||||||
/// Format path
|
/// Format path
|
||||||
fn fmt_path(
|
fn fmt_path(
|
||||||
&self,
|
&self,
|
||||||
fsentry: &FsEntry,
|
fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
fmt_len: Option<&usize>,
|
fmt_len: Option<&usize>,
|
||||||
fmt_extra: Option<&String>,
|
fmt_extra: Option<&String>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let p = match fmt_extra {
|
let p = match fmt_extra {
|
||||||
None => fsentry.get_abs_path(),
|
None => fsentry.path().to_path_buf(),
|
||||||
Some(rel) => diff_paths(
|
Some(rel) => diff_paths(fsentry.path(), PathBuf::from(rel.as_str()).as_path())
|
||||||
fsentry.get_abs_path().as_path(),
|
.unwrap_or_else(|| fsentry.path().to_path_buf()),
|
||||||
PathBuf::from(rel.as_str()).as_path(),
|
|
||||||
)
|
|
||||||
.unwrap_or_else(|| fsentry.get_abs_path()),
|
|
||||||
};
|
};
|
||||||
format!(
|
format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
@@ -374,7 +371,7 @@ impl Formatter {
|
|||||||
/// Format file permissions
|
/// Format file permissions
|
||||||
fn fmt_pex(
|
fn fmt_pex(
|
||||||
&self,
|
&self,
|
||||||
fsentry: &FsEntry,
|
fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
_fmt_len: Option<&usize>,
|
_fmt_len: Option<&usize>,
|
||||||
@@ -382,7 +379,7 @@ impl Formatter {
|
|||||||
) -> String {
|
) -> String {
|
||||||
// Create mode string
|
// Create mode string
|
||||||
let mut pex: String = String::with_capacity(10);
|
let mut pex: String = String::with_capacity(10);
|
||||||
let file_type: char = match fsentry.is_symlink() {
|
let file_type: char = match fsentry.metadata().symlink.is_some() {
|
||||||
true => 'l',
|
true => 'l',
|
||||||
false => match fsentry.is_dir() {
|
false => match fsentry.is_dir() {
|
||||||
true => 'd',
|
true => 'd',
|
||||||
@@ -390,10 +387,16 @@ impl Formatter {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
pex.push(file_type);
|
pex.push(file_type);
|
||||||
match fsentry.get_unix_pex() {
|
match fsentry.metadata().mode {
|
||||||
None => pex.push_str("?????????"),
|
None => pex.push_str("?????????"),
|
||||||
Some((owner, group, others)) => pex.push_str(
|
Some(mode) => pex.push_str(
|
||||||
format!("{}{}{}", fmt_pex(owner), fmt_pex(group), fmt_pex(others)).as_str(),
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
fmt_pex(mode.user()),
|
||||||
|
fmt_pex(mode.group()),
|
||||||
|
fmt_pex(mode.others())
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
// Add to cur str, prefix and the key value
|
// Add to cur str, prefix and the key value
|
||||||
@@ -405,7 +408,7 @@ impl Formatter {
|
|||||||
/// Format file size
|
/// Format file size
|
||||||
fn fmt_size(
|
fn fmt_size(
|
||||||
&self,
|
&self,
|
||||||
fsentry: &FsEntry,
|
fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
_fmt_len: Option<&usize>,
|
_fmt_len: Option<&usize>,
|
||||||
@@ -413,7 +416,7 @@ impl Formatter {
|
|||||||
) -> String {
|
) -> String {
|
||||||
if fsentry.is_file() {
|
if fsentry.is_file() {
|
||||||
// Get byte size
|
// Get byte size
|
||||||
let size: ByteSize = ByteSize(fsentry.get_size() as u64);
|
let size: ByteSize = ByteSize(fsentry.metadata().size);
|
||||||
// Add to cur str, prefix and the key value
|
// Add to cur str, prefix and the key value
|
||||||
format!("{}{}{:10}", cur_str, prefix, size.to_string())
|
format!("{}{}{:10}", cur_str, prefix, size.to_string())
|
||||||
} else {
|
} else {
|
||||||
@@ -427,7 +430,7 @@ impl Formatter {
|
|||||||
/// Format file symlink (if any)
|
/// Format file symlink (if any)
|
||||||
fn fmt_symlink(
|
fn fmt_symlink(
|
||||||
&self,
|
&self,
|
||||||
fsentry: &FsEntry,
|
fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
fmt_len: Option<&usize>,
|
fmt_len: Option<&usize>,
|
||||||
@@ -439,16 +442,13 @@ impl Formatter {
|
|||||||
None => 21,
|
None => 21,
|
||||||
};
|
};
|
||||||
// Replace `FMT_KEY_NAME` with name
|
// Replace `FMT_KEY_NAME` with name
|
||||||
match fsentry.is_symlink() {
|
match fsentry.metadata().symlink.as_deref() {
|
||||||
false => format!("{}{} ", cur_str, prefix),
|
None => format!("{}{} ", cur_str, prefix),
|
||||||
true => format!(
|
Some(p) => format!(
|
||||||
"{}{}-> {:0width$}",
|
"{}{}-> {:0width$}",
|
||||||
cur_str,
|
cur_str,
|
||||||
prefix,
|
prefix,
|
||||||
fmt_path_elide(
|
fmt_path_elide(p, file_len - 1),
|
||||||
fsentry.get_realfile().get_abs_path().as_path(),
|
|
||||||
file_len - 1
|
|
||||||
),
|
|
||||||
width = file_len
|
width = file_len
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
@@ -459,7 +459,7 @@ impl Formatter {
|
|||||||
/// Format owner user
|
/// Format owner user
|
||||||
fn fmt_user(
|
fn fmt_user(
|
||||||
&self,
|
&self,
|
||||||
fsentry: &FsEntry,
|
fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
_fmt_len: Option<&usize>,
|
_fmt_len: Option<&usize>,
|
||||||
@@ -467,7 +467,7 @@ impl Formatter {
|
|||||||
) -> String {
|
) -> String {
|
||||||
// Get username
|
// Get username
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
let username: String = match fsentry.get_user() {
|
let username: String = match fsentry.metadata().uid {
|
||||||
Some(uid) => match get_user_by_uid(uid) {
|
Some(uid) => match get_user_by_uid(uid) {
|
||||||
Some(user) => user.name().to_string_lossy().to_string(),
|
Some(user) => user.name().to_string_lossy().to_string(),
|
||||||
None => uid.to_string(),
|
None => uid.to_string(),
|
||||||
@@ -475,7 +475,7 @@ impl Formatter {
|
|||||||
None => 0.to_string(),
|
None => 0.to_string(),
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let username: String = match fsentry.get_user() {
|
let username: String = match fsentry.metadata().uid {
|
||||||
Some(uid) => uid.to_string(),
|
Some(uid) => uid.to_string(),
|
||||||
None => 0.to_string(),
|
None => 0.to_string(),
|
||||||
};
|
};
|
||||||
@@ -489,7 +489,7 @@ impl Formatter {
|
|||||||
/// It does nothing, just returns cur_str
|
/// It does nothing, just returns cur_str
|
||||||
fn fmt_fallback(
|
fn fmt_fallback(
|
||||||
&self,
|
&self,
|
||||||
_fsentry: &FsEntry,
|
_fsentry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
_fmt_len: Option<&usize>,
|
_fmt_len: Option<&usize>,
|
||||||
@@ -574,9 +574,9 @@ impl Formatter {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::fs::{FsDirectory, FsFile, UnixPex};
|
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use remotefs::fs::{Directory, File, Metadata, UnixPex};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
|
|
||||||
@@ -585,19 +585,21 @@ mod tests {
|
|||||||
// Make a dummy formatter
|
// Make a dummy formatter
|
||||||
let dummy_formatter: Formatter = Formatter::new("");
|
let dummy_formatter: Formatter = Formatter::new("");
|
||||||
// Make a dummy entry
|
// Make a dummy entry
|
||||||
let t_now: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
let dummy_entry: FsEntry = FsEntry::File(FsFile {
|
let dummy_entry: Entry = Entry::File(File {
|
||||||
name: String::from("bar.txt"),
|
name: String::from("bar.txt"),
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
path: PathBuf::from("/bar.txt"),
|
||||||
last_change_time: t_now,
|
extension: Some(String::from("txt")),
|
||||||
last_access_time: t_now,
|
metadata: Metadata {
|
||||||
creation_time: t_now,
|
atime: t,
|
||||||
|
ctime: t,
|
||||||
|
mtime: t,
|
||||||
size: 8192,
|
size: 8192,
|
||||||
ftype: Some(String::from("txt")),
|
symlink: None,
|
||||||
symlink: None, // UNIX only
|
uid: Some(0),
|
||||||
user: Some(0), // UNIX only
|
gid: Some(0),
|
||||||
group: Some(0), // UNIX only
|
mode: Some(UnixPex::from(0o644)),
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
},
|
||||||
});
|
});
|
||||||
let prefix: String = String::from("h");
|
let prefix: String = String::from("h");
|
||||||
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
|
let mut callchain: CallChainBlock = CallChainBlock::new(dummy_fmt, prefix, None, None);
|
||||||
@@ -626,18 +628,20 @@ mod tests {
|
|||||||
let formatter: Formatter = Formatter::default();
|
let formatter: Formatter = Formatter::default();
|
||||||
// Experiments :D
|
// Experiments :D
|
||||||
let t: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
let entry: Entry = Entry::File(File {
|
||||||
name: String::from("bar.txt"),
|
name: String::from("bar.txt"),
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
path: PathBuf::from("/bar.txt"),
|
||||||
last_change_time: t,
|
extension: Some(String::from("txt")),
|
||||||
last_access_time: t,
|
metadata: Metadata {
|
||||||
creation_time: t,
|
atime: t,
|
||||||
|
ctime: t,
|
||||||
|
mtime: t,
|
||||||
size: 8192,
|
size: 8192,
|
||||||
ftype: Some(String::from("txt")),
|
symlink: None,
|
||||||
symlink: None, // UNIX only
|
uid: Some(0),
|
||||||
user: Some(0), // UNIX only
|
gid: Some(0),
|
||||||
group: Some(0), // UNIX only
|
mode: Some(UnixPex::from(0o644)),
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
},
|
||||||
});
|
});
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -656,18 +660,20 @@ mod tests {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
// Elide name
|
// Elide name
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
let entry: Entry = Entry::File(File {
|
||||||
name: String::from("piroparoporoperoperupupu.txt"),
|
name: String::from("piroparoporoperoperupupu.txt"),
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
path: PathBuf::from("/bar.txt"),
|
||||||
last_change_time: t,
|
extension: Some(String::from("txt")),
|
||||||
last_access_time: t,
|
metadata: Metadata {
|
||||||
creation_time: t,
|
atime: t,
|
||||||
|
ctime: t,
|
||||||
|
mtime: t,
|
||||||
size: 8192,
|
size: 8192,
|
||||||
ftype: Some(String::from("txt")),
|
symlink: None,
|
||||||
symlink: None, // UNIX only
|
uid: Some(0),
|
||||||
user: Some(0), // UNIX only
|
gid: Some(0),
|
||||||
group: Some(0), // UNIX only
|
mode: Some(UnixPex::from(0o644)),
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
},
|
||||||
});
|
});
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -686,18 +692,20 @@ mod tests {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
// No pex
|
// No pex
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
let entry: Entry = Entry::File(File {
|
||||||
name: String::from("bar.txt"),
|
name: String::from("bar.txt"),
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
path: PathBuf::from("/bar.txt"),
|
||||||
last_change_time: t,
|
extension: Some(String::from("txt")),
|
||||||
last_access_time: t,
|
metadata: Metadata {
|
||||||
creation_time: t,
|
atime: t,
|
||||||
|
ctime: t,
|
||||||
|
mtime: t,
|
||||||
size: 8192,
|
size: 8192,
|
||||||
ftype: Some(String::from("txt")),
|
symlink: None,
|
||||||
symlink: None, // UNIX only
|
uid: Some(0),
|
||||||
user: Some(0), // UNIX only
|
gid: Some(0),
|
||||||
group: Some(0), // UNIX only
|
mode: None,
|
||||||
unix_pex: None, // UNIX only
|
},
|
||||||
});
|
});
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -716,18 +724,20 @@ mod tests {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
// No user
|
// No user
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
let entry: Entry = Entry::File(File {
|
||||||
name: String::from("bar.txt"),
|
name: String::from("bar.txt"),
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
path: PathBuf::from("/bar.txt"),
|
||||||
last_change_time: t,
|
extension: Some(String::from("txt")),
|
||||||
last_access_time: t,
|
metadata: Metadata {
|
||||||
creation_time: t,
|
atime: t,
|
||||||
|
ctime: t,
|
||||||
|
mtime: t,
|
||||||
size: 8192,
|
size: 8192,
|
||||||
ftype: Some(String::from("txt")),
|
symlink: None,
|
||||||
symlink: None, // UNIX only
|
uid: None,
|
||||||
user: None, // UNIX only
|
gid: Some(0),
|
||||||
group: Some(0), // UNIX only
|
mode: None,
|
||||||
unix_pex: None, // UNIX only
|
},
|
||||||
});
|
});
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -752,24 +762,27 @@ mod tests {
|
|||||||
// Make default
|
// Make default
|
||||||
let formatter: Formatter = Formatter::default();
|
let formatter: Formatter = Formatter::default();
|
||||||
// Experiments :D
|
// Experiments :D
|
||||||
let t_now: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
let entry: Entry = Entry::Directory(Directory {
|
||||||
name: String::from("projects"),
|
name: String::from("projects"),
|
||||||
abs_path: PathBuf::from("/home/cvisintin/projects"),
|
path: PathBuf::from("/home/cvisintin/projects"),
|
||||||
last_change_time: t_now,
|
metadata: Metadata {
|
||||||
last_access_time: t_now,
|
atime: t,
|
||||||
creation_time: t_now,
|
ctime: t,
|
||||||
symlink: None, // UNIX only
|
mtime: t,
|
||||||
user: Some(0), // UNIX only
|
size: 4096,
|
||||||
group: Some(0), // UNIX only
|
symlink: None,
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
|
uid: Some(0),
|
||||||
|
gid: Some(0),
|
||||||
|
mode: Some(UnixPex::from(0o755)),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
"projects/ drwxr-xr-x root {}",
|
"projects/ drwxr-xr-x root {}",
|
||||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -777,27 +790,30 @@ mod tests {
|
|||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
"projects/ drwxr-xr-x 0 {}",
|
"projects/ drwxr-xr-x 0 {}",
|
||||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
// No pex, no user
|
// No pex, no user
|
||||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
let entry: Entry = Entry::Directory(Directory {
|
||||||
name: String::from("projects"),
|
name: String::from("projects"),
|
||||||
abs_path: PathBuf::from("/home/cvisintin/projects"),
|
path: PathBuf::from("/home/cvisintin/projects"),
|
||||||
last_change_time: t_now,
|
metadata: Metadata {
|
||||||
last_access_time: t_now,
|
atime: t,
|
||||||
creation_time: t_now,
|
ctime: t,
|
||||||
symlink: None, // UNIX only
|
mtime: t,
|
||||||
user: None, // UNIX only
|
size: 4096,
|
||||||
group: Some(0), // UNIX only
|
symlink: None,
|
||||||
unix_pex: None, // UNIX only
|
uid: None,
|
||||||
|
gid: Some(0),
|
||||||
|
mode: None,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
"projects/ d????????? 0 {}",
|
"projects/ d????????? 0 {}",
|
||||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -805,7 +821,7 @@ mod tests {
|
|||||||
formatter.fmt(&entry),
|
formatter.fmt(&entry),
|
||||||
format!(
|
format!(
|
||||||
"projects/ d????????? 0 {}",
|
"projects/ d????????? 0 {}",
|
||||||
fmt_time(t_now, "%b %d %Y %H:%M")
|
fmt_time(t, "%b %d %Y %H:%M")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -816,29 +832,19 @@ mod tests {
|
|||||||
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
|
Formatter::new("{NAME:16} {SYMLINK:12} {GROUP} {USER} {PEX} {SIZE} {ATIME:20:%a %b %d %Y %H:%M} {CTIME:20:%a %b %d %Y %H:%M} {MTIME:20:%a %b %d %Y %H:%M}");
|
||||||
// Directory (with symlink)
|
// Directory (with symlink)
|
||||||
let t: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
let pointer: FsEntry = FsEntry::File(FsFile {
|
let entry: Entry = Entry::Directory(Directory {
|
||||||
name: String::from("project.info"),
|
|
||||||
abs_path: PathBuf::from("/project.info"),
|
|
||||||
last_change_time: t,
|
|
||||||
last_access_time: t,
|
|
||||||
creation_time: t,
|
|
||||||
size: 8192,
|
|
||||||
ftype: Some(String::from("txt")),
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: None, // UNIX only
|
|
||||||
group: None, // UNIX only
|
|
||||||
unix_pex: None, // UNIX only
|
|
||||||
});
|
|
||||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
|
||||||
name: String::from("projects"),
|
name: String::from("projects"),
|
||||||
abs_path: PathBuf::from("/home/cvisintin/project"),
|
path: PathBuf::from("/home/cvisintin/project"),
|
||||||
last_change_time: t,
|
metadata: Metadata {
|
||||||
last_access_time: t,
|
atime: t,
|
||||||
creation_time: t,
|
ctime: t,
|
||||||
symlink: Some(Box::new(pointer)), // UNIX only
|
mtime: t,
|
||||||
user: None, // UNIX only
|
size: 4096,
|
||||||
group: None, // UNIX only
|
symlink: Some(PathBuf::from("project.info")),
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
|
uid: None,
|
||||||
|
gid: None,
|
||||||
|
mode: Some(UnixPex::from(0o755)),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
assert_eq!(formatter.fmt(&entry), format!(
|
assert_eq!(formatter.fmt(&entry), format!(
|
||||||
"projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}",
|
"projects/ -> project.info 0 0 lrwxr-xr-x {} {} {}",
|
||||||
@@ -847,16 +853,19 @@ mod tests {
|
|||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||||
));
|
));
|
||||||
// Directory without symlink
|
// Directory without symlink
|
||||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
let entry: Entry = Entry::Directory(Directory {
|
||||||
name: String::from("projects"),
|
name: String::from("projects"),
|
||||||
abs_path: PathBuf::from("/home/cvisintin/project"),
|
path: PathBuf::from("/home/cvisintin/project"),
|
||||||
last_change_time: t,
|
metadata: Metadata {
|
||||||
last_access_time: t,
|
atime: t,
|
||||||
creation_time: t,
|
ctime: t,
|
||||||
symlink: None, // UNIX only
|
mtime: t,
|
||||||
user: None, // UNIX only
|
size: 4096,
|
||||||
group: None, // UNIX only
|
symlink: None,
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
|
uid: None,
|
||||||
|
gid: None,
|
||||||
|
mode: Some(UnixPex::from(0o755)),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
assert_eq!(formatter.fmt(&entry), format!(
|
assert_eq!(formatter.fmt(&entry), format!(
|
||||||
"projects/ 0 0 drwxr-xr-x {} {} {}",
|
"projects/ 0 0 drwxr-xr-x {} {} {}",
|
||||||
@@ -865,31 +874,20 @@ mod tests {
|
|||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||||
));
|
));
|
||||||
// File with symlink
|
// File with symlink
|
||||||
let pointer: FsEntry = FsEntry::File(FsFile {
|
let entry: Entry = Entry::File(File {
|
||||||
name: String::from("project.info"),
|
|
||||||
abs_path: PathBuf::from("/project.info"),
|
|
||||||
last_change_time: t,
|
|
||||||
last_access_time: t,
|
|
||||||
creation_time: t,
|
|
||||||
size: 8192,
|
|
||||||
ftype: Some(String::from("txt")),
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: None, // UNIX only
|
|
||||||
group: None, // UNIX only
|
|
||||||
unix_pex: None, // UNIX only
|
|
||||||
});
|
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
|
||||||
name: String::from("bar.txt"),
|
name: String::from("bar.txt"),
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
path: PathBuf::from("/bar.txt"),
|
||||||
last_change_time: t,
|
extension: Some(String::from("txt")),
|
||||||
last_access_time: t,
|
metadata: Metadata {
|
||||||
creation_time: t,
|
atime: t,
|
||||||
|
ctime: t,
|
||||||
|
mtime: t,
|
||||||
size: 8192,
|
size: 8192,
|
||||||
ftype: Some(String::from("txt")),
|
symlink: Some(PathBuf::from("project.info")),
|
||||||
symlink: Some(Box::new(pointer)), // UNIX only
|
uid: None,
|
||||||
user: None, // UNIX only
|
gid: None,
|
||||||
group: None, // UNIX only
|
mode: Some(UnixPex::from(0o644)),
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
},
|
||||||
});
|
});
|
||||||
assert_eq!(formatter.fmt(&entry), format!(
|
assert_eq!(formatter.fmt(&entry), format!(
|
||||||
"bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
|
"bar.txt -> project.info 0 0 lrw-r--r-- 8.2 KB {} {} {}",
|
||||||
@@ -898,18 +896,20 @@ mod tests {
|
|||||||
fmt_time(t, "%a %b %d %Y %H:%M"),
|
fmt_time(t, "%a %b %d %Y %H:%M"),
|
||||||
));
|
));
|
||||||
// File without symlink
|
// File without symlink
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
let entry: Entry = Entry::File(File {
|
||||||
name: String::from("bar.txt"),
|
name: String::from("bar.txt"),
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
path: PathBuf::from("/bar.txt"),
|
||||||
last_change_time: t,
|
extension: Some(String::from("txt")),
|
||||||
last_access_time: t,
|
metadata: Metadata {
|
||||||
creation_time: t,
|
atime: t,
|
||||||
|
ctime: t,
|
||||||
|
mtime: t,
|
||||||
size: 8192,
|
size: 8192,
|
||||||
ftype: Some(String::from("txt")),
|
symlink: None,
|
||||||
symlink: None, // UNIX only
|
uid: None,
|
||||||
user: None, // UNIX only
|
gid: None,
|
||||||
group: None, // UNIX only
|
mode: Some(UnixPex::from(0o644)),
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
},
|
||||||
});
|
});
|
||||||
assert_eq!(formatter.fmt(&entry), format!(
|
assert_eq!(formatter.fmt(&entry), format!(
|
||||||
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
|
"bar.txt 0 0 -rw-r--r-- 8.2 KB {} {} {}",
|
||||||
@@ -923,18 +923,20 @@ mod tests {
|
|||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
fn should_fmt_path() {
|
fn should_fmt_path() {
|
||||||
let t: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
let entry: Entry = Entry::File(File {
|
||||||
name: String::from("bar.txt"),
|
name: String::from("bar.txt"),
|
||||||
abs_path: PathBuf::from("/tmp/a/b/c/bar.txt"),
|
path: PathBuf::from("/tmp/a/b/c/bar.txt"),
|
||||||
last_change_time: t,
|
extension: Some(String::from("txt")),
|
||||||
last_access_time: t,
|
metadata: Metadata {
|
||||||
creation_time: t,
|
atime: t,
|
||||||
|
ctime: t,
|
||||||
|
mtime: t,
|
||||||
size: 8192,
|
size: 8192,
|
||||||
ftype: Some(String::from("txt")),
|
symlink: Some(PathBuf::from("project.info")),
|
||||||
symlink: None, // UNIX only
|
uid: None,
|
||||||
user: None, // UNIX only
|
gid: None,
|
||||||
group: None, // UNIX only
|
mode: Some(UnixPex::from(0o644)),
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
},
|
||||||
});
|
});
|
||||||
let formatter: Formatter = Formatter::new("File path: {PATH}");
|
let formatter: Formatter = Formatter::new("File path: {PATH}");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -955,7 +957,7 @@ mod tests {
|
|||||||
/// Dummy formatter, just yelds an 'A' at the end of the current string
|
/// Dummy formatter, just yelds an 'A' at the end of the current string
|
||||||
fn dummy_fmt(
|
fn dummy_fmt(
|
||||||
_fmt: &Formatter,
|
_fmt: &Formatter,
|
||||||
_entry: &FsEntry,
|
_entry: &Entry,
|
||||||
cur_str: &str,
|
cur_str: &str,
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
_fmt_len: Option<&usize>,
|
_fmt_len: Option<&usize>,
|
||||||
@@ -29,9 +29,9 @@
|
|||||||
pub(crate) mod builder;
|
pub(crate) mod builder;
|
||||||
mod formatter;
|
mod formatter;
|
||||||
// Locals
|
// Locals
|
||||||
use super::FsEntry;
|
|
||||||
use formatter::Formatter;
|
use formatter::Formatter;
|
||||||
// Ext
|
// Ext
|
||||||
|
use remotefs::fs::Entry;
|
||||||
use std::cmp::Reverse;
|
use std::cmp::Reverse;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -77,8 +77,8 @@ pub struct FileExplorer {
|
|||||||
pub(crate) file_sorting: FileSorting, // File sorting criteria
|
pub(crate) file_sorting: FileSorting, // File sorting criteria
|
||||||
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
|
pub(crate) group_dirs: Option<GroupDirs>, // If Some, defines how to group directories
|
||||||
pub(crate) opts: ExplorerOpts, // Explorer options
|
pub(crate) opts: ExplorerOpts, // Explorer options
|
||||||
pub(crate) fmt: Formatter, // FsEntry formatter
|
pub(crate) fmt: Formatter, // Entry formatter
|
||||||
files: Vec<FsEntry>, // Files in directory
|
files: Vec<Entry>, // Files in directory
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for FileExplorer {
|
impl Default for FileExplorer {
|
||||||
@@ -121,7 +121,7 @@ impl FileExplorer {
|
|||||||
/// Set Explorer files
|
/// Set Explorer files
|
||||||
/// This method will also sort entries based on current options
|
/// This method will also sort entries based on current options
|
||||||
/// Once all sorting have been performed, index is moved to first valid entry.
|
/// Once all sorting have been performed, index is moved to first valid entry.
|
||||||
pub fn set_files(&mut self, files: Vec<FsEntry>) {
|
pub fn set_files(&mut self, files: Vec<Entry>) {
|
||||||
self.files = files;
|
self.files = files;
|
||||||
// Sort
|
// Sort
|
||||||
self.sort();
|
self.sort();
|
||||||
@@ -149,7 +149,7 @@ impl FileExplorer {
|
|||||||
///
|
///
|
||||||
/// Iterate over files
|
/// Iterate over files
|
||||||
/// Filters are applied based on current options (e.g. hidden files not returned)
|
/// Filters are applied based on current options (e.g. hidden files not returned)
|
||||||
pub fn iter_files(&self) -> impl Iterator<Item = &FsEntry> + '_ {
|
pub fn iter_files(&self) -> impl Iterator<Item = &Entry> + '_ {
|
||||||
// Filter
|
// Filter
|
||||||
let opts: ExplorerOpts = self.opts;
|
let opts: ExplorerOpts = self.opts;
|
||||||
Box::new(self.files.iter().filter(move |x| {
|
Box::new(self.files.iter().filter(move |x| {
|
||||||
@@ -166,14 +166,14 @@ impl FileExplorer {
|
|||||||
/// ### iter_files_all
|
/// ### iter_files_all
|
||||||
///
|
///
|
||||||
/// Iterate all files; doesn't care about options
|
/// Iterate all files; doesn't care about options
|
||||||
pub fn iter_files_all(&self) -> impl Iterator<Item = &FsEntry> + '_ {
|
pub fn iter_files_all(&self) -> impl Iterator<Item = &Entry> + '_ {
|
||||||
Box::new(self.files.iter())
|
Box::new(self.files.iter())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### get
|
/// ### get
|
||||||
///
|
///
|
||||||
/// Get file at relative index
|
/// Get file at relative index
|
||||||
pub fn get(&self, idx: usize) -> Option<&FsEntry> {
|
pub fn get(&self, idx: usize) -> Option<&Entry> {
|
||||||
let opts: ExplorerOpts = self.opts;
|
let opts: ExplorerOpts = self.opts;
|
||||||
let filtered = self
|
let filtered = self
|
||||||
.files
|
.files
|
||||||
@@ -196,7 +196,7 @@ impl FileExplorer {
|
|||||||
/// ### fmt_file
|
/// ### fmt_file
|
||||||
///
|
///
|
||||||
/// Format a file entry
|
/// Format a file entry
|
||||||
pub fn fmt_file(&self, entry: &FsEntry) -> String {
|
pub fn fmt_file(&self, entry: &Entry) -> String {
|
||||||
self.fmt.fmt(entry)
|
self.fmt.fmt(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,17 +256,15 @@ impl FileExplorer {
|
|||||||
///
|
///
|
||||||
/// Sort explorer files by their name. All names are converted to lowercase
|
/// Sort explorer files by their name. All names are converted to lowercase
|
||||||
fn sort_files_by_name(&mut self) {
|
fn sort_files_by_name(&mut self) {
|
||||||
self.files
|
self.files.sort_by_key(|x: &Entry| x.name().to_lowercase());
|
||||||
.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### sort_files_by_mtime
|
/// ### sort_files_by_mtime
|
||||||
///
|
///
|
||||||
/// Sort files by mtime; the newest comes first
|
/// Sort files by mtime; the newest comes first
|
||||||
fn sort_files_by_mtime(&mut self) {
|
fn sort_files_by_mtime(&mut self) {
|
||||||
self.files.sort_by(|a: &FsEntry, b: &FsEntry| {
|
self.files
|
||||||
b.get_last_change_time().cmp(&a.get_last_change_time())
|
.sort_by(|a: &Entry, b: &Entry| b.metadata().mtime.cmp(&a.metadata().mtime));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### sort_files_by_creation_time
|
/// ### sort_files_by_creation_time
|
||||||
@@ -274,28 +272,29 @@ impl FileExplorer {
|
|||||||
/// Sort files by creation time; the newest comes first
|
/// Sort files by creation time; the newest comes first
|
||||||
fn sort_files_by_creation_time(&mut self) {
|
fn sort_files_by_creation_time(&mut self) {
|
||||||
self.files
|
self.files
|
||||||
.sort_by_key(|b: &FsEntry| Reverse(b.get_creation_time()));
|
.sort_by_key(|b: &Entry| Reverse(b.metadata().ctime));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### sort_files_by_size
|
/// ### sort_files_by_size
|
||||||
///
|
///
|
||||||
/// Sort files by size
|
/// Sort files by size
|
||||||
fn sort_files_by_size(&mut self) {
|
fn sort_files_by_size(&mut self) {
|
||||||
self.files.sort_by_key(|b: &FsEntry| Reverse(b.get_size()));
|
self.files
|
||||||
|
.sort_by_key(|b: &Entry| Reverse(b.metadata().size));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### sort_files_directories_first
|
/// ### sort_files_directories_first
|
||||||
///
|
///
|
||||||
/// Sort files; directories come first
|
/// Sort files; directories come first
|
||||||
fn sort_files_directories_first(&mut self) {
|
fn sort_files_directories_first(&mut self) {
|
||||||
self.files.sort_by_key(|x: &FsEntry| x.is_file());
|
self.files.sort_by_key(|x: &Entry| x.is_file());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### sort_files_directories_last
|
/// ### sort_files_directories_last
|
||||||
///
|
///
|
||||||
/// Sort files; directories come last
|
/// Sort files; directories come last
|
||||||
fn sort_files_directories_last(&mut self) {
|
fn sort_files_directories_last(&mut self) {
|
||||||
self.files.sort_by_key(|x: &FsEntry| x.is_dir());
|
self.files.sort_by_key(|x: &Entry| x.is_dir());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### toggle_hidden_files
|
/// ### toggle_hidden_files
|
||||||
@@ -363,10 +362,10 @@ impl FromStr for GroupDirs {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::fs::{FsDirectory, FsFile, UnixPex};
|
|
||||||
use crate::utils::fmt::fmt_time;
|
use crate::utils::fmt::fmt_time;
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
use remotefs::fs::{Directory, File, Metadata, UnixPex};
|
||||||
use std::thread::sleep;
|
use std::thread::sleep;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
@@ -430,10 +429,7 @@ mod tests {
|
|||||||
assert!(explorer.get(100).is_none());
|
assert!(explorer.get(100).is_none());
|
||||||
//assert_eq!(explorer.count(), 6);
|
//assert_eq!(explorer.count(), 6);
|
||||||
// Verify (files are sorted by name)
|
// Verify (files are sorted by name)
|
||||||
assert_eq!(
|
assert_eq!(explorer.files.get(0).unwrap().name(), ".git/");
|
||||||
explorer.files.get(0).unwrap().get_name(),
|
|
||||||
String::from(".git/")
|
|
||||||
);
|
|
||||||
// Iter files (all)
|
// Iter files (all)
|
||||||
assert_eq!(explorer.iter_files_all().count(), 6);
|
assert_eq!(explorer.iter_files_all().count(), 6);
|
||||||
// Iter files (hidden excluded) (.git, .gitignore are hidden)
|
// Iter files (hidden excluded) (.git, .gitignore are hidden)
|
||||||
@@ -461,47 +457,41 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
explorer.sort_by(FileSorting::Name);
|
explorer.sort_by(FileSorting::Name);
|
||||||
// First entry should be "Cargo.lock"
|
// First entry should be "Cargo.lock"
|
||||||
assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock");
|
assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock");
|
||||||
// Last should be "src/"
|
// Last should be "src/"
|
||||||
assert_eq!(explorer.files.get(8).unwrap().get_name(), "src/");
|
assert_eq!(explorer.files.get(8).unwrap().name(), "src/");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fs_explorer_sort_by_mtime() {
|
fn test_fs_explorer_sort_by_mtime() {
|
||||||
let mut explorer: FileExplorer = FileExplorer::default();
|
let mut explorer: FileExplorer = FileExplorer::default();
|
||||||
let entry1: FsEntry = make_fs_entry("README.md", false);
|
let entry1: Entry = make_fs_entry("README.md", false);
|
||||||
// Wait 1 sec
|
// Wait 1 sec
|
||||||
sleep(Duration::from_secs(1));
|
sleep(Duration::from_secs(1));
|
||||||
let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false);
|
let entry2: Entry = make_fs_entry("CODE_OF_CONDUCT.md", false);
|
||||||
// Create files (files are then sorted by name)
|
// Create files (files are then sorted by name)
|
||||||
explorer.set_files(vec![entry1, entry2]);
|
explorer.set_files(vec![entry1, entry2]);
|
||||||
explorer.sort_by(FileSorting::ModifyTime);
|
explorer.sort_by(FileSorting::ModifyTime);
|
||||||
// First entry should be "CODE_OF_CONDUCT.md"
|
// First entry should be "CODE_OF_CONDUCT.md"
|
||||||
assert_eq!(
|
assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
|
||||||
explorer.files.get(0).unwrap().get_name(),
|
|
||||||
"CODE_OF_CONDUCT.md"
|
|
||||||
);
|
|
||||||
// Last should be "src/"
|
// Last should be "src/"
|
||||||
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
|
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_fs_explorer_sort_by_creation_time() {
|
fn test_fs_explorer_sort_by_creation_time() {
|
||||||
let mut explorer: FileExplorer = FileExplorer::default();
|
let mut explorer: FileExplorer = FileExplorer::default();
|
||||||
let entry1: FsEntry = make_fs_entry("README.md", false);
|
let entry1: Entry = make_fs_entry("README.md", false);
|
||||||
// Wait 1 sec
|
// Wait 1 sec
|
||||||
sleep(Duration::from_secs(1));
|
sleep(Duration::from_secs(1));
|
||||||
let entry2: FsEntry = make_fs_entry("CODE_OF_CONDUCT.md", false);
|
let entry2: Entry = make_fs_entry("CODE_OF_CONDUCT.md", false);
|
||||||
// Create files (files are then sorted by name)
|
// Create files (files are then sorted by name)
|
||||||
explorer.set_files(vec![entry1, entry2]);
|
explorer.set_files(vec![entry1, entry2]);
|
||||||
explorer.sort_by(FileSorting::CreationTime);
|
explorer.sort_by(FileSorting::CreationTime);
|
||||||
// First entry should be "CODE_OF_CONDUCT.md"
|
// First entry should be "CODE_OF_CONDUCT.md"
|
||||||
assert_eq!(
|
assert_eq!(explorer.files.get(0).unwrap().name(), "CODE_OF_CONDUCT.md");
|
||||||
explorer.files.get(0).unwrap().get_name(),
|
|
||||||
"CODE_OF_CONDUCT.md"
|
|
||||||
);
|
|
||||||
// Last should be "src/"
|
// Last should be "src/"
|
||||||
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
|
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -510,14 +500,14 @@ mod tests {
|
|||||||
// Create files (files are then sorted by name)
|
// Create files (files are then sorted by name)
|
||||||
explorer.set_files(vec![
|
explorer.set_files(vec![
|
||||||
make_fs_entry_with_size("README.md", false, 1024),
|
make_fs_entry_with_size("README.md", false, 1024),
|
||||||
make_fs_entry("src/", true),
|
make_fs_entry_with_size("src/", true, 4096),
|
||||||
make_fs_entry_with_size("CONTRIBUTING.md", false, 256),
|
make_fs_entry_with_size("CONTRIBUTING.md", false, 256),
|
||||||
]);
|
]);
|
||||||
explorer.sort_by(FileSorting::Size);
|
explorer.sort_by(FileSorting::Size);
|
||||||
// Directory has size 4096
|
// Directory has size 4096
|
||||||
assert_eq!(explorer.files.get(0).unwrap().get_name(), "src/");
|
assert_eq!(explorer.files.get(0).unwrap().name(), "src/");
|
||||||
assert_eq!(explorer.files.get(1).unwrap().get_name(), "README.md");
|
assert_eq!(explorer.files.get(1).unwrap().name(), "README.md");
|
||||||
assert_eq!(explorer.files.get(2).unwrap().get_name(), "CONTRIBUTING.md");
|
assert_eq!(explorer.files.get(2).unwrap().name(), "CONTRIBUTING.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -539,12 +529,12 @@ mod tests {
|
|||||||
explorer.sort_by(FileSorting::Name);
|
explorer.sort_by(FileSorting::Name);
|
||||||
explorer.group_dirs_by(Some(GroupDirs::First));
|
explorer.group_dirs_by(Some(GroupDirs::First));
|
||||||
// First entry should be "docs"
|
// First entry should be "docs"
|
||||||
assert_eq!(explorer.files.get(0).unwrap().get_name(), "docs/");
|
assert_eq!(explorer.files.get(0).unwrap().name(), "docs/");
|
||||||
assert_eq!(explorer.files.get(1).unwrap().get_name(), "src/");
|
assert_eq!(explorer.files.get(1).unwrap().name(), "src/");
|
||||||
// 3rd is file first for alphabetical order
|
// 3rd is file first for alphabetical order
|
||||||
assert_eq!(explorer.files.get(2).unwrap().get_name(), "Cargo.lock");
|
assert_eq!(explorer.files.get(2).unwrap().name(), "Cargo.lock");
|
||||||
// Last should be "README.md" (last file for alphabetical ordening)
|
// Last should be "README.md" (last file for alphabetical ordening)
|
||||||
assert_eq!(explorer.files.get(9).unwrap().get_name(), "README.md");
|
assert_eq!(explorer.files.get(9).unwrap().name(), "README.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -566,12 +556,12 @@ mod tests {
|
|||||||
explorer.sort_by(FileSorting::Name);
|
explorer.sort_by(FileSorting::Name);
|
||||||
explorer.group_dirs_by(Some(GroupDirs::Last));
|
explorer.group_dirs_by(Some(GroupDirs::Last));
|
||||||
// Last entry should be "src"
|
// Last entry should be "src"
|
||||||
assert_eq!(explorer.files.get(8).unwrap().get_name(), "docs/");
|
assert_eq!(explorer.files.get(8).unwrap().name(), "docs/");
|
||||||
assert_eq!(explorer.files.get(9).unwrap().get_name(), "src/");
|
assert_eq!(explorer.files.get(9).unwrap().name(), "src/");
|
||||||
// first is file for alphabetical order
|
// first is file for alphabetical order
|
||||||
assert_eq!(explorer.files.get(0).unwrap().get_name(), "Cargo.lock");
|
assert_eq!(explorer.files.get(0).unwrap().name(), "Cargo.lock");
|
||||||
// Last in files should be "README.md" (last file for alphabetical ordening)
|
// Last in files should be "README.md" (last file for alphabetical ordening)
|
||||||
assert_eq!(explorer.files.get(7).unwrap().get_name(), "README.md");
|
assert_eq!(explorer.files.get(7).unwrap().name(), "README.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -579,18 +569,20 @@ mod tests {
|
|||||||
let explorer: FileExplorer = FileExplorer::default();
|
let explorer: FileExplorer = FileExplorer::default();
|
||||||
// Create fs entry
|
// Create fs entry
|
||||||
let t: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
let entry: Entry = Entry::File(File {
|
||||||
name: String::from("bar.txt"),
|
name: String::from("bar.txt"),
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
path: PathBuf::from("/bar.txt"),
|
||||||
last_change_time: t,
|
extension: Some(String::from("txt")),
|
||||||
last_access_time: t,
|
metadata: Metadata {
|
||||||
creation_time: t,
|
atime: t,
|
||||||
|
ctime: t,
|
||||||
size: 8192,
|
size: 8192,
|
||||||
ftype: Some(String::from("txt")),
|
mtime: t,
|
||||||
symlink: None, // UNIX only
|
symlink: None,
|
||||||
user: Some(0), // UNIX only
|
uid: Some(0),
|
||||||
group: Some(0), // UNIX only
|
gid: Some(0),
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
mode: Some(UnixPex::from(0o644)),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -654,67 +646,61 @@ mod tests {
|
|||||||
]);
|
]);
|
||||||
explorer.del_entry(0);
|
explorer.del_entry(0);
|
||||||
assert_eq!(explorer.files.len(), 3);
|
assert_eq!(explorer.files.len(), 3);
|
||||||
assert_eq!(explorer.files[0].get_name(), "docs/");
|
assert_eq!(explorer.files[0].name(), "docs/");
|
||||||
explorer.del_entry(5);
|
explorer.del_entry(5);
|
||||||
assert_eq!(explorer.files.len(), 3);
|
assert_eq!(explorer.files.len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_fs_entry(name: &str, is_dir: bool) -> FsEntry {
|
fn make_fs_entry(name: &str, is_dir: bool) -> Entry {
|
||||||
let t_now: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
match is_dir {
|
let metadata = Metadata {
|
||||||
false => FsEntry::File(FsFile {
|
atime: t,
|
||||||
name: name.to_string(),
|
ctime: t,
|
||||||
abs_path: PathBuf::from(name),
|
mtime: t,
|
||||||
last_change_time: t_now,
|
symlink: None,
|
||||||
last_access_time: t_now,
|
gid: Some(0),
|
||||||
creation_time: t_now,
|
uid: Some(0),
|
||||||
|
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
|
||||||
size: 64,
|
size: 64,
|
||||||
ftype: None, // File type
|
};
|
||||||
symlink: None, // UNIX only
|
match is_dir {
|
||||||
user: Some(0), // UNIX only
|
false => Entry::File(File {
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
|
||||||
}),
|
|
||||||
true => FsEntry::Directory(FsDirectory {
|
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
abs_path: PathBuf::from(name),
|
path: PathBuf::from(name),
|
||||||
last_change_time: t_now,
|
extension: None,
|
||||||
last_access_time: t_now,
|
metadata,
|
||||||
creation_time: t_now,
|
}),
|
||||||
symlink: None, // UNIX only
|
true => Entry::Directory(Directory {
|
||||||
user: Some(0), // UNIX only
|
name: name.to_string(),
|
||||||
group: Some(0), // UNIX only
|
path: PathBuf::from(name),
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
|
metadata,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> FsEntry {
|
fn make_fs_entry_with_size(name: &str, is_dir: bool, size: usize) -> Entry {
|
||||||
let t_now: SystemTime = SystemTime::now();
|
let t: SystemTime = SystemTime::now();
|
||||||
|
let metadata = Metadata {
|
||||||
|
atime: t,
|
||||||
|
ctime: t,
|
||||||
|
mtime: t,
|
||||||
|
symlink: None,
|
||||||
|
gid: Some(0),
|
||||||
|
uid: Some(0),
|
||||||
|
mode: Some(UnixPex::from(if is_dir { 0o755 } else { 0o644 })),
|
||||||
|
size: size as u64,
|
||||||
|
};
|
||||||
match is_dir {
|
match is_dir {
|
||||||
false => FsEntry::File(FsFile {
|
false => Entry::File(File {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
abs_path: PathBuf::from(name),
|
path: PathBuf::from(name),
|
||||||
last_change_time: t_now,
|
extension: None,
|
||||||
last_access_time: t_now,
|
metadata,
|
||||||
creation_time: t_now,
|
|
||||||
size: size,
|
|
||||||
ftype: None, // File type
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
|
||||||
}),
|
}),
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => Entry::Directory(Directory {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
abs_path: PathBuf::from(name),
|
path: PathBuf::from(name),
|
||||||
last_change_time: t_now,
|
metadata,
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
//! ## builder
|
||||||
|
//!
|
||||||
|
//! Remotefs client builder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MIT License
|
||||||
|
*
|
||||||
|
* termscp - Copyright (c) 2021 Christian Visintin
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*/
|
||||||
|
use super::params::{AwsS3Params, GenericProtocolParams};
|
||||||
|
use super::{FileTransferProtocol, ProtocolParams};
|
||||||
|
use crate::system::config_client::ConfigClient;
|
||||||
|
use crate::system::sshkey_storage::SshKeyStorage;
|
||||||
|
|
||||||
|
use remotefs::client::{
|
||||||
|
aws_s3::AwsS3Fs,
|
||||||
|
ftp::FtpFs,
|
||||||
|
ssh::{ScpFs, SftpFs, SshOpts},
|
||||||
|
};
|
||||||
|
use remotefs::RemoteFs;
|
||||||
|
|
||||||
|
/// Remotefs builder
|
||||||
|
pub struct Builder;
|
||||||
|
|
||||||
|
impl Builder {
|
||||||
|
/// Build RemoteFs client from protocol and params.
|
||||||
|
///
|
||||||
|
/// if protocol and parameters are inconsistent, the function will panic.
|
||||||
|
pub fn build(
|
||||||
|
protocol: FileTransferProtocol,
|
||||||
|
params: ProtocolParams,
|
||||||
|
config_client: &ConfigClient,
|
||||||
|
) -> Box<dyn RemoteFs> {
|
||||||
|
match (protocol, params) {
|
||||||
|
(FileTransferProtocol::AwsS3, ProtocolParams::AwsS3(params)) => {
|
||||||
|
Box::new(Self::aws_s3_client(params))
|
||||||
|
}
|
||||||
|
(FileTransferProtocol::Ftp(secure), ProtocolParams::Generic(params)) => {
|
||||||
|
Box::new(Self::ftp_client(params, secure))
|
||||||
|
}
|
||||||
|
(FileTransferProtocol::Scp, ProtocolParams::Generic(params)) => {
|
||||||
|
Box::new(Self::scp_client(params, config_client))
|
||||||
|
}
|
||||||
|
(FileTransferProtocol::Sftp, ProtocolParams::Generic(params)) => {
|
||||||
|
Box::new(Self::sftp_client(params, config_client))
|
||||||
|
}
|
||||||
|
(protocol, params) => {
|
||||||
|
error!("Invalid params for protocol '{:?}'", protocol);
|
||||||
|
panic!(
|
||||||
|
"Invalid protocol '{:?}' with parameters of type {:?}",
|
||||||
|
protocol, params
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build aws s3 client from parameters
|
||||||
|
fn aws_s3_client(params: AwsS3Params) -> AwsS3Fs {
|
||||||
|
let mut client = AwsS3Fs::new(params.bucket_name, params.region);
|
||||||
|
if let Some(profile) = params.profile {
|
||||||
|
client = client.profile(profile);
|
||||||
|
}
|
||||||
|
client
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build ftp client from parameters
|
||||||
|
fn ftp_client(params: GenericProtocolParams, secure: bool) -> FtpFs {
|
||||||
|
let mut client = FtpFs::new(params.address, params.port).passive_mode();
|
||||||
|
if let Some(username) = params.username {
|
||||||
|
client = client.username(username);
|
||||||
|
}
|
||||||
|
if let Some(password) = params.password {
|
||||||
|
client = client.password(password);
|
||||||
|
}
|
||||||
|
if secure {
|
||||||
|
client = client.secure(true, true);
|
||||||
|
}
|
||||||
|
client
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build scp client
|
||||||
|
fn scp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> ScpFs {
|
||||||
|
Self::build_ssh_opts(params, config_client).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build sftp client
|
||||||
|
fn sftp_client(params: GenericProtocolParams, config_client: &ConfigClient) -> SftpFs {
|
||||||
|
Self::build_ssh_opts(params, config_client).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build ssh options from generic protocol params and client configuration
|
||||||
|
fn build_ssh_opts(params: GenericProtocolParams, config_client: &ConfigClient) -> SshOpts {
|
||||||
|
let mut opts = SshOpts::new(params.address)
|
||||||
|
.key_storage(Box::new(Self::make_ssh_storage(config_client)))
|
||||||
|
.port(params.port);
|
||||||
|
if let Some(username) = params.username {
|
||||||
|
opts = opts.username(username);
|
||||||
|
}
|
||||||
|
if let Some(password) = params.password {
|
||||||
|
opts = opts.password(password);
|
||||||
|
}
|
||||||
|
opts
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded)
|
||||||
|
fn make_ssh_storage(config_client: &ConfigClient) -> SshKeyStorage {
|
||||||
|
SshKeyStorage::storage_from_config(config_client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_build_aws_s3_fs() {
|
||||||
|
let params = ProtocolParams::AwsS3(AwsS3Params::new("omar", "eu-west-1", Some("test")));
|
||||||
|
let config_client = get_config_client();
|
||||||
|
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_build_ftp_fs() {
|
||||||
|
let params = ProtocolParams::Generic(
|
||||||
|
GenericProtocolParams::default()
|
||||||
|
.address("127.0.0.1")
|
||||||
|
.port(21)
|
||||||
|
.username(Some("omar"))
|
||||||
|
.password(Some("qwerty123")),
|
||||||
|
);
|
||||||
|
let config_client = get_config_client();
|
||||||
|
let _ = Builder::build(FileTransferProtocol::Ftp(true), params, &config_client);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_build_scp_fs() {
|
||||||
|
let params = ProtocolParams::Generic(
|
||||||
|
GenericProtocolParams::default()
|
||||||
|
.address("127.0.0.1")
|
||||||
|
.port(22)
|
||||||
|
.username(Some("omar"))
|
||||||
|
.password(Some("qwerty123")),
|
||||||
|
);
|
||||||
|
let config_client = get_config_client();
|
||||||
|
let _ = Builder::build(FileTransferProtocol::Scp, params, &config_client);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn should_build_sftp_fs() {
|
||||||
|
let params = ProtocolParams::Generic(
|
||||||
|
GenericProtocolParams::default()
|
||||||
|
.address("127.0.0.1")
|
||||||
|
.port(22)
|
||||||
|
.username(Some("omar"))
|
||||||
|
.password(Some("qwerty123")),
|
||||||
|
);
|
||||||
|
let config_client = get_config_client();
|
||||||
|
let _ = Builder::build(FileTransferProtocol::Sftp, params, &config_client);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn should_not_build_fs() {
|
||||||
|
let params = ProtocolParams::Generic(
|
||||||
|
GenericProtocolParams::default()
|
||||||
|
.address("127.0.0.1")
|
||||||
|
.port(22)
|
||||||
|
.username(Some("omar"))
|
||||||
|
.password(Some("qwerty123")),
|
||||||
|
);
|
||||||
|
let config_client = get_config_client();
|
||||||
|
let _ = Builder::build(FileTransferProtocol::AwsS3, params, &config_client);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_config_client() -> ConfigClient {
|
||||||
|
let tmp_dir: TempDir = TempDir::new().ok().unwrap();
|
||||||
|
let (cfg_path, ssh_keys_path): (PathBuf, PathBuf) = get_paths(tmp_dir.path());
|
||||||
|
ConfigClient::new(cfg_path.as_path(), ssh_keys_path.as_path())
|
||||||
|
.ok()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get paths for configuration and keys directory
|
||||||
|
fn get_paths(dir: &Path) -> (PathBuf, PathBuf) {
|
||||||
|
let mut k: PathBuf = PathBuf::from(dir);
|
||||||
|
let mut c: PathBuf = k.clone();
|
||||||
|
k.push("ssh-keys/");
|
||||||
|
c.push("config.toml");
|
||||||
|
(c, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-423
@@ -1,6 +1,6 @@
|
|||||||
//! ## FileTransfer
|
//! ## FileTransfer
|
||||||
//!
|
//!
|
||||||
//! `filetransfer` is the module which provides the trait file transfers must implement and the different file transfers
|
//! `filetransfer` is the module which provides the file transfer protocols and remotefs builders
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MIT License
|
* MIT License
|
||||||
@@ -25,21 +25,12 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
mod builder;
|
||||||
use crate::fs::{FsEntry, FsFile};
|
|
||||||
// ext
|
|
||||||
use std::fs::File;
|
|
||||||
use std::io::{self, Read, Write};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use thiserror::Error;
|
|
||||||
use wildmatch::WildMatch;
|
|
||||||
// exports
|
|
||||||
pub mod params;
|
pub mod params;
|
||||||
mod transfer;
|
|
||||||
|
|
||||||
// -- export types
|
// -- export types
|
||||||
|
pub use builder::Builder;
|
||||||
pub use params::{FileTransferParams, ProtocolParams};
|
pub use params::{FileTransferParams, ProtocolParams};
|
||||||
pub use transfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
|
|
||||||
|
|
||||||
/// ## FileTransferProtocol
|
/// ## FileTransferProtocol
|
||||||
///
|
///
|
||||||
@@ -53,325 +44,6 @@ pub enum FileTransferProtocol {
|
|||||||
AwsS3,
|
AwsS3,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ## FileTransferError
|
|
||||||
///
|
|
||||||
/// FileTransferError defines the possible errors available for a file transfer
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FileTransferError {
|
|
||||||
code: FileTransferErrorType,
|
|
||||||
msg: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## FileTransferErrorType
|
|
||||||
///
|
|
||||||
/// FileTransferErrorType defines the possible errors available for a file transfer
|
|
||||||
#[derive(Error, Debug, Clone, Copy, PartialEq)]
|
|
||||||
pub enum FileTransferErrorType {
|
|
||||||
#[error("Authentication failed")]
|
|
||||||
AuthenticationFailed,
|
|
||||||
#[error("Bad address syntax")]
|
|
||||||
BadAddress,
|
|
||||||
#[error("Connection error")]
|
|
||||||
ConnectionError,
|
|
||||||
#[error("SSL error")]
|
|
||||||
SslError,
|
|
||||||
#[error("Could not stat directory")]
|
|
||||||
DirStatFailed,
|
|
||||||
#[error("Directory already exists")]
|
|
||||||
DirectoryAlreadyExists,
|
|
||||||
#[error("Failed to create file")]
|
|
||||||
FileCreateDenied,
|
|
||||||
#[error("No such file or directory")]
|
|
||||||
NoSuchFileOrDirectory,
|
|
||||||
#[error("Not enough permissions")]
|
|
||||||
PexError,
|
|
||||||
#[error("Protocol error")]
|
|
||||||
ProtocolError,
|
|
||||||
#[error("Uninitialized session")]
|
|
||||||
UninitializedSession,
|
|
||||||
#[error("Unsupported feature")]
|
|
||||||
UnsupportedFeature,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileTransferError {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiates a new FileTransferError
|
|
||||||
pub fn new(code: FileTransferErrorType) -> FileTransferError {
|
|
||||||
FileTransferError { code, msg: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### new_ex
|
|
||||||
///
|
|
||||||
/// Instantiates a new FileTransferError with message
|
|
||||||
pub fn new_ex(code: FileTransferErrorType, msg: String) -> FileTransferError {
|
|
||||||
let mut err: FileTransferError = FileTransferError::new(code);
|
|
||||||
err.msg = Some(msg);
|
|
||||||
err
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### kind
|
|
||||||
///
|
|
||||||
/// Returns the error kind
|
|
||||||
pub fn kind(&self) -> FileTransferErrorType {
|
|
||||||
self.code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for FileTransferError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
||||||
match &self.msg {
|
|
||||||
Some(msg) => write!(f, "{} ({})", self.code, msg),
|
|
||||||
None => write!(f, "{}", self.code),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## FileTransferResult
|
|
||||||
///
|
|
||||||
/// Result type returned by a `FileTransfer` implementation
|
|
||||||
pub type FileTransferResult<T> = Result<T, FileTransferError>;
|
|
||||||
|
|
||||||
/// ## FileTransfer
|
|
||||||
///
|
|
||||||
/// File transfer trait must be implemented by all the file transfers and defines the method used by a generic file transfer
|
|
||||||
pub trait FileTransfer {
|
|
||||||
/// ### connect
|
|
||||||
///
|
|
||||||
/// Connect to the remote server
|
|
||||||
/// Can return banner / welcome message on success
|
|
||||||
fn connect(&mut self, params: &ProtocolParams) -> FileTransferResult<Option<String>>;
|
|
||||||
|
|
||||||
/// ### disconnect
|
|
||||||
///
|
|
||||||
/// Disconnect from the remote server
|
|
||||||
fn disconnect(&mut self) -> FileTransferResult<()>;
|
|
||||||
|
|
||||||
/// ### is_connected
|
|
||||||
///
|
|
||||||
/// Indicates whether the client is connected to remote
|
|
||||||
fn is_connected(&self) -> bool;
|
|
||||||
|
|
||||||
/// ### pwd
|
|
||||||
///
|
|
||||||
/// Print working directory
|
|
||||||
|
|
||||||
fn pwd(&mut self) -> FileTransferResult<PathBuf>;
|
|
||||||
|
|
||||||
/// ### change_dir
|
|
||||||
///
|
|
||||||
/// Change working directory
|
|
||||||
|
|
||||||
fn change_dir(&mut self, dir: &Path) -> FileTransferResult<PathBuf>;
|
|
||||||
|
|
||||||
/// ### copy
|
|
||||||
///
|
|
||||||
/// Copy file to destination
|
|
||||||
fn copy(&mut self, src: &FsEntry, dst: &Path) -> FileTransferResult<()>;
|
|
||||||
|
|
||||||
/// ### list_dir
|
|
||||||
///
|
|
||||||
/// List directory entries
|
|
||||||
|
|
||||||
fn list_dir(&mut self, path: &Path) -> FileTransferResult<Vec<FsEntry>>;
|
|
||||||
|
|
||||||
/// ### mkdir
|
|
||||||
///
|
|
||||||
/// Make directory
|
|
||||||
/// In case the directory already exists, it must return an Error of kind `FileTransferErrorType::DirectoryAlreadyExists`
|
|
||||||
fn mkdir(&mut self, dir: &Path) -> FileTransferResult<()>;
|
|
||||||
|
|
||||||
/// ### remove
|
|
||||||
///
|
|
||||||
/// Remove a file or a directory
|
|
||||||
fn remove(&mut self, file: &FsEntry) -> FileTransferResult<()>;
|
|
||||||
|
|
||||||
/// ### rename
|
|
||||||
///
|
|
||||||
/// Rename file or a directory
|
|
||||||
fn rename(&mut self, file: &FsEntry, dst: &Path) -> FileTransferResult<()>;
|
|
||||||
|
|
||||||
/// ### stat
|
|
||||||
///
|
|
||||||
/// Stat file and return FsEntry
|
|
||||||
fn stat(&mut self, path: &Path) -> FileTransferResult<FsEntry>;
|
|
||||||
|
|
||||||
/// ### exec
|
|
||||||
///
|
|
||||||
/// Execute a command on remote host
|
|
||||||
fn exec(&mut self, cmd: &str) -> FileTransferResult<String>;
|
|
||||||
|
|
||||||
/// ### send_file
|
|
||||||
///
|
|
||||||
/// Send file to remote
|
|
||||||
/// File name is referred to the name of the file as it will be saved
|
|
||||||
/// Data contains the file data
|
|
||||||
/// Returns file and its size.
|
|
||||||
/// By default returns unsupported feature
|
|
||||||
fn send_file(
|
|
||||||
&mut self,
|
|
||||||
_local: &FsFile,
|
|
||||||
_file_name: &Path,
|
|
||||||
) -> FileTransferResult<Box<dyn Write>> {
|
|
||||||
Err(FileTransferError::new(
|
|
||||||
FileTransferErrorType::UnsupportedFeature,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### recv_file
|
|
||||||
///
|
|
||||||
/// Receive file from remote with provided name
|
|
||||||
/// Returns file and its size
|
|
||||||
/// By default returns unsupported feature
|
|
||||||
fn recv_file(&mut self, _file: &FsFile) -> FileTransferResult<Box<dyn Read>> {
|
|
||||||
Err(FileTransferError::new(
|
|
||||||
FileTransferErrorType::UnsupportedFeature,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### on_sent
|
|
||||||
///
|
|
||||||
/// Finalize send method.
|
|
||||||
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
|
|
||||||
/// The purpose of this method is to finalize the connection with the peer when writing data.
|
|
||||||
/// This is necessary for some protocols such as FTP.
|
|
||||||
/// You must call this method each time you want to finalize the write of the remote file.
|
|
||||||
/// By default this function returns already `Ok(())`
|
|
||||||
fn on_sent(&mut self, _writable: Box<dyn Write>) -> FileTransferResult<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### on_recv
|
|
||||||
///
|
|
||||||
/// Finalize recv method.
|
|
||||||
/// This method must be implemented only if necessary; in case you don't need it, just return `Ok(())`
|
|
||||||
/// The purpose of this method is to finalize the connection with the peer when reading data.
|
|
||||||
/// This mighe be necessary for some protocols.
|
|
||||||
/// You must call this method each time you want to finalize the read of the remote file.
|
|
||||||
/// By default this function returns already `Ok(())`
|
|
||||||
fn on_recv(&mut self, _readable: Box<dyn Read>) -> FileTransferResult<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### send_file_wno_stream
|
|
||||||
///
|
|
||||||
/// Send a file to remote WITHOUT using streams.
|
|
||||||
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
|
|
||||||
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
|
|
||||||
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
|
|
||||||
/// By default this function uses the streams function to copy content from reader to writer
|
|
||||||
fn send_file_wno_stream(
|
|
||||||
&mut self,
|
|
||||||
src: &FsFile,
|
|
||||||
dest: &Path,
|
|
||||||
mut reader: Box<dyn Read>,
|
|
||||||
) -> FileTransferResult<()> {
|
|
||||||
match self.is_connected() {
|
|
||||||
true => {
|
|
||||||
let mut stream = self.send_file(src, dest)?;
|
|
||||||
io::copy(&mut reader, &mut stream).map_err(|e| {
|
|
||||||
FileTransferError::new_ex(FileTransferErrorType::ProtocolError, e.to_string())
|
|
||||||
})?;
|
|
||||||
self.on_sent(stream)
|
|
||||||
}
|
|
||||||
false => Err(FileTransferError::new(
|
|
||||||
FileTransferErrorType::UninitializedSession,
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### recv_file_wno_stream
|
|
||||||
///
|
|
||||||
/// Receive a file from remote WITHOUT using streams.
|
|
||||||
/// This method SHOULD be implemented ONLY when streams are not supported by the current file transfer.
|
|
||||||
/// The developer implementing the filetransfer user should FIRST try with `send_file` followed by `on_sent`
|
|
||||||
/// If the function returns error kind() `UnsupportedFeature`, then he should call this function.
|
|
||||||
/// For safety reasons this function doesn't accept the `Write` trait, but the destination path.
|
|
||||||
/// By default this function uses the streams function to copy content from reader to writer
|
|
||||||
fn recv_file_wno_stream(&mut self, src: &FsFile, dest: &Path) -> FileTransferResult<()> {
|
|
||||||
match self.is_connected() {
|
|
||||||
true => {
|
|
||||||
let mut writer = File::create(dest).map_err(|e| {
|
|
||||||
FileTransferError::new_ex(
|
|
||||||
FileTransferErrorType::FileCreateDenied,
|
|
||||||
format!("Could not open local file: {}", e),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let mut stream = self.recv_file(src)?;
|
|
||||||
io::copy(&mut stream, &mut writer)
|
|
||||||
.map(|_| ())
|
|
||||||
.map_err(|e| {
|
|
||||||
FileTransferError::new_ex(
|
|
||||||
FileTransferErrorType::ProtocolError,
|
|
||||||
e.to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
self.on_recv(stream)
|
|
||||||
}
|
|
||||||
false => Err(FileTransferError::new(
|
|
||||||
FileTransferErrorType::UninitializedSession,
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### find
|
|
||||||
///
|
|
||||||
/// Find files from current directory (in all subdirectories) whose name matches the provided search
|
|
||||||
/// Search supports wildcards ('?', '*')
|
|
||||||
fn find(&mut self, search: &str) -> FileTransferResult<Vec<FsEntry>> {
|
|
||||||
match self.is_connected() {
|
|
||||||
true => {
|
|
||||||
// Starting from current directory, iter dir
|
|
||||||
match self.pwd() {
|
|
||||||
Ok(p) => self.iter_search(p.as_path(), &WildMatch::new(search)),
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false => Err(FileTransferError::new(
|
|
||||||
FileTransferErrorType::UninitializedSession,
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### iter_search
|
|
||||||
///
|
|
||||||
/// Search recursively in `dir` for file matching the wildcard.
|
|
||||||
/// NOTE: DON'T RE-IMPLEMENT THIS FUNCTION, unless the file transfer provides a faster way to do so
|
|
||||||
/// NOTE: don't call this method from outside; consider it as private
|
|
||||||
fn iter_search(&mut self, dir: &Path, filter: &WildMatch) -> FileTransferResult<Vec<FsEntry>> {
|
|
||||||
let mut drained: Vec<FsEntry> = Vec::new();
|
|
||||||
// Scan directory
|
|
||||||
match self.list_dir(dir) {
|
|
||||||
Ok(entries) => {
|
|
||||||
/* For each entry:
|
|
||||||
- if is dir: call iter_search with `dir`
|
|
||||||
- push `iter_search` result to `drained`
|
|
||||||
- if is file: check if it matches `filter`
|
|
||||||
- if it matches `filter`: push to to filter
|
|
||||||
*/
|
|
||||||
for entry in entries.iter() {
|
|
||||||
match entry {
|
|
||||||
FsEntry::Directory(dir) => {
|
|
||||||
// If directory name, matches wildcard, push it to drained
|
|
||||||
if filter.matches(dir.name.as_str()) {
|
|
||||||
drained.push(FsEntry::Directory(dir.clone()));
|
|
||||||
}
|
|
||||||
drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?);
|
|
||||||
}
|
|
||||||
FsEntry::File(file) => {
|
|
||||||
if filter.matches(file.name.as_str()) {
|
|
||||||
drained.push(FsEntry::File(file.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(drained)
|
|
||||||
}
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Traits
|
// Traits
|
||||||
|
|
||||||
impl std::string::ToString for FileTransferProtocol {
|
impl std::string::ToString for FileTransferProtocol {
|
||||||
@@ -479,96 +151,4 @@ mod tests {
|
|||||||
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
|
assert_eq!(FileTransferProtocol::Sftp.to_string(), String::from("SFTP"));
|
||||||
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
|
assert_eq!(FileTransferProtocol::AwsS3.to_string(), String::from("S3"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_filetransfer_mod_error() {
|
|
||||||
let err: FileTransferError = FileTransferError::new_ex(
|
|
||||||
FileTransferErrorType::NoSuchFileOrDirectory,
|
|
||||||
String::from("non va una mazza"),
|
|
||||||
);
|
|
||||||
assert_eq!(*err.msg.as_ref().unwrap(), String::from("non va una mazza"));
|
|
||||||
assert_eq!(
|
|
||||||
format!("{}", err),
|
|
||||||
String::from("No such file or directory (non va una mazza)")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::AuthenticationFailed)
|
|
||||||
),
|
|
||||||
String::from("Authentication failed")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::BadAddress)
|
|
||||||
),
|
|
||||||
String::from("Bad address syntax")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::ConnectionError)
|
|
||||||
),
|
|
||||||
String::from("Connection error")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::DirStatFailed)
|
|
||||||
),
|
|
||||||
String::from("Could not stat directory")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::FileCreateDenied)
|
|
||||||
),
|
|
||||||
String::from("Failed to create file")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::NoSuchFileOrDirectory)
|
|
||||||
),
|
|
||||||
String::from("No such file or directory")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::PexError)
|
|
||||||
),
|
|
||||||
String::from("Not enough permissions")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::ProtocolError)
|
|
||||||
),
|
|
||||||
String::from("Protocol error")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::SslError)
|
|
||||||
),
|
|
||||||
String::from("SSL error")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::UninitializedSession)
|
|
||||||
),
|
|
||||||
String::from("Uninitialized session")
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
format!(
|
|
||||||
"{}",
|
|
||||||
FileTransferError::new(FileTransferErrorType::UnsupportedFeature)
|
|
||||||
),
|
|
||||||
String::from("Unsupported feature")
|
|
||||||
);
|
|
||||||
let err = FileTransferError::new(FileTransferErrorType::UnsupportedFeature);
|
|
||||||
assert_eq!(err.kind(), FileTransferErrorType::UnsupportedFeature);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,8 +103,7 @@ impl Default for ProtocolParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProtocolParams {
|
impl ProtocolParams {
|
||||||
/// ### generic_params
|
#[cfg(test)]
|
||||||
///
|
|
||||||
/// Retrieve generic parameters from protocol params if any
|
/// Retrieve generic parameters from protocol params if any
|
||||||
pub fn generic_params(&self) -> Option<&GenericProtocolParams> {
|
pub fn generic_params(&self) -> Option<&GenericProtocolParams> {
|
||||||
match self {
|
match self {
|
||||||
@@ -120,8 +119,7 @@ impl ProtocolParams {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### s3_params
|
#[cfg(test)]
|
||||||
///
|
|
||||||
/// Retrieve AWS S3 parameters if any
|
/// Retrieve AWS S3 parameters if any
|
||||||
pub fn s3_params(&self) -> Option<&AwsS3Params> {
|
pub fn s3_params(&self) -> Option<&AwsS3Params> {
|
||||||
match self {
|
match self {
|
||||||
|
|||||||
@@ -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 ¶ms.username {
|
|
||||||
Some(u) => u.to_string(),
|
|
||||||
None => String::from("anonymous"),
|
|
||||||
};
|
|
||||||
let password: String = match ¶ms.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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
@@ -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(¶ms).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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
//! ## S3 object
|
|
||||||
//!
|
|
||||||
//! This module exposes the S3Object structure, which is an intermediate structure to work with
|
|
||||||
//! S3 objects. Easy to be converted into a FsEntry.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
use super::{FsDirectory, FsEntry, FsFile, Object};
|
|
||||||
use crate::utils::parser::parse_datetime;
|
|
||||||
use crate::utils::path;
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
/// ## S3Object
|
|
||||||
///
|
|
||||||
/// An intermediate struct to work with s3 `Object`.
|
|
||||||
/// Really easy to be converted into a `FsEntry`
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct S3Object {
|
|
||||||
pub name: String,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub size: usize,
|
|
||||||
pub last_modified: SystemTime,
|
|
||||||
/// Whether or not represents a directory. I already know directories don't exist in s3!
|
|
||||||
pub is_dir: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Object> for S3Object {
|
|
||||||
fn from(obj: &Object) -> Self {
|
|
||||||
let is_dir: bool = obj.key.ends_with('/');
|
|
||||||
let abs_path: PathBuf = path::absolutize(
|
|
||||||
PathBuf::from("/").as_path(),
|
|
||||||
PathBuf::from(obj.key.as_str()).as_path(),
|
|
||||||
);
|
|
||||||
let last_modified: SystemTime =
|
|
||||||
match parse_datetime(obj.last_modified.as_str(), "%Y-%m-%dT%H:%M:%S%Z") {
|
|
||||||
Ok(dt) => dt,
|
|
||||||
Err(_) => UNIX_EPOCH,
|
|
||||||
};
|
|
||||||
Self {
|
|
||||||
name: Self::object_name(obj.key.as_str()),
|
|
||||||
path: abs_path,
|
|
||||||
size: obj.size as usize,
|
|
||||||
last_modified,
|
|
||||||
is_dir,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<S3Object> for FsEntry {
|
|
||||||
fn from(obj: S3Object) -> Self {
|
|
||||||
let abs_path: PathBuf = path::absolutize(Path::new("/"), obj.path.as_path());
|
|
||||||
match obj.is_dir {
|
|
||||||
true => FsEntry::Directory(FsDirectory {
|
|
||||||
name: obj.name,
|
|
||||||
abs_path,
|
|
||||||
last_change_time: obj.last_modified,
|
|
||||||
last_access_time: obj.last_modified,
|
|
||||||
creation_time: obj.last_modified,
|
|
||||||
symlink: None,
|
|
||||||
user: None,
|
|
||||||
group: None,
|
|
||||||
unix_pex: None,
|
|
||||||
}),
|
|
||||||
false => FsEntry::File(FsFile {
|
|
||||||
name: obj.name,
|
|
||||||
ftype: obj
|
|
||||||
.path
|
|
||||||
.extension()
|
|
||||||
.map(|x| x.to_string_lossy().to_string()),
|
|
||||||
abs_path,
|
|
||||||
size: obj.size,
|
|
||||||
last_change_time: obj.last_modified,
|
|
||||||
last_access_time: obj.last_modified,
|
|
||||||
creation_time: obj.last_modified,
|
|
||||||
symlink: None,
|
|
||||||
user: None,
|
|
||||||
group: None,
|
|
||||||
unix_pex: None,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl S3Object {
|
|
||||||
/// ### object_name
|
|
||||||
///
|
|
||||||
/// Get object name from key
|
|
||||||
pub fn object_name(key: &str) -> String {
|
|
||||||
let mut tokens = key.split('/');
|
|
||||||
let count = tokens.clone().count();
|
|
||||||
let demi_last: String = match count > 1 {
|
|
||||||
true => tokens.nth(count - 2).unwrap().to_string(),
|
|
||||||
false => String::new(),
|
|
||||||
};
|
|
||||||
if let Some(last) = tokens.last() {
|
|
||||||
// If last is not empty, return last one
|
|
||||||
if !last.is_empty() {
|
|
||||||
return last.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Return demi last
|
|
||||||
demi_last
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn object_to_s3object_file() {
|
|
||||||
let obj: Object = Object {
|
|
||||||
key: String::from("pippo/sottocartella/chiedo.gif"),
|
|
||||||
e_tag: String::default(),
|
|
||||||
size: 1516966,
|
|
||||||
owner: None,
|
|
||||||
storage_class: String::default(),
|
|
||||||
last_modified: String::from("2021-08-28T10:20:37.000Z"),
|
|
||||||
};
|
|
||||||
let s3_obj: S3Object = S3Object::from(&obj);
|
|
||||||
assert_eq!(s3_obj.name.as_str(), "chiedo.gif");
|
|
||||||
assert_eq!(
|
|
||||||
s3_obj.path.as_path(),
|
|
||||||
Path::new("/pippo/sottocartella/chiedo.gif")
|
|
||||||
);
|
|
||||||
assert_eq!(s3_obj.size, 1516966);
|
|
||||||
assert_eq!(s3_obj.is_dir, false);
|
|
||||||
assert_eq!(
|
|
||||||
s3_obj
|
|
||||||
.last_modified
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.ok()
|
|
||||||
.unwrap(),
|
|
||||||
Duration::from_secs(1630146037)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn object_to_s3object_dir() {
|
|
||||||
let obj: Object = Object {
|
|
||||||
key: String::from("temp/"),
|
|
||||||
e_tag: String::default(),
|
|
||||||
size: 0,
|
|
||||||
owner: None,
|
|
||||||
storage_class: String::default(),
|
|
||||||
last_modified: String::from("2021-08-28T10:20:37.000Z"),
|
|
||||||
};
|
|
||||||
let s3_obj: S3Object = S3Object::from(&obj);
|
|
||||||
assert_eq!(s3_obj.name.as_str(), "temp");
|
|
||||||
assert_eq!(s3_obj.path.as_path(), Path::new("/temp"));
|
|
||||||
assert_eq!(s3_obj.size, 0);
|
|
||||||
assert_eq!(s3_obj.is_dir, true);
|
|
||||||
assert_eq!(
|
|
||||||
s3_obj
|
|
||||||
.last_modified
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.ok()
|
|
||||||
.unwrap(),
|
|
||||||
Duration::from_secs(1630146037)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fsentry_from_s3obj_file() {
|
|
||||||
let obj: S3Object = S3Object {
|
|
||||||
name: String::from("chiedo.gif"),
|
|
||||||
path: PathBuf::from("/pippo/sottocartella/chiedo.gif"),
|
|
||||||
size: 1516966,
|
|
||||||
is_dir: false,
|
|
||||||
last_modified: UNIX_EPOCH,
|
|
||||||
};
|
|
||||||
let entry: FsFile = FsEntry::from(obj).unwrap_file();
|
|
||||||
assert_eq!(entry.name.as_str(), "chiedo.gif");
|
|
||||||
assert_eq!(
|
|
||||||
entry.abs_path.as_path(),
|
|
||||||
Path::new("/pippo/sottocartella/chiedo.gif")
|
|
||||||
);
|
|
||||||
assert_eq!(entry.creation_time, UNIX_EPOCH);
|
|
||||||
assert_eq!(entry.last_change_time, UNIX_EPOCH);
|
|
||||||
assert_eq!(entry.last_access_time, UNIX_EPOCH);
|
|
||||||
assert_eq!(entry.size, 1516966);
|
|
||||||
assert_eq!(entry.ftype.unwrap().as_str(), "gif");
|
|
||||||
assert_eq!(entry.user, None);
|
|
||||||
assert_eq!(entry.group, None);
|
|
||||||
assert_eq!(entry.unix_pex, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn fsentry_from_s3obj_directory() {
|
|
||||||
let obj: S3Object = S3Object {
|
|
||||||
name: String::from("temp"),
|
|
||||||
path: PathBuf::from("/temp"),
|
|
||||||
size: 0,
|
|
||||||
is_dir: true,
|
|
||||||
last_modified: UNIX_EPOCH,
|
|
||||||
};
|
|
||||||
let entry: FsDirectory = FsEntry::from(obj).unwrap_dir();
|
|
||||||
assert_eq!(entry.name.as_str(), "temp");
|
|
||||||
assert_eq!(entry.abs_path.as_path(), Path::new("/temp"));
|
|
||||||
assert_eq!(entry.creation_time, UNIX_EPOCH);
|
|
||||||
assert_eq!(entry.last_change_time, UNIX_EPOCH);
|
|
||||||
assert_eq!(entry.last_access_time, UNIX_EPOCH);
|
|
||||||
assert_eq!(entry.user, None);
|
|
||||||
assert_eq!(entry.group, None);
|
|
||||||
assert_eq!(entry.unix_pex, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn object_name() {
|
|
||||||
assert_eq!(
|
|
||||||
S3Object::object_name("pippo/sottocartella/chiedo.gif").as_str(),
|
|
||||||
"chiedo.gif"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
S3Object::object_name("pippo/sottocartella/").as_str(),
|
|
||||||
"sottocartella"
|
|
||||||
);
|
|
||||||
assert_eq!(S3Object::object_name("pippo/").as_str(), "pippo");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
-578
@@ -1,578 +0,0 @@
|
|||||||
//! ## Fs
|
|
||||||
//!
|
|
||||||
//! `fs` is the module which provides file system entities
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MIT License
|
|
||||||
*
|
|
||||||
* termscp - Copyright (c) 2021 Christian Visintin
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// Mod
|
|
||||||
pub mod explorer;
|
|
||||||
// Ext
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
/// ## FsEntry
|
|
||||||
///
|
|
||||||
/// FsEntry represents a generic entry in a directory
|
|
||||||
|
|
||||||
#[derive(Clone, std::fmt::Debug)]
|
|
||||||
pub enum FsEntry {
|
|
||||||
Directory(FsDirectory),
|
|
||||||
File(FsFile),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## FsDirectory
|
|
||||||
///
|
|
||||||
/// Directory provides an interface to file system directories
|
|
||||||
|
|
||||||
#[derive(Clone, std::fmt::Debug)]
|
|
||||||
pub struct FsDirectory {
|
|
||||||
pub name: String,
|
|
||||||
pub abs_path: PathBuf,
|
|
||||||
pub last_change_time: SystemTime,
|
|
||||||
pub last_access_time: SystemTime,
|
|
||||||
pub creation_time: SystemTime,
|
|
||||||
pub symlink: Option<Box<FsEntry>>, // UNIX only
|
|
||||||
pub user: Option<u32>, // UNIX only
|
|
||||||
pub group: Option<u32>, // UNIX only
|
|
||||||
pub unix_pex: Option<(UnixPex, UnixPex, UnixPex)>, // UNIX only
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### FsFile
|
|
||||||
///
|
|
||||||
/// FsFile provides an interface to file system files
|
|
||||||
|
|
||||||
#[derive(Clone, std::fmt::Debug)]
|
|
||||||
pub struct FsFile {
|
|
||||||
pub name: String,
|
|
||||||
pub abs_path: PathBuf,
|
|
||||||
pub last_change_time: SystemTime,
|
|
||||||
pub last_access_time: SystemTime,
|
|
||||||
pub creation_time: SystemTime,
|
|
||||||
pub size: usize,
|
|
||||||
pub ftype: Option<String>, // File type
|
|
||||||
pub symlink: Option<Box<FsEntry>>, // UNIX only
|
|
||||||
pub user: Option<u32>, // UNIX only
|
|
||||||
pub group: Option<u32>, // UNIX only
|
|
||||||
pub unix_pex: Option<(UnixPex, UnixPex, UnixPex)>, // UNIX only
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ## UnixPex
|
|
||||||
///
|
|
||||||
/// Describes the permissions on POSIX system.
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
|
||||||
pub struct UnixPex {
|
|
||||||
read: bool,
|
|
||||||
write: bool,
|
|
||||||
execute: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UnixPex {
|
|
||||||
/// ### new
|
|
||||||
///
|
|
||||||
/// Instantiates a new `UnixPex`
|
|
||||||
pub fn new(read: bool, write: bool, execute: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
read,
|
|
||||||
write,
|
|
||||||
execute,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### can_read
|
|
||||||
///
|
|
||||||
/// Returns whether user can read
|
|
||||||
pub fn can_read(&self) -> bool {
|
|
||||||
self.read
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### can_write
|
|
||||||
///
|
|
||||||
/// Returns whether user can write
|
|
||||||
pub fn can_write(&self) -> bool {
|
|
||||||
self.write
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### can_execute
|
|
||||||
///
|
|
||||||
/// Returns whether user can execute
|
|
||||||
pub fn can_execute(&self) -> bool {
|
|
||||||
self.execute
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### as_byte
|
|
||||||
///
|
|
||||||
/// Convert permission to byte as on POSIX systems
|
|
||||||
pub fn as_byte(&self) -> u8 {
|
|
||||||
((self.read as u8) << 2) + ((self.write as u8) << 1) + (self.execute as u8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<u8> for UnixPex {
|
|
||||||
fn from(bits: u8) -> Self {
|
|
||||||
Self {
|
|
||||||
read: ((bits >> 2) & 0x01) != 0,
|
|
||||||
write: ((bits >> 1) & 0x01) != 0,
|
|
||||||
execute: (bits & 0x01) != 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FsEntry {
|
|
||||||
/// ### get_abs_path
|
|
||||||
///
|
|
||||||
/// Get absolute path from `FsEntry`
|
|
||||||
pub fn get_abs_path(&self) -> PathBuf {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => dir.abs_path.clone(),
|
|
||||||
FsEntry::File(file) => file.abs_path.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_name
|
|
||||||
///
|
|
||||||
/// Get file name from `FsEntry`
|
|
||||||
pub fn get_name(&self) -> &'_ str {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => dir.name.as_ref(),
|
|
||||||
FsEntry::File(file) => file.name.as_ref(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_last_change_time
|
|
||||||
///
|
|
||||||
/// Get last change time from `FsEntry`
|
|
||||||
pub fn get_last_change_time(&self) -> SystemTime {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => dir.last_change_time,
|
|
||||||
FsEntry::File(file) => file.last_change_time,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_last_access_time
|
|
||||||
///
|
|
||||||
/// Get access time from `FsEntry`
|
|
||||||
pub fn get_last_access_time(&self) -> SystemTime {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => dir.last_access_time,
|
|
||||||
FsEntry::File(file) => file.last_access_time,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_creation_time
|
|
||||||
///
|
|
||||||
/// Get creation time from `FsEntry`
|
|
||||||
pub fn get_creation_time(&self) -> SystemTime {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => dir.creation_time,
|
|
||||||
FsEntry::File(file) => file.creation_time,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_size
|
|
||||||
///
|
|
||||||
/// Get size from `FsEntry`. For directories is always 4096
|
|
||||||
pub fn get_size(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(_) => 4096,
|
|
||||||
FsEntry::File(file) => file.size,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_ftype
|
|
||||||
///
|
|
||||||
/// Get file type from `FsEntry`. For directories is always None
|
|
||||||
pub fn get_ftype(&self) -> Option<String> {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(_) => None,
|
|
||||||
FsEntry::File(file) => file.ftype.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_user
|
|
||||||
///
|
|
||||||
/// Get uid from `FsEntry`
|
|
||||||
pub fn get_user(&self) -> Option<u32> {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => dir.user,
|
|
||||||
FsEntry::File(file) => file.user,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_group
|
|
||||||
///
|
|
||||||
/// Get gid from `FsEntry`
|
|
||||||
pub fn get_group(&self) -> Option<u32> {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => dir.group,
|
|
||||||
FsEntry::File(file) => file.group,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_unix_pex
|
|
||||||
///
|
|
||||||
/// Get unix pex from `FsEntry`
|
|
||||||
pub fn get_unix_pex(&self) -> Option<(UnixPex, UnixPex, UnixPex)> {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => dir.unix_pex,
|
|
||||||
FsEntry::File(file) => file.unix_pex,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### is_symlink
|
|
||||||
///
|
|
||||||
/// Returns whether the `FsEntry` is a symlink
|
|
||||||
pub fn is_symlink(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => dir.symlink.is_some(),
|
|
||||||
FsEntry::File(file) => file.symlink.is_some(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### is_dir
|
|
||||||
///
|
|
||||||
/// Returns whether a FsEntry is a directory
|
|
||||||
pub fn is_dir(&self) -> bool {
|
|
||||||
matches!(self, FsEntry::Directory(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### is_file
|
|
||||||
///
|
|
||||||
/// Returns whether a FsEntry is a File
|
|
||||||
pub fn is_file(&self) -> bool {
|
|
||||||
matches!(self, FsEntry::File(_))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### is_hidden
|
|
||||||
///
|
|
||||||
/// Returns whether FsEntry is hidden
|
|
||||||
pub fn is_hidden(&self) -> bool {
|
|
||||||
self.get_name().starts_with('.')
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### get_realfile
|
|
||||||
///
|
|
||||||
/// Return the real file pointed by a `FsEntry`
|
|
||||||
pub fn get_realfile(&self) -> FsEntry {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => match &dir.symlink {
|
|
||||||
Some(symlink) => symlink.get_realfile(),
|
|
||||||
None => self.clone(),
|
|
||||||
},
|
|
||||||
FsEntry::File(file) => match &file.symlink {
|
|
||||||
Some(symlink) => symlink.get_realfile(),
|
|
||||||
None => self.clone(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### unwrap_file
|
|
||||||
///
|
|
||||||
/// Unwrap FsEntry as FsFile
|
|
||||||
pub fn unwrap_file(self) -> FsFile {
|
|
||||||
match self {
|
|
||||||
FsEntry::File(file) => file,
|
|
||||||
_ => panic!("unwrap_file: not a file"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
/// ### unwrap_dir
|
|
||||||
///
|
|
||||||
/// Unwrap FsEntry as FsDirectory
|
|
||||||
pub fn unwrap_dir(self) -> FsDirectory {
|
|
||||||
match self {
|
|
||||||
FsEntry::Directory(dir) => dir,
|
|
||||||
_ => panic!("unwrap_dir: not a directory"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fs_fsentry_dir() {
|
|
||||||
let t_now: SystemTime = SystemTime::now();
|
|
||||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
|
||||||
name: String::from("foo"),
|
|
||||||
abs_path: PathBuf::from("/foo"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
|
|
||||||
});
|
|
||||||
assert_eq!(entry.get_abs_path(), PathBuf::from("/foo"));
|
|
||||||
assert_eq!(entry.get_name(), String::from("foo"));
|
|
||||||
assert_eq!(entry.get_last_access_time(), t_now);
|
|
||||||
assert_eq!(entry.get_last_change_time(), t_now);
|
|
||||||
assert_eq!(entry.get_creation_time(), t_now);
|
|
||||||
assert_eq!(entry.get_size(), 4096);
|
|
||||||
assert_eq!(entry.get_ftype(), None);
|
|
||||||
assert_eq!(entry.get_user(), Some(0));
|
|
||||||
assert_eq!(entry.get_group(), Some(0));
|
|
||||||
assert_eq!(entry.is_symlink(), false);
|
|
||||||
assert_eq!(entry.is_dir(), true);
|
|
||||||
assert_eq!(entry.is_file(), false);
|
|
||||||
assert_eq!(
|
|
||||||
entry.get_unix_pex(),
|
|
||||||
Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5)))
|
|
||||||
);
|
|
||||||
assert_eq!(entry.unwrap_dir().abs_path, PathBuf::from("/foo"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fs_fsentry_file() {
|
|
||||||
let t_now: SystemTime = SystemTime::now();
|
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
|
||||||
name: String::from("bar.txt"),
|
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
size: 8192,
|
|
||||||
ftype: Some(String::from("txt")),
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
|
||||||
});
|
|
||||||
assert_eq!(entry.get_abs_path(), PathBuf::from("/bar.txt"));
|
|
||||||
assert_eq!(entry.get_name(), String::from("bar.txt"));
|
|
||||||
assert_eq!(entry.get_last_access_time(), t_now);
|
|
||||||
assert_eq!(entry.get_last_change_time(), t_now);
|
|
||||||
assert_eq!(entry.get_creation_time(), t_now);
|
|
||||||
assert_eq!(entry.get_size(), 8192);
|
|
||||||
assert_eq!(entry.get_ftype(), Some(String::from("txt")));
|
|
||||||
assert_eq!(entry.get_user(), Some(0));
|
|
||||||
assert_eq!(entry.get_group(), Some(0));
|
|
||||||
assert_eq!(
|
|
||||||
entry.get_unix_pex(),
|
|
||||||
Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4)))
|
|
||||||
);
|
|
||||||
assert_eq!(entry.is_symlink(), false);
|
|
||||||
assert_eq!(entry.is_dir(), false);
|
|
||||||
assert_eq!(entry.is_file(), true);
|
|
||||||
assert_eq!(entry.unwrap_file().abs_path, PathBuf::from("/bar.txt"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_fs_fsentry_file_unwrap_bad() {
|
|
||||||
let t_now: SystemTime = SystemTime::now();
|
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
|
||||||
name: String::from("bar.txt"),
|
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
size: 8192,
|
|
||||||
ftype: Some(String::from("txt")),
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
|
||||||
});
|
|
||||||
entry.unwrap_dir();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[should_panic]
|
|
||||||
fn test_fs_fsentry_dir_unwrap_bad() {
|
|
||||||
let t_now: SystemTime = SystemTime::now();
|
|
||||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
|
||||||
name: String::from("foo"),
|
|
||||||
abs_path: PathBuf::from("/foo"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
|
|
||||||
});
|
|
||||||
entry.unwrap_file();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fs_fsentry_hidden_files() {
|
|
||||||
let t_now: SystemTime = SystemTime::now();
|
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
|
||||||
name: String::from("bar.txt"),
|
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
size: 8192,
|
|
||||||
ftype: Some(String::from("txt")),
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
|
||||||
});
|
|
||||||
assert_eq!(entry.is_hidden(), false);
|
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
|
||||||
name: String::from(".gitignore"),
|
|
||||||
abs_path: PathBuf::from("/.gitignore"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
size: 8192,
|
|
||||||
ftype: Some(String::from("txt")),
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
|
||||||
});
|
|
||||||
assert_eq!(entry.is_hidden(), true);
|
|
||||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
|
||||||
name: String::from(".git"),
|
|
||||||
abs_path: PathBuf::from("/.git"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
|
|
||||||
});
|
|
||||||
assert_eq!(entry.is_hidden(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fs_fsentry_realfile_none() {
|
|
||||||
let t_now: SystemTime = SystemTime::now();
|
|
||||||
// With file...
|
|
||||||
let entry: FsEntry = FsEntry::File(FsFile {
|
|
||||||
name: String::from("bar.txt"),
|
|
||||||
abs_path: PathBuf::from("/bar.txt"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
size: 8192,
|
|
||||||
ftype: Some(String::from("txt")),
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
|
||||||
});
|
|
||||||
// Symlink is None...
|
|
||||||
assert_eq!(
|
|
||||||
entry.get_realfile().get_abs_path(),
|
|
||||||
PathBuf::from("/bar.txt")
|
|
||||||
);
|
|
||||||
// With directory...
|
|
||||||
let entry: FsEntry = FsEntry::Directory(FsDirectory {
|
|
||||||
name: String::from("foo"),
|
|
||||||
abs_path: PathBuf::from("/foo"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(5), UnixPex::from(5))), // UNIX only
|
|
||||||
});
|
|
||||||
assert_eq!(entry.get_realfile().get_abs_path(), PathBuf::from("/foo"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_fs_fsentry_realfile_some() {
|
|
||||||
let t_now: SystemTime = SystemTime::now();
|
|
||||||
// Prepare entries
|
|
||||||
// root -> child -> target
|
|
||||||
let entry_target: FsEntry = FsEntry::Directory(FsDirectory {
|
|
||||||
name: String::from("projects"),
|
|
||||||
abs_path: PathBuf::from("/home/cvisintin/projects"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))), // UNIX only
|
|
||||||
});
|
|
||||||
let entry_child: FsEntry = FsEntry::Directory(FsDirectory {
|
|
||||||
name: String::from("projects"),
|
|
||||||
abs_path: PathBuf::from("/develop/projects"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
symlink: Some(Box::new(entry_target)),
|
|
||||||
user: Some(0),
|
|
||||||
group: Some(0),
|
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))),
|
|
||||||
});
|
|
||||||
let entry_root: FsEntry = FsEntry::File(FsFile {
|
|
||||||
name: String::from("projects"),
|
|
||||||
abs_path: PathBuf::from("/projects"),
|
|
||||||
last_change_time: t_now,
|
|
||||||
last_access_time: t_now,
|
|
||||||
creation_time: t_now,
|
|
||||||
size: 8,
|
|
||||||
ftype: None,
|
|
||||||
symlink: Some(Box::new(entry_child)),
|
|
||||||
user: Some(0),
|
|
||||||
group: Some(0),
|
|
||||||
unix_pex: Some((UnixPex::from(7), UnixPex::from(7), UnixPex::from(7))),
|
|
||||||
});
|
|
||||||
assert_eq!(entry_root.is_symlink(), true);
|
|
||||||
// get real file
|
|
||||||
let real_file: FsEntry = entry_root.get_realfile();
|
|
||||||
// real file must be projects in /home/cvisintin
|
|
||||||
assert_eq!(
|
|
||||||
real_file.get_abs_path(),
|
|
||||||
PathBuf::from("/home/cvisintin/projects")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unix_pex() {
|
|
||||||
let pex: UnixPex = UnixPex::from(4);
|
|
||||||
assert_eq!(pex.can_read(), true);
|
|
||||||
assert_eq!(pex.can_write(), false);
|
|
||||||
assert_eq!(pex.can_execute(), false);
|
|
||||||
let pex: UnixPex = UnixPex::from(0);
|
|
||||||
assert_eq!(pex.can_read(), false);
|
|
||||||
assert_eq!(pex.can_write(), false);
|
|
||||||
assert_eq!(pex.can_execute(), false);
|
|
||||||
let pex: UnixPex = UnixPex::from(3);
|
|
||||||
assert_eq!(pex.can_read(), false);
|
|
||||||
assert_eq!(pex.can_write(), true);
|
|
||||||
assert_eq!(pex.can_execute(), true);
|
|
||||||
let pex: UnixPex = UnixPex::from(7);
|
|
||||||
assert_eq!(pex.can_read(), true);
|
|
||||||
assert_eq!(pex.can_write(), true);
|
|
||||||
assert_eq!(pex.can_execute(), true);
|
|
||||||
let pex: UnixPex = UnixPex::from(3);
|
|
||||||
assert_eq!(pex.as_byte(), 3);
|
|
||||||
let pex: UnixPex = UnixPex::from(7);
|
|
||||||
assert_eq!(pex.as_byte(), 7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+165
-207
@@ -26,7 +26,10 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// ext
|
// ext
|
||||||
use std::fs::{self, File, Metadata, OpenOptions};
|
#[cfg(target_family = "unix")]
|
||||||
|
use remotefs::fs::UnixPex;
|
||||||
|
use remotefs::fs::{Directory, Entry, File, Metadata};
|
||||||
|
use std::fs::{self, File as StdFile, OpenOptions};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
@@ -38,9 +41,6 @@ use std::fs::set_permissions;
|
|||||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||||
|
|
||||||
// Locals
|
// Locals
|
||||||
#[cfg(target_family = "unix")]
|
|
||||||
use crate::fs::UnixPex;
|
|
||||||
use crate::fs::{FsDirectory, FsEntry, FsFile};
|
|
||||||
use crate::utils::path;
|
use crate::utils::path;
|
||||||
|
|
||||||
/// ## HostErrorType
|
/// ## HostErrorType
|
||||||
@@ -118,7 +118,7 @@ impl std::fmt::Display for HostError {
|
|||||||
/// It provides functions to navigate across the local host file system
|
/// It provides functions to navigate across the local host file system
|
||||||
pub struct Localhost {
|
pub struct Localhost {
|
||||||
wrkdir: PathBuf,
|
wrkdir: PathBuf,
|
||||||
files: Vec<FsEntry>,
|
files: Vec<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Localhost {
|
impl Localhost {
|
||||||
@@ -169,7 +169,7 @@ impl Localhost {
|
|||||||
///
|
///
|
||||||
/// List files in current directory
|
/// List files in current directory
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn list_dir(&self) -> Vec<FsEntry> {
|
pub fn list_dir(&self) -> Vec<Entry> {
|
||||||
self.files.clone()
|
self.files.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +177,7 @@ impl Localhost {
|
|||||||
///
|
///
|
||||||
/// Change working directory with the new provided directory
|
/// Change working directory with the new provided directory
|
||||||
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
|
pub fn change_wrkdir(&mut self, new_dir: &Path) -> Result<PathBuf, HostError> {
|
||||||
let new_dir: PathBuf = self.to_abs_path(new_dir);
|
let new_dir: PathBuf = self.to_path(new_dir);
|
||||||
info!("Changing localhost directory to {}...", new_dir.display());
|
info!("Changing localhost directory to {}...", new_dir.display());
|
||||||
// Check whether directory exists
|
// Check whether directory exists
|
||||||
if !self.file_exists(new_dir.as_path()) {
|
if !self.file_exists(new_dir.as_path()) {
|
||||||
@@ -227,7 +227,7 @@ impl Localhost {
|
|||||||
/// Extended option version of makedir.
|
/// Extended option version of makedir.
|
||||||
/// ignex: don't report error if directory already exists
|
/// ignex: don't report error if directory already exists
|
||||||
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
|
pub fn mkdir_ex(&mut self, dir_name: &Path, ignex: bool) -> Result<(), HostError> {
|
||||||
let dir_path: PathBuf = self.to_abs_path(dir_name);
|
let dir_path: PathBuf = self.to_path(dir_name);
|
||||||
info!("Making directory {}", dir_path.display());
|
info!("Making directory {}", dir_path.display());
|
||||||
// If dir already exists, return Error
|
// If dir already exists, return Error
|
||||||
if dir_path.exists() {
|
if dir_path.exists() {
|
||||||
@@ -265,25 +265,25 @@ impl Localhost {
|
|||||||
/// ### remove
|
/// ### remove
|
||||||
///
|
///
|
||||||
/// Remove file entry
|
/// Remove file entry
|
||||||
pub fn remove(&mut self, entry: &FsEntry) -> Result<(), HostError> {
|
pub fn remove(&mut self, entry: &Entry) -> Result<(), HostError> {
|
||||||
match entry {
|
match entry {
|
||||||
FsEntry::Directory(dir) => {
|
Entry::Directory(dir) => {
|
||||||
// If file doesn't exist; return error
|
// If file doesn't exist; return error
|
||||||
debug!("Removing directory {}", dir.abs_path.display());
|
debug!("Removing directory {}", dir.path.display());
|
||||||
if !dir.abs_path.as_path().exists() {
|
if !dir.path.as_path().exists() {
|
||||||
error!("Directory doesn't exist");
|
error!("Directory doesn't exist");
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
dir.abs_path.as_path(),
|
dir.path.as_path(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Remove
|
// Remove
|
||||||
match std::fs::remove_dir_all(dir.abs_path.as_path()) {
|
match std::fs::remove_dir_all(dir.path.as_path()) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Update dir
|
// Update dir
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
info!("Removed directory {}", dir.abs_path.display());
|
info!("Removed directory {}", dir.path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -291,28 +291,28 @@ impl Localhost {
|
|||||||
Err(HostError::new(
|
Err(HostError::new(
|
||||||
HostErrorType::DeleteFailed,
|
HostErrorType::DeleteFailed,
|
||||||
Some(err),
|
Some(err),
|
||||||
dir.abs_path.as_path(),
|
dir.path.as_path(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FsEntry::File(file) => {
|
Entry::File(file) => {
|
||||||
// If file doesn't exist; return error
|
// If file doesn't exist; return error
|
||||||
debug!("Removing file {}", file.abs_path.display());
|
debug!("Removing file {}", file.path.display());
|
||||||
if !file.abs_path.as_path().exists() {
|
if !file.path.as_path().exists() {
|
||||||
error!("File doesn't exist");
|
error!("File doesn't exist");
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::NoSuchFileOrDirectory,
|
HostErrorType::NoSuchFileOrDirectory,
|
||||||
None,
|
None,
|
||||||
file.abs_path.as_path(),
|
file.path.as_path(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Remove
|
// Remove
|
||||||
match std::fs::remove_file(file.abs_path.as_path()) {
|
match std::fs::remove_file(file.path.as_path()) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Update dir
|
// Update dir
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
info!("Removed file {}", file.abs_path.display());
|
info!("Removed file {}", file.path.display());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -320,7 +320,7 @@ impl Localhost {
|
|||||||
Err(HostError::new(
|
Err(HostError::new(
|
||||||
HostErrorType::DeleteFailed,
|
HostErrorType::DeleteFailed,
|
||||||
Some(err),
|
Some(err),
|
||||||
file.abs_path.as_path(),
|
file.path.as_path(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -331,15 +331,14 @@ impl Localhost {
|
|||||||
/// ### rename
|
/// ### rename
|
||||||
///
|
///
|
||||||
/// Rename file or directory to new name
|
/// Rename file or directory to new name
|
||||||
pub fn rename(&mut self, entry: &FsEntry, dst_path: &Path) -> Result<(), HostError> {
|
pub fn rename(&mut self, entry: &Entry, dst_path: &Path) -> Result<(), HostError> {
|
||||||
let abs_path: PathBuf = entry.get_abs_path();
|
match std::fs::rename(entry.path(), dst_path) {
|
||||||
match std::fs::rename(abs_path.as_path(), dst_path) {
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Scan dir
|
// Scan dir
|
||||||
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
self.files = self.scan_dir(self.wrkdir.as_path())?;
|
||||||
debug!(
|
debug!(
|
||||||
"Moved file {} to {}",
|
"Moved file {} to {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dst_path.display()
|
dst_path.display()
|
||||||
);
|
);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -347,14 +346,14 @@ impl Localhost {
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(
|
error!(
|
||||||
"Failed to move {} to {}: {}",
|
"Failed to move {} to {}: {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dst_path.display(),
|
dst_path.display(),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
Err(HostError::new(
|
Err(HostError::new(
|
||||||
HostErrorType::CouldNotCreateFile,
|
HostErrorType::CouldNotCreateFile,
|
||||||
Some(err),
|
Some(err),
|
||||||
abs_path.as_path(),
|
entry.path(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -363,17 +362,17 @@ impl Localhost {
|
|||||||
/// ### copy
|
/// ### copy
|
||||||
///
|
///
|
||||||
/// Copy file to destination path
|
/// Copy file to destination path
|
||||||
pub fn copy(&mut self, entry: &FsEntry, dst: &Path) -> Result<(), HostError> {
|
pub fn copy(&mut self, entry: &Entry, dst: &Path) -> Result<(), HostError> {
|
||||||
// Get absolute path of dest
|
// Get absolute path of dest
|
||||||
let dst: PathBuf = self.to_abs_path(dst);
|
let dst: PathBuf = self.to_path(dst);
|
||||||
info!(
|
info!(
|
||||||
"Copying file {} to {}",
|
"Copying file {} to {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dst.display()
|
dst.display()
|
||||||
);
|
);
|
||||||
// Match entry
|
// Match entry
|
||||||
match entry {
|
match entry {
|
||||||
FsEntry::File(file) => {
|
Entry::File(file) => {
|
||||||
// Copy file
|
// Copy file
|
||||||
// If destination path is a directory, push file name
|
// If destination path is a directory, push file name
|
||||||
let dst: PathBuf = match dst.as_path().is_dir() {
|
let dst: PathBuf = match dst.as_path().is_dir() {
|
||||||
@@ -385,29 +384,29 @@ impl Localhost {
|
|||||||
false => dst.clone(),
|
false => dst.clone(),
|
||||||
};
|
};
|
||||||
// Copy entry path to dst path
|
// Copy entry path to dst path
|
||||||
if let Err(err) = std::fs::copy(file.abs_path.as_path(), dst.as_path()) {
|
if let Err(err) = std::fs::copy(file.path.as_path(), dst.as_path()) {
|
||||||
error!("Failed to copy file: {}", err);
|
error!("Failed to copy file: {}", err);
|
||||||
return Err(HostError::new(
|
return Err(HostError::new(
|
||||||
HostErrorType::CouldNotCreateFile,
|
HostErrorType::CouldNotCreateFile,
|
||||||
Some(err),
|
Some(err),
|
||||||
file.abs_path.as_path(),
|
file.path.as_path(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
info!("File copied");
|
info!("File copied");
|
||||||
}
|
}
|
||||||
FsEntry::Directory(dir) => {
|
Entry::Directory(dir) => {
|
||||||
// If destination path doesn't exist, create destination
|
// If destination path doesn't exist, create destination
|
||||||
if !dst.exists() {
|
if !dst.exists() {
|
||||||
debug!("Directory {} doesn't exist; creating it", dst.display());
|
debug!("Directory {} doesn't exist; creating it", dst.display());
|
||||||
self.mkdir(dst.as_path())?;
|
self.mkdir(dst.as_path())?;
|
||||||
}
|
}
|
||||||
// Scan dir
|
// Scan dir
|
||||||
let dir_files: Vec<FsEntry> = self.scan_dir(dir.abs_path.as_path())?;
|
let dir_files: Vec<Entry> = self.scan_dir(dir.path.as_path())?;
|
||||||
// Iterate files
|
// Iterate files
|
||||||
for dir_entry in dir_files.iter() {
|
for dir_entry in dir_files.iter() {
|
||||||
// Calculate dst
|
// Calculate dst
|
||||||
let mut sub_dst: PathBuf = dst.clone();
|
let mut sub_dst: PathBuf = dst.clone();
|
||||||
sub_dst.push(dir_entry.get_name());
|
sub_dst.push(dir_entry.name());
|
||||||
// Call function recursively
|
// Call function recursively
|
||||||
self.copy(dir_entry, sub_dst.as_path())?;
|
self.copy(dir_entry, sub_dst.as_path())?;
|
||||||
}
|
}
|
||||||
@@ -439,12 +438,12 @@ impl Localhost {
|
|||||||
|
|
||||||
/// ### stat
|
/// ### stat
|
||||||
///
|
///
|
||||||
/// Stat file and create a FsEntry
|
/// Stat file and create a Entry
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
pub fn stat(&self, path: &Path) -> Result<Entry, HostError> {
|
||||||
info!("Stating file {}", path.display());
|
info!("Stating file {}", path.display());
|
||||||
let path: PathBuf = self.to_abs_path(path);
|
let path: PathBuf = self.to_path(path);
|
||||||
let attr: Metadata = match fs::metadata(path.as_path()) {
|
let attr = match fs::metadata(path.as_path()) {
|
||||||
Ok(metadata) => metadata,
|
Ok(metadata) => metadata,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Could not read file metadata: {}", err);
|
error!("Could not read file metadata: {}", err);
|
||||||
@@ -455,49 +454,38 @@ impl Localhost {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
let name = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
||||||
// Match dir / file
|
// Match dir / file
|
||||||
|
let metadata = Metadata {
|
||||||
|
atime: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||||
|
ctime: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||||
|
gid: Some(attr.gid()),
|
||||||
|
mode: Some(UnixPex::from(attr.mode())),
|
||||||
|
mtime: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||||
|
size: if path.is_dir() {
|
||||||
|
attr.blksize()
|
||||||
|
} else {
|
||||||
|
attr.len()
|
||||||
|
},
|
||||||
|
symlink: fs::read_link(path.as_path()).ok(),
|
||||||
|
uid: Some(attr.uid()),
|
||||||
|
};
|
||||||
Ok(match path.is_dir() {
|
Ok(match path.is_dir() {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => Entry::Directory(Directory {
|
||||||
name: file_name,
|
name,
|
||||||
abs_path: path.clone(),
|
path,
|
||||||
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
metadata,
|
||||||
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
|
|
||||||
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
|
|
||||||
symlink: match fs::read_link(path.as_path()) {
|
|
||||||
Ok(p) => match self.stat(p.as_path()) {
|
|
||||||
Ok(entry) => Some(Box::new(entry)),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
user: Some(attr.uid()),
|
|
||||||
group: Some(attr.gid()),
|
|
||||||
unix_pex: Some(self.u32_to_mode(attr.mode())),
|
|
||||||
}),
|
}),
|
||||||
false => {
|
false => {
|
||||||
// Is File
|
// Is File
|
||||||
let extension: Option<String> = path
|
let extension = path
|
||||||
.extension()
|
.extension()
|
||||||
.map(|s| String::from(s.to_str().unwrap_or("")));
|
.map(|s| String::from(s.to_str().unwrap_or("")));
|
||||||
FsEntry::File(FsFile {
|
Entry::File(File {
|
||||||
name: file_name,
|
name,
|
||||||
abs_path: path.clone(),
|
path,
|
||||||
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
extension,
|
||||||
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
|
metadata,
|
||||||
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
|
|
||||||
size: attr.len() as usize,
|
|
||||||
ftype: extension,
|
|
||||||
symlink: match fs::read_link(path.as_path()) {
|
|
||||||
Ok(p) => match self.stat(p.as_path()) {
|
|
||||||
Ok(entry) => Some(Box::new(entry)),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
Err(_) => None, // Ignore errors
|
|
||||||
},
|
|
||||||
user: Some(attr.uid()),
|
|
||||||
group: Some(attr.gid()),
|
|
||||||
unix_pex: Some(self.u32_to_mode(attr.mode())),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -505,12 +493,12 @@ impl Localhost {
|
|||||||
|
|
||||||
/// ### stat
|
/// ### stat
|
||||||
///
|
///
|
||||||
/// Stat file and create a FsEntry
|
/// Stat file and create a Entry
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub fn stat(&self, path: &Path) -> Result<FsEntry, HostError> {
|
pub fn stat(&self, path: &Path) -> Result<Entry, HostError> {
|
||||||
let path: PathBuf = self.to_abs_path(path);
|
let path: PathBuf = self.to_path(path);
|
||||||
info!("Stating file {}", path.display());
|
info!("Stating file {}", path.display());
|
||||||
let attr: Metadata = match fs::metadata(path.as_path()) {
|
let attr = match fs::metadata(path.as_path()) {
|
||||||
Ok(metadata) => metadata,
|
Ok(metadata) => metadata,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Could not read file metadata: {}", err);
|
error!("Could not read file metadata: {}", err);
|
||||||
@@ -521,49 +509,38 @@ impl Localhost {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let file_name: String = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
let name = String::from(path.file_name().unwrap().to_str().unwrap_or(""));
|
||||||
|
let metadata = Metadata {
|
||||||
|
atime: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||||
|
ctime: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||||
|
mtime: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
||||||
|
size: if path.is_dir() {
|
||||||
|
attr.blksize()
|
||||||
|
} else {
|
||||||
|
attr.len()
|
||||||
|
},
|
||||||
|
symlink: fs::read_link(path.as_path()).ok(),
|
||||||
|
uid: None,
|
||||||
|
gid: None,
|
||||||
|
mode: None,
|
||||||
|
};
|
||||||
// Match dir / file
|
// Match dir / file
|
||||||
Ok(match path.is_dir() {
|
Ok(match path.is_dir() {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => Entry::Directory(Directory {
|
||||||
name: file_name,
|
name,
|
||||||
abs_path: path.clone(),
|
path,
|
||||||
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
metadata,
|
||||||
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
|
|
||||||
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
|
|
||||||
symlink: match fs::read_link(path.as_path()) {
|
|
||||||
Ok(p) => match self.stat(p.as_path()) {
|
|
||||||
Ok(entry) => Some(Box::new(entry)),
|
|
||||||
Err(_) => None, // Ignore errors
|
|
||||||
},
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
user: None,
|
|
||||||
group: None,
|
|
||||||
unix_pex: None,
|
|
||||||
}),
|
}),
|
||||||
false => {
|
false => {
|
||||||
// Is File
|
// Is File
|
||||||
let extension: Option<String> = path
|
let extension = path
|
||||||
.extension()
|
.extension()
|
||||||
.map(|s| String::from(s.to_str().unwrap_or("")));
|
.map(|s| String::from(s.to_str().unwrap_or("")));
|
||||||
FsEntry::File(FsFile {
|
Entry::File(File {
|
||||||
name: file_name,
|
name,
|
||||||
abs_path: path.clone(),
|
path,
|
||||||
last_change_time: attr.modified().unwrap_or(SystemTime::UNIX_EPOCH),
|
extension,
|
||||||
last_access_time: attr.accessed().unwrap_or(SystemTime::UNIX_EPOCH),
|
metadata,
|
||||||
creation_time: attr.created().unwrap_or(SystemTime::UNIX_EPOCH),
|
|
||||||
size: attr.len() as usize,
|
|
||||||
ftype: extension,
|
|
||||||
symlink: match fs::read_link(path.as_path()) {
|
|
||||||
Ok(p) => match self.stat(p.as_path()) {
|
|
||||||
Ok(entry) => Some(Box::new(entry)),
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
Err(_) => None,
|
|
||||||
},
|
|
||||||
user: None,
|
|
||||||
group: None,
|
|
||||||
unix_pex: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -601,13 +578,13 @@ impl Localhost {
|
|||||||
///
|
///
|
||||||
/// Change file mode to file, according to UNIX permissions
|
/// Change file mode to file, according to UNIX permissions
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
pub fn chmod(&self, path: &Path, pex: (u8, u8, u8)) -> Result<(), HostError> {
|
pub fn chmod(&self, path: &Path, pex: UnixPex) -> Result<(), HostError> {
|
||||||
let path: PathBuf = self.to_abs_path(path);
|
let path: PathBuf = self.to_path(path);
|
||||||
// Get metadta
|
// Get metadta
|
||||||
match fs::metadata(path.as_path()) {
|
match fs::metadata(path.as_path()) {
|
||||||
Ok(metadata) => {
|
Ok(metadata) => {
|
||||||
let mut mpex = metadata.permissions();
|
let mut mpex = metadata.permissions();
|
||||||
mpex.set_mode(self.mode_to_u32(pex));
|
mpex.set_mode(pex.into());
|
||||||
match set_permissions(path.as_path(), mpex) {
|
match set_permissions(path.as_path(), mpex) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
info!("Changed mode for {} to {:?}", path.display(), pex);
|
info!("Changed mode for {} to {:?}", path.display(), pex);
|
||||||
@@ -641,8 +618,8 @@ impl Localhost {
|
|||||||
/// ### open_file_read
|
/// ### open_file_read
|
||||||
///
|
///
|
||||||
/// Open file for read
|
/// Open file for read
|
||||||
pub fn open_file_read(&self, file: &Path) -> Result<File, HostError> {
|
pub fn open_file_read(&self, file: &Path) -> Result<StdFile, HostError> {
|
||||||
let file: PathBuf = self.to_abs_path(file);
|
let file: PathBuf = self.to_path(file);
|
||||||
info!("Opening file {} for read", file.display());
|
info!("Opening file {} for read", file.display());
|
||||||
if !self.file_exists(file.as_path()) {
|
if !self.file_exists(file.as_path()) {
|
||||||
error!("File doesn't exist!");
|
error!("File doesn't exist!");
|
||||||
@@ -673,8 +650,8 @@ impl Localhost {
|
|||||||
/// ### open_file_write
|
/// ### open_file_write
|
||||||
///
|
///
|
||||||
/// Open file for write
|
/// Open file for write
|
||||||
pub fn open_file_write(&self, file: &Path) -> Result<File, HostError> {
|
pub fn open_file_write(&self, file: &Path) -> Result<StdFile, HostError> {
|
||||||
let file: PathBuf = self.to_abs_path(file);
|
let file: PathBuf = self.to_path(file);
|
||||||
info!("Opening file {} for write", file.display());
|
info!("Opening file {} for write", file.display());
|
||||||
match OpenOptions::new()
|
match OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
@@ -711,11 +688,11 @@ impl Localhost {
|
|||||||
/// ### scan_dir
|
/// ### scan_dir
|
||||||
///
|
///
|
||||||
/// Get content of the current directory as a list of fs entry
|
/// Get content of the current directory as a list of fs entry
|
||||||
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<FsEntry>, HostError> {
|
pub fn scan_dir(&self, dir: &Path) -> Result<Vec<Entry>, HostError> {
|
||||||
info!("Reading directory {}", dir.display());
|
info!("Reading directory {}", dir.display());
|
||||||
match std::fs::read_dir(dir) {
|
match std::fs::read_dir(dir) {
|
||||||
Ok(e) => {
|
Ok(e) => {
|
||||||
let mut fs_entries: Vec<FsEntry> = Vec::new();
|
let mut fs_entries: Vec<Entry> = Vec::new();
|
||||||
for entry in e.flatten() {
|
for entry in e.flatten() {
|
||||||
// NOTE: 0.4.1, don't fail if stat for one file fails
|
// NOTE: 0.4.1, don't fail if stat for one file fails
|
||||||
match self.stat(entry.path().as_path()) {
|
match self.stat(entry.path().as_path()) {
|
||||||
@@ -737,7 +714,7 @@ impl Localhost {
|
|||||||
///
|
///
|
||||||
/// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course.
|
/// Find files matching `search` on localhost starting from current directory. Search supports recursive search of course.
|
||||||
/// The `search` argument supports wilcards ('*', '?')
|
/// The `search` argument supports wilcards ('*', '?')
|
||||||
pub fn find(&self, search: &str) -> Result<Vec<FsEntry>, HostError> {
|
pub fn find(&self, search: &str) -> Result<Vec<Entry>, HostError> {
|
||||||
self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search))
|
self.iter_search(self.wrkdir.as_path(), &WildMatch::new(search))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,9 +725,9 @@ impl Localhost {
|
|||||||
/// Recursive call for `find` method.
|
/// Recursive call for `find` method.
|
||||||
/// Search in current directory for files which match `filter`.
|
/// Search in current directory for files which match `filter`.
|
||||||
/// If a directory is found in current directory, `iter_search` will be called using that dir as argument.
|
/// If a directory is found in current directory, `iter_search` will be called using that dir as argument.
|
||||||
fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result<Vec<FsEntry>, HostError> {
|
fn iter_search(&self, dir: &Path, filter: &WildMatch) -> Result<Vec<Entry>, HostError> {
|
||||||
// Scan directory
|
// Scan directory
|
||||||
let mut drained: Vec<FsEntry> = Vec::new();
|
let mut drained: Vec<Entry> = Vec::new();
|
||||||
match self.scan_dir(dir) {
|
match self.scan_dir(dir) {
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
@@ -763,16 +740,16 @@ impl Localhost {
|
|||||||
*/
|
*/
|
||||||
for entry in entries.iter() {
|
for entry in entries.iter() {
|
||||||
match entry {
|
match entry {
|
||||||
FsEntry::Directory(dir) => {
|
Entry::Directory(dir) => {
|
||||||
// If directory matches; push directory to drained
|
// If directory matches; push directory to drained
|
||||||
if filter.matches(dir.name.as_str()) {
|
if filter.matches(dir.name.as_str()) {
|
||||||
drained.push(FsEntry::Directory(dir.clone()));
|
drained.push(Entry::Directory(dir.clone()));
|
||||||
}
|
}
|
||||||
drained.append(&mut self.iter_search(dir.abs_path.as_path(), filter)?);
|
drained.append(&mut self.iter_search(dir.path.as_path(), filter)?);
|
||||||
}
|
}
|
||||||
FsEntry::File(file) => {
|
Entry::File(file) => {
|
||||||
if filter.matches(file.name.as_str()) {
|
if filter.matches(file.name.as_str()) {
|
||||||
drained.push(FsEntry::File(file.clone()));
|
drained.push(Entry::File(file.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -782,29 +759,10 @@ impl Localhost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### u32_to_mode
|
/// ### to_path
|
||||||
///
|
|
||||||
/// Return string with format xxxxxx to tuple of permissions (user, group, others)
|
|
||||||
#[cfg(target_family = "unix")]
|
|
||||||
fn u32_to_mode(&self, mode: u32) -> (UnixPex, UnixPex, UnixPex) {
|
|
||||||
let user: UnixPex = UnixPex::from(((mode >> 6) & 0x7) as u8);
|
|
||||||
let group: UnixPex = UnixPex::from(((mode >> 3) & 0x7) as u8);
|
|
||||||
let others: UnixPex = UnixPex::from((mode & 0x7) as u8);
|
|
||||||
(user, group, others)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// mode_to_u32
|
|
||||||
///
|
|
||||||
/// Convert owner,group,others to u32
|
|
||||||
#[cfg(target_family = "unix")]
|
|
||||||
fn mode_to_u32(&self, mode: (u8, u8, u8)) -> u32 {
|
|
||||||
((mode.0 as u32) << 6) + ((mode.1 as u32) << 3) + mode.2 as u32
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### to_abs_path
|
|
||||||
///
|
///
|
||||||
/// Convert path to absolute path
|
/// Convert path to absolute path
|
||||||
fn to_abs_path(&self, p: &Path) -> PathBuf {
|
fn to_path(&self, p: &Path) -> PathBuf {
|
||||||
path::absolutize(self.wrkdir.as_path(), p)
|
path::absolutize(self.wrkdir.as_path(), p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -819,7 +777,7 @@ mod tests {
|
|||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
use std::fs::File;
|
use std::fs::File as StdFile;
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
@@ -970,7 +928,7 @@ mod tests {
|
|||||||
fn test_host_localhost_symlinks() {
|
fn test_host_localhost_symlinks() {
|
||||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||||
// Create sample file
|
// Create sample file
|
||||||
assert!(File::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
|
assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
|
||||||
// Create symlink
|
// Create symlink
|
||||||
assert!(symlink(
|
assert!(symlink(
|
||||||
format!("{}/foo.txt", tmpdir.path().display()),
|
format!("{}/foo.txt", tmpdir.path().display()),
|
||||||
@@ -979,33 +937,33 @@ mod tests {
|
|||||||
.is_ok());
|
.is_ok());
|
||||||
// Get dir
|
// Get dir
|
||||||
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||||
let files: Vec<FsEntry> = host.list_dir();
|
let files: Vec<Entry> = host.list_dir();
|
||||||
// Verify files
|
// Verify files
|
||||||
let file_0: &FsEntry = files.get(0).unwrap();
|
let file_0: &Entry = files.get(0).unwrap();
|
||||||
match file_0 {
|
match file_0 {
|
||||||
FsEntry::File(file_0) => {
|
Entry::File(file_0) => {
|
||||||
if file_0.name == String::from("foo.txt") {
|
if file_0.name == String::from("foo.txt") {
|
||||||
assert!(file_0.symlink.is_none());
|
assert!(file_0.metadata.symlink.is_none());
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*file_0.symlink.as_ref().unwrap().get_abs_path(),
|
file_0.metadata.symlink.as_ref().unwrap(),
|
||||||
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
|
&PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => panic!("expected entry 0 to be file: {:?}", file_0),
|
_ => panic!("expected entry 0 to be file: {:?}", file_0),
|
||||||
};
|
};
|
||||||
// Verify simlink
|
// Verify simlink
|
||||||
let file_1: &FsEntry = files.get(1).unwrap();
|
let file_1: &Entry = files.get(1).unwrap();
|
||||||
match file_1 {
|
match file_1 {
|
||||||
FsEntry::File(file_1) => {
|
Entry::File(file_1) => {
|
||||||
if file_1.name == String::from("bar.txt") {
|
if file_1.name == String::from("bar.txt") {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*file_1.symlink.as_ref().unwrap().get_abs_path(),
|
file_1.metadata.symlink.as_ref().unwrap(),
|
||||||
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
|
&PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()))
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
assert!(file_1.symlink.is_none());
|
assert!(file_1.metadata.symlink.is_none());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => panic!("expected entry 0 to be file: {:?}", file_1),
|
_ => panic!("expected entry 0 to be file: {:?}", file_1),
|
||||||
@@ -1017,10 +975,10 @@ mod tests {
|
|||||||
fn test_host_localhost_mkdir() {
|
fn test_host_localhost_mkdir() {
|
||||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||||
let files: Vec<FsEntry> = host.list_dir();
|
let files: Vec<Entry> = host.list_dir();
|
||||||
assert_eq!(files.len(), 0); // There should be 0 files now
|
assert_eq!(files.len(), 0); // There should be 0 files now
|
||||||
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
|
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
|
||||||
let files: Vec<FsEntry> = host.list_dir();
|
let files: Vec<Entry> = host.list_dir();
|
||||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||||
// Try to re-create directory
|
// Try to re-create directory
|
||||||
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
|
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_err());
|
||||||
@@ -1042,19 +1000,19 @@ mod tests {
|
|||||||
fn test_host_localhost_remove() {
|
fn test_host_localhost_remove() {
|
||||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||||
// Create sample file
|
// Create sample file
|
||||||
assert!(File::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
|
assert!(StdFile::create(format!("{}/foo.txt", tmpdir.path().display()).as_str()).is_ok());
|
||||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||||
let files: Vec<FsEntry> = host.list_dir();
|
let files: Vec<Entry> = host.list_dir();
|
||||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||||
// Remove file
|
// Remove file
|
||||||
assert!(host.remove(files.get(0).unwrap()).is_ok());
|
assert!(host.remove(files.get(0).unwrap()).is_ok());
|
||||||
// There should be 0 files now
|
// There should be 0 files now
|
||||||
let files: Vec<FsEntry> = host.list_dir();
|
let files: Vec<Entry> = host.list_dir();
|
||||||
assert_eq!(files.len(), 0); // There should be 0 files now
|
assert_eq!(files.len(), 0); // There should be 0 files now
|
||||||
// Create directory
|
// Create directory
|
||||||
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
|
assert!(host.mkdir(PathBuf::from("test_dir").as_path()).is_ok());
|
||||||
// Delete directory
|
// Delete directory
|
||||||
let files: Vec<FsEntry> = host.list_dir();
|
let files: Vec<Entry> = host.list_dir();
|
||||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||||
assert!(host.remove(files.get(0).unwrap()).is_ok());
|
assert!(host.remove(files.get(0).unwrap()).is_ok());
|
||||||
// Remove unexisting directory
|
// Remove unexisting directory
|
||||||
@@ -1073,11 +1031,11 @@ mod tests {
|
|||||||
// Create sample file
|
// Create sample file
|
||||||
let src_path: PathBuf =
|
let src_path: PathBuf =
|
||||||
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str());
|
PathBuf::from(format!("{}/foo.txt", tmpdir.path().display()).as_str());
|
||||||
assert!(File::create(src_path.as_path()).is_ok());
|
assert!(StdFile::create(src_path.as_path()).is_ok());
|
||||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||||
let files: Vec<FsEntry> = host.list_dir();
|
let files: Vec<Entry> = host.list_dir();
|
||||||
assert_eq!(files.len(), 1); // There should be 1 file now
|
assert_eq!(files.len(), 1); // There should be 1 file now
|
||||||
assert_eq!(files.get(0).unwrap().get_name(), "foo.txt");
|
assert_eq!(files.get(0).unwrap().name(), "foo.txt");
|
||||||
// Rename file
|
// Rename file
|
||||||
let dst_path: PathBuf =
|
let dst_path: PathBuf =
|
||||||
PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str());
|
PathBuf::from(format!("{}/bar.txt", tmpdir.path().display()).as_str());
|
||||||
@@ -1085,9 +1043,9 @@ mod tests {
|
|||||||
.rename(files.get(0).unwrap(), dst_path.as_path())
|
.rename(files.get(0).unwrap(), dst_path.as_path())
|
||||||
.is_ok());
|
.is_ok());
|
||||||
// There should be still 1 file now, but named bar.txt
|
// There should be still 1 file now, but named bar.txt
|
||||||
let files: Vec<FsEntry> = host.list_dir();
|
let files: Vec<Entry> = host.list_dir();
|
||||||
assert_eq!(files.len(), 1); // There should be 0 files now
|
assert_eq!(files.len(), 1); // There should be 0 files now
|
||||||
assert_eq!(files.get(0).unwrap().get_name(), "bar.txt");
|
assert_eq!(files.get(0).unwrap().name(), "bar.txt");
|
||||||
// Fail
|
// Fail
|
||||||
let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu");
|
let bad_path: PathBuf = PathBuf::from("/asdailsjoidoewojdijow/ashdiuahu");
|
||||||
assert!(host
|
assert!(host
|
||||||
@@ -1101,16 +1059,16 @@ mod tests {
|
|||||||
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
let tmpdir: tempfile::TempDir = tempfile::TempDir::new().unwrap();
|
||||||
let file: tempfile::NamedTempFile = create_sample_file();
|
let file: tempfile::NamedTempFile = create_sample_file();
|
||||||
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
let host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||||
// mode_to_u32
|
|
||||||
assert_eq!(host.mode_to_u32((6, 4, 4)), 0o644);
|
|
||||||
assert_eq!(host.mode_to_u32((7, 7, 5)), 0o775);
|
|
||||||
// Chmod to file
|
// Chmod to file
|
||||||
assert!(host.chmod(file.path(), (7, 7, 5)).is_ok());
|
assert!(host.chmod(file.path(), UnixPex::from(0o755)).is_ok());
|
||||||
// Chmod to dir
|
// Chmod to dir
|
||||||
assert!(host.chmod(tmpdir.path(), (7, 5, 0)).is_ok());
|
assert!(host.chmod(tmpdir.path(), UnixPex::from(0o750)).is_ok());
|
||||||
// Error
|
// Error
|
||||||
assert!(host
|
assert!(host
|
||||||
.chmod(Path::new("/tmp/krgiogoiegj/kwrgnoerig"), (7, 7, 7))
|
.chmod(
|
||||||
|
Path::new("/tmp/krgiogoiegj/kwrgnoerig"),
|
||||||
|
UnixPex::from(0o777)
|
||||||
|
)
|
||||||
.is_err());
|
.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1122,15 +1080,15 @@ mod tests {
|
|||||||
let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
|
let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
|
||||||
file1_path.push("foo.txt");
|
file1_path.push("foo.txt");
|
||||||
// Write file 1
|
// Write file 1
|
||||||
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap();
|
let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
|
||||||
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
||||||
// Get file 2 path
|
// Get file 2 path
|
||||||
let mut file2_path: PathBuf = PathBuf::from(tmpdir.path());
|
let mut file2_path: PathBuf = PathBuf::from(tmpdir.path());
|
||||||
file2_path.push("bar.txt");
|
file2_path.push("bar.txt");
|
||||||
// Create host
|
// Create host
|
||||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||||
let file1_entry: FsEntry = host.files.get(0).unwrap().clone();
|
let file1_entry: Entry = host.files.get(0).unwrap().clone();
|
||||||
assert_eq!(file1_entry.get_name(), String::from("foo.txt"));
|
assert_eq!(file1_entry.name(), String::from("foo.txt"));
|
||||||
// Copy
|
// Copy
|
||||||
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
|
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
|
||||||
// Verify host has two files
|
// Verify host has two files
|
||||||
@@ -1152,14 +1110,14 @@ mod tests {
|
|||||||
let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
|
let mut file1_path: PathBuf = PathBuf::from(tmpdir.path());
|
||||||
file1_path.push("foo.txt");
|
file1_path.push("foo.txt");
|
||||||
// Write file 1
|
// Write file 1
|
||||||
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap();
|
let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
|
||||||
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
||||||
// Get file 2 path
|
// Get file 2 path
|
||||||
let file2_path: PathBuf = PathBuf::from("bar.txt");
|
let file2_path: PathBuf = PathBuf::from("bar.txt");
|
||||||
// Create host
|
// Create host
|
||||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||||
let file1_entry: FsEntry = host.files.get(0).unwrap().clone();
|
let file1_entry: Entry = host.files.get(0).unwrap().clone();
|
||||||
assert_eq!(file1_entry.get_name(), String::from("foo.txt"));
|
assert_eq!(file1_entry.name(), String::from("foo.txt"));
|
||||||
// Copy
|
// Copy
|
||||||
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
|
assert!(host.copy(&file1_entry, file2_path.as_path()).is_ok());
|
||||||
// Verify host has two files
|
// Verify host has two files
|
||||||
@@ -1178,15 +1136,15 @@ mod tests {
|
|||||||
let mut file1_path: PathBuf = dir_src.clone();
|
let mut file1_path: PathBuf = dir_src.clone();
|
||||||
file1_path.push("foo.txt");
|
file1_path.push("foo.txt");
|
||||||
// Write file 1
|
// Write file 1
|
||||||
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap();
|
let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
|
||||||
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
||||||
// Copy dir src to dir ddest
|
// Copy dir src to dir ddest
|
||||||
let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path());
|
let mut dir_dest: PathBuf = PathBuf::from(tmpdir.path());
|
||||||
dir_dest.push("test_dest_dir/");
|
dir_dest.push("test_dest_dir/");
|
||||||
// Create host
|
// Create host
|
||||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||||
let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone();
|
let dir_src_entry: Entry = host.files.get(0).unwrap().clone();
|
||||||
assert_eq!(dir_src_entry.get_name(), String::from("test_dir"));
|
assert_eq!(dir_src_entry.name(), String::from("test_dir"));
|
||||||
// Copy
|
// Copy
|
||||||
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
|
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
|
||||||
// Verify host has two files
|
// Verify host has two files
|
||||||
@@ -1209,14 +1167,14 @@ mod tests {
|
|||||||
let mut file1_path: PathBuf = dir_src.clone();
|
let mut file1_path: PathBuf = dir_src.clone();
|
||||||
file1_path.push("foo.txt");
|
file1_path.push("foo.txt");
|
||||||
// Write file 1
|
// Write file 1
|
||||||
let mut file1: File = File::create(file1_path.as_path()).ok().unwrap();
|
let mut file1 = StdFile::create(file1_path.as_path()).ok().unwrap();
|
||||||
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
assert!(file1.write_all(b"Hello world!\n").is_ok());
|
||||||
// Copy dir src to dir ddest
|
// Copy dir src to dir ddest
|
||||||
let dir_dest: PathBuf = PathBuf::from("test_dest_dir/");
|
let dir_dest: PathBuf = PathBuf::from("test_dest_dir/");
|
||||||
// Create host
|
// Create host
|
||||||
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
let mut host: Localhost = Localhost::new(PathBuf::from(tmpdir.path())).ok().unwrap();
|
||||||
let dir_src_entry: FsEntry = host.files.get(0).unwrap().clone();
|
let dir_src_entry: Entry = host.files.get(0).unwrap().clone();
|
||||||
assert_eq!(dir_src_entry.get_name(), String::from("test_dir"));
|
assert_eq!(dir_src_entry.name(), String::from("test_dir"));
|
||||||
// Copy
|
// Copy
|
||||||
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
|
assert!(host.copy(&dir_src_entry, dir_dest.as_path()).is_ok());
|
||||||
// Verify host has two files
|
// Verify host has two files
|
||||||
@@ -1255,20 +1213,20 @@ mod tests {
|
|||||||
assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok());
|
assert!(make_file_at(subdir.as_path(), "examples.csv").is_ok());
|
||||||
let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
|
let host: Localhost = Localhost::new(PathBuf::from(dir_path)).ok().unwrap();
|
||||||
// Find txt files
|
// Find txt files
|
||||||
let mut result: Vec<FsEntry> = host.find("*.txt").ok().unwrap();
|
let mut result: Vec<Entry> = host.find("*.txt").ok().unwrap();
|
||||||
result.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase());
|
result.sort_by_key(|x: &Entry| x.name().to_lowercase());
|
||||||
// There should be 3 entries
|
// There should be 3 entries
|
||||||
assert_eq!(result.len(), 3);
|
assert_eq!(result.len(), 3);
|
||||||
// Check names (they should be sorted alphabetically already; NOTE: examples/ comes before pippo.txt)
|
// Check names (they should be sorted alphabetically already; NOTE: examples/ comes before pippo.txt)
|
||||||
assert_eq!(result[0].get_name(), "errors.txt");
|
assert_eq!(result[0].name(), "errors.txt");
|
||||||
assert_eq!(result[1].get_name(), "omar.txt");
|
assert_eq!(result[1].name(), "omar.txt");
|
||||||
assert_eq!(result[2].get_name(), "pippo.txt");
|
assert_eq!(result[2].name(), "pippo.txt");
|
||||||
// Search for directory
|
// Search for directory
|
||||||
let mut result: Vec<FsEntry> = host.find("examples*").ok().unwrap();
|
let mut result: Vec<Entry> = host.find("examples*").ok().unwrap();
|
||||||
result.sort_by_key(|x: &FsEntry| x.get_name().to_lowercase());
|
result.sort_by_key(|x: &Entry| x.name().to_lowercase());
|
||||||
assert_eq!(result.len(), 2);
|
assert_eq!(result.len(), 2);
|
||||||
assert_eq!(result[0].get_name(), "examples");
|
assert_eq!(result[0].name(), "examples");
|
||||||
assert_eq!(result[1].get_name(), "examples.csv");
|
assert_eq!(result[1].name(), "examples.csv");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
+1
-1
@@ -45,8 +45,8 @@ use std::time::Duration;
|
|||||||
// Include
|
// Include
|
||||||
mod activity_manager;
|
mod activity_manager;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod explorer;
|
||||||
mod filetransfer;
|
mod filetransfer;
|
||||||
mod fs;
|
|
||||||
mod host;
|
mod host;
|
||||||
mod support;
|
mod support;
|
||||||
mod system;
|
mod system;
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ pub struct Release {
|
|||||||
///
|
///
|
||||||
/// The update structure defines the options used to install the update.
|
/// The update structure defines the options used to install the update.
|
||||||
/// Once you're fine with the options, just call the `upgrade()` method to upgrade termscp.
|
/// Once you're fine with the options, just call the `upgrade()` method to upgrade termscp.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Update {
|
pub struct Update {
|
||||||
ask_confirm: bool,
|
ask_confirm: bool,
|
||||||
progress: bool,
|
progress: bool,
|
||||||
@@ -141,16 +141,6 @@ impl Update {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Update {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
progress: false,
|
|
||||||
ask_confirm: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Status> for UpdateStatus {
|
impl From<Status> for UpdateStatus {
|
||||||
fn from(s: Status) -> Self {
|
fn from(s: Status) -> Self {
|
||||||
match s {
|
match s {
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ use crate::config::{
|
|||||||
params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD},
|
params::{UserConfig, DEFAULT_NOTIFICATION_TRANSFER_THRESHOLD},
|
||||||
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
serialization::{deserialize, serialize, SerializerError, SerializerErrorKind},
|
||||||
};
|
};
|
||||||
|
use crate::explorer::GroupDirs;
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
use crate::fs::explorer::GroupDirs;
|
|
||||||
// Ext
|
// Ext
|
||||||
use std::fs::{create_dir, remove_file, File, OpenOptions};
|
use std::fs::{create_dir, remove_file, File, OpenOptions};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|||||||
@@ -28,8 +28,9 @@
|
|||||||
// Locals
|
// Locals
|
||||||
use super::config_client::ConfigClient;
|
use super::config_client::ConfigClient;
|
||||||
// Ext
|
// Ext
|
||||||
|
use remotefs::client::ssh::SshKeyStorage as SshKeyStorageT;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
pub struct SshKeyStorage {
|
pub struct SshKeyStorage {
|
||||||
hosts: HashMap<String, PathBuf>, // Association between {user}@{host} and RSA key path
|
hosts: HashMap<String, PathBuf>, // Association between {user}@{host} and RSA key path
|
||||||
@@ -74,14 +75,6 @@ impl SshKeyStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### resolve
|
|
||||||
///
|
|
||||||
/// Return RSA key path from host and username
|
|
||||||
pub fn resolve(&self, host: &str, username: &str) -> Option<&PathBuf> {
|
|
||||||
let key: String = Self::make_mapkey(host, username);
|
|
||||||
self.hosts.get(&key)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### make_mapkey
|
/// ### make_mapkey
|
||||||
///
|
///
|
||||||
/// Make mapkey from host and username
|
/// Make mapkey from host and username
|
||||||
@@ -100,6 +93,13 @@ impl SshKeyStorage {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SshKeyStorageT for SshKeyStorage {
|
||||||
|
fn resolve(&self, host: &str, username: &str) -> Option<&Path> {
|
||||||
|
let key: String = Self::make_mapkey(host, username);
|
||||||
|
self.hosts.get(&key).map(|x| x.as_path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
|
|||||||
@@ -52,19 +52,11 @@ use tuirealm::{Component, MockComponent};
|
|||||||
|
|
||||||
// -- global listener
|
// -- global listener
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Default, MockComponent)]
|
||||||
pub struct GlobalListener {
|
pub struct GlobalListener {
|
||||||
component: Phantom,
|
component: Phantom,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GlobalListener {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
component: Phantom::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for GlobalListener {
|
impl Component<Msg, NoUserEvent> for GlobalListener {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
|
|||||||
@@ -26,7 +26,9 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry};
|
use super::FileTransferActivity;
|
||||||
|
|
||||||
|
use remotefs::Directory;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -34,71 +36,25 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Enter a directory on local host from entry
|
/// Enter a directory on local host from entry
|
||||||
/// Return true whether the directory changed
|
/// Return true whether the directory changed
|
||||||
pub(crate) fn action_enter_local_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
|
pub(crate) fn action_enter_local_dir(&mut self, dir: Directory, block_sync: bool) -> bool {
|
||||||
match entry {
|
self.local_changedir(dir.path.as_path(), true);
|
||||||
FsEntry::Directory(dir) => {
|
|
||||||
self.local_changedir(dir.abs_path.as_path(), true);
|
|
||||||
if self.browser.sync_browsing && !block_sync {
|
if self.browser.sync_browsing && !block_sync {
|
||||||
self.action_change_remote_dir(dir.name, true);
|
self.action_change_remote_dir(dir.name, true);
|
||||||
}
|
}
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### action_enter_remote_dir
|
/// ### action_enter_remote_dir
|
||||||
///
|
///
|
||||||
/// Enter a directory on local host from entry
|
/// Enter a directory on local host from entry
|
||||||
/// Return true whether the directory changed
|
/// Return true whether the directory changed
|
||||||
pub(crate) fn action_enter_remote_dir(&mut self, entry: FsEntry, block_sync: bool) -> bool {
|
pub(crate) fn action_enter_remote_dir(&mut self, dir: Directory, block_sync: bool) -> bool {
|
||||||
match entry {
|
self.remote_changedir(dir.path.as_path(), true);
|
||||||
FsEntry::Directory(dir) => {
|
|
||||||
self.remote_changedir(dir.abs_path.as_path(), true);
|
|
||||||
if self.browser.sync_browsing && !block_sync {
|
if self.browser.sync_browsing && !block_sync {
|
||||||
self.action_change_local_dir(dir.name, true);
|
self.action_change_local_dir(dir.name, true);
|
||||||
}
|
}
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### action_change_local_dir
|
/// ### action_change_local_dir
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -26,9 +26,9 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
|
use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
|
||||||
use crate::filetransfer::FileTransferErrorType;
|
|
||||||
use crate::fs::FsFile;
|
use remotefs::{Entry, RemoteErrorType};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -49,7 +49,7 @@ impl FileTransferActivity {
|
|||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for entry in entries.iter() {
|
||||||
let mut dest_path: PathBuf = base_path.clone();
|
let mut dest_path: PathBuf = base_path.clone();
|
||||||
dest_path.push(entry.get_name());
|
dest_path.push(entry.name());
|
||||||
self.local_copy_file(entry, dest_path.as_path());
|
self.local_copy_file(entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
// Reload entries
|
// Reload entries
|
||||||
@@ -76,7 +76,7 @@ impl FileTransferActivity {
|
|||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.into_iter() {
|
for entry in entries.into_iter() {
|
||||||
let mut dest_path: PathBuf = base_path.clone();
|
let mut dest_path: PathBuf = base_path.clone();
|
||||||
dest_path.push(entry.get_name());
|
dest_path.push(entry.name());
|
||||||
self.remote_copy_file(entry, dest_path.as_path());
|
self.remote_copy_file(entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
// Reload entries
|
// Reload entries
|
||||||
@@ -86,14 +86,14 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_copy_file(&mut self, entry: &FsEntry, dest: &Path) {
|
fn local_copy_file(&mut self, entry: &Entry, dest: &Path) {
|
||||||
match self.host.copy(entry, dest) {
|
match self.host.copy(entry, dest) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Copied \"{}\" to \"{}\"",
|
"Copied \"{}\" to \"{}\"",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display()
|
dest.display()
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -102,7 +102,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not copy \"{}\" to \"{}\": {}",
|
"Could not copy \"{}\" to \"{}\": {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display(),
|
dest.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
@@ -110,20 +110,20 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remote_copy_file(&mut self, entry: FsEntry, dest: &Path) {
|
fn remote_copy_file(&mut self, entry: Entry, dest: &Path) {
|
||||||
match self.client.as_mut().copy(&entry, dest) {
|
match self.client.as_mut().copy(entry.path(), dest) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Copied \"{}\" to \"{}\"",
|
"Copied \"{}\" to \"{}\"",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display()
|
dest.display()
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => match err.kind() {
|
Err(err) => match err.kind {
|
||||||
FileTransferErrorType::UnsupportedFeature => {
|
RemoteErrorType::UnsupportedFeature => {
|
||||||
// If copy is not supported, perform the tricky copy
|
// If copy is not supported, perform the tricky copy
|
||||||
let _ = self.tricky_copy(entry, dest);
|
let _ = self.tricky_copy(entry, dest);
|
||||||
}
|
}
|
||||||
@@ -131,7 +131,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not copy \"{}\" to \"{}\": {}",
|
"Could not copy \"{}\" to \"{}\": {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display(),
|
dest.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
@@ -143,12 +143,12 @@ impl FileTransferActivity {
|
|||||||
/// ### tricky_copy
|
/// ### tricky_copy
|
||||||
///
|
///
|
||||||
/// Tricky copy will be used whenever copy command is not available on remote host
|
/// Tricky copy will be used whenever copy command is not available on remote host
|
||||||
pub(super) fn tricky_copy(&mut self, entry: FsEntry, dest: &Path) -> Result<(), String> {
|
pub(super) fn tricky_copy(&mut self, entry: Entry, dest: &Path) -> Result<(), String> {
|
||||||
// NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen
|
// NOTE: VERY IMPORTANT; wait block must be umounted or something really bad will happen
|
||||||
self.umount_wait();
|
self.umount_wait();
|
||||||
// match entry
|
// match entry
|
||||||
match entry {
|
match entry {
|
||||||
FsEntry::File(entry) => {
|
Entry::File(entry) => {
|
||||||
// Create tempfile
|
// Create tempfile
|
||||||
let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() {
|
let tmpfile: tempfile::NamedTempFile = match tempfile::NamedTempFile::new() {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
@@ -162,7 +162,7 @@ impl FileTransferActivity {
|
|||||||
};
|
};
|
||||||
// Download file
|
// Download file
|
||||||
let name = entry.name.clone();
|
let name = entry.name.clone();
|
||||||
let entry_path = entry.abs_path.clone();
|
let entry_path = entry.path.clone();
|
||||||
if let Err(err) =
|
if let Err(err) =
|
||||||
self.filetransfer_recv(TransferPayload::File(entry), tmpfile.path(), Some(name))
|
self.filetransfer_recv(TransferPayload::File(entry), tmpfile.path(), Some(name))
|
||||||
{
|
{
|
||||||
@@ -173,7 +173,7 @@ impl FileTransferActivity {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.path()) {
|
let tmpfile_entry = match self.host.stat(tmpfile.path()) {
|
||||||
Ok(e) => e.unwrap_file(),
|
Ok(e) => e.unwrap_file(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
@@ -206,7 +206,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
FsEntry::Directory(_) => {
|
Entry::Directory(_) => {
|
||||||
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
|
let tempdir: tempfile::TempDir = match tempfile::TempDir::new() {
|
||||||
Ok(d) => d,
|
Ok(d) => d,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -219,7 +219,7 @@ impl FileTransferActivity {
|
|||||||
};
|
};
|
||||||
// Get path of dest
|
// Get path of dest
|
||||||
let mut tempdir_path: PathBuf = tempdir.path().to_path_buf();
|
let mut tempdir_path: PathBuf = tempdir.path().to_path_buf();
|
||||||
tempdir_path.push(entry.get_name());
|
tempdir_path.push(entry.name());
|
||||||
// Download file
|
// Download file
|
||||||
if let Err(err) =
|
if let Err(err) =
|
||||||
self.filetransfer_recv(TransferPayload::Any(entry), tempdir.path(), None)
|
self.filetransfer_recv(TransferPayload::Any(entry), tempdir.path(), None)
|
||||||
@@ -231,7 +231,7 @@ impl FileTransferActivity {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
// Stat dir
|
// Stat dir
|
||||||
let tempdir_entry: FsEntry = match self.host.stat(tempdir_path.as_path()) {
|
let tempdir_entry = match self.host.stat(tempdir_path.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
|
|||||||
@@ -26,7 +26,9 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
use super::{FileTransferActivity, LogLevel, SelectedEntry};
|
||||||
|
|
||||||
|
use remotefs::Entry;
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_delete(&mut self) {
|
pub(crate) fn action_local_delete(&mut self) {
|
||||||
@@ -71,13 +73,13 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn local_remove_file(&mut self, entry: &FsEntry) {
|
pub(crate) fn local_remove_file(&mut self, entry: &Entry) {
|
||||||
match self.host.remove(entry) {
|
match self.host.remove(entry) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Log
|
// Log
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!("Removed file \"{}\"", entry.get_abs_path().display()),
|
format!("Removed file \"{}\"", entry.path().display()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -85,7 +87,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not delete file \"{}\": {}",
|
"Could not delete file \"{}\": {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -93,12 +95,12 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remote_remove_file(&mut self, entry: &FsEntry) {
|
pub(crate) fn remote_remove_file(&mut self, entry: &Entry) {
|
||||||
match self.client.remove(entry) {
|
match self.client.remove_dir_all(entry.path()) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!("Removed file \"{}\"", entry.get_abs_path().display()),
|
format!("Removed file \"{}\"", entry.path().display()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -106,7 +108,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not delete file \"{}\": {}",
|
"Could not delete file \"{}\": {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,9 +26,10 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
|
use super::{FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
|
||||||
use crate::fs::FsFile;
|
|
||||||
// ext
|
// ext
|
||||||
|
use remotefs::{Entry, File};
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -36,7 +37,7 @@ use std::time::SystemTime;
|
|||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_edit_local_file(&mut self) {
|
pub(crate) fn action_edit_local_file(&mut self) {
|
||||||
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
|
let entries: Vec<Entry> = match self.get_local_selected_entries() {
|
||||||
SelectedEntry::One(entry) => vec![entry],
|
SelectedEntry::One(entry) => vec![entry],
|
||||||
SelectedEntry::Many(entries) => entries,
|
SelectedEntry::Many(entries) => entries,
|
||||||
SelectedEntry::None => vec![],
|
SelectedEntry::None => vec![],
|
||||||
@@ -47,10 +48,10 @@ impl FileTransferActivity {
|
|||||||
if entry.is_file() {
|
if entry.is_file() {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!("Opening file \"{}\"…", entry.get_abs_path().display()),
|
format!("Opening file \"{}\"…", entry.path().display()),
|
||||||
);
|
);
|
||||||
// Edit file
|
// Edit file
|
||||||
if let Err(err) = self.edit_local_file(entry.get_abs_path().as_path()) {
|
if let Err(err) = self.edit_local_file(entry.path()) {
|
||||||
self.log_and_alert(LogLevel::Error, err);
|
self.log_and_alert(LogLevel::Error, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,7 +61,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_edit_remote_file(&mut self) {
|
pub(crate) fn action_edit_remote_file(&mut self) {
|
||||||
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
|
let entries: Vec<Entry> = match self.get_remote_selected_entries() {
|
||||||
SelectedEntry::One(entry) => vec![entry],
|
SelectedEntry::One(entry) => vec![entry],
|
||||||
SelectedEntry::Many(entries) => entries,
|
SelectedEntry::Many(entries) => entries,
|
||||||
SelectedEntry::None => vec![],
|
SelectedEntry::None => vec![],
|
||||||
@@ -68,10 +69,10 @@ impl FileTransferActivity {
|
|||||||
// Edit all entries
|
// Edit all entries
|
||||||
for entry in entries.into_iter() {
|
for entry in entries.into_iter() {
|
||||||
// Check if file
|
// Check if file
|
||||||
if let FsEntry::File(file) = entry {
|
if let Entry::File(file) = entry {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!("Opening file \"{}\"…", file.abs_path.display()),
|
format!("Opening file \"{}\"…", file.path.display()),
|
||||||
);
|
);
|
||||||
// Edit file
|
// Edit file
|
||||||
if let Err(err) = self.edit_remote_file(file) {
|
if let Err(err) = self.edit_remote_file(file) {
|
||||||
@@ -149,7 +150,7 @@ impl FileTransferActivity {
|
|||||||
/// ### edit_remote_file
|
/// ### edit_remote_file
|
||||||
///
|
///
|
||||||
/// Edit file on remote host
|
/// Edit file on remote host
|
||||||
fn edit_remote_file(&mut self, file: FsFile) -> Result<(), String> {
|
fn edit_remote_file(&mut self, file: File) -> Result<(), String> {
|
||||||
// Create temp file
|
// Create temp file
|
||||||
let tmpfile: PathBuf = match self.download_file_as_temp(&file) {
|
let tmpfile: PathBuf = match self.download_file_as_temp(&file) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
@@ -157,7 +158,7 @@ impl FileTransferActivity {
|
|||||||
};
|
};
|
||||||
// Download file
|
// Download file
|
||||||
let file_name = file.name.clone();
|
let file_name = file.name.clone();
|
||||||
let file_path = file.abs_path.clone();
|
let file_path = file.path.clone();
|
||||||
if let Err(err) = self.filetransfer_recv(
|
if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::File(file),
|
TransferPayload::File(file),
|
||||||
tmpfile.as_path(),
|
tmpfile.as_path(),
|
||||||
@@ -167,7 +168,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
// Get current file modification time
|
// Get current file modification time
|
||||||
let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) {
|
let prev_mtime: SystemTime = match self.host.stat(tmpfile.as_path()) {
|
||||||
Ok(e) => e.get_last_change_time(),
|
Ok(e) => e.metadata().mtime,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Could not stat \"{}\": {}",
|
"Could not stat \"{}\": {}",
|
||||||
@@ -181,7 +182,7 @@ impl FileTransferActivity {
|
|||||||
return Err(err);
|
return Err(err);
|
||||||
}
|
}
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry: FsEntry = match self.host.stat(tmpfile.as_path()) {
|
let tmpfile_entry: Entry = match self.host.stat(tmpfile.as_path()) {
|
||||||
Ok(e) => e,
|
Ok(e) => e,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@@ -192,7 +193,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Check if file has changed
|
// Check if file has changed
|
||||||
match prev_mtime != tmpfile_entry.get_last_change_time() {
|
match prev_mtime != tmpfile_entry.metadata().mtime {
|
||||||
true => {
|
true => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
@@ -202,7 +203,7 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Get local fs entry
|
// Get local fs entry
|
||||||
let tmpfile_entry: FsFile = match self.host.stat(tmpfile.as_path()) {
|
let tmpfile_entry = match self.host.stat(tmpfile.as_path()) {
|
||||||
Ok(e) => e.unwrap_file(),
|
Ok(e) => e.unwrap_file(),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
|
|||||||
@@ -49,9 +49,12 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
pub(crate) fn action_remote_exec(&mut self, input: String) {
|
||||||
match self.client.as_mut().exec(input.as_str()) {
|
match self.client.as_mut().exec(input.as_str()) {
|
||||||
Ok(output) => {
|
Ok((rc, output)) => {
|
||||||
// Reload files
|
// Reload files
|
||||||
self.log(LogLevel::Info, format!("\"{}\": {}", input, output));
|
self.log(
|
||||||
|
LogLevel::Info,
|
||||||
|
format!("\"{}\" (exitcode: {}): {}", input, rc, output),
|
||||||
|
);
|
||||||
self.reload_remote_dir();
|
self.reload_remote_dir();
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
@@ -27,21 +27,19 @@
|
|||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::super::browser::FileExplorerTab;
|
use super::super::browser::FileExplorerTab;
|
||||||
use super::{
|
use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferOpts, TransferPayload};
|
||||||
FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferOpts, TransferPayload,
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> {
|
pub(crate) fn action_local_find(&mut self, input: String) -> Result<Vec<Entry>, String> {
|
||||||
match self.host.find(input.as_str()) {
|
match self.host.find(input.as_str()) {
|
||||||
Ok(entries) => Ok(entries),
|
Ok(entries) => Ok(entries),
|
||||||
Err(err) => Err(format!("Could not search for files: {}", err)),
|
Err(err) => Err(format!("Could not search for files: {}", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<FsEntry>, String> {
|
pub(crate) fn action_remote_find(&mut self, input: String) -> Result<Vec<Entry>, String> {
|
||||||
match self.client.as_mut().find(input.as_str()) {
|
match self.client.as_mut().find(input.as_str()) {
|
||||||
Ok(entries) => Ok(entries),
|
Ok(entries) => Ok(entries),
|
||||||
Err(err) => Err(format!("Could not search for files: {}", err)),
|
Err(err) => Err(format!("Could not search for files: {}", err)),
|
||||||
@@ -53,8 +51,8 @@ impl FileTransferActivity {
|
|||||||
if let SelectedEntry::One(entry) = self.get_found_selected_entries() {
|
if let SelectedEntry::One(entry) = self.get_found_selected_entries() {
|
||||||
// Get path: if a directory, use directory path; if it is a File, get parent path
|
// Get path: if a directory, use directory path; if it is a File, get parent path
|
||||||
let path: PathBuf = match entry {
|
let path: PathBuf = match entry {
|
||||||
FsEntry::Directory(dir) => dir.abs_path,
|
Entry::Directory(dir) => dir.path,
|
||||||
FsEntry::File(file) => match file.abs_path.parent() {
|
Entry::File(file) => match file.path.parent() {
|
||||||
None => PathBuf::from("."),
|
None => PathBuf::from("."),
|
||||||
Some(p) => p.to_path_buf(),
|
Some(p) => p.to_path_buf(),
|
||||||
},
|
},
|
||||||
@@ -86,10 +84,10 @@ impl FileTransferActivity {
|
|||||||
{
|
{
|
||||||
// Save pending transfer
|
// Save pending transfer
|
||||||
self.set_pending_transfer(
|
self.set_pending_transfer(
|
||||||
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
|
opts.save_as.as_deref().unwrap_or_else(|| entry.name()),
|
||||||
);
|
);
|
||||||
} else if let Err(err) = self.filetransfer_send(
|
} else if let Err(err) = self.filetransfer_send(
|
||||||
TransferPayload::Any(entry.get_realfile()),
|
TransferPayload::Any(entry),
|
||||||
wrkdir.as_path(),
|
wrkdir.as_path(),
|
||||||
opts.save_as,
|
opts.save_as,
|
||||||
) {
|
) {
|
||||||
@@ -107,10 +105,10 @@ impl FileTransferActivity {
|
|||||||
{
|
{
|
||||||
// Save pending transfer
|
// Save pending transfer
|
||||||
self.set_pending_transfer(
|
self.set_pending_transfer(
|
||||||
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
|
opts.save_as.as_deref().unwrap_or_else(|| entry.name()),
|
||||||
);
|
);
|
||||||
} else if let Err(err) = self.filetransfer_recv(
|
} else if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::Any(entry.get_realfile()),
|
TransferPayload::Any(entry),
|
||||||
wrkdir.as_path(),
|
wrkdir.as_path(),
|
||||||
opts.save_as,
|
opts.save_as,
|
||||||
) {
|
) {
|
||||||
@@ -128,12 +126,11 @@ impl FileTransferActivity {
|
|||||||
dest_path.push(save_as);
|
dest_path.push(save_as);
|
||||||
}
|
}
|
||||||
// Iter files
|
// Iter files
|
||||||
let entries: Vec<FsEntry> = entries.iter().map(|x| x.get_realfile()).collect();
|
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||||
if opts.check_replace && self.config().get_prompt_on_file_replace() {
|
if opts.check_replace && self.config().get_prompt_on_file_replace() {
|
||||||
// Check which file would be replaced
|
// Check which file would be replaced
|
||||||
let existing_files: Vec<&FsEntry> = entries
|
let existing_files: Vec<&Entry> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| {
|
.filter(|x| {
|
||||||
self.remote_file_exists(
|
self.remote_file_exists(
|
||||||
@@ -166,7 +163,7 @@ impl FileTransferActivity {
|
|||||||
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
FileExplorerTab::FindRemote | FileExplorerTab::Remote => {
|
||||||
if opts.check_replace && self.config().get_prompt_on_file_replace() {
|
if opts.check_replace && self.config().get_prompt_on_file_replace() {
|
||||||
// Check which file would be replaced
|
// Check which file would be replaced
|
||||||
let existing_files: Vec<&FsEntry> = entries
|
let existing_files: Vec<&Entry> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| {
|
.filter(|x| {
|
||||||
self.local_file_exists(
|
self.local_file_exists(
|
||||||
@@ -218,7 +215,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_found_file(&mut self, entry: &FsEntry) {
|
fn remove_found_file(&mut self, entry: &Entry) {
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||||
self.local_remove_file(entry);
|
self.local_remove_file(entry);
|
||||||
@@ -263,7 +260,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_found_file(&mut self, entry: &FsEntry, with: Option<&str>) {
|
fn open_found_file(&mut self, entry: &Entry, with: Option<&str>) {
|
||||||
match self.browser.tab() {
|
match self.browser.tab() {
|
||||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||||
self.action_open_local_file(entry, with);
|
self.action_open_local_file(entry, with);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
|
use remotefs::fs::UnixPex;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -48,11 +49,10 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub(crate) fn action_remote_mkdir(&mut self, input: String) {
|
pub(crate) fn action_remote_mkdir(&mut self, input: String) {
|
||||||
match self
|
match self.client.as_mut().create_dir(
|
||||||
.client
|
PathBuf::from(input.as_str()).as_path(),
|
||||||
.as_mut()
|
UnixPex::from(0o755),
|
||||||
.mkdir(PathBuf::from(input.as_str()).as_path())
|
) {
|
||||||
{
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Reload files
|
// Reload files
|
||||||
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
|
self.log(LogLevel::Info, format!("Created directory \"{}\"", input));
|
||||||
|
|||||||
@@ -26,9 +26,9 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
pub(self) use super::{
|
pub(self) use super::{
|
||||||
browser::FileExplorerTab, FileTransferActivity, FsEntry, Id, LogLevel, TransferOpts,
|
browser::FileExplorerTab, FileTransferActivity, Id, LogLevel, TransferOpts, TransferPayload,
|
||||||
TransferPayload,
|
|
||||||
};
|
};
|
||||||
|
pub(self) use remotefs::Entry;
|
||||||
use tuirealm::{State, StateValue};
|
use tuirealm::{State, StateValue};
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
@@ -47,8 +47,8 @@ pub(crate) mod submit;
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum SelectedEntry {
|
pub(crate) enum SelectedEntry {
|
||||||
One(FsEntry),
|
One(Entry),
|
||||||
Many(Vec<FsEntry>),
|
Many(Vec<Entry>),
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,8 +59,8 @@ enum SelectedEntryIndex {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Option<&FsEntry>> for SelectedEntry {
|
impl From<Option<&Entry>> for SelectedEntry {
|
||||||
fn from(opt: Option<&FsEntry>) -> Self {
|
fn from(opt: Option<&Entry>) -> Self {
|
||||||
match opt {
|
match opt {
|
||||||
Some(e) => SelectedEntry::One(e.clone()),
|
Some(e) => SelectedEntry::One(e.clone()),
|
||||||
None => SelectedEntry::None,
|
None => SelectedEntry::None,
|
||||||
@@ -68,8 +68,8 @@ impl From<Option<&FsEntry>> for SelectedEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<&FsEntry>> for SelectedEntry {
|
impl From<Vec<&Entry>> for SelectedEntry {
|
||||||
fn from(files: Vec<&FsEntry>) -> Self {
|
fn from(files: Vec<&Entry>) -> Self {
|
||||||
SelectedEntry::Many(files.into_iter().cloned().collect())
|
SelectedEntry::Many(files.into_iter().cloned().collect())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,9 +82,9 @@ impl FileTransferActivity {
|
|||||||
match self.get_selected_index(&Id::ExplorerLocal) {
|
match self.get_selected_index(&Id::ExplorerLocal) {
|
||||||
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)),
|
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.local().get(idx)),
|
||||||
SelectedEntryIndex::Many(files) => {
|
SelectedEntryIndex::Many(files) => {
|
||||||
let files: Vec<&FsEntry> = files
|
let files: Vec<&Entry> = files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| self.local().get(*x)) // Usize to Option<FsEntry>
|
.map(|x| self.local().get(*x)) // Usize to Option<Entry>
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect();
|
.collect();
|
||||||
SelectedEntry::from(files)
|
SelectedEntry::from(files)
|
||||||
@@ -100,9 +100,9 @@ impl FileTransferActivity {
|
|||||||
match self.get_selected_index(&Id::ExplorerRemote) {
|
match self.get_selected_index(&Id::ExplorerRemote) {
|
||||||
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)),
|
SelectedEntryIndex::One(idx) => SelectedEntry::from(self.remote().get(idx)),
|
||||||
SelectedEntryIndex::Many(files) => {
|
SelectedEntryIndex::Many(files) => {
|
||||||
let files: Vec<&FsEntry> = files
|
let files: Vec<&Entry> = files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| self.remote().get(*x)) // Usize to Option<FsEntry>
|
.map(|x| self.remote().get(*x)) // Usize to Option<Entry>
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect();
|
.collect();
|
||||||
SelectedEntry::from(files)
|
SelectedEntry::from(files)
|
||||||
@@ -120,9 +120,9 @@ impl FileTransferActivity {
|
|||||||
SelectedEntry::from(self.found().as_ref().unwrap().get(idx))
|
SelectedEntry::from(self.found().as_ref().unwrap().get(idx))
|
||||||
}
|
}
|
||||||
SelectedEntryIndex::Many(files) => {
|
SelectedEntryIndex::Many(files) => {
|
||||||
let files: Vec<&FsEntry> = files
|
let files: Vec<&Entry> = files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option<FsEntry>
|
.map(|x| self.found().as_ref().unwrap().get(*x)) // Usize to Option<Entry>
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect();
|
.collect();
|
||||||
SelectedEntry::from(files)
|
SelectedEntry::from(files)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel};
|
use super::{Entry, FileTransferActivity, LogLevel};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ impl FileTransferActivity {
|
|||||||
// Check if file exists
|
// Check if file exists
|
||||||
let mut file_exists: bool = false;
|
let mut file_exists: bool = false;
|
||||||
for file in self.local().iter_files_all() {
|
for file in self.local().iter_files_all() {
|
||||||
if input == file.get_name() {
|
if input == file.name() {
|
||||||
file_exists = true;
|
file_exists = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ impl FileTransferActivity {
|
|||||||
// Check if file exists
|
// Check if file exists
|
||||||
let mut file_exists: bool = false;
|
let mut file_exists: bool = false;
|
||||||
for file in self.remote().iter_files_all() {
|
for file in self.remote().iter_files_all() {
|
||||||
if input == file.get_name() {
|
if input == file.name() {
|
||||||
file_exists = true;
|
file_exists = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
Ok(tfile) => {
|
Ok(tfile) => {
|
||||||
// Stat tempfile
|
// Stat tempfile
|
||||||
let local_file: FsEntry = match self.host.stat(tfile.path()) {
|
let local_file: Entry = match self.host.stat(tfile.path()) {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
@@ -98,7 +98,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
};
|
};
|
||||||
if let FsEntry::File(local_file) = local_file {
|
if let Entry::File(local_file) = local_file {
|
||||||
// Create file
|
// Create file
|
||||||
let reader = Box::new(match File::open(tfile.path()) {
|
let reader = Box::new(match File::open(tfile.path()) {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
@@ -112,7 +112,7 @@ impl FileTransferActivity {
|
|||||||
});
|
});
|
||||||
match self
|
match self
|
||||||
.client
|
.client
|
||||||
.send_file_wno_stream(&local_file, file_path.as_path(), reader)
|
.create_file(file_path.as_path(), &local_file.metadata, reader)
|
||||||
{
|
{
|
||||||
Err(err) => self.log_and_alert(
|
Err(err) => self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry, TransferPayload};
|
use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry, TransferPayload};
|
||||||
// ext
|
// ext
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Open local file
|
/// Open local file
|
||||||
pub(crate) fn action_open_local(&mut self) {
|
pub(crate) fn action_open_local(&mut self) {
|
||||||
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
|
let entries: Vec<Entry> = match self.get_local_selected_entries() {
|
||||||
SelectedEntry::One(entry) => vec![entry],
|
SelectedEntry::One(entry) => vec![entry],
|
||||||
SelectedEntry::Many(entries) => entries,
|
SelectedEntry::Many(entries) => entries,
|
||||||
SelectedEntry::None => vec![],
|
SelectedEntry::None => vec![],
|
||||||
@@ -49,7 +49,7 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Open local file
|
/// Open local file
|
||||||
pub(crate) fn action_open_remote(&mut self) {
|
pub(crate) fn action_open_remote(&mut self) {
|
||||||
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
|
let entries: Vec<Entry> = match self.get_remote_selected_entries() {
|
||||||
SelectedEntry::One(entry) => vec![entry],
|
SelectedEntry::One(entry) => vec![entry],
|
||||||
SelectedEntry::Many(entries) => entries,
|
SelectedEntry::Many(entries) => entries,
|
||||||
SelectedEntry::None => vec![],
|
SelectedEntry::None => vec![],
|
||||||
@@ -62,19 +62,16 @@ impl FileTransferActivity {
|
|||||||
/// ### action_open_local_file
|
/// ### action_open_local_file
|
||||||
///
|
///
|
||||||
/// Perform open lopcal file
|
/// Perform open lopcal file
|
||||||
pub(crate) fn action_open_local_file(&mut self, entry: &FsEntry, open_with: Option<&str>) {
|
pub(crate) fn action_open_local_file(&mut self, entry: &Entry, open_with: Option<&str>) {
|
||||||
let entry: FsEntry = entry.get_realfile();
|
self.open_path_with(entry.path(), open_with);
|
||||||
self.open_path_with(entry.get_abs_path().as_path(), open_with);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### action_open_local
|
/// ### action_open_local
|
||||||
///
|
///
|
||||||
/// Open remote file. The file is first downloaded to a temporary directory on localhost
|
/// Open remote file. The file is first downloaded to a temporary directory on localhost
|
||||||
pub(crate) fn action_open_remote_file(&mut self, entry: &FsEntry, open_with: Option<&str>) {
|
pub(crate) fn action_open_remote_file(&mut self, entry: &Entry, open_with: Option<&str>) {
|
||||||
let entry: FsEntry = entry.get_realfile();
|
|
||||||
// Download file
|
// Download file
|
||||||
let tmpfile: String =
|
let tmpfile: String = match self.get_cache_tmp_name(entry.name(), entry.extension()) {
|
||||||
match self.get_cache_tmp_name(entry.get_name(), entry.get_ftype().as_deref()) {
|
|
||||||
None => {
|
None => {
|
||||||
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
self.log(LogLevel::Error, String::from("Could not create tempdir"));
|
||||||
return;
|
return;
|
||||||
@@ -89,7 +86,7 @@ impl FileTransferActivity {
|
|||||||
Some(p) => p.path().to_path_buf(),
|
Some(p) => p.path().to_path_buf(),
|
||||||
};
|
};
|
||||||
match self.filetransfer_recv(
|
match self.filetransfer_recv(
|
||||||
TransferPayload::Any(entry),
|
TransferPayload::Any(entry.clone()),
|
||||||
cache.as_path(),
|
cache.as_path(),
|
||||||
Some(tmpfile.clone()),
|
Some(tmpfile.clone()),
|
||||||
) {
|
) {
|
||||||
@@ -114,7 +111,7 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Open selected file with provided application
|
/// Open selected file with provided application
|
||||||
pub(crate) fn action_local_open_with(&mut self, with: &str) {
|
pub(crate) fn action_local_open_with(&mut self, with: &str) {
|
||||||
let entries: Vec<FsEntry> = match self.get_local_selected_entries() {
|
let entries: Vec<Entry> = match self.get_local_selected_entries() {
|
||||||
SelectedEntry::One(entry) => vec![entry],
|
SelectedEntry::One(entry) => vec![entry],
|
||||||
SelectedEntry::Many(entries) => entries,
|
SelectedEntry::Many(entries) => entries,
|
||||||
SelectedEntry::None => vec![],
|
SelectedEntry::None => vec![],
|
||||||
@@ -129,7 +126,7 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Open selected file with provided application
|
/// Open selected file with provided application
|
||||||
pub(crate) fn action_remote_open_with(&mut self, with: &str) {
|
pub(crate) fn action_remote_open_with(&mut self, with: &str) {
|
||||||
let entries: Vec<FsEntry> = match self.get_remote_selected_entries() {
|
let entries: Vec<Entry> = match self.get_remote_selected_entries() {
|
||||||
SelectedEntry::One(entry) => vec![entry],
|
SelectedEntry::One(entry) => vec![entry],
|
||||||
SelectedEntry::Many(entries) => entries,
|
SelectedEntry::Many(entries) => entries,
|
||||||
SelectedEntry::None => vec![],
|
SelectedEntry::None => vec![],
|
||||||
|
|||||||
@@ -26,8 +26,9 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry, LogLevel, SelectedEntry};
|
use super::{Entry, FileTransferActivity, LogLevel, SelectedEntry};
|
||||||
use crate::filetransfer::FileTransferErrorType;
|
|
||||||
|
use remotefs::RemoteErrorType;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -45,7 +46,7 @@ impl FileTransferActivity {
|
|||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for entry in entries.iter() {
|
||||||
let mut dest_path: PathBuf = base_path.clone();
|
let mut dest_path: PathBuf = base_path.clone();
|
||||||
dest_path.push(entry.get_name());
|
dest_path.push(entry.name());
|
||||||
self.local_rename_file(entry, dest_path.as_path());
|
self.local_rename_file(entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
// Reload entries
|
// Reload entries
|
||||||
@@ -69,7 +70,7 @@ impl FileTransferActivity {
|
|||||||
// Iter files
|
// Iter files
|
||||||
for entry in entries.iter() {
|
for entry in entries.iter() {
|
||||||
let mut dest_path: PathBuf = base_path.clone();
|
let mut dest_path: PathBuf = base_path.clone();
|
||||||
dest_path.push(entry.get_name());
|
dest_path.push(entry.name());
|
||||||
self.remote_rename_file(entry, dest_path.as_path());
|
self.remote_rename_file(entry, dest_path.as_path());
|
||||||
}
|
}
|
||||||
// Reload entries
|
// Reload entries
|
||||||
@@ -79,14 +80,14 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_rename_file(&mut self, entry: &FsEntry, dest: &Path) {
|
fn local_rename_file(&mut self, entry: &Entry, dest: &Path) {
|
||||||
match self.host.rename(entry, dest) {
|
match self.host.rename(entry, dest) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Moved \"{}\" to \"{}\"",
|
"Moved \"{}\" to \"{}\"",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display()
|
dest.display()
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -95,7 +96,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not move \"{}\" to \"{}\": {}",
|
"Could not move \"{}\" to \"{}\": {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display(),
|
dest.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
@@ -103,26 +104,26 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remote_rename_file(&mut self, entry: &FsEntry, dest: &Path) {
|
fn remote_rename_file(&mut self, entry: &Entry, dest: &Path) {
|
||||||
match self.client.as_mut().rename(entry, dest) {
|
match self.client.as_mut().mov(entry.path(), dest) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Moved \"{}\" to \"{}\"",
|
"Moved \"{}\" to \"{}\"",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display()
|
dest.display()
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
|
Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => {
|
||||||
self.tricky_move(entry, dest);
|
self.tricky_move(entry, dest);
|
||||||
}
|
}
|
||||||
Err(err) => self.log_and_alert(
|
Err(err) => self.log_and_alert(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not move \"{}\" to \"{}\": {}",
|
"Could not move \"{}\" to \"{}\": {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display(),
|
dest.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
@@ -134,21 +135,21 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Tricky move will be used whenever copy command is not available on remote host.
|
/// Tricky move will be used whenever copy command is not available on remote host.
|
||||||
/// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`)
|
/// It basically uses the tricky_copy function, then it just deletes the previous entry (`entry`)
|
||||||
fn tricky_move(&mut self, entry: &FsEntry, dest: &Path) {
|
fn tricky_move(&mut self, entry: &Entry, dest: &Path) {
|
||||||
debug!(
|
debug!(
|
||||||
"Using tricky-move to move entry {} to {}",
|
"Using tricky-move to move entry {} to {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display()
|
dest.display()
|
||||||
);
|
);
|
||||||
if self.tricky_copy(entry.clone(), dest).is_ok() {
|
if self.tricky_copy(entry.clone(), dest).is_ok() {
|
||||||
// Delete remote existing entry
|
// Delete remote existing entry
|
||||||
debug!("Tricky-copy worked; removing existing remote entry");
|
debug!("Tricky-copy worked; removing existing remote entry");
|
||||||
match self.client.remove(entry) {
|
match self.client.remove_dir_all(entry.path()) {
|
||||||
Ok(_) => self.log(
|
Ok(_) => self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Moved \"{}\" to \"{}\"",
|
"Moved \"{}\" to \"{}\"",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display()
|
dest.display()
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -156,7 +157,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Copied \"{}\" to \"{}\"; but failed to remove src: {}",
|
"Copied \"{}\" to \"{}\"; but failed to remove src: {}",
|
||||||
entry.get_abs_path().display(),
|
entry.path().display(),
|
||||||
dest.display(),
|
dest.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{
|
use super::{
|
||||||
super::STORAGE_PENDING_TRANSFER, FileExplorerTab, FileTransferActivity, FsEntry, LogLevel,
|
super::STORAGE_PENDING_TRANSFER, Entry, FileExplorerTab, FileTransferActivity, LogLevel,
|
||||||
SelectedEntry, TransferOpts, TransferPayload,
|
SelectedEntry, TransferOpts, TransferPayload,
|
||||||
};
|
};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -101,10 +101,10 @@ impl FileTransferActivity {
|
|||||||
{
|
{
|
||||||
// Save pending transfer
|
// Save pending transfer
|
||||||
self.set_pending_transfer(
|
self.set_pending_transfer(
|
||||||
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
|
opts.save_as.as_deref().unwrap_or_else(|| entry.name()),
|
||||||
);
|
);
|
||||||
} else if let Err(err) = self.filetransfer_send(
|
} else if let Err(err) = self.filetransfer_send(
|
||||||
TransferPayload::Any(entry.get_realfile()),
|
TransferPayload::Any(entry.clone()),
|
||||||
wrkdir.as_path(),
|
wrkdir.as_path(),
|
||||||
opts.save_as,
|
opts.save_as,
|
||||||
) {
|
) {
|
||||||
@@ -123,10 +123,9 @@ impl FileTransferActivity {
|
|||||||
dest_path.push(save_as);
|
dest_path.push(save_as);
|
||||||
}
|
}
|
||||||
// Iter files
|
// Iter files
|
||||||
let entries: Vec<FsEntry> = entries.iter().map(|x| x.get_realfile()).collect();
|
|
||||||
if opts.check_replace && self.config().get_prompt_on_file_replace() {
|
if opts.check_replace && self.config().get_prompt_on_file_replace() {
|
||||||
// Check which file would be replaced
|
// Check which file would be replaced
|
||||||
let existing_files: Vec<&FsEntry> = entries
|
let existing_files: Vec<&Entry> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| {
|
.filter(|x| {
|
||||||
self.remote_file_exists(
|
self.remote_file_exists(
|
||||||
@@ -171,10 +170,10 @@ impl FileTransferActivity {
|
|||||||
{
|
{
|
||||||
// Save pending transfer
|
// Save pending transfer
|
||||||
self.set_pending_transfer(
|
self.set_pending_transfer(
|
||||||
opts.save_as.as_deref().unwrap_or_else(|| entry.get_name()),
|
opts.save_as.as_deref().unwrap_or_else(|| entry.name()),
|
||||||
);
|
);
|
||||||
} else if let Err(err) = self.filetransfer_recv(
|
} else if let Err(err) = self.filetransfer_recv(
|
||||||
TransferPayload::Any(entry.get_realfile()),
|
TransferPayload::Any(entry.clone()),
|
||||||
wrkdir.as_path(),
|
wrkdir.as_path(),
|
||||||
opts.save_as,
|
opts.save_as,
|
||||||
) {
|
) {
|
||||||
@@ -193,10 +192,9 @@ impl FileTransferActivity {
|
|||||||
dest_path.push(save_as);
|
dest_path.push(save_as);
|
||||||
}
|
}
|
||||||
// Iter files
|
// Iter files
|
||||||
let entries: Vec<FsEntry> = entries.iter().map(|x| x.get_realfile()).collect();
|
|
||||||
if opts.check_replace && self.config().get_prompt_on_file_replace() {
|
if opts.check_replace && self.config().get_prompt_on_file_replace() {
|
||||||
// Check which file would be replaced
|
// Check which file would be replaced
|
||||||
let existing_files: Vec<&FsEntry> = entries
|
let existing_files: Vec<&Entry> = entries
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|x| {
|
.filter(|x| {
|
||||||
self.local_file_exists(
|
self.local_file_exists(
|
||||||
@@ -244,8 +242,8 @@ impl FileTransferActivity {
|
|||||||
/// ### set_pending_transfer_many
|
/// ### set_pending_transfer_many
|
||||||
///
|
///
|
||||||
/// Set pending transfer for many files into storage and mount radio
|
/// Set pending transfer for many files into storage and mount radio
|
||||||
pub(crate) fn set_pending_transfer_many(&mut self, files: Vec<&FsEntry>, dest_path: &str) {
|
pub(crate) fn set_pending_transfer_many(&mut self, files: Vec<&Entry>, dest_path: &str) {
|
||||||
let file_names: Vec<&str> = files.iter().map(|x| x.get_name()).collect();
|
let file_names: Vec<&str> = files.iter().map(|x| x.name()).collect();
|
||||||
self.mount_radio_replace_many(file_names.as_slice());
|
self.mount_radio_replace_many(file_names.as_slice());
|
||||||
self.context_mut()
|
self.context_mut()
|
||||||
.store_mut()
|
.store_mut()
|
||||||
@@ -255,16 +253,16 @@ impl FileTransferActivity {
|
|||||||
/// ### file_to_check
|
/// ### file_to_check
|
||||||
///
|
///
|
||||||
/// Get file to check for path
|
/// Get file to check for path
|
||||||
pub(crate) fn file_to_check(e: &FsEntry, alt: Option<&String>) -> PathBuf {
|
pub(crate) fn file_to_check(e: &Entry, alt: Option<&String>) -> PathBuf {
|
||||||
match alt {
|
match alt {
|
||||||
Some(s) => PathBuf::from(s),
|
Some(s) => PathBuf::from(s),
|
||||||
None => PathBuf::from(e.get_name()),
|
None => PathBuf::from(e.name()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn file_to_check_many(e: &FsEntry, wrkdir: &Path) -> PathBuf {
|
pub(crate) fn file_to_check_many(e: &Entry, wrkdir: &Path) -> PathBuf {
|
||||||
let mut p = wrkdir.to_path_buf();
|
let mut p = wrkdir.to_path_buf();
|
||||||
p.push(e.get_name());
|
p.push(e.name());
|
||||||
p
|
p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,9 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
// locals
|
// locals
|
||||||
use super::{FileTransferActivity, FsEntry};
|
use super::{Entry, FileTransferActivity};
|
||||||
|
|
||||||
|
use remotefs::fs::{File, Metadata};
|
||||||
|
|
||||||
enum SubmitAction {
|
enum SubmitAction {
|
||||||
ChangeDir,
|
ChangeDir,
|
||||||
@@ -38,25 +40,41 @@ impl FileTransferActivity {
|
|||||||
///
|
///
|
||||||
/// Decides which action to perform on submit for local explorer
|
/// Decides which action to perform on submit for local explorer
|
||||||
/// Return true whether the directory changed
|
/// Return true whether the directory changed
|
||||||
pub(crate) fn action_submit_local(&mut self, entry: FsEntry) -> bool {
|
pub(crate) fn action_submit_local(&mut self, entry: Entry) -> bool {
|
||||||
let action: SubmitAction = match &entry {
|
let (action, entry) = match &entry {
|
||||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
Entry::Directory(_) => (SubmitAction::ChangeDir, entry),
|
||||||
FsEntry::File(file) => {
|
Entry::File(File {
|
||||||
match &file.symlink {
|
path,
|
||||||
Some(symlink_entry) => {
|
metadata:
|
||||||
// If symlink and is directory, point to symlink
|
Metadata {
|
||||||
match &**symlink_entry {
|
symlink: Some(symlink),
|
||||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
..
|
||||||
_ => SubmitAction::None,
|
},
|
||||||
}
|
..
|
||||||
}
|
}) => {
|
||||||
None => SubmitAction::None,
|
// Stat file
|
||||||
}
|
let stat_file = match self.host.stat(symlink.as_path()) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"Could not stat file pointed by {} ({}): {}",
|
||||||
|
path.display(),
|
||||||
|
symlink.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
entry
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match action {
|
(SubmitAction::ChangeDir, stat_file)
|
||||||
SubmitAction::ChangeDir => self.action_enter_local_dir(entry, false),
|
}
|
||||||
SubmitAction::None => false,
|
Entry::File(_) => (SubmitAction::None, entry),
|
||||||
|
};
|
||||||
|
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
|
/// Decides which action to perform on submit for remote explorer
|
||||||
/// Return true whether the directory changed
|
/// Return true whether the directory changed
|
||||||
pub(crate) fn action_submit_remote(&mut self, entry: FsEntry) -> bool {
|
pub(crate) fn action_submit_remote(&mut self, entry: Entry) -> bool {
|
||||||
let action: SubmitAction = match &entry {
|
let (action, entry) = match &entry {
|
||||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
Entry::Directory(_) => (SubmitAction::ChangeDir, entry),
|
||||||
FsEntry::File(file) => {
|
Entry::File(File {
|
||||||
match &file.symlink {
|
path,
|
||||||
Some(symlink_entry) => {
|
metadata:
|
||||||
// If symlink and is directory, point to symlink
|
Metadata {
|
||||||
match &**symlink_entry {
|
symlink: Some(symlink),
|
||||||
FsEntry::Directory(_) => SubmitAction::ChangeDir,
|
..
|
||||||
_ => SubmitAction::None,
|
},
|
||||||
}
|
..
|
||||||
}
|
}) => {
|
||||||
None => SubmitAction::None,
|
// Stat file
|
||||||
}
|
let stat_file = match self.client.stat(symlink.as_path()) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"Could not stat file pointed by {} ({}): {}",
|
||||||
|
path.display(),
|
||||||
|
symlink.display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
|
entry
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match action {
|
(SubmitAction::ChangeDir, stat_file)
|
||||||
SubmitAction::ChangeDir => self.action_enter_remote_dir(entry, false),
|
}
|
||||||
SubmitAction::None => false,
|
Entry::File(_) => (SubmitAction::None, entry),
|
||||||
|
};
|
||||||
|
match (action, entry) {
|
||||||
|
(SubmitAction::ChangeDir, Entry::Directory(dir)) => {
|
||||||
|
self.action_enter_remote_dir(dir, false)
|
||||||
|
}
|
||||||
|
(SubmitAction::ChangeDir, _) => false,
|
||||||
|
(SubmitAction::None, _) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,21 +225,12 @@ impl Component<Msg, NoUserEvent> for Log {
|
|||||||
/// ## OwnStates
|
/// ## OwnStates
|
||||||
///
|
///
|
||||||
/// OwnStates contains states for this component
|
/// OwnStates contains states for this component
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Default)]
|
||||||
struct OwnStates {
|
struct OwnStates {
|
||||||
list_index: usize, // Index of selected element in list
|
list_index: usize, // Index of selected element in list
|
||||||
list_len: usize, // Length of file list
|
list_len: usize, // Length of file list
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for OwnStates {
|
|
||||||
fn default() -> Self {
|
|
||||||
OwnStates {
|
|
||||||
list_index: 0,
|
|
||||||
list_len: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OwnStates {
|
impl OwnStates {
|
||||||
/// ### set_list_len
|
/// ### set_list_len
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -27,12 +27,11 @@
|
|||||||
*/
|
*/
|
||||||
use super::super::Browser;
|
use super::super::Browser;
|
||||||
use super::{Msg, TransferMsg, UiMsg};
|
use super::{Msg, TransferMsg, UiMsg};
|
||||||
use crate::fs::explorer::FileSorting;
|
use crate::explorer::FileSorting;
|
||||||
use crate::fs::FsEntry;
|
|
||||||
use crate::utils::fmt::fmt_time;
|
use crate::utils::fmt::fmt_time;
|
||||||
|
|
||||||
use bytesize::ByteSize;
|
use bytesize::ByteSize;
|
||||||
use std::path::PathBuf;
|
use remotefs::Entry;
|
||||||
|
|
||||||
use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span};
|
use tui_realm_stdlib::{Input, List, Paragraph, ProgressBar, Radio, Span};
|
||||||
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
|
||||||
@@ -400,38 +399,32 @@ pub struct FileInfoPopup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FileInfoPopup {
|
impl FileInfoPopup {
|
||||||
pub fn new(file: &FsEntry) -> Self {
|
pub fn new(file: &Entry) -> Self {
|
||||||
let mut texts: TableBuilder = TableBuilder::default();
|
let mut texts: TableBuilder = TableBuilder::default();
|
||||||
// Abs path
|
// Abs path
|
||||||
let real_path: Option<PathBuf> = {
|
let real_path = file.metadata().symlink.as_deref();
|
||||||
let real_file: FsEntry = file.get_realfile();
|
|
||||||
match real_file.get_abs_path() != file.get_abs_path() {
|
|
||||||
true => Some(real_file.get_abs_path()),
|
|
||||||
false => None,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let path: String = match real_path {
|
let path: String = match real_path {
|
||||||
Some(symlink) => format!("{} -> {}", file.get_abs_path().display(), symlink.display()),
|
Some(symlink) => format!("{} -> {}", file.path().display(), symlink.display()),
|
||||||
None => format!("{}", file.get_abs_path().display()),
|
None => format!("{}", file.path().display()),
|
||||||
};
|
};
|
||||||
// Make texts
|
// Make texts
|
||||||
texts
|
texts
|
||||||
.add_col(TextSpan::from("Path: "))
|
.add_col(TextSpan::from("Path: "))
|
||||||
.add_col(TextSpan::new(path.as_str()).fg(Color::Yellow));
|
.add_col(TextSpan::new(path.as_str()).fg(Color::Yellow));
|
||||||
if let Some(filetype) = file.get_ftype() {
|
if let Some(filetype) = file.extension() {
|
||||||
texts
|
texts
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::from("File type: "))
|
.add_col(TextSpan::from("File type: "))
|
||||||
.add_col(TextSpan::new(filetype.as_str()).fg(Color::LightGreen));
|
.add_col(TextSpan::new(filetype).fg(Color::LightGreen));
|
||||||
}
|
}
|
||||||
let (bsize, size): (ByteSize, usize) = (ByteSize(file.get_size() as u64), file.get_size());
|
let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size);
|
||||||
texts
|
texts
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::from("Size: "))
|
.add_col(TextSpan::from("Size: "))
|
||||||
.add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan));
|
.add_col(TextSpan::new(format!("{} ({})", bsize, size).as_str()).fg(Color::Cyan));
|
||||||
let ctime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S");
|
let atime: String = fmt_time(file.metadata().atime, "%b %d %Y %H:%M:%S");
|
||||||
let atime: String = fmt_time(file.get_last_access_time(), "%b %d %Y %H:%M:%S");
|
let ctime: String = fmt_time(file.metadata().ctime, "%b %d %Y %H:%M:%S");
|
||||||
let mtime: String = fmt_time(file.get_creation_time(), "%b %d %Y %H:%M:%S");
|
let mtime: String = fmt_time(file.metadata().mtime, "%b %d %Y %H:%M:%S");
|
||||||
texts
|
texts
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::from("Creation time: "))
|
.add_col(TextSpan::from("Creation time: "))
|
||||||
@@ -446,7 +439,7 @@ impl FileInfoPopup {
|
|||||||
.add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed));
|
.add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed));
|
||||||
// User
|
// User
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
let username: String = match file.get_user() {
|
let username: String = match file.metadata().uid {
|
||||||
Some(uid) => match get_user_by_uid(uid) {
|
Some(uid) => match get_user_by_uid(uid) {
|
||||||
Some(user) => user.name().to_string_lossy().to_string(),
|
Some(user) => user.name().to_string_lossy().to_string(),
|
||||||
None => uid.to_string(),
|
None => uid.to_string(),
|
||||||
@@ -454,10 +447,10 @@ impl FileInfoPopup {
|
|||||||
None => String::from("0"),
|
None => String::from("0"),
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let username: String = format!("{}", file.get_user().unwrap_or(0));
|
let username: String = format!("{}", file.metadata().uid.unwrap_or(0));
|
||||||
// Group
|
// Group
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
let group: String = match file.get_group() {
|
let group: String = match file.metadata().gid {
|
||||||
Some(gid) => match get_group_by_gid(gid) {
|
Some(gid) => match get_group_by_gid(gid) {
|
||||||
Some(group) => group.name().to_string_lossy().to_string(),
|
Some(group) => group.name().to_string_lossy().to_string(),
|
||||||
None => gid.to_string(),
|
None => gid.to_string(),
|
||||||
@@ -465,7 +458,7 @@ impl FileInfoPopup {
|
|||||||
None => String::from("0"),
|
None => String::from("0"),
|
||||||
};
|
};
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
let group: String = format!("{}", file.get_group().unwrap_or(0));
|
let group: String = format!("{}", file.metadata().gid.unwrap_or(0));
|
||||||
texts
|
texts
|
||||||
.add_row()
|
.add_row()
|
||||||
.add_col(TextSpan::from("User: "))
|
.add_col(TextSpan::from("User: "))
|
||||||
@@ -478,7 +471,7 @@ impl FileInfoPopup {
|
|||||||
component: List::default()
|
component: List::default()
|
||||||
.borders(Borders::default().modifiers(BorderType::Rounded))
|
.borders(Borders::default().modifiers(BorderType::Rounded))
|
||||||
.scroll(false)
|
.scroll(false)
|
||||||
.title(file.get_name(), Alignment::Left)
|
.title(file.name(), Alignment::Left)
|
||||||
.rows(texts.build()),
|
.rows(texts.build()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,21 +39,12 @@ pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
|
|||||||
/// ## OwnStates
|
/// ## OwnStates
|
||||||
///
|
///
|
||||||
/// OwnStates contains states for this component
|
/// OwnStates contains states for this component
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Default)]
|
||||||
struct OwnStates {
|
struct OwnStates {
|
||||||
list_index: usize, // Index of selected element in list
|
list_index: usize, // Index of selected element in list
|
||||||
selected: Vec<usize>, // Selected files
|
selected: Vec<usize>, // Selected files
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for OwnStates {
|
|
||||||
fn default() -> Self {
|
|
||||||
OwnStates {
|
|
||||||
list_index: 0,
|
|
||||||
selected: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OwnStates {
|
impl OwnStates {
|
||||||
/// ### init_list_states
|
/// ### init_list_states
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -25,10 +25,10 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
use crate::fs::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
|
use crate::explorer::{builder::FileExplorerBuilder, FileExplorer, FileSorting, GroupDirs};
|
||||||
use crate::fs::FsEntry;
|
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
|
|
||||||
|
use remotefs::Entry;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// ## FileExplorerTab
|
/// ## FileExplorerTab
|
||||||
@@ -100,7 +100,7 @@ impl Browser {
|
|||||||
self.found.as_mut().map(|x| &mut x.1)
|
self.found.as_mut().map(|x| &mut x.1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec<FsEntry>, wrkdir: &Path) {
|
pub fn set_found(&mut self, tab: FoundExplorerTab, files: Vec<Entry>, wrkdir: &Path) {
|
||||||
let mut explorer = Self::build_found_explorer(wrkdir);
|
let mut explorer = Self::build_found_explorer(wrkdir);
|
||||||
explorer.set_files(files);
|
explorer.set_files(files);
|
||||||
self.found = Some((tab, explorer));
|
self.found = Some((tab, explorer));
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ use super::{
|
|||||||
use crate::filetransfer::ProtocolParams;
|
use crate::filetransfer::ProtocolParams;
|
||||||
use crate::system::environment;
|
use crate::system::environment;
|
||||||
use crate::system::notifications::Notification;
|
use crate::system::notifications::Notification;
|
||||||
use crate::system::sshkey_storage::SshKeyStorage;
|
|
||||||
use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex};
|
use crate::utils::fmt::{fmt_millis, fmt_path_elide_ex};
|
||||||
use crate::utils::path;
|
use crate::utils::path;
|
||||||
// Ext
|
// Ext
|
||||||
@@ -120,13 +119,6 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### make_ssh_storage
|
|
||||||
///
|
|
||||||
/// Make ssh storage from `ConfigClient` if possible, empty otherwise (empty is implicit if degraded)
|
|
||||||
pub(super) fn make_ssh_storage(cli: &ConfigClient) -> SshKeyStorage {
|
|
||||||
SshKeyStorage::storage_from_config(cli)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### setup_text_editor
|
/// ### setup_text_editor
|
||||||
///
|
///
|
||||||
/// Set text editor to use
|
/// Set text editor to use
|
||||||
@@ -227,7 +219,7 @@ impl FileTransferActivity {
|
|||||||
TransferPayload::Any(entry) => {
|
TransferPayload::Any(entry) => {
|
||||||
format!(
|
format!(
|
||||||
"\"{}\" has been successfully transferred ({})",
|
"\"{}\" has been successfully transferred ({})",
|
||||||
entry.get_name(),
|
entry.name(),
|
||||||
transfer_stats
|
transfer_stats
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,10 +37,8 @@ mod view;
|
|||||||
// locals
|
// locals
|
||||||
use super::{Activity, Context, ExitReason};
|
use super::{Activity, Context, ExitReason};
|
||||||
use crate::config::themes::Theme;
|
use crate::config::themes::Theme;
|
||||||
use crate::filetransfer::{FileTransfer, FileTransferProtocol};
|
use crate::explorer::{FileExplorer, FileSorting};
|
||||||
use crate::filetransfer::{FtpFileTransfer, S3FileTransfer, ScpFileTransfer, SftpFileTransfer};
|
use crate::filetransfer::{Builder, FileTransferParams};
|
||||||
use crate::fs::explorer::{FileExplorer, FileSorting};
|
|
||||||
use crate::fs::FsEntry;
|
|
||||||
use crate::host::Localhost;
|
use crate::host::Localhost;
|
||||||
use crate::system::config_client::ConfigClient;
|
use crate::system::config_client::ConfigClient;
|
||||||
pub(self) use lib::browser;
|
pub(self) use lib::browser;
|
||||||
@@ -50,6 +48,7 @@ pub(self) use session::TransferPayload;
|
|||||||
|
|
||||||
// Includes
|
// Includes
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
|
use remotefs::RemoteFs;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
@@ -217,8 +216,8 @@ pub struct FileTransferActivity {
|
|||||||
redraw: bool,
|
redraw: bool,
|
||||||
/// Localhost bridge
|
/// Localhost bridge
|
||||||
host: Localhost,
|
host: Localhost,
|
||||||
/// Remote host
|
/// Remote host client
|
||||||
client: Box<dyn FileTransfer>,
|
client: Box<dyn RemoteFs>,
|
||||||
/// Browser
|
/// Browser
|
||||||
browser: Browser,
|
browser: Browser,
|
||||||
/// Current log lines
|
/// Current log lines
|
||||||
@@ -232,7 +231,7 @@ impl FileTransferActivity {
|
|||||||
/// ### new
|
/// ### new
|
||||||
///
|
///
|
||||||
/// Instantiates a new FileTransferActivity
|
/// Instantiates a new FileTransferActivity
|
||||||
pub fn new(host: Localhost, protocol: FileTransferProtocol, ticks: Duration) -> Self {
|
pub fn new(host: Localhost, params: &FileTransferParams, ticks: Duration) -> Self {
|
||||||
// Get config client
|
// Get config client
|
||||||
let config_client: ConfigClient = Self::init_config_client();
|
let config_client: ConfigClient = Self::init_config_client();
|
||||||
Self {
|
Self {
|
||||||
@@ -245,16 +244,7 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
redraw: true,
|
redraw: true,
|
||||||
host,
|
host,
|
||||||
client: match protocol {
|
client: Builder::build(params.protocol, params.params.clone(), &config_client),
|
||||||
FileTransferProtocol::Sftp => Box::new(SftpFileTransfer::new(
|
|
||||||
Self::make_ssh_storage(&config_client),
|
|
||||||
)),
|
|
||||||
FileTransferProtocol::Ftp(ftps) => Box::new(FtpFileTransfer::new(ftps)),
|
|
||||||
FileTransferProtocol::Scp => {
|
|
||||||
Box::new(ScpFileTransfer::new(Self::make_ssh_storage(&config_client)))
|
|
||||||
}
|
|
||||||
FileTransferProtocol::AwsS3 => Box::new(S3FileTransfer::default()),
|
|
||||||
},
|
|
||||||
browser: Browser::new(&config_client),
|
browser: Browser::new(&config_client),
|
||||||
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
log_records: VecDeque::with_capacity(256), // 256 events is enough I guess
|
||||||
transfer: TransferStates::default(),
|
transfer: TransferStates::default(),
|
||||||
|
|||||||
@@ -27,14 +27,14 @@
|
|||||||
*/
|
*/
|
||||||
// Locals
|
// Locals
|
||||||
use super::{FileTransferActivity, LogLevel};
|
use super::{FileTransferActivity, LogLevel};
|
||||||
use crate::filetransfer::{FileTransferError, FileTransferErrorType};
|
|
||||||
use crate::fs::{FsEntry, FsFile};
|
|
||||||
use crate::host::HostError;
|
use crate::host::HostError;
|
||||||
use crate::utils::fmt::fmt_millis;
|
use crate::utils::fmt::fmt_millis;
|
||||||
|
|
||||||
// Ext
|
// Ext
|
||||||
use bytesize::ByteSize;
|
use bytesize::ByteSize;
|
||||||
use std::fs::File;
|
use remotefs::fs::{Entry, File, UnixPex, Welcome};
|
||||||
|
use remotefs::{RemoteError, RemoteErrorType};
|
||||||
|
use std::fs::File as StdFile;
|
||||||
use std::io::{Read, Seek, Write};
|
use std::io::{Read, Seek, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -56,20 +56,20 @@ enum TransferErrorReason {
|
|||||||
#[error("I/O error on remote: {0}")]
|
#[error("I/O error on remote: {0}")]
|
||||||
RemoteIoError(std::io::Error),
|
RemoteIoError(std::io::Error),
|
||||||
#[error("File transfer error: {0}")]
|
#[error("File transfer error: {0}")]
|
||||||
FileTransferError(FileTransferError),
|
FileTransferError(RemoteError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ## TransferPayload
|
/// ## TransferPayload
|
||||||
///
|
///
|
||||||
/// Represents the entity to send or receive during a transfer.
|
/// Represents the entity to send or receive during a transfer.
|
||||||
/// - File: describes an individual `FsFile` to send
|
/// - File: describes an individual `File` to send
|
||||||
/// - Any: Can be any kind of `FsEntry`, but just one
|
/// - Any: Can be any kind of `Entry`, but just one
|
||||||
/// - Many: a list of `FsEntry`
|
/// - Many: a list of `Entry`
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(super) enum TransferPayload {
|
pub(super) enum TransferPayload {
|
||||||
File(FsFile),
|
File(File),
|
||||||
Any(FsEntry),
|
Any(Entry),
|
||||||
Many(Vec<FsEntry>),
|
Many(Vec<Entry>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileTransferActivity {
|
impl FileTransferActivity {
|
||||||
@@ -78,11 +78,11 @@ impl FileTransferActivity {
|
|||||||
/// Connect to remote
|
/// Connect to remote
|
||||||
pub(super) fn connect(&mut self) {
|
pub(super) fn connect(&mut self) {
|
||||||
let ft_params = self.context().ft_params().unwrap().clone();
|
let ft_params = self.context().ft_params().unwrap().clone();
|
||||||
let entry_dir: Option<PathBuf> = ft_params.entry_directory.clone();
|
let entry_dir: Option<PathBuf> = ft_params.entry_directory;
|
||||||
// Connect to remote
|
// Connect to remote
|
||||||
match self.client.connect(&ft_params.params) {
|
match self.client.connect() {
|
||||||
Ok(welcome) => {
|
Ok(Welcome { banner, .. }) => {
|
||||||
if let Some(banner) = welcome {
|
if let Some(banner) = banner {
|
||||||
// Log welcome
|
// Log welcome
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
@@ -234,17 +234,17 @@ impl FileTransferActivity {
|
|||||||
/// Send one file to remote at specified path.
|
/// Send one file to remote at specified path.
|
||||||
fn filetransfer_send_file(
|
fn filetransfer_send_file(
|
||||||
&mut self,
|
&mut self,
|
||||||
file: &FsFile,
|
file: &File,
|
||||||
curr_remote_path: &Path,
|
curr_remote_path: &Path,
|
||||||
dst_name: Option<String>,
|
dst_name: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Reset states
|
// Reset states
|
||||||
self.transfer.reset();
|
self.transfer.reset();
|
||||||
// Calculate total size of transfer
|
// Calculate total size of transfer
|
||||||
let total_transfer_size: usize = file.size;
|
let total_transfer_size: usize = file.metadata.size as usize;
|
||||||
self.transfer.full.init(total_transfer_size);
|
self.transfer.full.init(total_transfer_size);
|
||||||
// Mount progress bar
|
// Mount progress bar
|
||||||
self.mount_progress_bar(format!("Uploading {}…", file.abs_path.display()));
|
self.mount_progress_bar(format!("Uploading {}…", file.path.display()));
|
||||||
// Get remote path
|
// Get remote path
|
||||||
let file_name: String = file.name.clone();
|
let file_name: String = file.name.clone();
|
||||||
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
|
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
|
||||||
@@ -266,7 +266,7 @@ impl FileTransferActivity {
|
|||||||
/// Send a `TransferPayload` of type `Any`
|
/// Send a `TransferPayload` of type `Any`
|
||||||
fn filetransfer_send_any(
|
fn filetransfer_send_any(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry: &FsEntry,
|
entry: &Entry,
|
||||||
curr_remote_path: &Path,
|
curr_remote_path: &Path,
|
||||||
dst_name: Option<String>,
|
dst_name: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -276,7 +276,7 @@ impl FileTransferActivity {
|
|||||||
let total_transfer_size: usize = self.get_total_transfer_size_local(entry);
|
let total_transfer_size: usize = self.get_total_transfer_size_local(entry);
|
||||||
self.transfer.full.init(total_transfer_size);
|
self.transfer.full.init(total_transfer_size);
|
||||||
// Mount progress bar
|
// Mount progress bar
|
||||||
self.mount_progress_bar(format!("Uploading {}…", entry.get_abs_path().display()));
|
self.mount_progress_bar(format!("Uploading {}…", entry.path().display()));
|
||||||
// Send recurse
|
// Send recurse
|
||||||
let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
|
let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
|
||||||
// Umount progress bar
|
// Umount progress bar
|
||||||
@@ -289,7 +289,7 @@ impl FileTransferActivity {
|
|||||||
/// Send many entries to remote
|
/// Send many entries to remote
|
||||||
fn filetransfer_send_many(
|
fn filetransfer_send_many(
|
||||||
&mut self,
|
&mut self,
|
||||||
entries: &[FsEntry],
|
entries: &[Entry],
|
||||||
curr_remote_path: &Path,
|
curr_remote_path: &Path,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Reset states
|
// Reset states
|
||||||
@@ -315,14 +315,14 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
fn filetransfer_send_recurse(
|
fn filetransfer_send_recurse(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry: &FsEntry,
|
entry: &Entry,
|
||||||
curr_remote_path: &Path,
|
curr_remote_path: &Path,
|
||||||
dst_name: Option<String>,
|
dst_name: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Write popup
|
// Write popup
|
||||||
let file_name: String = match entry {
|
let file_name: String = match entry {
|
||||||
FsEntry::Directory(dir) => dir.name.clone(),
|
Entry::Directory(dir) => dir.name.clone(),
|
||||||
FsEntry::File(file) => file.name.clone(),
|
Entry::File(file) => file.name.clone(),
|
||||||
};
|
};
|
||||||
// Get remote path
|
// Get remote path
|
||||||
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
|
let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
|
||||||
@@ -333,7 +333,7 @@ impl FileTransferActivity {
|
|||||||
remote_path.push(remote_file_name);
|
remote_path.push(remote_file_name);
|
||||||
// Match entry
|
// Match entry
|
||||||
let result: Result<(), String> = match entry {
|
let result: Result<(), String> = match entry {
|
||||||
FsEntry::File(file) => {
|
Entry::File(file) => {
|
||||||
match self.filetransfer_send_one(file, remote_path.as_path(), file_name) {
|
match self.filetransfer_send_one(file, remote_path.as_path(), file_name) {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
// If transfer was abrupted or there was an IO error on remote, remove file
|
// If transfer was abrupted or there was an IO error on remote, remove file
|
||||||
@@ -352,7 +352,7 @@ impl FileTransferActivity {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Ok(entry) => {
|
Ok(entry) => {
|
||||||
if let Err(err) = self.client.remove(&entry) {
|
if let Err(err) = self.client.remove_file(entry.path()) {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
@@ -370,16 +370,19 @@ impl FileTransferActivity {
|
|||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FsEntry::Directory(dir) => {
|
Entry::Directory(dir) => {
|
||||||
// Create directory on remote first
|
// Create directory on remote first
|
||||||
match self.client.mkdir(remote_path.as_path()) {
|
match self
|
||||||
|
.client
|
||||||
|
.create_dir(remote_path.as_path(), UnixPex::from(0o755))
|
||||||
|
{
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!("Created directory \"{}\"", remote_path.display()),
|
format!("Created directory \"{}\"", remote_path.display()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(err) if err.kind() == FileTransferErrorType::DirectoryAlreadyExists => {
|
Err(err) if err.kind == RemoteErrorType::DirectoryAlreadyExists => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
@@ -401,7 +404,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Get files in dir
|
// Get files in dir
|
||||||
match self.host.scan_dir(dir.abs_path.as_path()) {
|
match self.host.scan_dir(dir.path.as_path()) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
// Iterate over files
|
// Iterate over files
|
||||||
for entry in entries.iter() {
|
for entry in entries.iter() {
|
||||||
@@ -423,7 +426,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not scan directory \"{}\": {}",
|
"Could not scan directory \"{}\": {}",
|
||||||
dir.abs_path.display(),
|
dir.path.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -439,7 +442,7 @@ impl FileTransferActivity {
|
|||||||
// Log abort
|
// Log abort
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Warn,
|
LogLevel::Warn,
|
||||||
format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()),
|
format!("Upload aborted for \"{}\"!", entry.path().display()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
@@ -450,18 +453,24 @@ impl FileTransferActivity {
|
|||||||
/// Send local file and write it to remote path
|
/// Send local file and write it to remote path
|
||||||
fn filetransfer_send_one(
|
fn filetransfer_send_one(
|
||||||
&mut self,
|
&mut self,
|
||||||
local: &FsFile,
|
local: &File,
|
||||||
remote: &Path,
|
remote: &Path,
|
||||||
file_name: String,
|
file_name: String,
|
||||||
) -> Result<(), TransferErrorReason> {
|
) -> Result<(), TransferErrorReason> {
|
||||||
|
// Sync file size and attributes before transfer
|
||||||
|
let metadata = self
|
||||||
|
.host
|
||||||
|
.stat(local.path.as_path())
|
||||||
|
.map_err(TransferErrorReason::HostError)
|
||||||
|
.map(|x| x.metadata().clone())?;
|
||||||
// Upload file
|
// Upload file
|
||||||
// Try to open local file
|
// Try to open local file
|
||||||
match self.host.open_file_read(local.abs_path.as_path()) {
|
match self.host.open_file_read(local.path.as_path()) {
|
||||||
Ok(fhnd) => match self.client.send_file(local, remote) {
|
Ok(fhnd) => match self.client.create(remote, &metadata) {
|
||||||
Ok(rhnd) => {
|
Ok(rhnd) => {
|
||||||
self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd)
|
self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd)
|
||||||
}
|
}
|
||||||
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
|
Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => {
|
||||||
self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd)
|
self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd)
|
||||||
}
|
}
|
||||||
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
|
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
|
||||||
@@ -475,10 +484,10 @@ impl FileTransferActivity {
|
|||||||
/// Send file to remote using stream
|
/// Send file to remote using stream
|
||||||
fn filetransfer_send_one_with_stream(
|
fn filetransfer_send_one_with_stream(
|
||||||
&mut self,
|
&mut self,
|
||||||
local: &FsFile,
|
local: &File,
|
||||||
remote: &Path,
|
remote: &Path,
|
||||||
file_name: String,
|
file_name: String,
|
||||||
mut reader: File,
|
mut reader: StdFile,
|
||||||
mut writer: Box<dyn Write>,
|
mut writer: Box<dyn Write>,
|
||||||
) -> Result<(), TransferErrorReason> {
|
) -> Result<(), TransferErrorReason> {
|
||||||
// Write file
|
// Write file
|
||||||
@@ -548,7 +557,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Finalize stream
|
// Finalize stream
|
||||||
if let Err(err) = self.client.on_sent(writer) {
|
if let Err(err) = self.client.on_written(writer) {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Warn,
|
LogLevel::Warn,
|
||||||
format!("Could not finalize remote stream: \"{}\"", err),
|
format!("Could not finalize remote stream: \"{}\"", err),
|
||||||
@@ -562,7 +571,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||||
local.abs_path.display(),
|
local.path.display(),
|
||||||
remote.display(),
|
remote.display(),
|
||||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||||
@@ -573,14 +582,20 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
/// ### filetransfer_send_one_wno_stream
|
/// ### filetransfer_send_one_wno_stream
|
||||||
///
|
///
|
||||||
/// Send an `FsFile` to remote without using streams.
|
/// Send an `File` to remote without using streams.
|
||||||
fn filetransfer_send_one_wno_stream(
|
fn filetransfer_send_one_wno_stream(
|
||||||
&mut self,
|
&mut self,
|
||||||
local: &FsFile,
|
local: &File,
|
||||||
remote: &Path,
|
remote: &Path,
|
||||||
file_name: String,
|
file_name: String,
|
||||||
mut reader: File,
|
mut reader: StdFile,
|
||||||
) -> Result<(), TransferErrorReason> {
|
) -> Result<(), TransferErrorReason> {
|
||||||
|
// Sync file size and attributes before transfer
|
||||||
|
let metadata = self
|
||||||
|
.host
|
||||||
|
.stat(local.path.as_path())
|
||||||
|
.map_err(TransferErrorReason::HostError)
|
||||||
|
.map(|x| x.metadata().clone())?;
|
||||||
// Write file
|
// Write file
|
||||||
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
|
let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
|
||||||
// Init transfer
|
// Init transfer
|
||||||
@@ -593,10 +608,7 @@ impl FileTransferActivity {
|
|||||||
self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
|
self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
|
||||||
self.view();
|
self.view();
|
||||||
// Send file
|
// Send file
|
||||||
if let Err(err) = self
|
if let Err(err) = self.client.create_file(remote, &metadata, Box::new(reader)) {
|
||||||
.client
|
|
||||||
.send_file_wno_stream(local, remote, Box::new(reader))
|
|
||||||
{
|
|
||||||
return Err(TransferErrorReason::FileTransferError(err));
|
return Err(TransferErrorReason::FileTransferError(err));
|
||||||
}
|
}
|
||||||
// Set transfer size ok
|
// Set transfer size ok
|
||||||
@@ -610,7 +622,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||||
local.abs_path.display(),
|
local.path.display(),
|
||||||
remote.display(),
|
remote.display(),
|
||||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||||
@@ -656,7 +668,7 @@ impl FileTransferActivity {
|
|||||||
/// If entry is a directory, this applies to directory only
|
/// If entry is a directory, this applies to directory only
|
||||||
fn filetransfer_recv_any(
|
fn filetransfer_recv_any(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry: &FsEntry,
|
entry: &Entry,
|
||||||
local_path: &Path,
|
local_path: &Path,
|
||||||
dst_name: Option<String>,
|
dst_name: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -666,7 +678,7 @@ impl FileTransferActivity {
|
|||||||
let total_transfer_size: usize = self.get_total_transfer_size_remote(entry);
|
let total_transfer_size: usize = self.get_total_transfer_size_remote(entry);
|
||||||
self.transfer.full.init(total_transfer_size);
|
self.transfer.full.init(total_transfer_size);
|
||||||
// Mount progress bar
|
// Mount progress bar
|
||||||
self.mount_progress_bar(format!("Downloading {}…", entry.get_abs_path().display()));
|
self.mount_progress_bar(format!("Downloading {}…", entry.path().display()));
|
||||||
// Receive
|
// Receive
|
||||||
let result = self.filetransfer_recv_recurse(entry, local_path, dst_name);
|
let result = self.filetransfer_recv_recurse(entry, local_path, dst_name);
|
||||||
// Umount progress bar
|
// Umount progress bar
|
||||||
@@ -677,14 +689,14 @@ impl FileTransferActivity {
|
|||||||
/// ### filetransfer_recv_file
|
/// ### filetransfer_recv_file
|
||||||
///
|
///
|
||||||
/// Receive a single file from remote.
|
/// Receive a single file from remote.
|
||||||
fn filetransfer_recv_file(&mut self, entry: &FsFile, local_path: &Path) -> Result<(), String> {
|
fn filetransfer_recv_file(&mut self, entry: &File, local_path: &Path) -> Result<(), String> {
|
||||||
// Reset states
|
// Reset states
|
||||||
self.transfer.reset();
|
self.transfer.reset();
|
||||||
// Calculate total transfer size
|
// Calculate total transfer size
|
||||||
let total_transfer_size: usize = entry.size;
|
let total_transfer_size: usize = entry.metadata.size as usize;
|
||||||
self.transfer.full.init(total_transfer_size);
|
self.transfer.full.init(total_transfer_size);
|
||||||
// Mount progress bar
|
// Mount progress bar
|
||||||
self.mount_progress_bar(format!("Downloading {}…", entry.abs_path.display()));
|
self.mount_progress_bar(format!("Downloading {}…", entry.path.display()));
|
||||||
// Receive
|
// Receive
|
||||||
let result = self.filetransfer_recv_one(local_path, entry, entry.name.clone());
|
let result = self.filetransfer_recv_one(local_path, entry, entry.name.clone());
|
||||||
// Umount progress bar
|
// Umount progress bar
|
||||||
@@ -698,7 +710,7 @@ impl FileTransferActivity {
|
|||||||
/// Send many entries to remote
|
/// Send many entries to remote
|
||||||
fn filetransfer_recv_many(
|
fn filetransfer_recv_many(
|
||||||
&mut self,
|
&mut self,
|
||||||
entries: &[FsEntry],
|
entries: &[Entry],
|
||||||
curr_remote_path: &Path,
|
curr_remote_path: &Path,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Reset states
|
// Reset states
|
||||||
@@ -724,18 +736,18 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
fn filetransfer_recv_recurse(
|
fn filetransfer_recv_recurse(
|
||||||
&mut self,
|
&mut self,
|
||||||
entry: &FsEntry,
|
entry: &Entry,
|
||||||
local_path: &Path,
|
local_path: &Path,
|
||||||
dst_name: Option<String>,
|
dst_name: Option<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
// Write popup
|
// Write popup
|
||||||
let file_name: String = match entry {
|
let file_name: String = match entry {
|
||||||
FsEntry::Directory(dir) => dir.name.clone(),
|
Entry::Directory(dir) => dir.name.clone(),
|
||||||
FsEntry::File(file) => file.name.clone(),
|
Entry::File(file) => file.name.clone(),
|
||||||
};
|
};
|
||||||
// Match entry
|
// Match entry
|
||||||
let result: Result<(), String> = match entry {
|
let result: Result<(), String> = match entry {
|
||||||
FsEntry::File(file) => {
|
Entry::File(file) => {
|
||||||
// Get local file
|
// Get local file
|
||||||
let mut local_file_path: PathBuf = PathBuf::from(local_path);
|
let mut local_file_path: PathBuf = PathBuf::from(local_path);
|
||||||
let local_file_name: String = match dst_name {
|
let local_file_name: String = match dst_name {
|
||||||
@@ -781,7 +793,7 @@ impl FileTransferActivity {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FsEntry::Directory(dir) => {
|
Entry::Directory(dir) => {
|
||||||
// Get dir name
|
// Get dir name
|
||||||
let mut local_dir_path: PathBuf = PathBuf::from(local_path);
|
let mut local_dir_path: PathBuf = PathBuf::from(local_path);
|
||||||
match dst_name {
|
match dst_name {
|
||||||
@@ -797,16 +809,13 @@ impl FileTransferActivity {
|
|||||||
target_os = "macos",
|
target_os = "macos",
|
||||||
target_os = "linux"
|
target_os = "linux"
|
||||||
))]
|
))]
|
||||||
if let Some((owner, group, others)) = dir.unix_pex {
|
if let Some(mode) = dir.metadata.mode {
|
||||||
if let Err(err) = self.host.chmod(
|
if let Err(err) = self.host.chmod(local_dir_path.as_path(), mode) {
|
||||||
local_dir_path.as_path(),
|
|
||||||
(owner.as_byte(), group.as_byte(), others.as_byte()),
|
|
||||||
) {
|
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not apply file mode {:?} to \"{}\": {}",
|
"Could not apply file mode {:o} to \"{}\": {}",
|
||||||
(owner.as_byte(), group.as_byte(), others.as_byte()),
|
u32::from(mode),
|
||||||
local_dir_path.display(),
|
local_dir_path.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
@@ -818,7 +827,7 @@ impl FileTransferActivity {
|
|||||||
format!("Created directory \"{}\"", local_dir_path.display()),
|
format!("Created directory \"{}\"", local_dir_path.display()),
|
||||||
);
|
);
|
||||||
// Get files in dir
|
// Get files in dir
|
||||||
match self.client.list_dir(dir.abs_path.as_path()) {
|
match self.client.list_dir(dir.path.as_path()) {
|
||||||
Ok(entries) => {
|
Ok(entries) => {
|
||||||
// Iterate over files
|
// Iterate over files
|
||||||
for entry in entries.iter() {
|
for entry in entries.iter() {
|
||||||
@@ -843,7 +852,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not scan directory \"{}\": {}",
|
"Could not scan directory \"{}\": {}",
|
||||||
dir.abs_path.display(),
|
dir.path.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -872,10 +881,7 @@ impl FileTransferActivity {
|
|||||||
// Log abort
|
// Log abort
|
||||||
self.log_and_alert(
|
self.log_and_alert(
|
||||||
LogLevel::Warn,
|
LogLevel::Warn,
|
||||||
format!(
|
format!("Download aborted for \"{}\"!", entry.path().display()),
|
||||||
"Download aborted for \"{}\"!",
|
|
||||||
entry.get_abs_path().display()
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
@@ -887,18 +893,18 @@ impl FileTransferActivity {
|
|||||||
fn filetransfer_recv_one(
|
fn filetransfer_recv_one(
|
||||||
&mut self,
|
&mut self,
|
||||||
local: &Path,
|
local: &Path,
|
||||||
remote: &FsFile,
|
remote: &File,
|
||||||
file_name: String,
|
file_name: String,
|
||||||
) -> Result<(), TransferErrorReason> {
|
) -> Result<(), TransferErrorReason> {
|
||||||
// Try to open local file
|
// Try to open local file
|
||||||
match self.host.open_file_write(local) {
|
match self.host.open_file_write(local) {
|
||||||
Ok(local_file) => {
|
Ok(local_file) => {
|
||||||
// Download file from remote
|
// Download file from remote
|
||||||
match self.client.recv_file(remote) {
|
match self.client.open(remote.path.as_path()) {
|
||||||
Ok(rhnd) => self.filetransfer_recv_one_with_stream(
|
Ok(rhnd) => self.filetransfer_recv_one_with_stream(
|
||||||
local, remote, file_name, rhnd, local_file,
|
local, remote, file_name, rhnd, local_file,
|
||||||
),
|
),
|
||||||
Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
|
Err(err) if err.kind == RemoteErrorType::UnsupportedFeature => {
|
||||||
self.filetransfer_recv_one_wno_stream(local, remote, file_name)
|
self.filetransfer_recv_one_wno_stream(local, remote, file_name)
|
||||||
}
|
}
|
||||||
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
|
Err(err) => Err(TransferErrorReason::FileTransferError(err)),
|
||||||
@@ -910,24 +916,24 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
/// ### filetransfer_recv_one_with_stream
|
/// ### filetransfer_recv_one_with_stream
|
||||||
///
|
///
|
||||||
/// Receive an `FsEntry` from remote using stream
|
/// Receive an `Entry` from remote using stream
|
||||||
fn filetransfer_recv_one_with_stream(
|
fn filetransfer_recv_one_with_stream(
|
||||||
&mut self,
|
&mut self,
|
||||||
local: &Path,
|
local: &Path,
|
||||||
remote: &FsFile,
|
remote: &File,
|
||||||
file_name: String,
|
file_name: String,
|
||||||
mut reader: Box<dyn Read>,
|
mut reader: Box<dyn Read>,
|
||||||
mut writer: File,
|
mut writer: StdFile,
|
||||||
) -> Result<(), TransferErrorReason> {
|
) -> Result<(), TransferErrorReason> {
|
||||||
let mut total_bytes_written: usize = 0;
|
let mut total_bytes_written: usize = 0;
|
||||||
// Init transfer
|
// Init transfer
|
||||||
self.transfer.partial.init(remote.size);
|
self.transfer.partial.init(remote.metadata.size as usize);
|
||||||
// Write local file
|
// Write local file
|
||||||
let mut last_progress_val: f64 = 0.0;
|
let mut last_progress_val: f64 = 0.0;
|
||||||
let mut last_input_event_fetch: Option<Instant> = None;
|
let mut last_input_event_fetch: Option<Instant> = None;
|
||||||
// While the entire file hasn't been completely read,
|
// While the entire file hasn't been completely read,
|
||||||
// Or filetransfer has been aborted
|
// Or filetransfer has been aborted
|
||||||
while total_bytes_written < remote.size && !self.transfer.aborted() {
|
while total_bytes_written < remote.metadata.size as usize && !self.transfer.aborted() {
|
||||||
// Handle input events (each 500 ms) or is None
|
// Handle input events (each 500 ms) or is None
|
||||||
if last_input_event_fetch.is_none()
|
if last_input_event_fetch.is_none()
|
||||||
|| last_input_event_fetch
|
|| last_input_event_fetch
|
||||||
@@ -978,7 +984,7 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Finalize stream
|
// Finalize stream
|
||||||
if let Err(err) = self.client.on_recv(reader) {
|
if let Err(err) = self.client.on_read(reader) {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Warn,
|
LogLevel::Warn,
|
||||||
format!("Could not finalize remote stream: \"{}\"", err),
|
format!("Could not finalize remote stream: \"{}\"", err),
|
||||||
@@ -990,16 +996,13 @@ impl FileTransferActivity {
|
|||||||
}
|
}
|
||||||
// Apply file mode to file
|
// Apply file mode to file
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
if let Some((owner, group, others)) = remote.unix_pex {
|
if let Some(mode) = remote.metadata.mode {
|
||||||
if let Err(err) = self
|
if let Err(err) = self.host.chmod(local, mode) {
|
||||||
.host
|
|
||||||
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
|
|
||||||
{
|
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not apply file mode {:?} to \"{}\": {}",
|
"Could not apply file mode {:o} to \"{}\": {}",
|
||||||
(owner.as_byte(), group.as_byte(), others.as_byte()),
|
u32::from(mode),
|
||||||
local.display(),
|
local.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
@@ -1011,7 +1014,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||||
remote.abs_path.display(),
|
remote.path.display(),
|
||||||
local.display(),
|
local.display(),
|
||||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||||
@@ -1022,40 +1025,47 @@ impl FileTransferActivity {
|
|||||||
|
|
||||||
/// ### filetransfer_recv_one_with_stream
|
/// ### filetransfer_recv_one_with_stream
|
||||||
///
|
///
|
||||||
/// Receive an `FsEntry` from remote without using stream
|
/// Receive an `Entry` from remote without using stream
|
||||||
fn filetransfer_recv_one_wno_stream(
|
fn filetransfer_recv_one_wno_stream(
|
||||||
&mut self,
|
&mut self,
|
||||||
local: &Path,
|
local: &Path,
|
||||||
remote: &FsFile,
|
remote: &File,
|
||||||
file_name: String,
|
file_name: String,
|
||||||
) -> Result<(), TransferErrorReason> {
|
) -> Result<(), TransferErrorReason> {
|
||||||
|
// Open local file
|
||||||
|
let reader = self
|
||||||
|
.host
|
||||||
|
.open_file_write(local)
|
||||||
|
.map_err(TransferErrorReason::HostError)
|
||||||
|
.map(Box::new)?;
|
||||||
// Init transfer
|
// Init transfer
|
||||||
self.transfer.partial.init(remote.size);
|
self.transfer.partial.init(remote.metadata.size as usize);
|
||||||
// Draw before transfer
|
// Draw before transfer
|
||||||
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
|
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
|
||||||
self.view();
|
self.view();
|
||||||
// recv wno stream
|
// recv wno stream
|
||||||
if let Err(err) = self.client.recv_file_wno_stream(remote, local) {
|
if let Err(err) = self.client.open_file(remote.path.as_path(), reader) {
|
||||||
return Err(TransferErrorReason::FileTransferError(err));
|
return Err(TransferErrorReason::FileTransferError(err));
|
||||||
}
|
}
|
||||||
// Update progress at the end
|
// Update progress at the end
|
||||||
self.transfer.partial.update_progress(remote.size);
|
self.transfer
|
||||||
self.transfer.full.update_progress(remote.size);
|
.partial
|
||||||
|
.update_progress(remote.metadata.size as usize);
|
||||||
|
self.transfer
|
||||||
|
.full
|
||||||
|
.update_progress(remote.metadata.size as usize);
|
||||||
// Draw after transfer
|
// Draw after transfer
|
||||||
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
|
self.update_progress_bar(format!("Downloading \"{}\"", file_name));
|
||||||
self.view();
|
self.view();
|
||||||
// Apply file mode to file
|
// Apply file mode to file
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
if let Some((owner, group, others)) = remote.unix_pex {
|
if let Some(mode) = remote.metadata.mode {
|
||||||
if let Err(err) = self
|
if let Err(err) = self.host.chmod(local, mode) {
|
||||||
.host
|
|
||||||
.chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
|
|
||||||
{
|
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!(
|
||||||
"Could not apply file mode {:?} to \"{}\": {}",
|
"Could not apply file mode {:o} to \"{}\": {}",
|
||||||
(owner.as_byte(), group.as_byte(), others.as_byte()),
|
u32::from(mode),
|
||||||
local.display(),
|
local.display(),
|
||||||
err
|
err
|
||||||
),
|
),
|
||||||
@@ -1067,7 +1077,7 @@ impl FileTransferActivity {
|
|||||||
LogLevel::Info,
|
LogLevel::Info,
|
||||||
format!(
|
format!(
|
||||||
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
"Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
|
||||||
remote.abs_path.display(),
|
remote.path.display(),
|
||||||
local.display(),
|
local.display(),
|
||||||
fmt_millis(self.transfer.partial.started().elapsed()),
|
fmt_millis(self.transfer.partial.started().elapsed()),
|
||||||
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
ByteSize(self.transfer.partial.calc_bytes_per_second()),
|
||||||
@@ -1136,7 +1146,7 @@ impl FileTransferActivity {
|
|||||||
/// ### download_file_as_temp
|
/// ### download_file_as_temp
|
||||||
///
|
///
|
||||||
/// Download provided file as a temporary file
|
/// Download provided file as a temporary file
|
||||||
pub(super) fn download_file_as_temp(&mut self, file: &FsFile) -> Result<PathBuf, String> {
|
pub(super) fn download_file_as_temp(&mut self, file: &File) -> Result<PathBuf, String> {
|
||||||
let tmpfile: PathBuf = match self.cache.as_ref() {
|
let tmpfile: PathBuf = match self.cache.as_ref() {
|
||||||
Some(cache) => {
|
Some(cache) => {
|
||||||
let mut p: PathBuf = cache.path().to_path_buf();
|
let mut p: PathBuf = cache.path().to_path_buf();
|
||||||
@@ -1157,7 +1167,7 @@ impl FileTransferActivity {
|
|||||||
) {
|
) {
|
||||||
Err(err) => Err(format!(
|
Err(err) => Err(format!(
|
||||||
"Could not download {} to temporary file: {}",
|
"Could not download {} to temporary file: {}",
|
||||||
file.abs_path.display(),
|
file.path.display(),
|
||||||
err
|
err
|
||||||
)),
|
)),
|
||||||
Ok(()) => Ok(tmpfile),
|
Ok(()) => Ok(tmpfile),
|
||||||
@@ -1169,12 +1179,12 @@ impl FileTransferActivity {
|
|||||||
/// ### get_total_transfer_size_local
|
/// ### get_total_transfer_size_local
|
||||||
///
|
///
|
||||||
/// Get total size of transfer for localhost
|
/// Get total size of transfer for localhost
|
||||||
fn get_total_transfer_size_local(&mut self, entry: &FsEntry) -> usize {
|
fn get_total_transfer_size_local(&mut self, entry: &Entry) -> usize {
|
||||||
match entry {
|
match entry {
|
||||||
FsEntry::File(file) => file.size,
|
Entry::File(file) => file.metadata.size as usize,
|
||||||
FsEntry::Directory(dir) => {
|
Entry::Directory(dir) => {
|
||||||
// List dir
|
// List dir
|
||||||
match self.host.scan_dir(dir.abs_path.as_path()) {
|
match self.host.scan_dir(dir.path.as_path()) {
|
||||||
Ok(files) => files
|
Ok(files) => files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| self.get_total_transfer_size_local(x))
|
.map(|x| self.get_total_transfer_size_local(x))
|
||||||
@@ -1182,11 +1192,7 @@ impl FileTransferActivity {
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!("Could not list directory {}: {}", dir.path.display(), err),
|
||||||
"Could not list directory {}: {}",
|
|
||||||
dir.abs_path.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
@@ -1198,12 +1204,12 @@ impl FileTransferActivity {
|
|||||||
/// ### get_total_transfer_size_remote
|
/// ### get_total_transfer_size_remote
|
||||||
///
|
///
|
||||||
/// Get total size of transfer for remote host
|
/// Get total size of transfer for remote host
|
||||||
fn get_total_transfer_size_remote(&mut self, entry: &FsEntry) -> usize {
|
fn get_total_transfer_size_remote(&mut self, entry: &Entry) -> usize {
|
||||||
match entry {
|
match entry {
|
||||||
FsEntry::File(file) => file.size,
|
Entry::File(file) => file.metadata.size as usize,
|
||||||
FsEntry::Directory(dir) => {
|
Entry::Directory(dir) => {
|
||||||
// List directory
|
// List directory
|
||||||
match self.client.list_dir(dir.abs_path.as_path()) {
|
match self.client.list_dir(dir.path.as_path()) {
|
||||||
Ok(files) => files
|
Ok(files) => files
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| self.get_total_transfer_size_remote(x))
|
.map(|x| self.get_total_transfer_size_remote(x))
|
||||||
@@ -1211,11 +1217,7 @@ impl FileTransferActivity {
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.log(
|
self.log(
|
||||||
LogLevel::Error,
|
LogLevel::Error,
|
||||||
format!(
|
format!("Could not list directory {}: {}", dir.path.display(), err),
|
||||||
"Could not list directory {}: {}",
|
|
||||||
dir.abs_path.display(),
|
|
||||||
err
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ use super::{
|
|||||||
browser::{FileExplorerTab, FoundExplorerTab},
|
browser::{FileExplorerTab, FoundExplorerTab},
|
||||||
ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg,
|
ExitReason, FileTransferActivity, Id, Msg, TransferMsg, TransferOpts, UiMsg,
|
||||||
};
|
};
|
||||||
use crate::fs::FsEntry;
|
|
||||||
// externals
|
// externals
|
||||||
|
use remotefs::fs::Entry;
|
||||||
use tuirealm::{
|
use tuirealm::{
|
||||||
props::{AttrValue, Attribute},
|
props::{AttrValue, Attribute},
|
||||||
State, StateValue, Update,
|
State, StateValue, Update,
|
||||||
@@ -282,7 +282,7 @@ impl FileTransferActivity {
|
|||||||
// Mount wait
|
// Mount wait
|
||||||
self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, search).as_str());
|
self.mount_blocking_wait(format!(r#"Searching for "{}"…"#, search).as_str());
|
||||||
// Find
|
// Find
|
||||||
let res: Result<Vec<FsEntry>, String> = match self.browser.tab() {
|
let res: Result<Vec<Entry>, String> = match self.browser.tab() {
|
||||||
FileExplorerTab::Local => self.action_local_find(search.clone()),
|
FileExplorerTab::Local => self.action_local_find(search.clone()),
|
||||||
FileExplorerTab::Remote => self.action_remote_find(search.clone()),
|
FileExplorerTab::Remote => self.action_remote_find(search.clone()),
|
||||||
_ => panic!("Trying to search for files, while already in a find result"),
|
_ => panic!("Trying to search for files, while already in a find result"),
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ use super::{
|
|||||||
browser::{FileExplorerTab, FoundExplorerTab},
|
browser::{FileExplorerTab, FoundExplorerTab},
|
||||||
components, Context, FileTransferActivity, Id,
|
components, Context, FileTransferActivity, Id,
|
||||||
};
|
};
|
||||||
use crate::fs::explorer::FileSorting;
|
use crate::explorer::FileSorting;
|
||||||
use crate::fs::FsEntry;
|
|
||||||
use crate::ui::store::Store;
|
use crate::ui::store::Store;
|
||||||
use crate::utils::ui::draw_area_in;
|
use crate::utils::ui::draw_area_in;
|
||||||
// Ext
|
// Ext
|
||||||
|
use remotefs::fs::Entry;
|
||||||
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
use tuirealm::event::{Key, KeyEvent, KeyModifiers};
|
||||||
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
use tuirealm::tui::layout::{Constraint, Direction, Layout};
|
||||||
use tuirealm::tui::widgets::Clear;
|
use tuirealm::tui::widgets::Clear;
|
||||||
@@ -747,7 +747,7 @@ impl FileTransferActivity {
|
|||||||
let _ = self.app.umount(&Id::ReplacingFilesListPopup); // NOTE: replace anyway
|
let _ = self.app.umount(&Id::ReplacingFilesListPopup); // NOTE: replace anyway
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn mount_file_info(&mut self, file: &FsEntry) {
|
pub(super) fn mount_file_info(&mut self, file: &Entry) {
|
||||||
assert!(self
|
assert!(self
|
||||||
.app
|
.app
|
||||||
.remount(
|
.remount(
|
||||||
|
|||||||
@@ -26,8 +26,8 @@
|
|||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
use super::{ConfigMsg, Msg};
|
use super::{ConfigMsg, Msg};
|
||||||
|
use crate::explorer::GroupDirs as GroupDirsEnum;
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
use crate::fs::explorer::GroupDirs as GroupDirsEnum;
|
|
||||||
use crate::utils::parser::parse_bytesize;
|
use crate::utils::parser::parse_bytesize;
|
||||||
|
|
||||||
use tui_realm_stdlib::{Input, Radio};
|
use tui_realm_stdlib::{Input, Radio};
|
||||||
|
|||||||
@@ -46,19 +46,11 @@ use tuirealm::{Component, MockComponent};
|
|||||||
|
|
||||||
// -- global listener
|
// -- global listener
|
||||||
|
|
||||||
#[derive(MockComponent)]
|
#[derive(Default, MockComponent)]
|
||||||
pub struct GlobalListener {
|
pub struct GlobalListener {
|
||||||
component: Phantom,
|
component: Phantom,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GlobalListener {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
component: Phantom::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Component<Msg, NoUserEvent> for GlobalListener {
|
impl Component<Msg, NoUserEvent> for GlobalListener {
|
||||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||||
match ev {
|
match ev {
|
||||||
|
|||||||
@@ -28,8 +28,8 @@
|
|||||||
*/
|
*/
|
||||||
// Locals
|
// Locals
|
||||||
use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout};
|
use super::{components, Context, Id, IdCommon, IdConfig, SetupActivity, ViewLayout};
|
||||||
|
use crate::explorer::GroupDirs;
|
||||||
use crate::filetransfer::FileTransferProtocol;
|
use crate::filetransfer::FileTransferProtocol;
|
||||||
use crate::fs::explorer::GroupDirs;
|
|
||||||
use crate::utils::fmt::fmt_bytes;
|
use crate::utils::fmt::fmt_bytes;
|
||||||
|
|
||||||
// Ext
|
// Ext
|
||||||
|
|||||||
+8
-8
@@ -25,7 +25,7 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
use crate::fs::UnixPex;
|
use remotefs::fs::UnixPexClass;
|
||||||
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -35,18 +35,18 @@ use tuirealm::tui::style::Color;
|
|||||||
/// ### fmt_pex
|
/// ### fmt_pex
|
||||||
///
|
///
|
||||||
/// Convert permissions bytes of permissions value into ls notation (e.g. rwx,-wx,--x)
|
/// Convert permissions bytes of permissions value into ls notation (e.g. rwx,-wx,--x)
|
||||||
pub fn fmt_pex(pex: UnixPex) -> String {
|
pub fn fmt_pex(pex: UnixPexClass) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
match pex.can_read() {
|
match pex.read() {
|
||||||
true => 'r',
|
true => 'r',
|
||||||
false => '-',
|
false => '-',
|
||||||
},
|
},
|
||||||
match pex.can_write() {
|
match pex.write() {
|
||||||
true => 'w',
|
true => 'w',
|
||||||
false => '-',
|
false => '-',
|
||||||
},
|
},
|
||||||
match pex.can_execute() {
|
match pex.execute() {
|
||||||
true => 'x',
|
true => 'x',
|
||||||
false => '-',
|
false => '-',
|
||||||
}
|
}
|
||||||
@@ -315,9 +315,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_utils_fmt_pex() {
|
fn test_utils_fmt_pex() {
|
||||||
assert_eq!(fmt_pex(UnixPex::from(7)), String::from("rwx"));
|
assert_eq!(fmt_pex(UnixPexClass::from(7)), String::from("rwx"));
|
||||||
assert_eq!(fmt_pex(UnixPex::from(5)), String::from("r-x"));
|
assert_eq!(fmt_pex(UnixPexClass::from(5)), String::from("r-x"));
|
||||||
assert_eq!(fmt_pex(UnixPex::from(6)), String::from("rw-"));
|
assert_eq!(fmt_pex(UnixPexClass::from(6)), String::from("rw-"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -37,12 +37,9 @@ use crate::system::environment;
|
|||||||
|
|
||||||
// Ext
|
// Ext
|
||||||
use bytesize::ByteSize;
|
use bytesize::ByteSize;
|
||||||
use chrono::format::ParseError;
|
|
||||||
use chrono::prelude::*;
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::{Duration, SystemTime};
|
|
||||||
use tuirealm::tui::style::Color;
|
use tuirealm::tui::style::Color;
|
||||||
|
|
||||||
// Regex
|
// Regex
|
||||||
@@ -267,54 +264,6 @@ fn parse_s3_remote_opt(s: &str) -> Result<FileTransferParams, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### parse_lstime
|
|
||||||
///
|
|
||||||
/// Convert ls syntax time to System Time
|
|
||||||
/// ls time has two possible syntax:
|
|
||||||
/// 1. if year is current: %b %d %H:%M (e.g. Nov 5 13:46)
|
|
||||||
/// 2. else: %b %d %Y (e.g. Nov 5 2019)
|
|
||||||
pub fn parse_lstime(tm: &str, fmt_year: &str, fmt_hours: &str) -> Result<SystemTime, ParseError> {
|
|
||||||
let datetime: NaiveDateTime = match NaiveDate::parse_from_str(tm, fmt_year) {
|
|
||||||
Ok(date) => {
|
|
||||||
// Case 2.
|
|
||||||
// Return NaiveDateTime from NaiveDate with time 00:00:00
|
|
||||||
date.and_hms(0, 0, 0)
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
// Might be case 1.
|
|
||||||
// We need to add Current Year at the end of the string
|
|
||||||
let this_year: i32 = Utc::now().year();
|
|
||||||
let date_time_str: String = format!("{} {}", tm, this_year);
|
|
||||||
// Now parse
|
|
||||||
NaiveDateTime::parse_from_str(
|
|
||||||
date_time_str.as_ref(),
|
|
||||||
format!("{} %Y", fmt_hours).as_ref(),
|
|
||||||
)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// Convert datetime to system time
|
|
||||||
let sys_time: SystemTime = SystemTime::UNIX_EPOCH;
|
|
||||||
Ok(sys_time
|
|
||||||
.checked_add(Duration::from_secs(datetime.timestamp() as u64))
|
|
||||||
.unwrap_or(SystemTime::UNIX_EPOCH))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### parse_datetime
|
|
||||||
///
|
|
||||||
/// Parse date time string representation and transform it into `SystemTime`
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn parse_datetime(tm: &str, fmt: &str) -> Result<SystemTime, ParseError> {
|
|
||||||
match NaiveDateTime::parse_from_str(tm, fmt) {
|
|
||||||
Ok(dt) => {
|
|
||||||
let sys_time: SystemTime = SystemTime::UNIX_EPOCH;
|
|
||||||
Ok(sys_time
|
|
||||||
.checked_add(Duration::from_secs(dt.timestamp() as u64))
|
|
||||||
.unwrap_or(SystemTime::UNIX_EPOCH))
|
|
||||||
}
|
|
||||||
Err(err) => Err(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### parse_semver
|
/// ### parse_semver
|
||||||
///
|
///
|
||||||
/// Parse semver string
|
/// Parse semver string
|
||||||
@@ -611,7 +560,6 @@ pub fn parse_bytesize<S: AsRef<str>>(bytes: S) -> Option<ByteSize> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::utils::fmt::fmt_time;
|
|
||||||
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
@@ -800,68 +748,6 @@ mod tests {
|
|||||||
assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err());
|
assert!(parse_remote_opt(&String::from("s3://mybucket:default:/foobar")).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_utils_parse_lstime() {
|
|
||||||
// Good cases
|
|
||||||
assert_eq!(
|
|
||||||
fmt_time(
|
|
||||||
parse_lstime("Nov 5 16:32", "%b %d %Y", "%b %d %H:%M")
|
|
||||||
.ok()
|
|
||||||
.unwrap(),
|
|
||||||
"%m %d %M"
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
"11 05 32"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
fmt_time(
|
|
||||||
parse_lstime("Dec 2 21:32", "%b %d %Y", "%b %d %H:%M")
|
|
||||||
.ok()
|
|
||||||
.unwrap(),
|
|
||||||
"%m %d %M"
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
"12 02 32"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_lstime("Nov 5 2018", "%b %d %Y", "%b %d %H:%M")
|
|
||||||
.ok()
|
|
||||||
.unwrap()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.ok()
|
|
||||||
.unwrap(),
|
|
||||||
Duration::from_secs(1541376000)
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
parse_lstime("Mar 18 2018", "%b %d %Y", "%b %d %H:%M")
|
|
||||||
.ok()
|
|
||||||
.unwrap()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.ok()
|
|
||||||
.unwrap(),
|
|
||||||
Duration::from_secs(1521331200)
|
|
||||||
);
|
|
||||||
// bad cases
|
|
||||||
assert!(parse_lstime("Oma 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
|
|
||||||
assert!(parse_lstime("Feb 31 2018", "%b %d %Y", "%b %d %H:%M").is_err());
|
|
||||||
assert!(parse_lstime("Feb 15 25:32", "%b %d %Y", "%b %d %H:%M").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_utils_parse_datetime() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_datetime("04-08-14 03:09PM", "%d-%m-%y %I:%M%p")
|
|
||||||
.ok()
|
|
||||||
.unwrap()
|
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
|
||||||
.ok()
|
|
||||||
.unwrap(),
|
|
||||||
Duration::from_secs(1407164940)
|
|
||||||
);
|
|
||||||
// Not enough argument for datetime
|
|
||||||
assert!(parse_datetime("04-08-14", "%d-%m-%y").is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_utils_parse_semver() {
|
fn test_utils_parse_semver() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
+23
-124
@@ -25,39 +25,27 @@
|
|||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
* SOFTWARE.
|
* SOFTWARE.
|
||||||
*/
|
*/
|
||||||
use crate::fs::{FsDirectory, FsEntry, FsFile, UnixPex};
|
use remotefs::fs::{Directory, Entry, File, Metadata};
|
||||||
// ext
|
// ext
|
||||||
use std::fs::File;
|
use std::fs::File as StdFile;
|
||||||
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
|
|
||||||
use std::fs::OpenOptions;
|
|
||||||
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
|
|
||||||
use std::io::Read;
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::SystemTime;
|
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
pub fn create_sample_file_entry() -> (FsFile, NamedTempFile) {
|
pub fn create_sample_file_entry() -> (File, NamedTempFile) {
|
||||||
// Write
|
// Write
|
||||||
let tmpfile = create_sample_file();
|
let tmpfile = create_sample_file();
|
||||||
(
|
(
|
||||||
FsFile {
|
File {
|
||||||
name: tmpfile
|
name: tmpfile
|
||||||
.path()
|
.path()
|
||||||
.file_name()
|
.file_name()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
abs_path: tmpfile.path().to_path_buf(),
|
path: tmpfile.path().to_path_buf(),
|
||||||
last_change_time: SystemTime::UNIX_EPOCH,
|
extension: None,
|
||||||
last_access_time: SystemTime::UNIX_EPOCH,
|
metadata: Metadata::default(),
|
||||||
creation_time: SystemTime::UNIX_EPOCH,
|
|
||||||
size: 127,
|
|
||||||
ftype: None, // File type
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
|
||||||
},
|
},
|
||||||
tmpfile,
|
tmpfile,
|
||||||
)
|
)
|
||||||
@@ -80,7 +68,7 @@ pub fn create_sample_file() -> NamedTempFile {
|
|||||||
pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> {
|
pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> {
|
||||||
let mut p: PathBuf = PathBuf::from(dir);
|
let mut p: PathBuf = PathBuf::from(dir);
|
||||||
p.push(filename);
|
p.push(filename);
|
||||||
let mut file: File = File::create(p.as_path())?;
|
let mut file = StdFile::create(p.as_path())?;
|
||||||
writeln!(
|
writeln!(
|
||||||
file,
|
file,
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus."
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus."
|
||||||
@@ -97,88 +85,20 @@ pub fn make_dir_at(dir: &Path, dirname: &str) -> std::io::Result<()> {
|
|||||||
std::fs::create_dir(p.as_path())
|
std::fs::create_dir(p.as_path())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(feature = "with-containers", feature = "with-s3-ci"))]
|
/// Create a Entry at specified path
|
||||||
pub fn write_file(file: &NamedTempFile, writable: &mut Box<dyn Write>) {
|
pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> Entry {
|
||||||
let mut fhnd = OpenOptions::new()
|
|
||||||
.create(false)
|
|
||||||
.read(true)
|
|
||||||
.write(false)
|
|
||||||
.open(file.path())
|
|
||||||
.ok()
|
|
||||||
.unwrap();
|
|
||||||
// Read file
|
|
||||||
let mut buffer: [u8; 65536] = [0; 65536];
|
|
||||||
assert!(fhnd.read(&mut buffer).is_ok());
|
|
||||||
// Write file
|
|
||||||
assert!(writable.write(&buffer).is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "with-containers")]
|
|
||||||
pub fn write_ssh_key() -> NamedTempFile {
|
|
||||||
let mut tmpfile: NamedTempFile = NamedTempFile::new().unwrap();
|
|
||||||
writeln!(
|
|
||||||
tmpfile,
|
|
||||||
r"-----BEGIN OPENSSH PRIVATE KEY-----
|
|
||||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn
|
|
||||||
NhAAAAAwEAAQAAAQEAxKyYUMRCNPlb4ZV1VMofrzApu2l3wgP4Ot9wBvHsw/+RMpcHIbQK
|
|
||||||
9iQqAVp8Z+M1fJyPXTKjoJtIzuCLF6Sjo0KI7/tFTh+yPnA5QYNLZOIRZb8skumL4gwHww
|
|
||||||
5Z942FDPuUDQ30C2mZR9lr3Cd5pA8S1ZSPTAV9QQHkpgoS8cAL8QC6dp3CJjUC8wzvXh3I
|
|
||||||
oN3bTKxCpM10KMEVuWO3lM4Nvr71auB9gzo1sFJ3bwebCZIRH01FROyA/GXRiaOtJFG/9N
|
|
||||||
nWWI/iG5AJzArKpLZNHIP+FxV/NoRH0WBXm9Wq5MrBYrD1NQzm+kInpS/2sXk3m1aZWqLm
|
|
||||||
HF2NKRXSbQAAA8iI+KSniPikpwAAAAdzc2gtcnNhAAABAQDErJhQxEI0+VvhlXVUyh+vMC
|
|
||||||
m7aXfCA/g633AG8ezD/5EylwchtAr2JCoBWnxn4zV8nI9dMqOgm0jO4IsXpKOjQojv+0VO
|
|
||||||
H7I+cDlBg0tk4hFlvyyS6YviDAfDDln3jYUM+5QNDfQLaZlH2WvcJ3mkDxLVlI9MBX1BAe
|
|
||||||
SmChLxwAvxALp2ncImNQLzDO9eHcig3dtMrEKkzXQowRW5Y7eUzg2+vvVq4H2DOjWwUndv
|
|
||||||
B5sJkhEfTUVE7ID8ZdGJo60kUb/02dZYj+IbkAnMCsqktk0cg/4XFX82hEfRYFeb1arkys
|
|
||||||
FisPU1DOb6QielL/axeTebVplaouYcXY0pFdJtAAAAAwEAAQAAAP8u3PFuTVV5SfGazwIm
|
|
||||||
MgNaux82iOsAT/HWFWecQAkqqrruUw5f+YajH/riV61NE9aq2qNOkcJrgpTWtqpt980GGd
|
|
||||||
SHWlgpRWQzfIooEiDk6Pk8RVFZsEykkDlJQSIu2onZjhi5A5ojHgZoGGabDsztSqoyOjPq
|
|
||||||
6WPvGYRiDAR3leBMyp1WufBCJqAsC4L8CjPJSmnZhc5a0zXkC9Syz74Fa08tdM7bGhtvP1
|
|
||||||
GmzuYxkgxHH2IFeoumUSBHRiTZayGuRUDel6jgEiUMxenaDKXe7FpYzMm9tQZA10Mm4LhK
|
|
||||||
5rP9nd2/KRTFRnfZMnKvtIRC9vtlSLBe14qw+4ZCl60AAACAf1kghlO3+HIWplOmk/lCL0
|
|
||||||
w75Zz+RdvueL9UuoyNN1QrUEY420LsixgWSeRPby+Rb/hW+XSAZJQHowQ8acFJhU85So7f
|
|
||||||
4O4wcDuE4f6hpsW9tTfkCEUdLCQJ7EKLCrod6jIV7hvI6rvXiVucRpeAzdOaq4uzj2cwDd
|
|
||||||
tOdYVsnmQAAACBAOVxBsvO/Sr3rZUbNtA6KewZh/09HNGoKNaCeiD7vaSn2UJbbPRByF/o
|
|
||||||
Oo5zv8ee8r3882NnmG808XfSn7pPZAzbbTmOaJt0fmyZhivCghSNzV6njW3o0PdnC0fGZQ
|
|
||||||
ruVXgkd7RJFbsIiD4dDcF4VCjwWHfTK21EOgJUA5pN6TNvAAAAgQDbcJWRx8Uyhkj2+srb
|
|
||||||
3n2Rt6CR7kEl9cw17ItFjMn+pO81/5U2aGw0iLlX7E06TAMQC+dyW/WaxQRey8RRdtbJ1e
|
|
||||||
TNKCN34QCWkyuYRHGhcNc0quEDayPw5QWGXlP4BzjfRUcPxY9cCXLe5wDLYsX33HwOAc59
|
|
||||||
RorU9FCmS/654wAAABFyb290QDhjNTBmZDRjMzQ1YQECAw==
|
|
||||||
-----END OPENSSH PRIVATE KEY-----"
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
tmpfile
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ### make_fsentry
|
|
||||||
///
|
|
||||||
/// Create a FsEntry at specified path
|
|
||||||
pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> FsEntry {
|
|
||||||
let path: PathBuf = path.as_ref().to_path_buf();
|
let path: PathBuf = path.as_ref().to_path_buf();
|
||||||
match is_dir {
|
match is_dir {
|
||||||
true => FsEntry::Directory(FsDirectory {
|
true => Entry::Directory(Directory {
|
||||||
name: path.file_name().unwrap().to_string_lossy().to_string(),
|
name: path.file_name().unwrap().to_string_lossy().to_string(),
|
||||||
abs_path: path,
|
path,
|
||||||
last_change_time: SystemTime::UNIX_EPOCH,
|
metadata: Metadata::default(),
|
||||||
last_access_time: SystemTime::UNIX_EPOCH,
|
|
||||||
creation_time: SystemTime::UNIX_EPOCH,
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
|
||||||
}),
|
}),
|
||||||
false => FsEntry::File(FsFile {
|
false => Entry::File(File {
|
||||||
name: path.file_name().unwrap().to_string_lossy().to_string(),
|
name: path.file_name().unwrap().to_string_lossy().to_string(),
|
||||||
abs_path: path,
|
path,
|
||||||
last_change_time: SystemTime::UNIX_EPOCH,
|
extension: None,
|
||||||
last_access_time: SystemTime::UNIX_EPOCH,
|
metadata: Metadata::default(),
|
||||||
creation_time: SystemTime::UNIX_EPOCH,
|
|
||||||
size: 127,
|
|
||||||
ftype: None, // File type
|
|
||||||
symlink: None, // UNIX only
|
|
||||||
user: Some(0), // UNIX only
|
|
||||||
group: Some(0), // UNIX only
|
|
||||||
unix_pex: Some((UnixPex::from(6), UnixPex::from(4), UnixPex::from(4))), // UNIX only
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,8 +106,11 @@ pub fn make_fsentry<P: AsRef<Path>>(path: P, is_dir: bool) -> FsEntry {
|
|||||||
/// ### create_file_ioers
|
/// ### create_file_ioers
|
||||||
///
|
///
|
||||||
/// Open a file with two handlers, the first is to read, the second is to write
|
/// Open a file with two handlers, the first is to read, the second is to write
|
||||||
pub fn create_file_ioers(p: &Path) -> (File, File) {
|
pub fn create_file_ioers(p: &Path) -> (StdFile, StdFile) {
|
||||||
(File::open(p).ok().unwrap(), File::create(p).ok().unwrap())
|
(
|
||||||
|
StdFile::open(p).ok().unwrap(),
|
||||||
|
StdFile::create(p).ok().unwrap(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
mod test {
|
mod test {
|
||||||
@@ -197,31 +120,7 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_utils_test_helpers_sample_file() {
|
fn test_utils_test_helpers_sample_file() {
|
||||||
let (file, _) = create_sample_file_entry();
|
let _ = create_sample_file_entry();
|
||||||
assert!(file.symlink.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(feature = "with-containers")]
|
|
||||||
fn test_utils_test_helpers_write_file() {
|
|
||||||
let (_, temp) = create_sample_file_entry();
|
|
||||||
let tempdest = NamedTempFile::new().unwrap();
|
|
||||||
let mut dest: Box<dyn Write> = Box::new(
|
|
||||||
OpenOptions::new()
|
|
||||||
.create(true)
|
|
||||||
.read(false)
|
|
||||||
.write(true)
|
|
||||||
.open(tempdest.path())
|
|
||||||
.ok()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
write_file(&temp, &mut dest);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[cfg(feature = "with-containers")]
|
|
||||||
fn test_utils_test_helpers_write_ssh_key() {
|
|
||||||
let _ = write_ssh_key();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
Reference in New Issue
Block a user