diff --git a/Cargo.lock b/Cargo.lock index 6195f18..5c10cad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,30 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.18" @@ -52,6 +76,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -64,12 +109,47 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.37" @@ -125,6 +205,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "directories" version = "6.0.0" @@ -173,6 +259,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "find-msvc-tools" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -182,6 +274,95 @@ dependencies = [ "libc", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -193,6 +374,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "hashbrown" version = "0.15.3" @@ -205,6 +392,30 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "2.9.0" @@ -235,12 +446,33 @@ dependencies = [ "libc", ] +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "js-sys" +version = "0.3.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "kqueue" version = "1.1.1" @@ -278,6 +510,16 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.27" @@ -290,6 +532,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.3" @@ -327,6 +578,24 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -339,6 +608,41 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro2" version = "1.0.95" @@ -352,12 +656,16 @@ dependencies = [ name = "project-sync" version = "0.1.0" dependencies = [ + "chrono", "clap", "colored", "directories", + "futures", "notify", "serde", "shellexpand", + "tokio", + "tokio-util", "toml", ] @@ -390,6 +698,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "same-file" version = "1.0.6" @@ -399,6 +719,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.219" @@ -437,6 +763,43 @@ dependencies = [ "dirs", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "strsim" version = "0.11.1" @@ -474,6 +837,51 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.22" @@ -543,6 +951,65 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi-util" version = "0.1.9" @@ -552,6 +1019,65 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 22bbfc6..d78dfd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,12 +4,16 @@ version = "0.1.0" edition = "2024" [dependencies] +chrono = "0.4.42" clap = { version = "4.5.37", features = ["derive"] } colored = "3.0.0" directories = "6.0.0" +futures = "0.3.31" notify = "8.0.0" serde = { version = "1.0.219", features = ["derive"] } shellexpand = "3.1.1" +tokio = { version = "1.47.1", features = ["full"] } +tokio-util = { version = "0.7.16", features = ["rt"] } toml = "0.8.22" [profile.release] diff --git a/README.md b/README.md index 06a6a68..3412933 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ source = '~/projects/my-project/' destinations = ['username@remote-server:~/my-project'] sync_on_start = true -options = '-az -e ssh --delete' +options = '--delete' ignore = ''' .git @@ -93,6 +93,7 @@ target #### Global Options * `debounce`: Debounce interval in seconds for file watching. Prevents excessive syncing during rapid file changes. +* `verbose`: Enables additional logging; can be overridden via command-line option. #### Per-Sync Job Fields @@ -100,7 +101,7 @@ target * `source` (string): Source directory. `~` is expanded to home. * `destinations` (array of strings): List of destination paths. Can be local or remote via SSH. * `sync_on_start` (boolean):: If `true`, will sync automatically when the tool starts. -* `options` (string, optional): Optional `rsync` options to override the default (`-az -e ssh`). +* `options` (string, optional): Optional `rsync` options to be specified after the default (`-az -e ssh`). * **Note**: If `options` is not specified, it defaults to `-az -e ssh`. * Including `--delete` in the options will cause files in the destination that are not present in the source to be removed. **Use with caution!** diff --git a/src/config.rs b/src/config.rs index b1c3b4a..70951db 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,7 @@ -use std::{fs, path::Path}; +use std::{ + fs, + path::{Path, PathBuf}, +}; use serde::Deserialize; @@ -7,11 +10,17 @@ pub struct Config { #[serde(default = "default_debounce")] pub debounce: f32, - pub sync: Vec, + #[serde(default)] // missing field → false + pub verbose: bool, + + pub sync: Vec, + + #[serde(skip)] + pub config_path: PathBuf, } #[derive(Debug, Deserialize)] -pub struct SyncRule { +pub struct SyncRuleToml { pub name: String, pub source: String, pub destinations: Vec, @@ -22,29 +31,38 @@ pub struct SyncRule { #[serde(default = "default_ignore")] pub ignore: String, - #[serde(default = "default_options")] + #[serde(default)] pub options: String, } fn default_sync_on_start() -> bool { - true + false } fn default_ignore() -> String { ".git\n".into() } -fn default_options() -> String { - "-az -e ssh".into() -} +// fn default_options() -> String { +// "-az -e ssh".into() +// } fn default_debounce() -> f32 { 0.08 } -pub fn read_config(path: &Path) -> Config { - let toml_str = fs::read_to_string(path).unwrap_or_else(|e| panic!("Error: failed to read config file: {} {e}", path.display())); - let config: Config = toml::from_str(&toml_str).expect("Failed to parse TOML"); - //println!("Parsed config: {:#?}", config); - config +impl Config { + pub fn from_config_path(path: &Path) -> Self { + let toml_str = fs::read_to_string(path).unwrap_or_else(|e| panic!("Error: failed to read config file: {} {e}", path.display())); + let mut config: Config = toml::from_str(&toml_str).expect("Failed to parse TOML"); + //println!("Parsed config: {:#?}", config); + + config.config_path = path.into(); + config + } + pub fn retain_destinations(&mut self, filter: &str) { + for s in self.sync.iter_mut() { + s.destinations.retain(|dest| dest.contains(filter)); + } + } } diff --git a/src/main.rs b/src/main.rs index 5a53312..b3e9cd2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ use std::path::PathBuf; -use clap::Parser as clap_Parser; +use clap::Parser; + +use crate::config::Config; mod config; -mod sync; +mod sync_item; +mod sync_manager; -#[derive(clap_Parser, Debug)] +#[derive(Parser, Debug)] #[command(author, version, about)] struct Args { /// Activate debug mode @@ -24,9 +27,22 @@ struct Args { config: PathBuf, } -fn main() { +#[tokio::main] +async fn main() { let args = Args::parse(); - let config_path = args.config; - sync::run(&config_path, args.filter); + loop { + let mut config = Config::from_config_path(&args.config); + + if let Some(filter) = &args.filter { + config.retain_destinations(filter); + } + + config.verbose = args.verbose; + // if let Some(verbose) = args.verbose { + // config.verbose = verbose; + // } + + sync_manager::sync_projects(config).await; + } } diff --git a/src/sync.rs b/src/sync.rs deleted file mode 100644 index d630755..0000000 --- a/src/sync.rs +++ /dev/null @@ -1,232 +0,0 @@ -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, - thread::sleep, - time::{Duration, Instant}, -}; - -use colored::Colorize; - -use crate::config; - -const _SELF_CONFIG_: &str = "project-sync-self-config"; - -#[derive(Debug)] -//#[allow(dead_code)] -pub struct Sync { - pub name: String, - pub source: String, - pub destinations: Vec, - pub synced: Instant, - //updated: Instant, - pub sync_on_start: bool, - pub ignore: String, - pub options: String, -} - -#[derive(Debug)] -struct SyncUpdate { - name: String, - update: Instant, -} - -impl Sync { - fn sync_extras(&self) -> String { - // let git_ignore = ".git\n"; - // let rsync_ignore = self.ignore.as_deref().unwrap_or(git_ignore); - // let rsync_ignore = if rsync_ignore.starts_with(git_ignore) { - // git_ignore - // } else { - // &format!("{}{}", git_ignore, rsync_ignore) - // }; - - let rsync_ignore_dir = PathBuf::from(shellexpand::tilde("~/.cache/project-sync").as_ref()); - std::fs::create_dir_all(&rsync_ignore_dir).unwrap(); - - let rsync_ignore_path = rsync_ignore_dir.join(self.name.clone() + ".rsync-ignore"); - - std::fs::write(&rsync_ignore_path, &self.ignore).expect("Can not create rsync-ignore file..."); - - format!("{} --exclude-from='{}'", self.options, rsync_ignore_path.display()) - } - - fn sync(&mut self) { - for destination in &self.destinations { - //std::thread::sleep(Duration::from_secs_f32(0.25)); - print!("Syncing {} ", self.name.bright_green()); - - let verbosity = "--itemize-changes"; // -v - let extras = self.sync_extras(); - let command_line = format!("rsync {verbosity} -az -e ssh {extras} {} '{}'", self.source, destination); - - println!("{}", command_line.white().dimmed()); - - let mut attempts = 0; - let start = Instant::now(); - - self.synced = Instant::now(); - while !std::process::Command::new("sh") - .arg("-c") - .arg(&command_line) - .status() - .expect("could not run shell command...") - .success() - { - println!("{}... going to sleep and retry...", "FAILED".on_red()); - std::thread::sleep(Duration::from_secs(4)); - if attempts > 7 { - println!("Too many failed attempts: {}, giving up...", attempts.to_string().bright_red()); - break; - } - attempts += 1; - } - - let _duration = start.elapsed(); - //println!("Elapsed time: {:.2?}", _duration); - } - } - fn create_project_watcher(&self, transmitter: std::sync::mpsc::Sender) -> notify::RecommendedWatcher { - println!( - "Adding {:<16} {:>24} → [", // {:<32} - self.name.bright_green(), - self.source, - ); - for d in &self.destinations { - println!(" {d}"); - } - println!("] sync-on-start: {}", self.sync_on_start); - - let path = PathBuf::from(shellexpand::tilde(&self.source).as_ref()); - - //println!("Creating watcher for project {} in directory {}", name.green(), path.display()); - - let project_name = self.name.clone(); - let mut watcher = notify::recommended_watcher(move |res: Result| match res { - Ok(_) => transmitter - .send(SyncUpdate { - name: project_name.clone(), - update: Instant::now(), - }) - .unwrap(), - Err(e) => println!("Watcher for project {} got Error {:?}", project_name.clone(), e), - }) - .unwrap(); - notify::Watcher::watch(&mut watcher, &path, notify::RecursiveMode::Recursive).unwrap(); - watcher - } -} - -struct ProjectConfig { - projects: BTreeMap, - debounce: f32, -} - -impl ProjectConfig { - fn sync_projects(&mut self) { - let (tx, rx) = std::sync::mpsc::channel::(); - - let mut watchers = Vec::<_>::new(); - for (name, p) in &self.projects { - watchers.push(p.create_project_watcher(tx.clone())); - - if p.sync_on_start { - //println!("Scheduling initial sync for {}...", name.green()); - tx.send(SyncUpdate { - name: name.clone(), - update: Instant::now(), - }) - .unwrap(); - } - } - println!(); - - while let Ok(project_update) = rx.recv() { - // println!("{project_update:?}"); - - let p = self.projects.get_mut(&project_update.name).unwrap(); - - if project_update.name == _SELF_CONFIG_ { - println!( - "{} file {} detected, reloading...\n", - "UPDATE to configuration".truecolor(239, 134, 62).underline(), - p.source.green() - ); - return; - } - - if project_update.update > p.synced { - let duration = project_update.update.duration_since(p.synced); - if duration.as_secs_f32() < self.debounce { - sleep(Duration::from_secs_f32(self.debounce) - duration); - } - p.sync(); - } - } - } -} - -fn run_with_project_config_factory(project_config_factory: F, config_path: &Path) -where - F: Fn() -> ProjectConfig, -{ - loop { - let mut project_config: ProjectConfig = project_config_factory(); - - project_config.projects.insert( - _SELF_CONFIG_.into(), - Sync { - name: _SELF_CONFIG_.into(), - source: config_path.to_str().unwrap().to_string(), - destinations: vec![config_path.to_str().unwrap().to_string()], - synced: Instant::now(), - sync_on_start: false, - ignore: "".into(), - options: "".into(), - }, - ); - - project_config.sync_projects(); - } -} - -pub fn run(config_path: &Path, filter: Option) { - let projects_factory = move || -> ProjectConfig { - println!("Reading sync config from {}...", config_path.display().to_string().bright_yellow()); - let config = config::read_config(config_path); - // println!("Parsed config: {:#?}", config); - - // Retain only those entries that match the filter string - fn destination_filter(mut destinations: Vec, filter: &Option) -> Vec { - if let Some(filter) = filter { - destinations.retain(|dest| dest.contains(filter)); - } - destinations - } - - let projects = config - .sync - .into_iter() - .map(|c| { - ( - c.name.clone(), - Sync { - name: c.name, - source: c.source, - destinations: destination_filter(c.destinations, &filter), - synced: Instant::now(), - sync_on_start: c.sync_on_start, - ignore: c.ignore, - options: c.options, - }, - ) - }) - .collect(); - - ProjectConfig { - projects, - debounce: config.debounce, - } - }; - - run_with_project_config_factory(projects_factory, config_path); -} diff --git a/src/sync_item.rs b/src/sync_item.rs new file mode 100644 index 0000000..a487349 --- /dev/null +++ b/src/sync_item.rs @@ -0,0 +1,183 @@ +use std::path::Path; +use std::path::PathBuf; +use std::time::Duration; + +use colored::Colorize; +use tokio::sync::mpsc::Sender; +use tokio::time::Instant; +use tokio::time::sleep; +use tokio_util::sync::CancellationToken; + +pub const _SELF_CONFIG_: &str = "project-sync-self-config"; +pub const _RSYNC_DEFAULT_OPTIONS_: &str = "--timeout=64 -az -e 'ssh -o ConnectTimeout=64'"; + +#[derive(Debug)] +pub struct SyncUpdate { + pub name: String, + pub update: Instant, +} + +#[derive(Debug)] +pub struct SyncItem { + pub name: String, + pub source: String, + pub destination: String, + pub sync_on_start: bool, + pub ignore: String, + pub options: String, + pub debounce: f32, + pub verbose: bool, +} + +impl SyncItem { + pub fn new_config_file_sync(config_path: &Path, debounce: f32, verbose: bool) -> Self { + SyncItem { + name: _SELF_CONFIG_.into(), + source: config_path.to_str().unwrap().to_string(), + destination: config_path.to_str().unwrap().to_string(), + sync_on_start: false, + ignore: "".into(), + options: "".into(), + debounce, + verbose, + } + } + + pub fn create_project_watcher(&self, transmitter: Sender) -> notify::RecommendedWatcher { + println!( + // "Adding {:<16} {:>24} → {:?} sync-on-start: {}", // {:<32} + "Adding {} {} → {:?}", // {:<32} + self.name.bright_green(), + self.source, + self.destination, + //self.sync_on_start + ); + + let path = PathBuf::from(shellexpand::tilde(&self.source).as_ref()); + + //println!("Creating watcher for project {} in directory {}", name.green(), path.display()); + + let project_name = self.name.clone(); + let mut watcher = notify::recommended_watcher(move |res: Result| match res { + Ok(_) => transmitter + .blocking_send(SyncUpdate { + name: project_name.clone(), + update: Instant::now(), + }) + .unwrap(), + Err(e) => println!("Watcher for project {} got Error {:?}", project_name.clone(), e), + }) + .unwrap(); + notify::Watcher::watch(&mut watcher, &path, notify::RecursiveMode::Recursive).unwrap(); + watcher + } + + fn rsync_extras(&self) -> String { + let rsync_ignore_file_path_root = PathBuf::from(shellexpand::tilde("~/.cache/project-sync").as_ref()); + std::fs::create_dir_all(&rsync_ignore_file_path_root).unwrap(); + let rsync_ignore_path = rsync_ignore_file_path_root.join(self.name.clone() + ".rsync-ignore"); + std::fs::write(&rsync_ignore_path, &self.ignore).expect("Can not create rsync-ignore file..."); + + let options = if self.options.is_empty() { "" } else { &format!("{} ", self.options) }; + + format!("{}--exclude-from='{}'", options, rsync_ignore_path.display()) + } + + async fn run_rsync(&self, previous_sync_time: Instant) -> Instant { + let extras = self.rsync_extras(); + let verbosity = if self.verbose { "--itemize-changes " } else { "" }; + let command_line = format!("rsync {verbosity}{_RSYNC_DEFAULT_OPTIONS_} {extras} {} '{}'", self.source, self.destination); + + let synced = Instant::now(); + + println!( + "{} Syncing {} {}", + format!("[{}]", chrono::Local::now().format("%Y-%m-%d %H:%M:%S")).bright_blue(), + self.name.bright_green(), + command_line.white().dimmed() + ); + + let mut attempts = 1; + while !tokio::process::Command::new("sh") + .arg("-c") + .arg(&command_line) + .status() + .await + .expect("could not run shell command...") + .success() + { + println!("{} to sync {} → {}... going to sleep and retry...", "[FAILED]".on_red(), self.name, self.destination); + std::thread::sleep(Duration::from_secs(4 + attempts * 2)); + if attempts > 1 { + println!("Too many failed attempts: {}, giving up...", attempts.to_string().bright_red()); + return previous_sync_time; + } + attempts += 1; + } + //sleep(Duration::from_secs_f32(10.25)); + println!( + "{} Synced {} {}", + format!("[{}]", chrono::Local::now().format("%Y-%m-%d %H:%M:%S")).bright_blue(), + self.name.bright_green(), + command_line.white().dimmed() + ); + synced + } + + async fn process_update_event(&self, cancellation_token: &CancellationToken, SyncUpdate { name, update }: SyncUpdate, mut synced: Instant) -> Instant { + // println!("{project_update:?}"); + if name == _SELF_CONFIG_ { + println!( + "{} file {} detected, reloading...\n", + "UPDATE to configuration".truecolor(239, 134, 62).underline(), + self.source.green() + ); + cancellation_token.cancel(); + return synced; + } + + if update >= synced { + let duration = update.duration_since(synced); + if duration.as_secs_f32() < self.debounce { + sleep(Duration::from_secs_f32(self.debounce) - duration).await; + } + synced = self.run_rsync(synced).await; + } + synced + } + + pub async fn sync(&self, cancellation_token: CancellationToken) { + let mut synced = Instant::now(); + + let (tx, mut rx) = tokio::sync::mpsc::channel::(1024); + + let _watcher = self.create_project_watcher(tx.clone()); + + if self.sync_on_start { + //println!("Scheduling initial sync for {}...", name.green()); + tx.send(SyncUpdate { + name: self.name.clone(), + update: Instant::now(), + }) + .await + .unwrap(); + } + + loop { + tokio::select! { + maybe_update = rx.recv() => { + if let Some(project_update) = maybe_update { + synced = self.process_update_event(&cancellation_token, project_update, synced).await; + } + else { + break; // channel closed + } + } + _ = cancellation_token.cancelled() => { + // println!("Cancellation requested, terminating..."); + break; + } + } + } + } +} diff --git a/src/sync_manager.rs b/src/sync_manager.rs new file mode 100644 index 0000000..70984cd --- /dev/null +++ b/src/sync_manager.rs @@ -0,0 +1,44 @@ +use colored::Colorize; +use tokio_util::sync::CancellationToken; + +use crate::config::Config; +use crate::sync_item::SyncItem; + +pub struct Projects(Vec); + +impl Projects { + fn new(config: Config) -> Self { + println!("Reading sync config from {}...", config.config_path.display().to_string().bright_yellow()); + + let mut p: Vec = config + .sync + .into_iter() + .flat_map(|c| { + c.destinations.into_iter().map(move |d| SyncItem { + name: c.name.clone(), + source: c.source.clone(), + destination: d.clone(), + sync_on_start: c.sync_on_start, + ignore: c.ignore.clone(), + options: c.options.clone(), + debounce: config.debounce, + verbose: config.verbose, + }) + }) + .collect(); + + let config_sync = SyncItem::new_config_file_sync(&config.config_path, config.debounce, config.verbose); + p.push(config_sync); + + Projects(p) + } + pub async fn sync_projects(&self) { + let cancellation_token = CancellationToken::new(); + futures::future::join_all(self.0.iter().map(|p| async { p.sync(cancellation_token.clone()).await })).await; + } +} + +pub async fn sync_projects(config: Config) { + let projects_watch = Projects::new(config); + projects_watch.sync_projects().await; +}