Compare commits

..

2 Commits

Author SHA1 Message Date
3a0684c33e Ran 'cargo fmt' to adjust the files.
Just trying to get closer to my preferred coding style.
2025-03-25 22:23:34 -04:00
fe7537f238 Moving personal review and adjusting README.
Just making the project a little more tidy.
2025-03-25 22:22:19 -04:00
8 changed files with 603 additions and 600 deletions

View File

@ -1,33 +1,17 @@
# FileMonitor # FileMonitor #
This was a Rust Challenge that I completed in 17 hours. This was a Rust Challenge that I completed in 17 hours.
## Review ## The [document](./docs/rust_coding_challenge_filemonitor.pdf)
I thought I solved the challenge pretty well. I took my time and went back describing the challenge can be found in the docs directory.
and refactored and commented well.
Notify was a crate I hadn't used before so that took some time to understand
and then determine how I wanted to wrap it up. I like the solution that I
came up with because it keeps most of the notify mess contained in one file
with just a little seepage out when it has to be used for monitoring.
One thing I was not happy about was that I had very poor git commiting during ## Platform ##
the challenge. I think it was because I was trying to keep my time lower, but This was created on Ubuntu and was only tested there. It should
would it really have hurt to spend a little bit of time to commit and give work on most other platforms, but without testing it, I do not
myself better backups incase of a big problem? Probably not. know for certain.
This let me work on and understand rust async. I had read the rust book and
the tokio tutorial completely before doing this challenge. It wasn't that
bad. It reminds me of coding for threads a lot, which is good, but sometimes
the await gets me because I don't understand where it is blocked when it is
running sometimes, tokio-console helps here, and why calls block or what they
are blocking for. For example, why does tokio::io::stdout().write_all() seem
to block forever? It is given a few bytes to write to out, it should be able
to dump those to the console quickly enough.
Really, i think my problem with the async IO calls was that they needed to ## Build ##
happen in a seperate task and be listening for messages on what to print. To build it just use a standard Cargo install with the current
That way they are never able to block a different task. Rust compiler. Make sure your Cargo is set for Crates.IO as
your registry because it uses several crates from there.
Next I think I should checkout the rayon crate, but we'll see what the next
challenge I get is.

33
docs/personal_review.md Normal file
View File

@ -0,0 +1,33 @@
# Review #
I thought I solved the challenge pretty well. I took my time and went back
and refactored and commented well. The overall amount of time I spent solving
it, 17 hours, may be rather problematic. I did have to learn some things
while I was working on it, but it took what it took.
Notify was a crate I hadn't used before so that took some time to understand
and then determine how I wanted to wrap it up. I like the solution that I
came up with because it keeps most of the notify mess contained in one file
with just a little seepage out when it has to be used for monitoring. Perhaps
I should have fully wrapped the library? It wouldn't have taken much more to
do so, it just didn't seem necessary to solving the problem.
One thing I was not happy about was that I had very poor git commiting during
the challenge. I think it was because I was trying to keep my time lower, but
would it really have hurt to spend a little bit of time to commit and give
myself better backups incase of a big problem? Probably not.
Ultimately, his let me work on and understand rust async. I had read the rust
book and the tokio tutorial completely before doing this challenge. It wasn't
that bad. It reminds me of coding for threads a lot, which is good, but
sometimesthe await gets me because I don't understand where it is blocked when
it is running sometimes, tokio-console helps here, and why calls block or what
they are blocking for. For example, why does tokio::io::stdout().write_all()
seem to block forever? It is given a few bytes to write to out, it should be
able to dump those to the console quickly enough.
Really, i think my problem with the async IO calls was that they needed to
happen in a seperate task and be listening for messages on what to print.
That way they are never able to block a different task.
Next I think I should checkout the rayon crate, but we'll see what the next
challenge I get is.

View File

