From ded4064f4be7f1cb406c0adf1cc59ef6d6087d16 Mon Sep 17 00:00:00 2001 From: Myrddin Dundragon Date: Thu, 20 Mar 2025 02:18:30 -0400 Subject: [PATCH] Finished coding up the Rust challenge. --- Cargo.lock | 934 ++++++++++++++++++++- Cargo.toml | 2 + docs/rust_coding_challenge_filemonitor.pdf | Bin 0 -> 46463 bytes src/dir_monitor.rs | 114 ++- src/event.rs | 87 +- src/main.rs | 46 +- src/monitored_files.rs | 107 ++- src/notify_sender.rs | 245 ++++++ src/util.rs | 36 + 9 files changed, 1419 insertions(+), 152 deletions(-) create mode 100644 docs/rust_coding_challenge_filemonitor.pdf create mode 100644 src/notify_sender.rs create mode 100644 src/util.rs diff --git a/Cargo.lock b/Cargo.lock index cbfb9d3..1f6759c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -82,12 +91,104 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "anyhow" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -103,6 +204,18 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "bitflags" version = "1.3.2" @@ -121,6 +234,18 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[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.16" @@ -196,18 +321,95 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "console-api" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "hyper-util", + "prost", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + [[package]] name = "file_monitor" version = "0.0.0" dependencies = [ "chrono", "clap", + "console-subscriber", + "home", "notify", "tokio", ] @@ -224,6 +426,22 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "flate2" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "fsevent-sys" version = "4.1.0" @@ -233,18 +451,226 @@ dependencies = [ "libc", ] +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[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-core", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.8.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "flate2", + "nom", + "num-traits", +] + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -268,6 +694,26 @@ dependencies = [ "cc", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + [[package]] name = "inotify" version = "0.11.0" @@ -294,6 +740,21 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "js-sys" version = "0.3.77" @@ -324,6 +785,12 @@ dependencies = [ "libc", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.171" @@ -347,12 +814,39 @@ version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.5" @@ -374,6 +868,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "notify" version = "8.0.0" @@ -423,12 +927,53 @@ version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[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 = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -438,6 +983,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + [[package]] name = "quote" version = "1.0.40" @@ -447,6 +1024,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.5.10" @@ -456,6 +1063,50 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -468,6 +1119,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "same-file" version = "1.0.6" @@ -477,6 +1134,47 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -492,6 +1190,31 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "strsim" version = "0.11.1" @@ -509,6 +1232,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tokio" version = "1.44.1" @@ -516,11 +1255,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" dependencies = [ "backtrace", + "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", + "tracing", "windows-sys 0.52.0", ] @@ -535,6 +1277,159 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "once_cell", + "regex", + "sharded-slab", + "thread_local", + "tracing", + "tracing-core", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "unicode-ident" version = "1.0.18" @@ -547,6 +1442,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "walkdir" version = "2.5.0" @@ -557,6 +1458,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -641,9 +1551,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-sys" @@ -726,3 +1636,23 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zerocopy" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index c837473..144497d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,5 @@ clap = { version = "4.5.32", features = ["derive"] } chrono = "0.4.40" notify = "8.0.0" tokio = { version = "1.44.1", features = ["macros", "rt-multi-thread", "io-std", "fs", "signal", "sync"] } +console-subscriber = "0.4.1" +home = "0.5.11" diff --git a/docs/rust_coding_challenge_filemonitor.pdf b/docs/rust_coding_challenge_filemonitor.pdf new file mode 100644 index 0000000000000000000000000000000000000000..7ab3278f3d7eac71ef05c115490c428e35a1feb3 GIT binary patch literal 46463 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,g)7)Dw>Je@lR((*9YP*N#f0o)9R$ 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%0AIDGh8D!n884lCEkTt2PWRTefKQ>v)VkR6law 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 { @@ -61,80 +66,65 @@ impl DirMonitor /// ``` pub fn print_monitored_files(&self) { - self.monitored_files.print(&self.dir); + println!("{}", self.monitored_files); } - /// + /// Start monitoring the directory asyncronously and process changes within the directory + /// until a termination message is received. pub async fn monitor(&mut self, mut term_receiver: tokio::sync::watch::Receiver) { let mut running: bool = true; - //let (notify_sender, notify_receiver) = std::sync::mpsc::channel::>(); - let (notify_sender, notify_receiver) = tokio::sync::mpsc::channel::>(100); - let mut fs_watcher: RecommendedWatcher = match notify::recommended_watcher(notify_sender) - { - Ok(watcher) => { watcher } - Err(e) => { eprintln!("Unable to create watcher: {}", e); panic!(); } - }; - fs_watcher.watch(&self.dir, notify::RecursiveMode::Recursive).unwrap(); + // Setup the notify crate to watch the INBOX directory. + let tokio_runtime = tokio::runtime::Handle::current(); + let (notify_sender, mut notify_receiver) = tokio::sync::mpsc::channel::(10); + let wrapped_sender = NotifySender::new(tokio_runtime, notify_sender); + let mut fs_watcher: RecommendedWatcher = + match notify::recommended_watcher(wrapped_sender) + { + Ok(watcher) => { watcher } + Err(e) => + { + // Just panic because we cannot watch the directories so the program + // would be useless and this saves from having to press CTRL-C. + eprintln!("Unable to create watcher: {}", e); + panic!(); + } + }; + if let Err(e) = fs_watcher.watch(&self.dir, notify::RecursiveMode::Recursive) + { + // Just panic because we cannot watch the directories so the program + // would be useless and this saves from having to press CTRL-C. + eprintln!("Error trying to watch the directory: {}", e); + panic!(); + } + + + // Loop until termination processing events from the notify watcher. while running { - // Listen for file changes until termination signal is received. - match notify_receiver.recv().await + // We are listening for messages from either the notify receiver or + // the termination receiver. When ever one of them comes across we + // will process it. + tokio::select! { - Ok(msg) => + // Handle listening for the notify watcher events. + // These are the changes that we care about from the file system. + Some(mut event) = notify_receiver.recv() => { - match msg - { - Ok(event) => - { - match event.kind - { - EventKind::Create(_) => - { - println!("File Created: {:?}", event.paths); - } - EventKind::Modify(_) => - { - println!("File Modified: {:?}", event.paths); - } - EventKind::Remove(_) => - { - println!("File Removed: {:?}", event.paths); - } - _ => - { - } - } - } - - Err(e) => - { - eprintln!("{}", e); - } - } + event.make_paths_relative(&self.dir); + println!("{}", event); + self.monitored_files.process_event(event); } - Err(e) => - { - eprintln!("Error receiving notify event: {}", e); - } - } - // Handle listening for the termination message, the boolean value will - // be changed to false when we are meant to terminate. - match term_receiver.has_changed() - { - Ok(_) => + // Handle listening for the termination message, the boolean value will + // be changed to false when we are meant to terminate. + _ = term_receiver.changed() => { running = *term_receiver.borrow_and_update(); } - - Err(e) => - { - eprintln!("Unable to receive: {}", e); - } } } } @@ -144,7 +134,7 @@ impl DirMonitor /// Scans a directory, and all of its sub directories, and creates a list of the files /// inside and their last modification time. -fn scan_dir(monitored_files: &mut MonitoredFiles, dir: &Path) -> std::io::Result<()> +fn scan_dir(base_dir: &Path, monitored_files: &mut MonitoredFiles, dir: &Path) -> std::io::Result<()> { let mut dir_list: Vec = Vec::new(); @@ -169,14 +159,14 @@ fn scan_dir(monitored_files: &mut MonitoredFiles, dir: &Path) -> std::io::Result { let last_mod_time: std::time::SystemTime = meta.modified()?; - monitored_files.add(&file.path(), last_mod_time); + monitored_files.add(&strip_path_prefix(&file.path(), base_dir), last_mod_time); } } // Recursively scan the sub directories. for sub_dir in dir_list { - scan_dir(monitored_files, &sub_dir)?; + scan_dir(base_dir, monitored_files, &sub_dir)?; } Ok(()) diff --git a/src/event.rs b/src/event.rs index 43cbf2f..b753795 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,35 +1,83 @@ use std::time::SystemTime; +use crate::util::strip_path_prefix; + +/// The events we are worried about. /// +/// These will be sent as messages in a channel. pub enum Event { - /// + /// A file has been created. New { - /// - path: String, + /// The path to the file. + path: std::path::PathBuf, - /// + /// The modification time of the file. mod_time: SystemTime }, - /// + /// A file has been modified. Modify { - /// - path: String, + /// The path to the file. + path: std::path::PathBuf, - /// + /// The modification time of the file. mod_time: SystemTime }, - /// + /// A file has been renamed, and it stayed in a monitored directory. + Rename + { + /// The path of the file that was renamed. + from: std::path::PathBuf, + + /// The path that the file was renamed to. + to: std::path::PathBuf, + + /// The modification time of the file. + mod_time: SystemTime + }, + + /// A file has been removed. Delete { - /// - path: String + /// The path of the removed file. + path: std::path::PathBuf + } +} + + + +impl Event +{ + /// Take all the paths in the Event and makes them relative + /// to the base_dir directory. + pub fn make_paths_relative(&mut self, base_dir: &std::path::Path) + { + match self + { + Event::New { path, mod_time: _ } => + { + *path = strip_path_prefix(&path, base_dir); + } + Event::Modify { path, mod_time: _ } => + { + *path = strip_path_prefix(&path, base_dir); + } + Event::Rename { from, to, mod_time: _ } => + { + *from = strip_path_prefix(&from, base_dir); + *to = strip_path_prefix(&to, base_dir); + } + Event::Delete { path } => + { + *path = strip_path_prefix(&path, base_dir); + } + } } } @@ -40,17 +88,22 @@ impl std::fmt::Display for Event { match self { - Event::New(_) => + Event::New { path, mod_time: _ } => { - write!(f, "[NEW] {}", self.path) + write!(f, "[NEW] {}", path.display()) } - Event::Modify(_) => + Event::Modify { path, mod_time: _ } => { - write!(f, "[MOD] {}", self.path) + write!(f, "[MOD] {}", path.display()) } - Event::Delete => + Event::Rename { from, to, mod_time: _ } => { - write!(f, "[DEL] {}", self.path) + writeln!(f, "[DEL] {}", from.display())?; + write!(f, "[NEW] {}", to.display()) + } + Event::Delete { path } => + { + write!(f, "[DEL] {}", path.display()) } } } diff --git a/src/main.rs b/src/main.rs index c604d1e..f47333b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,8 @@ mod dir_monitor; mod event; mod monitored_files; +mod notify_sender; +mod util; @@ -33,19 +35,28 @@ struct Options /// Creates the given directory, and its parents, if it does not exist. -fn create_directory(dir: &str) -> bool +fn create_directory(dir: &str) -> Option { - match std::fs::create_dir_all(dir) + let mut inbox_dir = String::from(dir); + if inbox_dir.starts_with("~") + { + if let Some(path) = home::home_dir() + { + inbox_dir = inbox_dir.replace("~", &path.display().to_string()); + } + } + + match std::fs::create_dir_all(inbox_dir.clone()) { Ok(_) => { - true + Some(inbox_dir) } Err(e) => { eprintln!("Creating directory error: {}", e); - false + None } } } @@ -72,14 +83,13 @@ async fn main() { let options: Options = Options::parse(); - println!("Inbox: `{}`", options.inbox_dir); - if create_directory(&options.inbox_dir) + if let Some(inbox_dir) = create_directory(&options.inbox_dir) { // This is our keep running channel. False will be sent // when the application is meant to be closed. let (sender, receiver) = watch::channel(true); - let mut directory_monitor: DirMonitor = DirMonitor::new(&options.inbox_dir); + let mut directory_monitor: DirMonitor = DirMonitor::new(&inbox_dir); // Program workflow step 1. // Async is not needed here. This can be done syncronously because @@ -99,11 +109,20 @@ async fn main() // preferably a green thread, but a fiber is sufficient // and less resource intensive. directory_monitor.monitor(receiver).await; + + // Program workflow step 4. + // Async is not needed here. This can be done + // syncronously because it is the final/clean up + // step of the program. + // + // This is done here because we have moved the directory + // monitor inside this task. + directory_monitor.print_monitored_files(); }); // Run until Ctrl-C is pressed. // Once it is pressed it will send a message to stop. - let term_task = tokio::spawn(async move + let _term_task = tokio::spawn(async move { // Program workflow step 3. // Async with message passing so it can run and listen @@ -115,11 +134,10 @@ async fn main() // Only need to wait for the monitor task to finish because it will only // finish once the termination task has finished. - mon_task.await; - - // Program workflow step 4. - // Async is not needed here. This can be done syncronously because it - // is the final/clean up step of the program. - directory_monitor.print_monitored_files(); + match mon_task.await + { + Ok(_) => {} + Err(e) => { eprintln!("Error during monitoring task: {}", e); } + } } } diff --git a/src/monitored_files.rs b/src/monitored_files.rs index f1d92a8..5883f58 100644 --- a/src/monitored_files.rs +++ b/src/monitored_files.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::path::{PathBuf, Path}; use std::time::SystemTime; @@ -7,104 +8,96 @@ use crate::event::Event; -/// +/// The files currently being monitored and their last modification time. pub struct MonitoredFiles { - /// - mod_dates: Vec, - - /// - paths: Vec + /// A mapping of a Path to the last known modification time. + files: HashMap } impl MonitoredFiles { + /// Create a new empty set of monitored files. pub fn new() -> Self { MonitoredFiles { - mod_dates: Vec::new(), - paths: Vec::new() + files: HashMap::new() } } - pub fn process_event(&mut self, path: &Path, event: Event) + /// Process an event and change the monitored files. + pub fn process_event(&mut self, event: Event) { match event { - Event::New(creation_time) => + Event::New { path, mod_time } => { - self.add(path, creation_time); + self.add(&path, mod_time); } - Event::Modify(mod_time) => + Event::Modify { path, mod_time} => { self.modify(&path, mod_time); } - Event::Delete => + Event::Rename { from, to, mod_time } => + { + self.remove(&from); + self.add(&to, mod_time); + } + + Event::Delete { path } => { self.remove(&path); } } } - pub fn add(&mut self, path: &Path, time: SystemTime) + /// Add a file that is to be monitored.. + pub fn add(&mut self, path: &Path, mod_time: SystemTime) { - self.paths.push(PathBuf::from(path)); - self.mod_dates.push(time); + self.files.insert(PathBuf::from(path), mod_time); } + /// Modify the stored modifcation time of a monitored file. pub fn modify(&mut self, path: &Path, time: SystemTime) { - if let Some(index) = self.get_path_index(path) + if let Some(entry) = self.files.get_mut(path) { - self.mod_dates[index] = time; + *entry = time; } } + /// Remove a file from being monitored. pub fn remove(&mut self, path: &Path) { - if let Some(index) = self.get_path_index(path) - { - self.mod_dates.remove(index); - self.paths.remove(index); - } - } - - pub fn print(&self, base: &Path) - { - for file in 0..self.paths.len() - { - let date_time: DateTime = DateTime::from(self.mod_dates[file]); - - match &self.paths[file].strip_prefix(base) - { - Ok(rel_path) => - { - println!("[{}] {}", date_time.format("%d/%m/%Y %H:%M"), rel_path.display()); - } - - Err(e) => - { - eprintln!("Unable to strip base directory: {}", e); - } - } - } - } - - fn get_path_index(&self, path: &Path) -> Option - { - for (index, stored_path) in self.paths.iter().enumerate() - { - if path == stored_path - { - return Some(index); - } - } - - None + self.files.remove(path); + } +} + + +impl std::fmt::Display for MonitoredFiles +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + { + let mut it = self.files.iter().peekable(); + while let Some((path, mod_time)) = it.next() + { + let date_time: DateTime = DateTime::from(*mod_time); + + if !it.peek().is_none() + { + writeln!(f, "[{}] {}", date_time.format("%m/%d/%Y %H:%M"), path.display())?; + } + else + { + write!(f, "[{}] {}", date_time.format("%m/%d/%Y %H:%M"), path.display())?; + } + } + + Ok(()) } } diff --git a/src/notify_sender.rs b/src/notify_sender.rs new file mode 100644 index 0000000..4dcad0c --- /dev/null +++ b/src/notify_sender.rs @@ -0,0 +1,245 @@ +use std::path::PathBuf; + +use crate::util::get_file_mod_time; + + + +/// NotifySender is a newtype idiom around the tokio mpsc Sender. +/// +/// This was done to allow the tokio mpsc Sender to be a Notify crate +/// EventHandler. +pub struct NotifySender +{ + /// The tokio runtime to spawn event handling tasks on. + tokio_runtime: tokio::runtime::Handle, + + /// The channel to send event messages on. + sender: tokio::sync::mpsc::Sender +} + + + +impl NotifySender +{ + /// Create a new type that wraps the tokio mpsc Sender. + pub fn new(tokio_runtime: tokio::runtime::Handle, + sender: tokio::sync::mpsc::Sender) + -> Self + { + NotifySender + { + tokio_runtime, + sender + } + } +} + +impl notify::EventHandler for NotifySender +{ + fn handle_event(&mut self, event: notify::Result) + { + match event + { + Ok(notify_event) => + { + // Process the notify event and turn it into an event + // our crate is interested in, one of our Events. If + // we get a valid one then create a tokio task that will + // send the event out as a message. + if let Some(monitor_event) = process_notify_event(notify_event) + { + let notify_sender = self.sender.clone(); + + self.tokio_runtime.spawn(async move + { + if let Err(e) = notify_sender.send(monitor_event).await + { + eprintln!("Notify EventHandler: {}", e); + } + }); + } + } + + Err(e) => + { + eprintln!("Notify crate error: {}", e); + } + } + } +} + +/// Processes the notify crate events into events that our program cares about. +fn process_notify_event(event: notify::Event) -> Option +{ + match event.kind + { + notify::EventKind::Create(kind) => + { + process_create_event(kind, event.paths) + } + + notify::EventKind::Modify(kind) => + { + process_modify_event(kind, event.paths) + } + + notify::EventKind::Remove(kind) => + { + process_remove_event(kind, event.paths) + } + + _ => { None } + } +} + +/// Process the Create event into a New event. +fn process_create_event(event: notify::event::CreateKind, + paths: Vec) + -> Option +{ + if event == notify::event::CreateKind::File + { + let mod_time: std::time::SystemTime = + match get_file_mod_time(&paths[0]) + { + Ok(time) => { time } + + Err(e) => + { + eprintln!("Unable to open file to retrieve modification time: {}", + e); + std::time::SystemTime::now() + } + }; + + return Some(crate::event::Event::New + { + path: paths[0].clone(), + mod_time: mod_time + }); + } + + None +} + +/// Process the modify event into either a Modify event or a Rename event. +fn process_modify_event(event: notify::event::ModifyKind, + paths: Vec) + -> Option +{ + match event + { + // This is just handling the events. On my system they came across as Any for + // the MetadataKind, but the documentation seemed to hint that modification + // times should be on write events. So for the sake of being done without testing + // this on different platforms the Any modification event will trigger a time lookup. + notify::event::ModifyKind::Any | notify::event::ModifyKind::Metadata(_) => + { + let mod_time: std::time::SystemTime = + match get_file_mod_time(&paths[0]) + { + Ok(time) => { time } + + Err(e) => + { + eprintln!("Unable to open file to retrieve modification time: {}", + e); + std::time::SystemTime::now() + } + }; + + Some(crate::event::Event::Modify + { + path: paths[0].clone(), + mod_time: mod_time + }) + } + + // Handling a rename event so that during testing when a file is + // moved it is still tracked. + notify::event::ModifyKind::Name(mode) => + { + match mode + { + // This is for when a file is renamed and both the to and from + // are in the monitored directories. + notify::event::RenameMode::Both => + { + let mod_time: std::time::SystemTime = + match get_file_mod_time(&paths[1]) + { + Ok(time) => { time } + + Err(e) => + { + eprintln!("Unable to open file to retrieve modification time: {}", + e); + std::time::SystemTime::now() + } + }; + + Some(crate::event::Event::Rename + { + from: paths[0].clone(), + to: paths[1].clone(), + mod_time: mod_time + }) + } + + // This is for when a file is renamed and only the from + // file is in the monitored directories. + notify::event::RenameMode::From => + { + Some(crate::event::Event::Delete + { + path: paths[0].clone(), + }) + } + + // This is for when a file is renamed and both the to file + // is in the monitored directories. + notify::event::RenameMode::To => + { + let mod_time: std::time::SystemTime = + match get_file_mod_time(&paths[0]) + { + Ok(time) => { time } + + Err(e) => + { + eprintln!("Unable to open file to retrieve modification time: {}", + e); + std::time::SystemTime::now() + } + }; + + Some(crate::event::Event::New + { + path: paths[0].clone(), + mod_time: mod_time + }) + } + + _ => { None } + } + } + + _ => { None } + } +} + +/// Process the remove event into a Delete event. +fn process_remove_event(event: notify::event::RemoveKind, + paths: Vec) + -> Option +{ + if event == notify::event::RemoveKind::File + { + return Some(crate::event::Event::Delete + { + path: paths[0].clone(), + }); + } + + None +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..8342c35 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,36 @@ +/// Return the modification time of a file. +/// +/// If it is unable to get the modification time from the file system +/// it will default to the current system time. +pub fn get_file_mod_time(path: &std::path::Path) -> std::io::Result +{ + let file = std::fs::File::open(path)?; + let meta = file.metadata()?; + + if meta.is_file() + { + return Ok(meta.modified()?); + } + + Ok(std::time::SystemTime::now()) +} + +/// Create a new PathBuf that is the same as path but has the base removed if it can. +pub fn strip_path_prefix(path: &std::path::Path, base: &std::path::Path) -> std::path::PathBuf +{ + match &path.strip_prefix(base) + { + Ok(rel_path) => + { + let mut final_path = std::path::PathBuf::new(); + final_path.push("./"); + final_path.push(rel_path); + final_path + } + + Err(_) => + { + std::path::PathBuf::from(path) + } + } +}