diff --git a/Cargo.lock b/Cargo.lock index 1879d6c..cbfb9d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,36 @@ # 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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[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 +82,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -64,12 +115,41 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clap" version = "4.5.32" @@ -116,12 +196,20 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "file_monitor" version = "0.0.0" dependencies = [ + "chrono", "clap", "notify", + "tokio", ] [[package]] @@ -145,12 +233,41 @@ dependencies = [ "libc", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "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 = "inotify" version = "0.11.0" @@ -177,6 +294,16 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "kqueue" version = "1.0.8" @@ -220,6 +347,21 @@ version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "1.0.3" @@ -257,12 +399,36 @@ 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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "proc-macro2" version = "1.0.94" @@ -290,6 +456,18 @@ dependencies = [ "bitflags 2.9.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + [[package]] name = "same-file" version = "1.0.6" @@ -299,6 +477,21 @@ dependencies = [ "winapi-util", ] +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "strsim" version = "0.11.1" @@ -316,6 +509,32 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tokio" +version = "1.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +dependencies = [ + "backtrace", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "windows-sys 0.52.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 = "unicode-ident" version = "1.0.18" @@ -344,6 +563,64 @@ 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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi-util" version = "0.1.9" @@ -353,6 +630,21 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-link" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 9163dac..c837473 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,4 +12,6 @@ license-file = "LICENSE.md" [dependencies] 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"] } diff --git a/src/dir_monitor.rs b/src/dir_monitor.rs index 458ea19..e325b3c 100644 --- a/src/dir_monitor.rs +++ b/src/dir_monitor.rs @@ -1,5 +1,7 @@ use std::path::{Path, PathBuf}; +use notify::{EventKind, RecommendedWatcher, Watcher}; + use crate::monitored_files::MonitoredFiles; @@ -11,9 +13,9 @@ use crate::monitored_files::MonitoredFiles; pub struct DirMonitor { /// The directory to monitor. - dir: std::path::PathBuf, + dir: PathBuf, - monitored_files: MonitoredFiles + monitored_files: MonitoredFiles, } @@ -31,12 +33,9 @@ impl DirMonitor } } - pub fn monitor(&mut self) - { - self.build_file_list(); - } - - fn build_file_list(&mut self) + /// Scan the monitored directory and all of its sub directories and monitors + /// the files found and their last modification time. + pub fn scan(&mut self) { // Start from the directory and add each file to our list, // then recurse through all sub directories and add them to @@ -49,8 +48,93 @@ impl DirMonitor Err(e) => { - println!("There was an issue during directory scanning."); - println!("{}", e); + eprintln!("Directory scanning error: {}", e); + } + } + } + + /// Print out all the monitored files and their last modified date. + /// + /// Each file entry will be printed out as: + /// ``` + /// [Date Time] PATH + /// ``` + pub fn print_monitored_files(&self) + { + self.monitored_files.print(&self.dir); + } + + /// + 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(); + + while running + { + // Listen for file changes until termination signal is received. + match notify_receiver.recv().await + { + Ok(msg) => + { + 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); + } + } + } + + 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(_) => + { + running = *term_receiver.borrow_and_update(); + } + + Err(e) => + { + eprintln!("Unable to receive: {}", e); + } } } } @@ -58,6 +142,8 @@ 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<()> { let mut dir_list: Vec = Vec::new(); @@ -65,12 +151,29 @@ fn scan_dir(monitored_files: &mut MonitoredFiles, dir: &Path) -> std::io::Result for file in std::fs::read_dir(dir)? { let file = file?; - if file.file_type()?.is_dir() + let meta = file.metadata()?; + + // Handle directory and file types. + // + // Directories will be added to the list and later recursively scanned. + // + // Files will be added to the list of monitored files and their + // last modification time will be stored. + // + // TODO: Handle symlinks. Symlinks can be either a file or another directory. + if meta.is_dir() { dir_list.push(file.path().clone()); } + else if meta.is_file() + { + let last_mod_time: std::time::SystemTime = meta.modified()?; + + monitored_files.add(&file.path(), last_mod_time); + } } + // Recursively scan the sub directories. for sub_dir in dir_list { scan_dir(monitored_files, &sub_dir)?; diff --git a/src/event.rs b/src/event.rs index 340574a..43cbf2f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1,8 +1,36 @@ -enum Event +use std::time::SystemTime; + + + +/// +pub enum Event { - New, - Modify, + /// + New + { + /// + path: String, + + /// + mod_time: SystemTime + }, + + /// + Modify + { + /// + path: String, + + /// + mod_time: SystemTime + }, + + /// Delete + { + /// + path: String + } } @@ -12,17 +40,17 @@ impl std::fmt::Display for Event { match self { - Event::New => + Event::New(_) => { - write!(f, "NEW") + write!(f, "[NEW] {}", self.path) } - Event::Modify => + Event::Modify(_) => { - write!(f, "MOD") + write!(f, "[MOD] {}", self.path) } Event::Delete => { - write!(f, "DEL") + write!(f, "[DEL] {}", self.path) } } } diff --git a/src/main.rs b/src/main.rs index 97d3380..c604d1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -//! Rust challenge +//! Rust challenge: FileMonitor @@ -10,6 +10,8 @@ mod monitored_files; use clap::Parser; +use tokio::sync::watch; + use crate::dir_monitor::DirMonitor; @@ -30,13 +32,94 @@ struct Options +/// Creates the given directory, and its parents, if it does not exist. +fn create_directory(dir: &str) -> bool +{ + match std::fs::create_dir_all(dir) + { + Ok(_) => + { + true + } + + Err(e) => + { + eprintln!("Creating directory error: {}", e); + false + } + } +} + +/// Asynchronously listen for CTRL-C to be pressed. +/// +/// This will send a false message across the channel +/// when it detects the termination signal. +async fn listen_for_termination(sender: watch::Sender) +{ + tokio::signal::ctrl_c().await.unwrap(); + + // CTRL-C was pressed so send a message to everyone that is + // watching that the program is to be terminated. Then close the + // sender. + sender.send_replace(false); + sender.closed().await; +} + + /// The usual starting point of your project. -fn main() +#[tokio::main] +async fn main() { let options: Options = Options::parse(); println!("Inbox: `{}`", options.inbox_dir); + if 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); - directory_monitor.monitor(); + let mut directory_monitor: DirMonitor = DirMonitor::new(&options.inbox_dir); + + // Program workflow step 1. + // Async is not needed here. This can be done syncronously because + // file systems are not async and the spawn_blocking blocking overhead + // isn't needed. + directory_monitor.scan(); + directory_monitor.print_monitored_files(); + + // Run our directory monitor. + let mon_task = tokio::spawn(async move + { + // Program workflow step 2 (terminated by step 3). + // This can be done async because we are waiting on + // events to be loaded from the notify library and + // we want this to run continously and not block the + // program. This could also be done on a seperate thread, + // preferably a green thread, but a fiber is sufficient + // and less resource intensive. + directory_monitor.monitor(receiver).await; + }); + + // Run until Ctrl-C is pressed. + // Once it is pressed it will send a message to stop. + let term_task = tokio::spawn(async move + { + // Program workflow step 3. + // Async with message passing so it can run and listen + // for the termination signal. It uses a watch channel + // because any task that will need to know when the + // program is to end can then catch the change. + listen_for_termination(sender).await; + }); + + // 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(); + } } diff --git a/src/monitored_files.rs b/src/monitored_files.rs index 07d1cfc..f1d92a8 100644 --- a/src/monitored_files.rs +++ b/src/monitored_files.rs @@ -1,14 +1,24 @@ +use std::path::{PathBuf, Path}; +use std::time::SystemTime; + +use chrono::{DateTime, Local}; + +use crate::event::Event; + + + /// pub struct MonitoredFiles { /// - mod_dates: Vec>, + mod_dates: Vec, /// - paths: Vec> + paths: Vec } + impl MonitoredFiles { pub fn new() -> Self @@ -19,13 +29,82 @@ impl MonitoredFiles paths: Vec::new() } } -} - -impl std::fmt::Display for Point -{ - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result + pub fn process_event(&mut self, path: &Path, event: Event) { - write!(f, "({}, {})", self.x, self.y) + match event + { + Event::New(creation_time) => + { + self.add(path, creation_time); + } + + Event::Modify(mod_time) => + { + self.modify(&path, mod_time); + } + + Event::Delete => + { + self.remove(&path); + } + } + } + + pub fn add(&mut self, path: &Path, time: SystemTime) + { + self.paths.push(PathBuf::from(path)); + self.mod_dates.push(time); + } + + pub fn modify(&mut self, path: &Path, time: SystemTime) + { + if let Some(index) = self.get_path_index(path) + { + self.mod_dates[index] = time; + } + } + + 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 } }