@ -15,159 +15,161 @@ use crate::util::strip_path_prefix;
/// of the given directory. /// of the given directory.
pub struct DirMonitor pub struct DirMonitor
{ {
/// The directory to monitor. /// The directory to monitor.
dir: PathBuf, dir: PathBuf,
/// The files that are being monitored within the directory. /// The files that are being monitored within the directory.
monitored_files: MonitoredFiles, monitored_files: MonitoredFiles
} }
impl DirMonitor impl DirMonitor
{ {
/// Create a new directory monitor for the desired directory. /// Create a new directory monitor for the desired directory.
/// ///
/// monitor_path: A string representation of directory to monitor. /// monitor_path: A string representation of directory to monitor.
pub fn new(monitor_path: &str) -> Self pub fn new(monitor_path: &str) -> Self
{ {
DirMonitor DirMonitor { dir: PathBuf::from(monitor_path),
{ monitored_files: MonitoredFiles::new() }
dir: PathBuf::from(monitor_path), }
monitored_files: MonitoredFiles::new()
}
}
/// Scan the monitored directory and all of its sub directories and monitors /// Scan the monitored directory and all of its sub directories and monitors
/// the files found and their last modification time. /// the files found and their last modification time.
pub fn scan(&mut self) pub fn scan(&mut self)
{ {
// Start from the directory and add each file to our list, // Start from the directory and add each file to our list,
// then recurse through all sub directories and add them to // then recurse through all sub directories and add them to
// the list and repeat. // the list and repeat.
match scan_dir(&self.dir, &mut self.monitored_files, &self.dir) match scan_dir(&self.dir, &mut self.monitored_files, &self.dir)
{ {
Ok(_) => Ok(_) =>
{ {}
}
Err(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)
{
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<bool>)
{
let mut running: bool = true;
// 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::<Event>(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) => Err(e) =>
{ {
eprintln!("Directory scanning error: {}", 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!();
} }
} };
}
/// Print out all the monitored files and their last modified date. if let Err(e) =
/// fs_watcher.watch(&self.dir, notify::RecursiveMode::Recursive)
/// Each file entry will be printed out as: {
/// ``` // Just panic because we cannot watch the directories so the program
/// [Date Time] PATH // would be useless and this saves from having to press CTRL-C.
/// ``` eprintln!("Error trying to watch the directory: {}", e);
pub fn print_monitored_files(&self) panic!();
{ }
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<bool>)
{
let mut running: bool = true;
// 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::<Event>(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. // Loop until termination processing events from the notify watcher.
while running while running
{ {
// We are listening for messages from either the notify receiver or // We are listening for messages from either the notify receiver or
// the termination receiver. When ever one of them comes across we // the termination receiver. When ever one of them comes across we
// will process it. // will process it.
tokio::select! tokio::select! {
{ // Handle listening for the notify watcher events.
// Handle listening for the notify watcher events. // These are the changes that we care about from the file system.
// These are the changes that we care about from the file system. Some(mut event) = notify_receiver.recv() =>
Some(mut event) = notify_receiver.recv() => {
{ event.make_paths_relative(&self.dir);
event.make_paths_relative(&self.dir); println!("{}", event);
println!("{}", event); self.monitored_files.process_event(event);
self.monitored_files.process_event(event); }
}
// Handle listening for the termination message, the boolean value will // Handle listening for the termination message, the boolean value will
// be changed to false when we are meant to terminate. // be changed to false when we are meant to terminate.
_ = term_receiver.changed() => _ = term_receiver.changed() =>
{ {
running = *term_receiver.borrow_and_update(); running = *term_receiver.borrow_and_update();
} }
} }
} }
} }
} }
/// Scans a directory, and all of its sub directories, and creates a list of the files /// Scans a directory, and all of its sub directories, and creates a list of the
/// inside and their last modification time. /// files inside and their last modification time.
fn scan_dir(base_dir: &Path, 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<PathBuf> = Vec::new(); let mut dir_list: Vec<PathBuf> = Vec::new();
for file in std::fs::read_dir(dir)? for file in std::fs::read_dir(dir)?
{ {
let file = file?; let file = file?;
let meta = file.metadata()?; let meta = file.metadata()?;
// Handle directory and file types. // Handle directory and file types.
// //
// Directories will be added to the list and later recursively scanned. // Directories will be added to the list and later recursively scanned.
// //
// Files will be added to the list of monitored files and their // Files will be added to the list of monitored files and their
// last modification time will be stored. // last modification time will be stored.
// //
// TODO: Handle symlinks. Symlinks can be either a file or another directory. // TODO: Handle symlinks. Symlinks can be either a file or another
if meta.is_dir() // directory.
{ if meta.is_dir()
dir_list.push(file.path().clone()); {
} dir_list.push(file.path().clone());
else if meta.is_file() }
{ else if meta.is_file()
let last_mod_time: std::time::SystemTime = meta.modified()?; {
let last_mod_time: std::time::SystemTime = meta.modified()?;
monitored_files.add(&strip_path_prefix(&file.path(), base_dir), last_mod_time); monitored_files.add(&strip_path_prefix(&file.path(), base_dir),
} last_mod_time);
} }
}
// Recursively scan the sub directories. // Recursively scan the sub directories.
for sub_dir in dir_list for sub_dir in dir_list
{ {
scan_dir(base_dir, monitored_files, &sub_dir)?; scan_dir(base_dir, monitored_files, &sub_dir)?;
} }
Ok(()) Ok(())
} }

