mirror of
https://github.com/veeso/termscp.git
synced 2025-12-06 17:15:35 -08:00
committed by
GitHub
parent
2caa0432df
commit
816270d545
@@ -45,6 +45,12 @@ Released on FIXME:
|
||||
- It is now possible to configure the directory path you want to enter when you connect to the remote host from the authentication form
|
||||
- This parameter can be stored into bookmarks as you already do with the other parameters
|
||||
- You can find this field scrolling down in the authentication form
|
||||
- **File system watcher**:
|
||||
- It is now possible to synchronize changes from a local path to the remote host
|
||||
- Press `<T>` to start synchronizing changes from the selected directory/file to the remote directory
|
||||
- The changes will be automatically applied to the remote host with a maximum delay of 5 seconds
|
||||
- These changes are (if possible) applied: file removed, file changed, file renamed
|
||||
- Press `<CTRL+T>` to show all the currently synchronized files
|
||||
- **Enhancements**:
|
||||
- Improved s3 auth form scrolling
|
||||
- Dependencies:
|
||||
|
||||
261
Cargo.lock
generated
261
Cargo.lock
generated
@@ -72,7 +72,7 @@ version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -138,7 +138,7 @@ dependencies = [
|
||||
"slab",
|
||||
"socket2",
|
||||
"waker-fn",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -328,7 +328,7 @@ dependencies = [
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"time",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -368,7 +368,7 @@ dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"terminal_size",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -448,11 +448,11 @@ dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 0.8.0",
|
||||
"parking_lot 0.12.0",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -461,7 +461,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -502,7 +502,7 @@ checksum = "de0a745c25b32caa56b82a3950f5fec7893a960f4c10ca3b02060b0c38d8c2ce"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"libdbus-sys",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -556,7 +556,7 @@ checksum = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users 0.3.5",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -576,7 +576,7 @@ checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"redox_users 0.4.0",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -658,7 +658,7 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"libc",
|
||||
"redox_syscall 0.2.11",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -704,6 +704,41 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fsevent-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fuchsia-zircon-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fuchsia-zircon-sys"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.21"
|
||||
@@ -936,7 +971,7 @@ checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"match_cfg",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1043,6 +1078,26 @@ dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.12"
|
||||
@@ -1052,6 +1107,15 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iovec"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.4.0"
|
||||
@@ -1073,6 +1137,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kernel32-sys"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
|
||||
dependencies = [
|
||||
"winapi 0.2.8",
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "keyring"
|
||||
version = "1.1.2"
|
||||
@@ -1082,7 +1156,7 @@ dependencies = [
|
||||
"byteorder",
|
||||
"secret-service",
|
||||
"security-framework",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1091,6 +1165,12 @@ version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.119"
|
||||
@@ -1250,6 +1330,25 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.6.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"fuchsia-zircon",
|
||||
"fuchsia-zircon-sys",
|
||||
"iovec",
|
||||
"kernel32-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"miow 0.2.2",
|
||||
"net2",
|
||||
"slab",
|
||||
"winapi 0.2.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.0"
|
||||
@@ -1258,9 +1357,33 @@ checksum = "ba272f85fa0b41fc91872be579b3bbe0f56b792aa361a380eb669469f68dafb2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"miow",
|
||||
"miow 0.3.7",
|
||||
"ntapi",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio-extras"
|
||||
version = "2.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
|
||||
dependencies = [
|
||||
"lazycell",
|
||||
"log",
|
||||
"mio 0.6.23",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "miow"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
|
||||
dependencies = [
|
||||
"kernel32-sys",
|
||||
"net2",
|
||||
"winapi 0.2.8",
|
||||
"ws2_32-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1269,7 +1392,7 @@ version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1300,6 +1423,17 @@ dependencies = [
|
||||
"socket2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "net2"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.10",
|
||||
"libc",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.17.0"
|
||||
@@ -1313,6 +1447,24 @@ dependencies = [
|
||||
"void",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "4.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"filetime",
|
||||
"fsevent",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"libc",
|
||||
"mio 0.6.23",
|
||||
"mio-extras",
|
||||
"walkdir",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-rust"
|
||||
version = "4.5.6"
|
||||
@@ -1330,7 +1482,7 @@ version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1473,7 +1625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9213e7b66aa06a7722828ee2980c1adff22a3922b582baaa1e62e30ca2a6c018"
|
||||
dependencies = [
|
||||
"pathdiff",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1525,7 +1677,7 @@ version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1566,7 +1718,7 @@ dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.2.11",
|
||||
"smallvec",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1637,7 +1789,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wepoll-ffi",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1850,7 +2002,7 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1896,7 +2048,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1956,6 +2108,15 @@ version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schannel"
|
||||
version = "0.1.19"
|
||||
@@ -1963,7 +2124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2178,7 +2339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 0.8.0",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
@@ -2227,7 +2388,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2333,7 +2494,7 @@ dependencies = [
|
||||
"libc",
|
||||
"redox_syscall 0.2.11",
|
||||
"remove_dir_all",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2352,7 +2513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2371,6 +2532,7 @@ dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"magic-crypt",
|
||||
"notify",
|
||||
"notify-rust",
|
||||
"open",
|
||||
"pretty_assertions",
|
||||
@@ -2446,7 +2608,7 @@ checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2473,11 +2635,11 @@ dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"memchr",
|
||||
"mio",
|
||||
"mio 0.8.0",
|
||||
"num_cpus",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2706,6 +2868,17 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi 0.3.9",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.0"
|
||||
@@ -2840,6 +3013,12 @@ version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.2.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -2850,6 +3029,12 @@ dependencies = [
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-build"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
@@ -2862,7 +3047,7 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2956,7 +3141,7 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2970,6 +3155,16 @@ dependencies = [
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ws2_32-sys"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
|
||||
dependencies = [
|
||||
"winapi 0.2.8",
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
version = "0.2.2"
|
||||
|
||||
@@ -43,6 +43,7 @@ keyring = { version = "1.1.2", optional = true }
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.14"
|
||||
magic-crypt = "3.1.9"
|
||||
notify = "4.0.17"
|
||||
notify-rust = { version = "4.5.6", default-features = false, features = [ "d" ] }
|
||||
open = "2.0.3"
|
||||
rand = "0.8.5"
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- [Text Editor ✏](#text-editor-)
|
||||
- [Logging 🩺](#logging-)
|
||||
- [Notifications 📫](#notifications-)
|
||||
- [File watcher 🔭](#file-watcher-)
|
||||
|
||||
> ❗ I need a help to translate this manual into German. If you want to contribute to the translations, please open a PR 🙏
|
||||
|
||||
@@ -206,6 +207,7 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
|
||||
| `<Q|F10>` | Quit termscp | Quit |
|
||||
| `<R|F6>` | Rename file | Rename |
|
||||
| `<S|F2>` | Save file as... | Save |
|
||||
| `<T>` | Synchronize changes to selected path to remote | Track |
|
||||
| `<U>` | Go to parent directory | Upper |
|
||||
| `<V|F3>` | Open file with default program for filetype | View |
|
||||
| `<W>` | Open file with provided program | With |
|
||||
@@ -213,6 +215,7 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
|
||||
| `<Y>` | Toggle synchronized browsing | sYnc |
|
||||
| `<CTRL+A>` | Select all files | |
|
||||
| `<CTRL+C>` | Abort file transfer process | |
|
||||
| `<CTRL+T>` | Show all synchronized paths | Track |
|
||||
|
||||
### Work on multiple files 🥷
|
||||
|
||||
@@ -512,3 +515,26 @@ Termscp will send Desktop notifications for these kind of events:
|
||||
|
||||
❗ If you prefer to keep notifications turned off, you can just enter setup and set `Enable notifications?` to `No` 😉.
|
||||
❗ If you want to change the minimum transfer size to display notifications, you can change the value in the configuration with key `Notifications: minimum transfer size` and set it to whatever suits better for you 🙂.
|
||||
|
||||
## File watcher 🔭
|
||||
|
||||
The file watcher allows you to setup a list of paths to synchronize with the remote hosts.
|
||||
This means that whenever a change on the local file system will be detected on the synchronized path, the change will be automatically reported to the configured remote host path, within 5 seconds.
|
||||
|
||||
You can set as many paths to synchronize as you prefer:
|
||||
|
||||
1. Put the cursor on the local explorer on the directory/file you want to keep synchronized
|
||||
2. Go to the directory you want the changes to be reported to on the remote host
|
||||
3. Press `<T>`
|
||||
4. Answer `<YES>` to the radio popup
|
||||
|
||||
To unwatch, just press `<T>` on the local synchronized path (or to any of its subfolders)
|
||||
OR you can just press `<CTRL+T>` and press `<ENTER>` to the synchronized path you want to unwatch.
|
||||
|
||||
These changes will be reported to the remote host:
|
||||
|
||||
- New files, file changes
|
||||
- File moved/renamed
|
||||
- File removed/unlinked
|
||||
|
||||
> ❗ The watcher works only in one direction (local > remote). It is NOT possible to synchronize automatically the changes from remote to local.
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- [Text Editor ✏](#text-editor-)
|
||||
- [Logging 🩺](#logging-)
|
||||
- [Notificaciones 📫](#notificaciones-)
|
||||
- [Observador de archivos 🔭](#observador-de-archivos-)
|
||||
|
||||
> ❗ Este documento ha sido traducido con Google Translator (y luego lo he revisado a grandes rasgos, pero no puedo hablar el idioma muy bien). Si habla l'idioma, abra un [issue](https://github.com/veeso/termscp/issues/new/choose) utilizando la label COPY o abra un PR 🙏
|
||||
|
||||
@@ -206,6 +207,7 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
|
||||
| `<Q|F10>` | Salir de termscp | Quit |
|
||||
| `<R|F6>` | Renombrar archivo | Rename |
|
||||
| `<S|F2>` | Guardar archivo como... | Save |
|
||||
| `<T>` | Sincronizar los cambios en la ruta seleccionada con el control remoto | Track |
|
||||
| `<U>` | Ir al directorio principal | Upper |
|
||||
| `<V|F3>` | Abrir archivo con el programa predeterminado | View |
|
||||
| `<W>` | Abrir archivo con el programa proporcionado | With |
|
||||
@@ -213,6 +215,7 @@ Para cambiar de panel, debe escribir `<LEFT>` para mover el panel del explorador
|
||||
| `<Y>` | Alternar navegación sincronizada | sYnc |
|
||||
| `<CTRL+A>` | Seleccionar todos los archivos | |
|
||||
| `<CTRL+C>` | Abortar el proceso de transferencia de archivos | |
|
||||
| `<CTRL+T>` | Mostrar todas las rutas sincronizadas | Track |
|
||||
|
||||
### Trabaja en varios archivos 🥷
|
||||
|
||||
@@ -511,3 +514,26 @@ Termscp enviará notificaciones de escritorio para este tipo de eventos:
|
||||
|
||||
❗ Si prefiere mantener las notificaciones desactivadas, puede simplemente ingresar a la configuración y configurar `Enable notifications?` En `No` 😉.
|
||||
❗ Si desea cambiar el tamaño mínimo de transferencia para mostrar notificaciones, puede cambiar el valor en la configuración con la tecla `Notifications: minimum transfer size` y configurarlo como mejor le convenga 🙂.
|
||||
|
||||
## Observador de archivos 🔭
|
||||
|
||||
El observador de archivos le permite configurar una lista de rutas para sincronizar con los hosts remotos.
|
||||
Esto significa que siempre que se detecte un cambio en el sistema de archivos local en la ruta sincronizada, el cambio se informará automáticamente a la ruta del host remoto configurado, dentro de los 5 segundos.
|
||||
|
||||
Puede establecer tantas rutas para sincronizar como prefiera:
|
||||
|
||||
1. Coloque el cursor en el explorador local en el directorio/archivo que desea mantener sincronizado
|
||||
2. Vaya al directorio en el que desea que se informen los cambios en el host remoto
|
||||
3. Presione `<T>`
|
||||
4. Responda `<YES>` a la ventana emergente de radio
|
||||
|
||||
Para dejar de mirar, simplemente presione `<T>` en la ruta sincronizada local (o en cualquiera de sus subcarpetas)
|
||||
O simplemente puede presionar `<CTRL + T>` y presionar `<ENTER>` en la ruta sincronizada que desea dejar de ver.
|
||||
|
||||
Estos cambios se informarán al host remoto:
|
||||
|
||||
- Nuevos archivos, cambios de archivos
|
||||
- Archivo movido / renombrado
|
||||
- Archivo eliminado/desvinculado
|
||||
|
||||
> ❗ El vigilante trabaja solo en una dirección (local > remota). NO es posible sincronizar automáticamente los cambios de remoto a local.
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- [Éditeur de texte ✏](#éditeur-de-texte-)
|
||||
- [Fichier Journal 🩺](#fichier-journal-)
|
||||
- [Notifications 📫](#notifications-)
|
||||
- [Observateur de fichiers 🔭](#observateur-de-fichiers-)
|
||||
|
||||
## Usage ❓
|
||||
|
||||
@@ -204,6 +205,7 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
|
||||
| `<Q|F10>` | Quitter termscp | Quit |
|
||||
| `<R|F6>` | Renommer le fichier | Rename |
|
||||
| `<S|F2>` | Enregistrer le fichier sous... | Save |
|
||||
| `<T>` | Synchroniser les modifications apportées au chemin sélectionné | Track |
|
||||
| `<U>` | Aller dans le répertoire parent | Upper |
|
||||
| `<V|F3>` | Ouvrir le fichier avec le programme défaut pour le type de fichier | View |
|
||||
| `<W>` | Ouvrir le fichier avec le programme spécifié | With |
|
||||
@@ -211,6 +213,7 @@ Pour changer de panneau, vous devez taper `<LEFT>` pour déplacer le panneau de
|
||||
| `<Y>` | Basculer la navigation synchronisée | sYnc |
|
||||
| `<CTRL+A>` | Sélectionner tous les fichiers | |
|
||||
| `<CTRL+C>` | Abandonner le processus de transfert de fichiers | |
|
||||
| `<CTRL+T>` | Afficher tous les chemins synchronisés | Track |
|
||||
|
||||
### Travailler sur plusieurs fichiers 🥷
|
||||
|
||||
@@ -510,3 +513,26 @@ Termscp enverra des notifications de bureau pour ce type d'événements :
|
||||
|
||||
❗ Si vous préférez désactiver les notifications, vous pouvez simplement accéder à la configuration et définir `Enable notifications?` sur `No` 😉.
|
||||
❗ Si vous souhaitez modifier la taille de transfert minimale pour afficher les notifications, vous pouvez modifier la valeur dans la configuration avec la touche `Notifications: minimum transfer size` et la définir sur ce qui vous convient le mieux 🙂.
|
||||
|
||||
## Observateur de fichiers 🔭
|
||||
|
||||
L'observateur de fichiers vous permet de configurer une liste de chemins à synchroniser avec les hôtes distants.
|
||||
Cela signifie que chaque fois qu'un changement sur le système de fichiers local sera détecté sur le chemin synchronisé, le changement sera automatiquement signalé au chemin de l'hôte distant configuré, dans les 5 secondes.
|
||||
|
||||
Vous pouvez définir autant de chemins à synchroniser que vous préférez :
|
||||
|
||||
1. Placez le curseur de l'explorateur local sur le répertoire/fichier que vous souhaitez conserver synchronisé
|
||||
2. Accédez au répertoire dans lequel vous souhaitez que les modifications soient signalées sur l'hôte distant
|
||||
3. Appuyez sur `<T>`
|
||||
4. Répondez `<YES>` à la fenêtre contextuelle de la radio
|
||||
|
||||
Pour annuler la surveillance, appuyez simplement sur `<T>` sur le chemin synchronisé local (ou sur l'un de ses sous-dossiers)
|
||||
OU vous pouvez simplement appuyer sur `<CTRL + T>` et appuyer sur `<ENTER>` jusqu'au chemin synchronisé que vous souhaitez désactiver.
|
||||
|
||||
Ces modifications seront signalées à l'hôte distant :
|
||||
|
||||
- Nouveaux fichiers, modifications de fichiers
|
||||
- Fichier déplacé / renommé
|
||||
- Fichier supprimé / dissocié
|
||||
|
||||
> ❗ Le watcher ne fonctionne que dans un sens (local > distant). Il n'est PAS possible de synchroniser automatiquement les changements de distant à local.
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- [Editor di testo ✏](#editor-di-testo-)
|
||||
- [Logging 🩺](#logging-)
|
||||
- [Notifiche 📫](#notifiche-)
|
||||
- [File watcher 🔭](#file-watcher-)
|
||||
|
||||
## Argomenti da linea di comando ❓
|
||||
|
||||
@@ -200,6 +201,7 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
|
||||
| `<Q|F10>` | Termina termscp | Quit |
|
||||
| `<R|F6>` | Rinomina file | Rename |
|
||||
| `<S|F2>` | Salva file con nome | Save |
|
||||
| `<T>` | Sincronizza il percorso locale con l'host remoto | Track |
|
||||
| `<U>` | Vai alla directory padre | Upper |
|
||||
| `<V|F3>` | Apri il file con il programma definito dal sistema | View |
|
||||
| `<W>` | Apri il file con il programma specificato | With |
|
||||
@@ -207,6 +209,7 @@ Per cambiare pannello ti puoi muovere con le frecce, `<LEFT>` per andare sul pan
|
||||
| `<Y>` | Abilita/disabilita Sync-Browsing | sYnc |
|
||||
| `<CTRL+A>` | Seleziona tutti i file | |
|
||||
| `<CTRL+C>` | Annulla trasferimento file | |
|
||||
| `<CTRL+T>` | Visualizza tutti i percorsi sincronizzati | Track |
|
||||
|
||||
### Lavora su più file 🥷
|
||||
|
||||
@@ -508,3 +511,26 @@ termscp invierà notifiche destkop per i seguenti eventi:
|
||||
|
||||
❗ Se vuoi disabilitare le notifiche, è sufficiente andare in configurazione ed impostare `Enable notifications?` a `No` 😉.
|
||||
❗ Se vuoi modificare la soglia minima per le notifiche dei trasferimenti, puoi impostare il valore di `Notifications: minimum transfer size` in configurazione 🙂.
|
||||
|
||||
## File watcher 🔭
|
||||
|
||||
Il file watcher ti permette di impostare una lista di percorsi da sincronizzare con l'host remoto.
|
||||
Ciò implica che ogni volta che una modifica verrà rilevata al percorso sincronizzato, la modifica verrà automaticamente sincronizzata con l'host remoto, entro 5 secondi.
|
||||
|
||||
Puoi impostare quanti percorsi preferisci da sincronizzare:
|
||||
|
||||
1. Porta il cursore dell'explorer sulla cartella/file che vuoi sincronizzare
|
||||
2. Vai alla directory sull'explorer remoto dove vuoi riportare le modifiche
|
||||
3. Premi `<T>`
|
||||
4. Rispondi `<YES>` alla domanda se vuoi sincronizzare il percorso
|
||||
|
||||
Per terminare la sincronizzazione, premi `<T>`, al percorso locale sincronizzato (od in qualsiasi sua sottocartella)
|
||||
OPPURE, puoi semplicemente premere `<CTRL+T>` e premi `<ENTER>` sul percorso che vuoi desincronizzare.
|
||||
|
||||
Queste modifiche verranno applicate sull'host remoto:
|
||||
|
||||
- Nuovi file, modifiche
|
||||
- File spostati o rinominati
|
||||
- File rimossi
|
||||
|
||||
> ❗ Il watcher funziona solo in maniera unidirezionale locale > remoto. NON è possibile tracciare le modifiche da remoto a locale.
|
||||
|
||||
28
docs/man.md
28
docs/man.md
@@ -28,6 +28,7 @@
|
||||
- [Text Editor ✏](#text-editor-)
|
||||
- [Logging 🩺](#logging-)
|
||||
- [Notifications 📫](#notifications-)
|
||||
- [File watcher 🔭](#file-watcher-)
|
||||
|
||||
## Usage ❓
|
||||
|
||||
@@ -204,6 +205,7 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
|
||||
| `<Q|F10>` | Quit termscp | Quit |
|
||||
| `<R|F6>` | Rename file | Rename |
|
||||
| `<S|F2>` | Save file as... | Save |
|
||||
| `<T>` | Synchronize changes to selected path to remote | Track |
|
||||
| `<U>` | Go to parent directory | Up |
|
||||
| `<V|F3>` | Open file with default program for filetype | View |
|
||||
| `<W>` | Open file with provided program | With |
|
||||
@@ -211,6 +213,7 @@ In order to change panel you need to type `<LEFT>` to move the remote explorer p
|
||||
| `<Y>` | Toggle synchronized browsing | sYnc |
|
||||
| `<CTRL+A>` | Select all files | |
|
||||
| `<CTRL+C>` | Abort file transfer process | |
|
||||
| `<CTRL+T>` | Show all synchronized paths | Track |
|
||||
|
||||
### Work on multiple files 🥷
|
||||
|
||||
@@ -510,3 +513,28 @@ Termscp will send Desktop notifications for these kind of events:
|
||||
|
||||
❗ If you prefer to keep notifications turned off, you can just enter setup and set `Enable notifications?` to `No` 😉.
|
||||
❗ If you want to change the minimum transfer size to display notifications, you can change the value in the configuration with key `Notifications: minimum transfer size` and set it to whatever suits better for you 🙂.
|
||||
|
||||
---
|
||||
|
||||
## File watcher 🔭
|
||||
|
||||
The file watcher allows you to setup a list of paths to synchronize with the remote hosts.
|
||||
This means that whenever a change on the local file system will be detected on the synchronized path, the change will be automatically reported to the configured remote host path, within 5 seconds.
|
||||
|
||||
You can set as many paths to synchronize as you prefer:
|
||||
|
||||
1. Put the cursor on the local explorer on the directory/file you want to keep synchronized
|
||||
2. Go to the directory you want the changes to be reported to on the remote host
|
||||
3. Press `<T>`
|
||||
4. Answer `<YES>` to the radio popup
|
||||
|
||||
To unwatch, just press `<T>` on the local synchronized path (or to any of its subfolders)
|
||||
OR you can just press `<CTRL+T>` and press `<ENTER>` to the synchronized path you want to unwatch.
|
||||
|
||||
These changes will be reported to the remote host:
|
||||
|
||||
- New files, file changes
|
||||
- File moved/renamed
|
||||
- File removed/unlinked
|
||||
|
||||
> ❗ The watcher works only in one direction (local > remote). It is NOT possible to synchronize automatically the changes from remote to local.
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
- [文本编辑器](#文本编辑器)
|
||||
- [日志](#日志)
|
||||
- [通知](#通知)
|
||||
- [文件观察者🔭](#文件观察者)
|
||||
|
||||
## 用法
|
||||
|
||||
@@ -201,6 +202,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
|
||||
| `<Q|F10>` | 退出termscp | Quit |
|
||||
| `<R|F7>` | 重命名文件 | Rename |
|
||||
| `<S|F2>` | 另存为... | Save |
|
||||
| `<T>` | 显示所有同步路径 | Track |
|
||||
| `<U>` | 进入上层目录 | Upper |
|
||||
| `<V|F3>` | 使用默认方式打开文件 | View |
|
||||
| `<W>` | 使用指定程序打开文件 | With |
|
||||
@@ -208,6 +210,7 @@ termscp中的文件资源管理器是指你与远程建立连接后可以看到
|
||||
| `<Y>` | 是否开启同步浏览 | sYnc |
|
||||
| `<CTRL+A>` | 选中所有文件 | |
|
||||
| `<CTRL+C>` | 终止文件传输 | |
|
||||
| `<CTRL+T>` | 显示所有同步路径 | Track |
|
||||
|
||||
### 处理多个文件
|
||||
|
||||
@@ -502,3 +505,26 @@ termscp 将针对这些类型的事件发送桌面通知:
|
||||
|
||||
❗ 如果您希望保持关闭通知,您只需进入设置并将 `Enable notifications?` 设置为 `No`😉。
|
||||
❗ 如果您想更改最小传输大小以显示通知,您可以使用键 `Notifications: minimum transfer size` 更改配置中的值,并将其设置为更适合您的任何值🙂。
|
||||
|
||||
## 文件观察者🔭
|
||||
|
||||
文件观察器允许您设置与远程主机同步的路径列表。
|
||||
这意味着每当在同步路径上检测到本地文件系统的更改时,该更改将在 5 秒内自动报告给配置的远程主机路径。
|
||||
|
||||
您可以根据需要设置尽可能多的同步路径:
|
||||
|
||||
1.将光标放在本地资源管理器上要保持同步的目录/文件上
|
||||
2. 转到远程主机上要向其报告更改的目录
|
||||
3. 按`<T>`
|
||||
4. 对无线电弹出窗口回答 `<YES>`
|
||||
|
||||
要取消观看,只需在本地同步路径(或其任何子文件夹)上按 `<T>`
|
||||
或者,您可以按 `<CTRL + T>` 并按 `<ENTER>` 进入要取消观看的同步路径。
|
||||
|
||||
这些更改将报告给远程主机:
|
||||
|
||||
- 新文件,文件更改
|
||||
- 文件移动/重命名
|
||||
- 文件删除/取消链接
|
||||
|
||||
> ❗ 观察者只在一个方向工作(本地>远程)。不可能自动同步远程到本地的更改。
|
||||
|
||||
@@ -35,3 +35,4 @@ pub mod logging;
|
||||
pub mod notifications;
|
||||
pub mod sshkey_storage;
|
||||
pub mod theme_provider;
|
||||
pub mod watcher;
|
||||
|
||||
312
src/system/watcher/change.rs
Normal file
312
src/system/watcher/change.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
//! ## File system change
|
||||
//!
|
||||
//! this module exposes the types to describe a change to sync on the remote file system
|
||||
|
||||
use crate::utils::path as path_utils;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// Describes an operation on the remote file system to sync
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum FsChange {
|
||||
/// Move file on remote
|
||||
Move(FileToRename),
|
||||
/// Remove file from remote
|
||||
Remove(FileToRemove),
|
||||
/// Upload file to remote
|
||||
Update(FileUpdate),
|
||||
}
|
||||
|
||||
impl FsChange {
|
||||
/// Instantiate a new `FsChange::Move`
|
||||
pub fn mov(
|
||||
source: PathBuf,
|
||||
destination: PathBuf,
|
||||
local_watched_path: &Path,
|
||||
remote_synched_path: &Path,
|
||||
) -> Self {
|
||||
Self::Move(FileToRename::new(
|
||||
source,
|
||||
destination,
|
||||
local_watched_path,
|
||||
remote_synched_path,
|
||||
))
|
||||
}
|
||||
|
||||
/// Instantiate a new `FsChange::Remove`
|
||||
pub fn remove(
|
||||
removed_path: PathBuf,
|
||||
local_watched_path: &Path,
|
||||
remote_synched_path: &Path,
|
||||
) -> Self {
|
||||
Self::Remove(FileToRemove::new(
|
||||
removed_path,
|
||||
local_watched_path,
|
||||
remote_synched_path,
|
||||
))
|
||||
}
|
||||
|
||||
/// Instantiate a new `FsChange::Update`
|
||||
pub fn update(
|
||||
changed_path: PathBuf,
|
||||
local_watched_path: &Path,
|
||||
remote_synched_path: &Path,
|
||||
) -> Self {
|
||||
Self::Update(FileUpdate::new(
|
||||
changed_path,
|
||||
local_watched_path,
|
||||
remote_synched_path,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a file to rename on the remote fs
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FileToRename {
|
||||
/// Path to file which has to be renamed
|
||||
source: PathBuf,
|
||||
/// new filename
|
||||
destination: PathBuf,
|
||||
}
|
||||
|
||||
impl FileToRename {
|
||||
/// Instantiate a new `FileToRename` given
|
||||
///
|
||||
/// - the path of the source on local fs
|
||||
/// - the path of the destination on local fs
|
||||
/// - the path of the file/directory watched on the local fs
|
||||
/// - the path of the remote file/directory synched with the local fs
|
||||
///
|
||||
/// the `remote` is resolved pushing to `remote_synched_path` the diff between `changed_path` and `local_watched_path`
|
||||
fn new(
|
||||
source: PathBuf,
|
||||
destination: PathBuf,
|
||||
local_watched_path: &Path,
|
||||
remote_synched_path: &Path,
|
||||
) -> Self {
|
||||
Self {
|
||||
source: remote_relative_path(&source, local_watched_path, remote_synched_path),
|
||||
destination: remote_relative_path(
|
||||
&destination,
|
||||
local_watched_path,
|
||||
remote_synched_path,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get path to the source to rename
|
||||
pub fn source(&self) -> &Path {
|
||||
self.source.as_path()
|
||||
}
|
||||
|
||||
/// Get path to the destination name
|
||||
pub fn destination(&self) -> &Path {
|
||||
self.destination.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a file to remove on remote fs
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FileToRemove {
|
||||
/// Path to the file which has to be removed
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileToRemove {
|
||||
/// Instantiate a new `FileToRemove` given
|
||||
///
|
||||
/// - the path of the file which has been removed on localhost
|
||||
/// - the path of the file/directory watched on the local fs
|
||||
/// - the path of the remote file/directory synched with the local fs
|
||||
///
|
||||
/// the `remote` is resolved pushing to `remote_synched_path` the diff between `removed_path` and `local_watched_path`
|
||||
fn new(removed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self {
|
||||
Self {
|
||||
path: remote_relative_path(&removed_path, local_watched_path, remote_synched_path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get path to the file to unlink
|
||||
pub fn path(&self) -> &Path {
|
||||
self.path.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes a file changed to sync
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct FileUpdate {
|
||||
/// Path to file which has changed
|
||||
local: PathBuf,
|
||||
/// Path to remote file to update
|
||||
remote: PathBuf,
|
||||
}
|
||||
|
||||
impl FileUpdate {
|
||||
/// Instantiate a new `FileUpdate` given
|
||||
///
|
||||
/// - the path of the file which has changed
|
||||
/// - the path of the file/directory watched on the local fs
|
||||
/// - the path of the remote file/directory synched with the local fs
|
||||
///
|
||||
/// the `remote` is resolved pushing to `remote_synched_path` the diff between `changed_path` and `local_watched_path`
|
||||
fn new(changed_path: PathBuf, local_watched_path: &Path, remote_synched_path: &Path) -> Self {
|
||||
Self {
|
||||
remote: remote_relative_path(&changed_path, local_watched_path, remote_synched_path),
|
||||
local: changed_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get path to local file to sync
|
||||
pub fn local(&self) -> &Path {
|
||||
self.local.as_path()
|
||||
}
|
||||
|
||||
/// Get path to remote file to sync
|
||||
pub fn remote(&self) -> &Path {
|
||||
self.remote.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
// -- utils
|
||||
|
||||
/// Get remote relative path, given the local target, the path of the local watched path and the path of the remote synched directory/file
|
||||
fn remote_relative_path(
|
||||
target: &Path,
|
||||
local_watched_path: &Path,
|
||||
remote_synched_path: &Path,
|
||||
) -> PathBuf {
|
||||
let local_diff = path_utils::diff_paths(target, local_watched_path);
|
||||
// get absolute path to remote file associated to local file
|
||||
match local_diff {
|
||||
None => remote_synched_path.to_path_buf(),
|
||||
Some(p) => {
|
||||
let mut remote = remote_synched_path.to_path_buf();
|
||||
remote.push(p);
|
||||
remote
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn should_get_remote_relative_path_from_subdir() {
|
||||
assert_eq!(
|
||||
remote_relative_path(
|
||||
Path::new("/tmp/abc/test.txt"),
|
||||
Path::new("/tmp"),
|
||||
Path::new("/home/foo")
|
||||
)
|
||||
.as_path(),
|
||||
Path::new("/home/foo/abc/test.txt")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_get_remote_relative_path_same_path() {
|
||||
assert_eq!(
|
||||
remote_relative_path(
|
||||
Path::new("/tmp/abc/test.txt"),
|
||||
Path::new("/tmp/abc/test.txt"),
|
||||
Path::new("/home/foo/test.txt")
|
||||
)
|
||||
.as_path(),
|
||||
Path::new("/home/foo/test.txt")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_move_from_same_directory() {
|
||||
let change = FsChange::mov(
|
||||
PathBuf::from("/tmp/foo.txt"),
|
||||
PathBuf::from("/tmp/bar.txt"),
|
||||
Path::new("/tmp"),
|
||||
Path::new("/home/foo"),
|
||||
);
|
||||
if let FsChange::Move(change) = change {
|
||||
assert_eq!(change.source(), Path::new("/home/foo/foo.txt"));
|
||||
assert_eq!(change.destination(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not a Move");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_move_from_subdirectory() {
|
||||
let change = FsChange::mov(
|
||||
PathBuf::from("/tmp/abc/foo.txt"),
|
||||
PathBuf::from("/tmp/abc/bar.txt"),
|
||||
Path::new("/tmp/abc"),
|
||||
Path::new("/home/foo"),
|
||||
);
|
||||
if let FsChange::Move(change) = change {
|
||||
assert_eq!(change.source(), Path::new("/home/foo/foo.txt"));
|
||||
assert_eq!(change.destination(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not a Move");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_remove_from_same_directory() {
|
||||
let change = FsChange::remove(
|
||||
PathBuf::from("/tmp/bar.txt"),
|
||||
Path::new("/tmp/bar.txt"),
|
||||
Path::new("/home/foo/bar.txt"),
|
||||
);
|
||||
if let FsChange::Remove(change) = change {
|
||||
assert_eq!(change.path(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not a remove");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_remove_from_subdirectory() {
|
||||
let change = FsChange::remove(
|
||||
PathBuf::from("/tmp/abc/bar.txt"),
|
||||
Path::new("/tmp/abc"),
|
||||
Path::new("/home/foo"),
|
||||
);
|
||||
if let FsChange::Remove(change) = change {
|
||||
assert_eq!(change.path(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not a remove");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_update_from_same_directory() {
|
||||
let change = FsChange::update(
|
||||
PathBuf::from("/tmp/bar.txt"),
|
||||
Path::new("/tmp/bar.txt"),
|
||||
Path::new("/home/foo/bar.txt"),
|
||||
);
|
||||
if let FsChange::Update(change) = change {
|
||||
assert_eq!(change.local(), Path::new("/tmp/bar.txt"),);
|
||||
assert_eq!(change.remote(), Path::new("/home/foo/bar.txt"));
|
||||
} else {
|
||||
panic!("not an update");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_make_fs_change_update_from_subdirectory() {
|
||||
let change = FsChange::update(
|
||||
PathBuf::from("/tmp/abc/foo.txt"),
|
||||
Path::new("/tmp"),
|
||||
Path::new("/home/foo/temp"),
|
||||
);
|
||||
if let FsChange::Update(change) = change {
|
||||
assert_eq!(change.local(), Path::new("/tmp/abc/foo.txt"),);
|
||||
assert_eq!(change.remote(), Path::new("/home/foo/temp/abc/foo.txt"));
|
||||
} else {
|
||||
panic!("not an update");
|
||||
}
|
||||
}
|
||||
}
|
||||
390
src/system/watcher/mod.rs
Normal file
390
src/system/watcher/mod.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
//! ## File system watcher
|
||||
//!
|
||||
//! A watcher for file system paths, which reports changes on local fs
|
||||
|
||||
mod change;
|
||||
|
||||
// -- export
|
||||
pub use change::FsChange;
|
||||
|
||||
use crate::utils::path as path_utils;
|
||||
|
||||
use notify::{
|
||||
watcher, DebouncedEvent, Error as WatcherError, RecommendedWatcher, RecursiveMode, Watcher,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
type FsWatcherResult<T> = Result<T, FsWatcherError>;
|
||||
|
||||
/// Describes an error returned by the `FsWatcher`
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FsWatcherError {
|
||||
#[error("unable to unwatch this path, since is not currently watched")]
|
||||
PathNotWatched,
|
||||
#[error("unable to watch path, since it's already watched")]
|
||||
PathAlreadyWatched,
|
||||
#[error("worker error: {0}")]
|
||||
WorkerError(WatcherError),
|
||||
}
|
||||
|
||||
impl From<WatcherError> for FsWatcherError {
|
||||
fn from(err: WatcherError) -> Self {
|
||||
Self::WorkerError(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// File system watcher
|
||||
pub struct FsWatcher {
|
||||
paths: HashMap<PathBuf, PathBuf>,
|
||||
receiver: Receiver<DebouncedEvent>,
|
||||
watcher: RecommendedWatcher,
|
||||
}
|
||||
|
||||
impl FsWatcher {
|
||||
/// Initialize a new `FsWatcher`
|
||||
pub fn init(delay: Duration) -> FsWatcherResult<Self> {
|
||||
let (tx, receiver) = channel();
|
||||
|
||||
Ok(Self {
|
||||
paths: HashMap::default(),
|
||||
receiver,
|
||||
watcher: watcher(tx, delay)?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Poll searching for the first available disk change
|
||||
pub fn poll(&self) -> FsWatcherResult<Option<FsChange>> {
|
||||
match self.receiver.recv_timeout(Duration::from_millis(1)) {
|
||||
Ok(DebouncedEvent::Rename(source, dest)) => Ok(self.build_fs_move(source, dest)),
|
||||
Ok(DebouncedEvent::Remove(p)) => Ok(self.build_fs_remove(p)),
|
||||
Ok(DebouncedEvent::Chmod(p) | DebouncedEvent::Create(p) | DebouncedEvent::Write(p)) => {
|
||||
Ok(self.build_fs_update(p))
|
||||
}
|
||||
Ok(
|
||||
DebouncedEvent::Rescan
|
||||
| DebouncedEvent::NoticeRemove(_)
|
||||
| DebouncedEvent::NoticeWrite(_),
|
||||
) => Ok(None),
|
||||
Ok(DebouncedEvent::Error(e, _)) => {
|
||||
error!("FsWatcher reported error: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
Err(RecvTimeoutError::Timeout) => Ok(None),
|
||||
Err(RecvTimeoutError::Disconnected) => panic!("File watcher died"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Watch `local` path on localhost
|
||||
pub fn watch(&mut self, local: &Path, remote: &Path) -> FsWatcherResult<()> {
|
||||
// Start watcher if unwatched
|
||||
if !self.watched(local) {
|
||||
self.watcher.watch(local, RecursiveMode::Recursive)?;
|
||||
// Insert new path to paths
|
||||
self.paths.insert(local.to_path_buf(), remote.to_path_buf());
|
||||
Ok(())
|
||||
} else {
|
||||
Err(FsWatcherError::PathAlreadyWatched)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether `path` is currently watched.
|
||||
/// This method looks also in path ancestors.
|
||||
///
|
||||
/// Example:
|
||||
/// if `/home` is watched, then if we call `watched("/home/foo/file.txt")` will return `true`
|
||||
pub fn watched(&self, path: &Path) -> bool {
|
||||
self.find_watched_path(path).is_some()
|
||||
}
|
||||
|
||||
/// Returns the list of watched paths
|
||||
pub fn watched_paths(&self) -> Vec<&Path> {
|
||||
Vec::from_iter(self.paths.keys().map(|x| x.as_path()))
|
||||
}
|
||||
|
||||
/// Unwatch provided path.
|
||||
/// When unwatching the path, it searches for the ancestor watched path if any.
|
||||
/// Returns the unwatched resolved path
|
||||
pub fn unwatch(&mut self, path: &Path) -> FsWatcherResult<PathBuf> {
|
||||
let watched_path = self.find_watched_path(path).map(|x| x.0.to_path_buf());
|
||||
if let Some(watched_path) = watched_path {
|
||||
self.watcher.unwatch(watched_path.as_path())?;
|
||||
self.paths.remove(watched_path.as_path());
|
||||
Ok(watched_path)
|
||||
} else {
|
||||
Err(FsWatcherError::PathNotWatched)
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a certain path, returns the path data associated to the path which
|
||||
/// is ancestor of that path in the current watched path
|
||||
fn find_watched_path(&self, p: &Path) -> Option<(&Path, &Path)> {
|
||||
self.paths
|
||||
.iter()
|
||||
.find(|(k, _)| path_utils::is_child_of(p, k))
|
||||
.map(|(k, v)| (k.as_path(), v.as_path()))
|
||||
}
|
||||
|
||||
/// Build `FsChange` from path to local `changed_file`
|
||||
fn build_fs_move(&self, source: PathBuf, destination: PathBuf) -> Option<FsChange> {
|
||||
if let Some((watched_local, watched_remote)) = self.find_watched_path(&source) {
|
||||
Some(FsChange::mov(
|
||||
source,
|
||||
destination,
|
||||
watched_local,
|
||||
watched_remote,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Build `FsChange` from path to local `changed_file`
|
||||
fn build_fs_remove(&self, removed_path: PathBuf) -> Option<FsChange> {
|
||||
if let Some((watched_local, watched_remote)) = self.find_watched_path(&removed_path) {
|
||||
Some(FsChange::remove(
|
||||
removed_path,
|
||||
watched_local,
|
||||
watched_remote,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Build `FsChange` from path to local `changed_file`
|
||||
fn build_fs_update(&self, changed_file: PathBuf) -> Option<FsChange> {
|
||||
if let Some((watched_local, watched_remote)) = self.find_watched_path(&changed_file) {
|
||||
Some(FsChange::update(
|
||||
changed_file,
|
||||
watched_local,
|
||||
watched_remote,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
use crate::utils::test_helpers;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn should_init_fswatcher() {
|
||||
let watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
assert!(watcher.paths.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_watch_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// check if in paths
|
||||
assert_eq!(
|
||||
watcher.paths.get(tempdir.path()).unwrap(),
|
||||
Path::new("/tmp/test")
|
||||
);
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_not_watch_path_if_subdir_of_watched_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// watch subdir
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
// should return already watched
|
||||
assert!(watcher
|
||||
.watch(subdir.as_path(), Path::new("/tmp/test/abc/def"))
|
||||
.is_err());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_unwatch_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// unwatch
|
||||
assert!(watcher.unwatch(tempdir.path()).is_ok());
|
||||
assert!(watcher.paths.get(tempdir.path()).is_none());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_unwatch_path_when_subdir() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// unwatch
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
assert_eq!(
|
||||
watcher.unwatch(subdir.as_path()).unwrap().as_path(),
|
||||
Path::new(tempdir.path())
|
||||
);
|
||||
assert!(watcher.paths.get(tempdir.path()).is_none());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_return_err_when_unwatching_unwatched_path() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
assert!(watcher.unwatch(Path::new("/tmp")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_tell_whether_path_is_watched() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert_eq!(watcher.watched(tempdir.path()), true);
|
||||
let mut subdir = tempdir.path().to_path_buf();
|
||||
subdir.push("abc/def");
|
||||
assert_eq!(watcher.watched(subdir.as_path()), true);
|
||||
assert_eq!(watcher.watched(Path::new("/tmp")), false);
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn should_poll_file_update() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||
assert!(watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// create file
|
||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||
// wait
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
// wait till update
|
||||
loop {
|
||||
let fs_change = watcher.poll().unwrap();
|
||||
if let Some(FsChange::Update(_)) = fs_change {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
assert!(std::fs::remove_file(file_path.as_path()).is_ok());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn should_poll_file_removed() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||
assert!(watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// create file
|
||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
// wait
|
||||
assert!(std::fs::remove_file(file_path.as_path()).is_ok());
|
||||
// poll till remove
|
||||
loop {
|
||||
let fs_change = watcher.poll().unwrap();
|
||||
if let Some(FsChange::Remove(remove)) = fs_change {
|
||||
assert_eq!(remove.path(), Path::new("/tmp/test/test.txt"));
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
/*
|
||||
#[test]
|
||||
#[cfg(target_family = "unix")]
|
||||
fn should_poll_file_moved() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_millis(100)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = PathBuf::from(format!("/private{}", tempdir.path().display()));
|
||||
assert!(watcher
|
||||
.watch(tempdir_path.as_path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
// create file
|
||||
let file_path = test_helpers::make_file_at(tempdir_path.as_path(), "test.txt").unwrap();
|
||||
// wait
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
// move file
|
||||
let mut new_file_path = tempdir.path().to_path_buf();
|
||||
new_file_path.push("new.txt");
|
||||
assert!(std::fs::rename(file_path.as_path(), new_file_path.as_path()).is_ok());
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
// wait till rename
|
||||
loop {
|
||||
let fs_change = watcher.poll().unwrap();
|
||||
if let Some(FsChange::Move(mov)) = fs_change {
|
||||
assert_eq!(mov.source(), Path::new("/tmp/test/test.txt"));
|
||||
assert_eq!(mov.destination(), Path::new("/tmp/test/new.txt"));
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
// remove file
|
||||
assert!(std::fs::remove_file(new_file_path.as_path()).is_ok());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn should_poll_nothing() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
assert!(watcher
|
||||
.watch(tempdir.path(), Path::new("/tmp/test"))
|
||||
.is_ok());
|
||||
assert!(watcher.poll().ok().unwrap().is_none());
|
||||
// close tempdir
|
||||
assert!(tempdir.close().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_os = "macos")]
|
||||
fn should_get_watched_paths() {
|
||||
let mut watcher = FsWatcher::init(Duration::from_secs(5)).unwrap();
|
||||
assert!(watcher.watch(Path::new("/tmp"), Path::new("/tmp")).is_ok());
|
||||
assert!(watcher
|
||||
.watch(Path::new("/home"), Path::new("/home"))
|
||||
.is_ok());
|
||||
let mut watched_paths = watcher.watched_paths();
|
||||
watched_paths.sort();
|
||||
assert_eq!(watched_paths, vec![Path::new("/home"), Path::new("/tmp")]);
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
*/
|
||||
pub(self) use super::{
|
||||
browser::FileExplorerTab, FileTransferActivity, Id, LogLevel, Msg, PendingActionMsg,
|
||||
TransferOpts, TransferPayload,
|
||||
TransferMsg, TransferOpts, TransferPayload, UiMsg,
|
||||
};
|
||||
pub(self) use remotefs::File;
|
||||
use tuirealm::{State, StateValue};
|
||||
@@ -47,6 +47,7 @@ pub(crate) mod rename;
|
||||
pub(crate) mod save;
|
||||
pub(crate) mod submit;
|
||||
pub(crate) mod symlink;
|
||||
pub(crate) mod watcher;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SelectedFile {
|
||||
|
||||
@@ -96,7 +96,7 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
|
||||
fn remote_rename_file(&mut self, entry: &File, dest: &Path) {
|
||||
pub(crate) fn remote_rename_file(&mut self, entry: &File, dest: &Path) {
|
||||
match self.client.as_mut().mov(entry.path(), dest) {
|
||||
Ok(_) => {
|
||||
self.log(
|
||||
|
||||
134
src/ui/activities/filetransfer/actions/watcher.rs
Normal file
134
src/ui/activities/filetransfer/actions/watcher.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
//! # watcher actions
|
||||
//!
|
||||
//! actions associated to the file watcher
|
||||
|
||||
use super::{FileTransferActivity, LogLevel, Msg, SelectedFile, TransferMsg, UiMsg};
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
impl FileTransferActivity {
|
||||
pub fn action_show_radio_watch(&mut self) {
|
||||
// return if fswatcher is not working
|
||||
if self.fswatcher.is_none() {
|
||||
return;
|
||||
}
|
||||
// get local entry
|
||||
if let Some((watched, local, remote)) = self.get_watcher_dirs() {
|
||||
self.mount_radio_watch(
|
||||
watched,
|
||||
local.to_string_lossy().to_string().as_str(),
|
||||
remote.to_string_lossy().to_string().as_str(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_show_watched_paths_list(&mut self) {
|
||||
// return if fswatcher is not working
|
||||
if self.fswatcher.is_none() {
|
||||
return;
|
||||
}
|
||||
let watched_paths: Vec<PathBuf> = self
|
||||
.map_on_fswatcher(|w| w.watched_paths().iter().map(|p| p.to_path_buf()).collect())
|
||||
.unwrap_or_default();
|
||||
self.mount_watched_paths_list(watched_paths.as_slice());
|
||||
}
|
||||
|
||||
pub fn action_toggle_watch(&mut self) {
|
||||
// umount radio
|
||||
self.umount_radio_watcher();
|
||||
// return if fswatcher is not working
|
||||
if self.fswatcher.is_none() {
|
||||
return;
|
||||
}
|
||||
match self.get_watcher_dirs() {
|
||||
Some((true, local, _)) => self.unwatch_path(&local),
|
||||
Some((false, local, remote)) => self.watch_path(&local, &remote),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn action_toggle_watch_for(&mut self, index: usize) {
|
||||
// umount
|
||||
self.umount_watched_paths_list();
|
||||
// return if fswatcher is not working
|
||||
if self.fswatcher.is_none() {
|
||||
return;
|
||||
}
|
||||
// get path
|
||||
if let Some(path) = self
|
||||
.map_on_fswatcher(|w| w.watched_paths().get(index).map(|p| p.to_path_buf()))
|
||||
.flatten()
|
||||
{
|
||||
// ask whether to unwatch
|
||||
self.mount_radio_watch(true, path.to_string_lossy().to_string().as_str(), "");
|
||||
// wait for response
|
||||
if let Msg::Transfer(TransferMsg::ToggleWatch) = self.wait_for_pending_msg(&[
|
||||
Msg::Ui(UiMsg::CloseWatcherPopup),
|
||||
Msg::Transfer(TransferMsg::ToggleWatch),
|
||||
]) {
|
||||
// unwatch path
|
||||
self.unwatch_path(&path);
|
||||
}
|
||||
self.umount_radio_watcher();
|
||||
}
|
||||
self.action_show_watched_paths_list();
|
||||
}
|
||||
|
||||
fn watch_path(&mut self, local: &Path, remote: &Path) {
|
||||
debug!(
|
||||
"tracking changes at {} to {}",
|
||||
local.display(),
|
||||
remote.display()
|
||||
);
|
||||
match self.map_on_fswatcher(|w| w.watch(local, remote)) {
|
||||
Some(Ok(())) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"changes to {} will now be synched with {}",
|
||||
local.display(),
|
||||
remote.display()
|
||||
),
|
||||
);
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
self.log_and_alert(
|
||||
LogLevel::Error,
|
||||
format!("could not track changes to {}: {}", local.display(), err),
|
||||
);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn unwatch_path(&mut self, path: &Path) {
|
||||
debug!("unwatching path at {}", path.display());
|
||||
match self.map_on_fswatcher(|w| w.unwatch(path)) {
|
||||
Some(Ok(path)) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("{} is no longer watched", path.display()),
|
||||
);
|
||||
}
|
||||
Some(Err(err)) => {
|
||||
self.log_and_alert(LogLevel::Error, format!("could not unwatch path: {}", err));
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_watcher_dirs(&mut self) -> Option<(bool, PathBuf, PathBuf)> {
|
||||
if let SelectedFile::One(file) = self.get_local_selected_entries() {
|
||||
// check if entry is already watched
|
||||
let watched = self
|
||||
.map_on_fswatcher(|w| w.watched(file.path()))
|
||||
.unwrap_or(false);
|
||||
// mount dialog
|
||||
let mut remote = self.remote().wrkdir.clone();
|
||||
remote.push(file.name().as_str());
|
||||
Some((watched, file.path().to_path_buf(), remote))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ pub use popups::{
|
||||
FindPopup, GoToPopup, KeybindingsPopup, MkdirPopup, NewfilePopup, OpenWithPopup,
|
||||
ProgressBarFull, ProgressBarPartial, QuitPopup, RenamePopup, ReplacePopup,
|
||||
ReplacingFilesListPopup, SaveAsPopup, SortingPopup, StatusBarLocal, StatusBarRemote,
|
||||
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup,
|
||||
SymlinkPopup, SyncBrowsingMkdirPopup, WaitPopup, WatchedPathsList, WatcherPopup,
|
||||
};
|
||||
pub use transfer::{ExplorerFind, ExplorerLocal, ExplorerRemote};
|
||||
|
||||
|
||||
@@ -683,6 +683,7 @@ impl KeybindingsPopup {
|
||||
.step(8)
|
||||
.highlighted_str("? ")
|
||||
.title("Keybindings", Alignment::Center)
|
||||
.rewind(true)
|
||||
.rows(
|
||||
TableBuilder::default()
|
||||
.add_col(TextSpan::new("<ESC>").bold().fg(key_color))
|
||||
@@ -759,9 +760,12 @@ impl KeybindingsPopup {
|
||||
.add_col(TextSpan::new("<R|F6>").bold().fg(key_color))
|
||||
.add_col(TextSpan::from(" Rename file"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<F2|S>").bold().fg(key_color))
|
||||
.add_col(TextSpan::new("<S|F2>").bold().fg(key_color))
|
||||
.add_col(TextSpan::from(" Save file as"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<T>").bold().fg(key_color))
|
||||
.add_col(TextSpan::from(" Watch/unwatch file changes"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<U>").bold().fg(key_color))
|
||||
.add_col(TextSpan::from(" Go to parent directory"))
|
||||
.add_row()
|
||||
@@ -791,6 +795,9 @@ impl KeybindingsPopup {
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color))
|
||||
.add_col(TextSpan::from(" Interrupt file transfer"))
|
||||
.add_row()
|
||||
.add_col(TextSpan::new("<CTRL+T>").bold().fg(key_color))
|
||||
.add_col(TextSpan::from(" Show watched paths"))
|
||||
.build(),
|
||||
),
|
||||
}
|
||||
@@ -1030,7 +1037,7 @@ impl OpenWithPopup {
|
||||
"Open file with…",
|
||||
Style::default().fg(Color::Rgb(128, 128, 128)),
|
||||
)
|
||||
.title("vscode", Alignment::Center),
|
||||
.title("Type the program to open the file with", Alignment::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1852,3 +1859,150 @@ impl Component<Msg, NoUserEvent> for WaitPopup {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct WatchedPathsList {
|
||||
component: List,
|
||||
}
|
||||
|
||||
impl WatchedPathsList {
|
||||
pub fn new(paths: &[std::path::PathBuf], color: Color) -> Self {
|
||||
Self {
|
||||
component: List::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.rewind(true)
|
||||
.scroll(true)
|
||||
.step(4)
|
||||
.highlighted_color(color)
|
||||
.highlighted_str("➤ ")
|
||||
.title(
|
||||
"These files are currently synched with the remote host",
|
||||
Alignment::Center,
|
||||
)
|
||||
.rows(
|
||||
paths
|
||||
.iter()
|
||||
.map(|x| vec![TextSpan::from(x.to_string_lossy().to_string())])
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for WatchedPathsList {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseWatchedPathsList))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Down, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
|
||||
self.perform(Cmd::Move(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageDown,
|
||||
..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Down));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::PageUp, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Scroll(Direction::Up));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Home, ..
|
||||
}) => {
|
||||
self.perform(Cmd::GoTo(Position::Begin));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::End, .. }) => {
|
||||
self.perform(Cmd::GoTo(Position::End));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => {
|
||||
// get state
|
||||
if let State::One(StateValue::Usize(idx)) = self.component.state() {
|
||||
Some(Msg::Transfer(TransferMsg::ToggleWatchFor(idx)))
|
||||
} else {
|
||||
Some(Msg::None)
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(MockComponent)]
|
||||
pub struct WatcherPopup {
|
||||
component: Radio,
|
||||
}
|
||||
|
||||
impl WatcherPopup {
|
||||
pub fn new(watched: bool, local: &str, remote: &str, color: Color) -> Self {
|
||||
let text = match watched {
|
||||
false => format!(r#"Synchronize changes from "{}" to "{}"?"#, local, remote),
|
||||
true => format!(r#"Stop synchronizing changes at "{}"?"#, local),
|
||||
};
|
||||
Self {
|
||||
component: Radio::default()
|
||||
.borders(
|
||||
Borders::default()
|
||||
.color(color)
|
||||
.modifiers(BorderType::Rounded),
|
||||
)
|
||||
.foreground(color)
|
||||
.choices(&["Yes", "No"])
|
||||
.title(text, Alignment::Center),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component<Msg, NoUserEvent> for WatcherPopup {
|
||||
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
|
||||
match ev {
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Left, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Left));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Right, ..
|
||||
}) => {
|
||||
self.perform(Cmd::Move(Direction::Right));
|
||||
Some(Msg::None)
|
||||
}
|
||||
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
|
||||
Some(Msg::Ui(UiMsg::CloseWatcherPopup))
|
||||
}
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Enter, ..
|
||||
}) => {
|
||||
if matches!(
|
||||
self.perform(Cmd::Submit),
|
||||
CmdResult::Submit(State::One(StateValue::Usize(0)))
|
||||
) {
|
||||
Some(Msg::Transfer(TransferMsg::ToggleWatch))
|
||||
} else {
|
||||
Some(Msg::Ui(UiMsg::CloseWatcherPopup))
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,6 +306,14 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
|
||||
code: Key::Char('s') | Key::Function(2),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('t'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowWatcherPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('t'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowWatchedPathsList)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('u'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
@@ -478,6 +486,14 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
|
||||
code: Key::Char('s') | Key::Function(2),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowSaveAsPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('t'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowWatcherPopup)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('t'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}) => Some(Msg::Ui(UiMsg::ShowWatchedPathsList)),
|
||||
Event::Keyboard(KeyEvent {
|
||||
code: Key::Char('u'),
|
||||
modifiers: KeyModifiers::NONE,
|
||||
|
||||
132
src/ui/activities/filetransfer/fswatcher.rs
Normal file
132
src/ui/activities/filetransfer/fswatcher.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use super::{FileTransferActivity, LogLevel, TransferPayload};
|
||||
use crate::system::watcher::FsChange;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
impl FileTransferActivity {
|
||||
/// poll file watcher
|
||||
pub(super) fn poll_watcher(&mut self) {
|
||||
if self.fswatcher.is_none() {
|
||||
return;
|
||||
}
|
||||
let watcher = self.fswatcher.as_mut().unwrap();
|
||||
match watcher.poll() {
|
||||
Ok(None) => {}
|
||||
Ok(Some(FsChange::Move(mov))) => {
|
||||
debug!(
|
||||
"fs watcher reported a `Move` from {} to {}",
|
||||
mov.source().display(),
|
||||
mov.destination().display()
|
||||
);
|
||||
self.move_watched_file(mov.source(), mov.destination());
|
||||
}
|
||||
Ok(Some(FsChange::Remove(remove))) => {
|
||||
debug!(
|
||||
"fs watcher reported a `Remove` of {}",
|
||||
remove.path().display()
|
||||
);
|
||||
self.remove_watched_file(remove.path());
|
||||
}
|
||||
Ok(Some(FsChange::Update(update))) => {
|
||||
debug!(
|
||||
"fs watcher reported an `Update` from {} to {}",
|
||||
update.local().display(),
|
||||
update.remote().display()
|
||||
);
|
||||
self.upload_watched_file(update.local(), update.remote());
|
||||
}
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!("error while polling file watcher: {}", err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn move_watched_file(&mut self, source: &Path, destination: &Path) {
|
||||
// stat remote file
|
||||
trace!(
|
||||
"renaming watched file {} to {}",
|
||||
source.display(),
|
||||
destination.display()
|
||||
);
|
||||
// stat fs entry
|
||||
let origin = match self.client.stat(source) {
|
||||
Ok(f) => f,
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"failed to stat file to rename {}: {}",
|
||||
source.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// rename using action
|
||||
self.remote_rename_file(&origin, destination)
|
||||
}
|
||||
|
||||
fn remove_watched_file(&mut self, file: &Path) {
|
||||
match self.client.remove_dir_all(file) {
|
||||
Ok(()) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!("removed watched file at {}", file.display()),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!("failed to remove watched file {}: {}", file.display(), err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn upload_watched_file(&mut self, local: &Path, remote: &Path) {
|
||||
// stat local file
|
||||
let entry = match self.host.stat(local) {
|
||||
Ok(e) => e,
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!(
|
||||
"failed to sync file {} with remote (stat failed): {}",
|
||||
remote.display(),
|
||||
err
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// send
|
||||
trace!(
|
||||
"syncing local file {} with remote {}",
|
||||
local.display(),
|
||||
remote.display()
|
||||
);
|
||||
let remote_path = remote.parent().unwrap_or_else(|| Path::new("/"));
|
||||
match self.filetransfer_send(TransferPayload::Any(entry), remote_path, None) {
|
||||
Ok(()) => {
|
||||
self.log(
|
||||
LogLevel::Info,
|
||||
format!(
|
||||
"synched watched file {} with {}",
|
||||
local.display(),
|
||||
remote.display()
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
self.log(
|
||||
LogLevel::Error,
|
||||
format!("failed to sync watched file {}: {}", remote.display(), err),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,8 @@ impl FileTransferActivity {
|
||||
self.log_records.push_front(record);
|
||||
// Update log
|
||||
self.update_logbox();
|
||||
// flag redraw
|
||||
self.redraw = true;
|
||||
}
|
||||
|
||||
/// Add message to log events and also display it as an alert
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
// This module is split into files, cause it's just too big
|
||||
mod actions;
|
||||
mod components;
|
||||
mod fswatcher;
|
||||
mod lib;
|
||||
mod misc;
|
||||
mod session;
|
||||
@@ -41,6 +42,7 @@ use crate::explorer::{FileExplorer, FileSorting};
|
||||
use crate::filetransfer::{Builder, FileTransferParams};
|
||||
use crate::host::Localhost;
|
||||
use crate::system::config_client::ConfigClient;
|
||||
use crate::system::watcher::FsWatcher;
|
||||
pub(self) use lib::browser;
|
||||
use lib::browser::Browser;
|
||||
use lib::transfer::{TransferOpts, TransferStates};
|
||||
@@ -90,6 +92,8 @@ enum Id {
|
||||
SymlinkPopup,
|
||||
SyncBrowsingMkdirPopup,
|
||||
WaitPopup,
|
||||
WatchedPathsList,
|
||||
WatcherPopup,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@@ -128,6 +132,8 @@ enum TransferMsg {
|
||||
RenameFile(String),
|
||||
SaveFileAs(String),
|
||||
SearchFile(String),
|
||||
ToggleWatch,
|
||||
ToggleWatchFor(usize),
|
||||
TransferFile,
|
||||
}
|
||||
|
||||
@@ -154,6 +160,8 @@ enum UiMsg {
|
||||
CloseRenamePopup,
|
||||
CloseSaveAsPopup,
|
||||
CloseSymlinkPopup,
|
||||
CloseWatchedPathsList,
|
||||
CloseWatcherPopup,
|
||||
Disconnect,
|
||||
ExplorerBackTabbed,
|
||||
LogBackTabbed,
|
||||
@@ -175,6 +183,8 @@ enum UiMsg {
|
||||
ShowRenamePopup,
|
||||
ShowSaveAsPopup,
|
||||
ShowSymlinkPopup,
|
||||
ShowWatchedPathsList,
|
||||
ShowWatcherPopup,
|
||||
ToggleHiddenFiles,
|
||||
ToggleSyncBrowsing,
|
||||
WindowResized,
|
||||
@@ -226,6 +236,8 @@ pub struct FileTransferActivity {
|
||||
transfer: TransferStates,
|
||||
/// Temporary directory where to store temporary stuff
|
||||
cache: Option<TempDir>,
|
||||
/// Fs watcher
|
||||
fswatcher: Option<FsWatcher>,
|
||||
}
|
||||
|
||||
impl FileTransferActivity {
|
||||
@@ -251,6 +263,13 @@ impl FileTransferActivity {
|
||||
Ok(d) => Some(d),
|
||||
Err(_) => None,
|
||||
},
|
||||
fswatcher: match FsWatcher::init(Duration::from_secs(5)) {
|
||||
Ok(w) => Some(w),
|
||||
Err(e) => {
|
||||
error!("failed to initialize fs watcher: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +334,14 @@ impl FileTransferActivity {
|
||||
fn theme(&self) -> &Theme {
|
||||
self.context().theme_provider().theme()
|
||||
}
|
||||
|
||||
/// Map a function to fs watcher if any
|
||||
fn map_on_fswatcher<F, T>(&mut self, mapper: F) -> Option<T>
|
||||
where
|
||||
F: FnOnce(&mut FsWatcher) -> T,
|
||||
{
|
||||
self.fswatcher.as_mut().map(mapper)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -377,6 +404,8 @@ impl Activity for FileTransferActivity {
|
||||
self.redraw = true;
|
||||
}
|
||||
self.tick();
|
||||
// poll
|
||||
self.poll_watcher();
|
||||
// View
|
||||
if self.redraw {
|
||||
self.view();
|
||||
|
||||
@@ -342,6 +342,8 @@ impl FileTransferActivity {
|
||||
}
|
||||
}
|
||||
}
|
||||
TransferMsg::ToggleWatch => self.action_toggle_watch(),
|
||||
TransferMsg::ToggleWatchFor(index) => self.action_toggle_watch_for(index),
|
||||
TransferMsg::TransferFile => {
|
||||
match self.browser.tab() {
|
||||
FileExplorerTab::Local => self.action_local_send(),
|
||||
@@ -421,6 +423,8 @@ impl FileTransferActivity {
|
||||
UiMsg::CloseRenamePopup => self.umount_rename(),
|
||||
UiMsg::CloseSaveAsPopup => self.umount_saveas(),
|
||||
UiMsg::CloseSymlinkPopup => self.umount_symlink(),
|
||||
UiMsg::CloseWatchedPathsList => self.umount_watched_paths_list(),
|
||||
UiMsg::CloseWatcherPopup => self.umount_radio_watcher(),
|
||||
UiMsg::Disconnect => {
|
||||
self.disconnect();
|
||||
self.umount_disconnect();
|
||||
@@ -487,6 +491,8 @@ impl FileTransferActivity {
|
||||
);
|
||||
}
|
||||
}
|
||||
UiMsg::ShowWatchedPathsList => self.action_show_watched_paths_list(),
|
||||
UiMsg::ShowWatcherPopup => self.action_show_radio_watch(),
|
||||
UiMsg::ToggleHiddenFiles => match self.browser.tab() {
|
||||
FileExplorerTab::FindLocal | FileExplorerTab::Local => {
|
||||
self.browser.local_mut().toggle_hidden_files();
|
||||
|
||||
@@ -287,13 +287,22 @@ impl FileTransferActivity {
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.app.view(&Id::QuitPopup, f, popup);
|
||||
} else if self.app.mounted(&Id::WatchedPathsList) {
|
||||
let popup = draw_area_in(f.size(), 60, 50);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.app.view(&Id::WatchedPathsList, f, popup);
|
||||
} else if self.app.mounted(&Id::WatcherPopup) {
|
||||
let popup = draw_area_in(f.size(), 60, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.app.view(&Id::WatcherPopup, f, popup);
|
||||
} else if self.app.mounted(&Id::SortingPopup) {
|
||||
let popup = draw_area_in(f.size(), 50, 10);
|
||||
f.render_widget(Clear, popup);
|
||||
// make popup
|
||||
self.app.view(&Id::SortingPopup, f, popup);
|
||||
} else if self.app.mounted(&Id::ErrorPopup) {
|
||||
// TODO: inject dynamic height here
|
||||
let popup = draw_area_in(
|
||||
f.size(),
|
||||
50,
|
||||
@@ -303,7 +312,6 @@ impl FileTransferActivity {
|
||||
// make popup
|
||||
self.app.view(&Id::ErrorPopup, f, popup);
|
||||
} else if self.app.mounted(&Id::FatalPopup) {
|
||||
// TODO: inject dynamic height here
|
||||
let popup = draw_area_in(
|
||||
f.size(),
|
||||
50,
|
||||
@@ -717,6 +725,42 @@ impl FileTransferActivity {
|
||||
let _ = self.app.umount(&Id::DeletePopup);
|
||||
}
|
||||
|
||||
pub(super) fn mount_radio_watch(&mut self, watch: bool, local: &str, remote: &str) {
|
||||
let info_color = self.theme().misc_info_dialog;
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::WatcherPopup,
|
||||
Box::new(components::WatcherPopup::new(
|
||||
watch, local, remote, info_color
|
||||
)),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::WatcherPopup).is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn umount_radio_watcher(&mut self) {
|
||||
let _ = self.app.umount(&Id::WatcherPopup);
|
||||
}
|
||||
|
||||
pub(super) fn mount_watched_paths_list(&mut self, paths: &[std::path::PathBuf]) {
|
||||
let info_color = self.theme().misc_info_dialog;
|
||||
assert!(self
|
||||
.app
|
||||
.remount(
|
||||
Id::WatchedPathsList,
|
||||
Box::new(components::WatchedPathsList::new(paths, info_color)),
|
||||
vec![],
|
||||
)
|
||||
.is_ok());
|
||||
assert!(self.app.active(&Id::WatchedPathsList).is_ok());
|
||||
}
|
||||
|
||||
pub(super) fn umount_watched_paths_list(&mut self) {
|
||||
let _ = self.app.umount(&Id::WatchedPathsList);
|
||||
}
|
||||
|
||||
pub(super) fn mount_radio_replace(&mut self, file_name: &str) {
|
||||
let warn_color = self.theme().misc_warn_dialog;
|
||||
assert!(self
|
||||
@@ -1040,9 +1084,19 @@ impl FileTransferActivity {
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::SymlinkPopup,
|
||||
)))),
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::WaitPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::WatcherPopup,
|
||||
)))),
|
||||
Box::new(SubClause::And(
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::WatchedPathsList,
|
||||
)))),
|
||||
Box::new(SubClause::Not(Box::new(SubClause::IsMounted(
|
||||
Id::WaitPopup,
|
||||
)))),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
)),
|
||||
|
||||
@@ -37,10 +37,9 @@ pub fn absolutize(wrkdir: &Path, target: &Path) -> PathBuf {
|
||||
///
|
||||
/// This function has been written by <https://github.com/Manishearth>
|
||||
/// and is licensed under the APACHE-2/MIT license <https://github.com/Manishearth/pathdiff>
|
||||
pub fn diff_paths<P, B>(path: P, base: B) -> Option<PathBuf>
|
||||
pub fn diff_paths<P>(path: P, base: P) -> Option<PathBuf>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
B: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
let base = base.as_ref();
|
||||
@@ -82,6 +81,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether `p` is child (direct/indirect) of ancestor `ancestor`
|
||||
pub fn is_child_of<P: AsRef<Path>>(p: P, ancestor: P) -> bool {
|
||||
p.as_ref().ancestors().any(|x| x == ancestor.as_ref())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
@@ -120,4 +124,27 @@ mod test {
|
||||
Path::new("foo/bar/chiedo.gif")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_tell_whether_path_is_child_of() {
|
||||
assert_eq!(
|
||||
is_child_of(Path::new("/home/foo/foo.txt"), Path::new("/home"),),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_child_of(Path::new("/home/foo/foo.txt"), Path::new("/home/foo/"),),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_child_of(
|
||||
Path::new("/home/foo/foo.txt"),
|
||||
Path::new("/home/foo/foo.txt"),
|
||||
),
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
is_child_of(Path::new("/home/foo/foo.txt"), Path::new("/tmp"),),
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ pub fn create_sample_file() -> NamedTempFile {
|
||||
/// ### make_file_at
|
||||
///
|
||||
/// Make a file with `name` at specified path
|
||||
pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> {
|
||||
pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<PathBuf> {
|
||||
let mut p: PathBuf = PathBuf::from(dir);
|
||||
p.push(filename);
|
||||
let mut file = StdFile::create(p.as_path())?;
|
||||
@@ -66,7 +66,7 @@ pub fn make_file_at(dir: &Path, filename: &str) -> std::io::Result<()> {
|
||||
file,
|
||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit.Mauris ultricies consequat eros,nec scelerisque magna imperdiet metus."
|
||||
)?;
|
||||
Ok(())
|
||||
Ok(p)
|
||||
}
|
||||
|
||||
/// ### make_dir_at
|
||||
|
||||
Reference in New Issue
Block a user