I recently was given a take home assignment for a job interview. The task was specified like so:
Create a command-line program that accepts an optional argument: “-d path”. If the
path is not supplied, it defaults to “~/inbox/”. If the path does not exist, it should be
created. We refer to this path as “INBOX” in the rest of the document.
Program workflow:
1. Scan the folder recursively and print to stdout all the files found and their last
modification date in the following format: “[Date Time] PATH”, where PATH is a
relative path to INBOX.
2. Start monitoring INBOX for file changes. When an event occurs, print it to stdout
in the following format: “[EVENT] PATH”, where EVENT is one of the following
[NEW, MOD, DEL].
3. Continue monitoring until the user inputs Ctrl-C.
4. Once Ctrl-C is detected, print to stdout the contents of INBOX again in the same
format, without rescanning or any other FS operations.
Bonus points for:
1. Using tokio
2. Using structured error handling
3. Not using mutexes
4. Having separation of concerns
Furthermore, this was supposed to be done in Rust and I was told that I could use external libraries. This was the code that I submitted:
use notify::{self, recommended_watcher, Event, EventKind, RecursiveMode, Watcher};
use tokio::{self, signal, sync::mpsc, task::JoinHandle};
use time::{OffsetDateTime, format_description};
use clap::{self, Parser};
use fxhash::FxHashMap as HashMap;
use walkdir::WalkDir;
use shellexpand;
use std::time::SystemTime;
use std::path::{Path, PathBuf};
use std::fmt::{Display, self};
use std::fs;
use std::io;
// If terminal printing gets any more complicated,
// it'd probably be better to use an external crate.
struct TermMode {}
impl TermMode {
pub const Black: &str = "\x1b[30m";
pub const Red: &str = "\x1b[31m";
pub const Green: &str = "\x1b[32m";
pub const Yellow: &str = "\x1b[33m";
pub const Bold: &str = "\x1b[1m";
pub const Reset: &str = "\x1b[0m";
}
#[derive(Debug)]
struct Inbox {
path: PathBuf,
// Use a HashMap for more efficient insert and delete operations.
// HashMap also makes it so we don't get duplicate entries.
// This messes up ordering but I think it's ok.
files: HashMap<PathBuf, SystemTime>,
}
const TIME_FORMAT: &str = "[year]-[month]-[day] [hour]:[minute]:[second]";
impl Display for Inbox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.files.len() == 0 {
write!(f, "{} is empty", self.path.display())
} else {
writeln!(f, "Contents of {}", self.path.display());
for (i, (path, last_modified)) in self.files.iter().enumerate() {
let datetime: OffsetDateTime = (*last_modified).into();
let date_str = match format_description::parse(TIME_FORMAT) {
Ok(format) => datetime.format(&format).unwrap_or("ERROR".to_string()),
_ => "ERROR".to_string()
};
write!(f, "[{}] {}", date_str, path.display());
if i < self.files.len() - 1 {
write!(f, "\n");
}
}
Ok(())
}
}
}
impl Inbox {
fn new(dir: &Path) -> io::Result<Self> {
let mut files = HashMap::default();
for entry in WalkDir::new(dir).into_iter().filter_map(Result::ok) {
let path = entry.path();
let rel_path = match path.strip_prefix(dir) {
Ok(p) => p.to_owned(),
_ => path.to_owned(),
};
let md = fs::metadata(&path)?;
let last_modified = md.modified()?;
// Don't print "." which with the prefix stripped looks empty.
if rel_path.components().next().is_some() {
files.insert(rel_path, last_modified);
}
}
let canon_path = match dir.canonicalize() {
Ok(p) => p,
_ => dir.to_owned(),
};
Ok(Inbox {
path: canon_path,
files: files,
})
}
fn log(&self, event: &Event) {
for path in &event.paths {
let rel_path = match path.strip_prefix(&self.path) {
Ok(p) => p.to_owned(),
_ => path.to_owned(),
};
let (kind_str, color) = match &event.kind {
EventKind::Create(_) => ("NEW", TermMode::Green),
EventKind::Modify(_) => ("MOD", TermMode::Yellow),
EventKind::Remove(_) => ("DEL", TermMode::Red),
_ => ("UNKNOWN", TermMode::Black),
};
println!(
"[{}{}{}{}] {}",
TermMode::Bold,
color,
kind_str,
TermMode::Reset,
rel_path.display()
);
}
}
fn handle(&mut self, event: &Event) {
let event_time = SystemTime::now();
for path in &event.paths {
let rel_path = match path.strip_prefix(&self.path) {
Ok(p) => p.to_owned(),
_ => path.to_owned(),
};
// We still don't care about operations to ".".
if rel_path.components().next().is_none() {
continue;
}
match &event.kind {
EventKind::Create(_) => {
self.files.insert(rel_path, event_time.clone());
},
EventKind::Modify(_) => {
self.files
.iter_mut()
.for_each(|(p, t)| {
if p == &rel_path {
*t = event_time.clone();
}
});
},
EventKind::Remove(_) => {
self.files.remove(&rel_path);
},
_ => {},
}
}
}
}
#[derive(Parser)]
pub struct Args {
#[arg(short = 'd', default_value = "~/inbox")]
path: String,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
let watcher_task: JoinHandle<notify::Result<()>> = tokio::spawn(async move {
let dir_ = shellexpand::tilde(&args.path).into_owned();
let dir = Path::new(&dir_);
let canon_dir = match dir.canonicalize() {
Ok(p) => p,
_ => dir.to_owned(),
};
if !canon_dir.exists() {
fs::create_dir_all(&canon_dir)
.expect(format!("Could not create inbox at {}", canon_dir.display()).as_str());
}
let mut inbox = Inbox::new(&canon_dir)?;
println!("{}", inbox);
let (mut tx, mut rx) = mpsc::channel::<notify::Result<Event>>(100);
// In the watcher, synchronously send the event on the receive channel.
let mut watcher = recommended_watcher(move |res| {
tx.blocking_send(res).expect("Could not send fs event to tokio channel");
})?;
watcher.watch(&canon_dir, RecursiveMode::Recursive)?;
loop {
tokio::select! {
Ok(_) = signal::ctrl_c() => {
println!("{}", inbox);
break;
},
Some(res) = rx.recv() => {
match res {
Ok(event) => {
inbox.log(&event);
inbox.handle(&event);
},
Err(err) => eprintln!("Error receiving fs event: {}", err),
};
},
}
}
Ok(())
});
match watcher_task.await {
Ok(_) => {},
Err(err) => {
eprintln!("Error awaiting fs watch task to finish: {}", err);
},
}
}
The code works to specification; however, after submitting this I was rejected for the job without explanation and my request for feedback was ghosted.
When trying to figure out why I was rejected, I realized that I had RUSTFLAGS="-Awarnings" in my bashrc so I'm aware that there are some warnings in my submission (though nothing serious imo).
Other than that, I'm not sure what I could've done to improve my submission.
How well does my solution match the prompt?
Here is my Cargo.toml:
[package]
name = "challenge"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = {version = "4.5.21", features = ["derive"]}
fxhash = "0.2.1"
notify = "7.0.0"
shellexpand = "3.1.0"
time = {version = "0.3.36", features = ["formatting"]}
tokio = {version = "1.41.1", features = ["full"]}
walkdir = "2.5.0"
Cargo.tomlfile too - there are several external deps with non-default feature flags. \$\endgroup\$