View File

@ -9,102 +9,106 @@ use crate::util::strip_path_prefix;
/// These will be sent as messages in a channel. /// These will be sent as messages in a channel.
pub enum Event pub enum Event
{ {
/// A file has been created. /// A file has been created.
New New
{ {
/// The path to the file. /// The path to the file.
path: std::path::PathBuf, path: std::path::PathBuf,
/// The modification time of the file. /// The modification time of the file.
mod_time: SystemTime mod_time: SystemTime
}, },
/// A file has been modified. /// A file has been modified.
Modify Modify
{ {
/// The path to the file. /// The path to the file.
path: std::path::PathBuf, path: std::path::PathBuf,
/// The modification time of the file. /// The modification time of the file.
mod_time: SystemTime mod_time: SystemTime
}, },
/// A file has been renamed, and it stayed in a monitored directory. /// A file has been renamed, and it stayed in a monitored directory.
Rename Rename
{ {
/// The path of the file that was renamed. /// The path of the file that was renamed.
from: std::path::PathBuf, from: std::path::PathBuf,
/// The path that the file was renamed to. /// The path that the file was renamed to.
to: std::path::PathBuf, to: std::path::PathBuf,
/// The modification time of the file. /// The modification time of the file.
mod_time: SystemTime mod_time: SystemTime
}, },
/// A file has been removed. /// A file has been removed.
Delete Delete
{ {
/// The path of the removed file. /// The path of the removed file.
path: std::path::PathBuf path: std::path::PathBuf
} }
} }
impl Event impl Event
{ {
/// Take all the paths in the Event and makes them relative /// Take all the paths in the Event and makes them relative
/// to the base_dir directory. /// to the base_dir directory.
pub fn make_paths_relative(&mut self, base_dir: &std::path::Path) pub fn make_paths_relative(&mut self, base_dir: &std::path::Path)
{ {
match self match self
{ {
Event::New { path, mod_time: _ } => Event::New { path, mod_time: _ } =>
{ {
*path = strip_path_prefix(&path, base_dir); *path = strip_path_prefix(&path, base_dir);
} }
Event::Modify { path, mod_time: _ } => Event::Modify { path, mod_time: _ } =>
{ {
*path = strip_path_prefix(&path, base_dir); *path = strip_path_prefix(&path, base_dir);
} }
Event::Rename { from, to, mod_time: _ } => Event::Rename { from,
{ to,
*from = strip_path_prefix(&from, base_dir); mod_time: _ } =>
*to = strip_path_prefix(&to, base_dir); {
} *from = strip_path_prefix(&from, base_dir);
Event::Delete { path } => *to = strip_path_prefix(&to, base_dir);
{ }
*path = strip_path_prefix(&path, base_dir); Event::Delete { path } =>
} {
} *path = strip_path_prefix(&path, base_dir);
} }
}
}
} }
impl std::fmt::Display for Event impl std::fmt::Display for Event
{ {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{ {
match self match self
{ {
Event::New { path, mod_time: _ } => Event::New { path, mod_time: _ } =>
{ {
write!(f, "[NEW] {}", path.display()) write!(f, "[NEW] {}", path.display())
} }
Event::Modify { path, mod_time: _ } => Event::Modify { path, mod_time: _ } =>
{ {
write!(f, "[MOD] {}", path.display()) write!(f, "[MOD] {}", path.display())
} }
Event::Rename { from, to, mod_time: _ } => Event::Rename { from,
{ to,
writeln!(f, "[DEL] {}", from.display())?; mod_time: _ } =>
write!(f, "[NEW] {}", to.display()) {
} writeln!(f, "[DEL] {}", from.display())?;
Event::Delete { path } => write!(f, "[NEW] {}", to.display())
{ }
write!(f, "[DEL] {}", path.display()) Event::Delete { path } =>
} {
} write!(f, "[DEL] {}", path.display())
} }
}
}
} }

View File

@ -11,7 +11,6 @@ mod util;
use clap::Parser; use clap::Parser;
use tokio::sync::watch; use tokio::sync::watch;
use crate::dir_monitor::DirMonitor; use crate::dir_monitor::DirMonitor;
@ -27,9 +26,9 @@ use crate::dir_monitor::DirMonitor;
/// files. /// files.
struct Options struct Options
{ {
#[arg(short = 'd', default_value = "~/inbox")] #[arg(short = 'd', default_value = "~/inbox")]
/// The path for the inbox to monitor. /// The path for the inbox to monitor.
inbox_dir: String inbox_dir: String
} }
@ -37,28 +36,25 @@ struct Options
/// Creates the given directory, and its parents, if it does not exist. /// Creates the given directory, and its parents, if it does not exist.
fn create_directory(dir: &str) -> Option<String> fn create_directory(dir: &str) -> Option<String>
{ {
let mut inbox_dir = String::from(dir); let mut inbox_dir = String::from(dir);
if inbox_dir.starts_with("~") if inbox_dir.starts_with("~")
{ {
if let Some(path) = home::home_dir() if let Some(path) = home::home_dir()
{ {
inbox_dir = inbox_dir.replace("~", &path.display().to_string()); inbox_dir = inbox_dir.replace("~", &path.display().to_string());
} }
} }
match std::fs::create_dir_all(inbox_dir.clone()) match std::fs::create_dir_all(inbox_dir.clone())
{ {
Ok(_) => Ok(_) => Some(inbox_dir),
{
Some(inbox_dir)
}
Err(e) => Err(e) =>
{ {
eprintln!("Creating directory error: {}", e); eprintln!("Creating directory error: {}", e);
None None
} }
} }
} }
/// Asynchronously listen for CTRL-C to be pressed. /// Asynchronously listen for CTRL-C to be pressed.
@ -67,13 +63,13 @@ fn create_directory(dir: &str) -> Option<String>
/// when it detects the termination signal. /// when it detects the termination signal.
async fn listen_for_termination(sender: watch::Sender<bool>) async fn listen_for_termination(sender: watch::Sender<bool>)
{ {
tokio::signal::ctrl_c().await.unwrap(); tokio::signal::ctrl_c().await.unwrap();
// CTRL-C was pressed so send a message to everyone that is // CTRL-C was pressed so send a message to everyone that is
// watching that the program is to be terminated. Then close the // watching that the program is to be terminated. Then close the
// sender. // sender.
sender.send_replace(false); sender.send_replace(false);
sender.closed().await; sender.closed().await;
} }
@ -81,63 +77,65 @@ async fn listen_for_termination(sender: watch::Sender<bool>)
#[tokio::main] #[tokio::main]
async fn main() async fn main()
{ {
let options: Options = Options::parse(); let options: Options = Options::parse();
if let Some(inbox_dir) = 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 // This is our keep running channel. False will be sent
// when the application is meant to be closed. // when the application is meant to be closed.
let (sender, receiver) = watch::channel(true); let (sender, receiver) = watch::channel(true);
let mut directory_monitor: DirMonitor = DirMonitor::new(&inbox_dir); let mut directory_monitor: DirMonitor = DirMonitor::new(&inbox_dir);
// Program workflow step 1. // Program workflow step 1.
// Async is not needed here. This can be done syncronously because // Async is not needed here. This can be done syncronously because
// file systems are not async and the spawn_blocking blocking overhead // file systems are not async and the spawn_blocking blocking overhead
// isn't needed. // isn't needed.
directory_monitor.scan(); directory_monitor.scan();
directory_monitor.print_monitored_files(); directory_monitor.print_monitored_files();
// Run our directory monitor. // Run our directory monitor.
let mon_task = tokio::spawn(async move let mon_task = tokio::spawn(async move {
{ // Program workflow step 2 (terminated by step 3).
// Program workflow step 2 (terminated by step 3). // This can be done async because we are waiting on
// This can be done async because we are waiting on // events to be loaded from the notify library and
// events to be loaded from the notify library and // we want this to run continously and not block the
// we want this to run continously and not block the // program. This could also be done on a seperate thread,
// program. This could also be done on a seperate thread, // preferably a green thread, but a fiber is sufficient
// preferably a green thread, but a fiber is sufficient // and less resource intensive.
// and less resource intensive. directory_monitor.monitor(receiver).await;
directory_monitor.monitor(receiver).await;
// Program workflow step 4. // Program workflow step 4.
// Async is not needed here. This can be done // Async is not needed here. This can be done
// syncronously because it is the final/clean up // syncronously because it is the final/clean up
// step of the program. // step of the program.
// //
// This is done here because we have moved the directory // This is done here because we have moved the directory
// monitor inside this task. // monitor inside this task.
directory_monitor.print_monitored_files(); directory_monitor.print_monitored_files();
}); });
// Run until Ctrl-C is pressed. // Run until Ctrl-C is pressed.
// Once it is pressed it will send a message to stop. // 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.
// Program workflow step 3. // Async with message passing so it can run and listen
// Async with message passing so it can run and listen // for the termination signal. It uses a watch channel
// for the termination signal. It uses a watch channel // because any task that will need to know when the
// because any task that will need to know when the // program is to end can then catch the change.
// program is to end can then catch the change. listen_for_termination(sender).await;
listen_for_termination(sender).await; });
});
// Only need to wait for the monitor task to finish because it will only // Only need to wait for the monitor task to finish because it will only
// finish once the termination task has finished. // finish once the termination task has finished.
match mon_task.await match mon_task.await
{ {
Ok(_) => {} Ok(_) =>
Err(e) => { eprintln!("Error during monitoring task: {}", e); } {}
} Err(e) =>
} {
eprintln!("Error during monitoring task: {}", e);
}
}
}
} }

View File

@ -1,5 +1,5 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::path::{PathBuf, Path}; use std::path::{Path, PathBuf};
use std::time::SystemTime; use std::time::SystemTime;
use chrono::{DateTime, Local}; use chrono::{DateTime, Local};
@ -11,93 +11,96 @@ use crate::event::Event;
/// The files currently being monitored and their last modification time. /// The files currently being monitored and their last modification time.
pub struct MonitoredFiles pub struct MonitoredFiles
{ {
/// A mapping of a Path to the last known modification time. /// A mapping of a Path to the last known modification time.
files: HashMap<PathBuf, SystemTime> files: HashMap<PathBuf, SystemTime>
} }
impl MonitoredFiles impl MonitoredFiles
{ {
/// Create a new empty set of monitored files. /// Create a new empty set of monitored files.
pub fn new() -> Self pub fn new() -> Self
{ {
MonitoredFiles MonitoredFiles { files: HashMap::new() }
{ }
files: HashMap::new()
}
}
/// Process an event and change the monitored files. /// Process an event and change the monitored files.
pub fn process_event(&mut self, event: Event) pub fn process_event(&mut self, event: Event)
{ {
match event match event
{ {
Event::New { path, mod_time } => Event::New { path, mod_time } =>
{ {
self.add(&path, mod_time); self.add(&path, mod_time);
} }
Event::Modify { path, mod_time} => Event::Modify { path, mod_time } =>
{ {
self.modify(&path, mod_time); self.modify(&path, mod_time);
} }
Event::Rename { from, to, mod_time } => Event::Rename { from, to, mod_time } =>
{ {
self.remove(&from); self.remove(&from);
self.add(&to, mod_time); self.add(&to, mod_time);
} }
Event::Delete { path } => Event::Delete { path } =>
{ {
self.remove(&path); self.remove(&path);
} }
} }
} }
/// Add a file that is to be monitored.. /// Add a file that is to be monitored..
pub fn add(&mut self, path: &Path, mod_time: SystemTime) pub fn add(&mut self, path: &Path, mod_time: SystemTime)
{ {
self.files.insert(PathBuf::from(path), mod_time); self.files.insert(PathBuf::from(path), mod_time);
} }
/// Modify the stored modifcation time of a monitored file. /// Modify the stored modifcation time of a monitored file.
pub fn modify(&mut self, path: &Path, time: SystemTime) pub fn modify(&mut self, path: &Path, time: SystemTime)
{ {
if let Some(entry) = self.files.get_mut(path) if let Some(entry) = self.files.get_mut(path)
{ {
*entry = time; *entry = time;
} }
} }
/// Remove a file from being monitored. /// Remove a file from being monitored.
pub fn remove(&mut self, path: &Path) pub fn remove(&mut self, path: &Path)
{ {
self.files.remove(path); self.files.remove(path);
} }
} }
impl std::fmt::Display for MonitoredFiles impl std::fmt::Display for MonitoredFiles
{ {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{ {
let mut it = self.files.iter().peekable(); let mut it = self.files.iter().peekable();
while let Some((path, mod_time)) = it.next() while let Some((path, mod_time)) = it.next()
{ {
let date_time: DateTime<Local> = DateTime::from(*mod_time); let date_time: DateTime<Local> = DateTime::from(*mod_time);
if !it.peek().is_none() if !it.peek().is_none()
{ {
writeln!(f, "[{}] {}", date_time.format("%m/%d/%Y %H:%M"), path.display())?; writeln!(f,
} "[{}] {}",
else date_time.format("%m/%d/%Y %H:%M"),
{ path.display())?;
write!(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(()) Ok(())
} }
} }

View File

@ -10,236 +10,215 @@ use crate::util::get_file_mod_time;
/// EventHandler. /// EventHandler.
pub struct NotifySender pub struct NotifySender
{ {
/// The tokio runtime to spawn event handling tasks on. /// The tokio runtime to spawn event handling tasks on.
tokio_runtime: tokio::runtime::Handle, tokio_runtime: tokio::runtime::Handle,
/// The channel to send event messages on. /// The channel to send event messages on.
sender: tokio::sync::mpsc::Sender<crate::event::Event> sender: tokio::sync::mpsc::Sender<crate::event::Event>
} }
impl NotifySender impl NotifySender
{ {
/// Create a new type that wraps the tokio mpsc Sender. /// Create a new type that wraps the tokio mpsc Sender.
pub fn new(tokio_runtime: tokio::runtime::Handle, pub fn new(tokio_runtime: tokio::runtime::Handle,
sender: tokio::sync::mpsc::Sender<crate::event::Event>) sender: tokio::sync::mpsc::Sender<crate::event::Event>)
-> Self -> Self
{ {
NotifySender NotifySender { tokio_runtime,
{ sender }
tokio_runtime, }
sender
}
}
} }
impl notify::EventHandler for NotifySender impl notify::EventHandler for NotifySender
{ {
fn handle_event(&mut self, event: notify::Result<notify::Event>) fn handle_event(&mut self, event: notify::Result<notify::Event>)
{ {
match event match event
{ {
Ok(notify_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)
{ {
// Process the notify event and turn it into an event let notify_sender = self.sender.clone();
// 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 self.tokio_runtime.spawn(async move {
{ if let Err(e) =
if let Err(e) = notify_sender.send(monitor_event).await notify_sender.send(monitor_event).await
{ {
eprintln!("Notify EventHandler: {}", e); eprintln!("Notify EventHandler: {}", e);
} }
}); });
}
} }
}
Err(e) => Err(e) =>
{ {
eprintln!("Notify crate error: {}", e); eprintln!("Notify crate error: {}", e);
} }
} }
} }
} }
/// Processes the notify crate events into events that our program cares about. /// Processes the notify crate events into events that our program cares about.
fn process_notify_event(event: notify::Event) -> Option<crate::event::Event> fn process_notify_event(event: notify::Event) -> Option<crate::event::Event>
{ {
match event.kind match event.kind
{ {
notify::EventKind::Create(kind) => notify::EventKind::Create(kind) =>
{ {
process_create_event(kind, event.paths) process_create_event(kind, event.paths)
} }
notify::EventKind::Modify(kind) => notify::EventKind::Modify(kind) =>
{ {
process_modify_event(kind, event.paths) process_modify_event(kind, event.paths)
} }
notify::EventKind::Remove(kind) => notify::EventKind::Remove(kind) =>
{ {
process_remove_event(kind, event.paths) process_remove_event(kind, event.paths)
} }
_ => { None } _ => None
} }
} }
/// Process the Create event into a New event. /// Process the Create event into a New event.
fn process_create_event(event: notify::event::CreateKind, fn process_create_event(event: notify::event::CreateKind, paths: Vec<PathBuf>)
paths: Vec<PathBuf>) -> Option<crate::event::Event>
-> Option<crate::event::Event>
{ {
if event == notify::event::CreateKind::File if event == notify::event::CreateKind::File
{ {
let mod_time: std::time::SystemTime = let mod_time: std::time::SystemTime = match get_file_mod_time(&paths[0])
match get_file_mod_time(&paths[0]) {
{ Ok(time) => time,
Ok(time) => { time }
Err(e) => Err(e) =>
{ {
eprintln!("Unable to open file to retrieve modification time: {}", eprintln!("Unable to open file to retrieve modification time: {}",
e); e);
std::time::SystemTime::now() std::time::SystemTime::now()
} }
}; };
return Some(crate::event::Event::New return Some(crate::event::Event::New { path: paths[0].clone(),
{ mod_time: mod_time });
path: paths[0].clone(), }
mod_time: mod_time
});
}
None None
} }
/// Process the modify event into either a Modify event or a Rename event. /// Process the modify event into either a Modify event or a Rename event.
fn process_modify_event(event: notify::event::ModifyKind, fn process_modify_event(event: notify::event::ModifyKind, paths: Vec<PathBuf>)
paths: Vec<PathBuf>) -> Option<crate::event::Event>
-> Option<crate::event::Event>
{ {
match event match event
{ {
// This is just handling the events. On my system they came across as Any for // This is just handling the events. On my system they came across as Any
// the MetadataKind, but the documentation seemed to hint that modification // for the MetadataKind, but the documentation seemed to hint that
// times should be on write events. So for the sake of being done without testing // modification times should be on write events. So for the sake of
// this on different platforms the Any modification event will trigger a time lookup. // being done without testing this on different platforms the Any
notify::event::ModifyKind::Any | notify::event::ModifyKind::Metadata(_) => // modification event will trigger a time lookup.
{ notify::event::ModifyKind::Any |
let mod_time: std::time::SystemTime = notify::event::ModifyKind::Metadata(_) =>
match get_file_mod_time(&paths[0]) {
{ let mod_time: std::time::SystemTime = match get_file_mod_time(&paths
Ok(time) => { time } [0])
{
Ok(time) => time,
Err(e) => Err(e) =>
{ {
eprintln!("Unable to open file to retrieve modification time: {}", eprintln!("Unable to open file to retrieve modification time: {}",
e); e);
std::time::SystemTime::now() 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 } 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. /// Process the remove event into a Delete event.
fn process_remove_event(event: notify::event::RemoveKind, fn process_remove_event(event: notify::event::RemoveKind, paths: Vec<PathBuf>)
paths: Vec<PathBuf>) -> Option<crate::event::Event>
-> Option<crate::event::Event>
{ {
if event == notify::event::RemoveKind::File if event == notify::event::RemoveKind::File
{ {
return Some(crate::event::Event::Delete return Some(crate::event::Event::Delete { path: paths[0].clone() });
{ }
path: paths[0].clone(),
});
}
None None
} }

View File

@ -2,35 +2,35 @@
/// ///
/// If it is unable to get the modification time from the file system /// If it is unable to get the modification time from the file system
/// it will default to the current system time. /// it will default to the current system time.
pub fn get_file_mod_time(path: &std::path::Path) -> std::io::Result<std::time::SystemTime> pub fn get_file_mod_time(path: &std::path::Path)
-> std::io::Result<std::time::SystemTime>
{ {
let file = std::fs::File::open(path)?; let file = std::fs::File::open(path)?;
let meta = file.metadata()?; let meta = file.metadata()?;
if meta.is_file() if meta.is_file()
{ {
return Ok(meta.modified()?); return Ok(meta.modified()?);
} }
Ok(std::time::SystemTime::now()) Ok(std::time::SystemTime::now())
} }
/// Create a new PathBuf that is the same as path but has the base removed if it can. /// Create a new PathBuf that is the same as path but has the base removed if it
pub fn strip_path_prefix(path: &std::path::Path, base: &std::path::Path) -> std::path::PathBuf /// can.
pub fn strip_path_prefix(path: &std::path::Path, base: &std::path::Path)
-> std::path::PathBuf
{ {
match &path.strip_prefix(base) match &path.strip_prefix(base)
{ {
Ok(rel_path) => Ok(rel_path) =>
{ {
let mut final_path = std::path::PathBuf::new(); let mut final_path = std::path::PathBuf::new();
final_path.push("./"); final_path.push("./");
final_path.push(rel_path); final_path.push(rel_path);
final_path final_path
} }
Err(_) => Err(_) => std::path::PathBuf::from(path)
{ }
std::path::PathBuf::from(path)
}
}
} }