Last active
March 16, 2016 21:56
-
-
Save sczizzo/88ed750b25ce0f16e18b to your computer and use it in GitHub Desktop.
Watch files, in Rust
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
extern crate rustc_serialize; | |
extern crate docopt; | |
extern crate glob; | |
use std::time::Duration; | |
use std::thread::sleep; | |
use std::os::unix::fs::MetadataExt; | |
use std::os::unix::raw::*; | |
use std::collections::HashMap; | |
use std::collections::HashSet; | |
use docopt::Docopt; | |
use glob::glob; | |
use std::fs; | |
const VERSION: &'static str = env!("CARGO_PKG_VERSION"); | |
const USAGE: &'static str = " | |
Julio. | |
Usage: | |
julio [--interval=<seconds>] <includes>... | |
julio (-h | --help) | |
julio (-v | --version) | |
Options: | |
--interval=<seconds> Interval between loops, in seconds [default: 1]. | |
-h --help Show this screen. | |
-v --version Show version. | |
"; | |
#[derive(Debug, RustcDecodable)] | |
struct Args { | |
flag_version: bool, | |
flag_interval: u64, | |
arg_includes: Vec<String> | |
} | |
#[derive(Debug, RustcDecodable, Clone, PartialEq, PartialOrd)] | |
struct Inode { | |
ino: ino_t, | |
dev: dev_t, | |
rdev: dev_t | |
} | |
#[derive(Debug, RustcDecodable, Clone, PartialEq, PartialOrd)] | |
struct Stat { | |
inode: Inode, | |
size: off_t, | |
mtime: time_t | |
} | |
fn stat(file: &String) -> Option<Stat> { | |
match fs::metadata(file) { | |
Ok(md) => { | |
if md.is_dir() { return None } | |
let inode = Inode { | |
ino: md.ino(), | |
dev: md.dev(), | |
rdev: md.rdev() | |
}; | |
let stat = Stat { | |
inode: inode, | |
size: md.size(), | |
mtime: md.mtime() | |
}; | |
Some(stat) | |
}, | |
Err(_) => None | |
} | |
} | |
fn was_replaced(old: &Option<&Stat>, new: &Option<Stat>) -> bool { | |
if let &Some(ref new_stat) = new { | |
if let &Some(old_stat) = old { | |
return new_stat.inode != old_stat.inode | |
} | |
} | |
false | |
} | |
fn was_appended(old: &Option<&Stat>, new: &Option<Stat>) -> bool { | |
if let &Some(ref new_stat) = new { | |
if let &Some(old_stat) = old { | |
return new_stat.size > old_stat.size | |
} | |
return new_stat.size > 0 | |
} | |
false | |
} | |
fn was_truncated(old: &Option<&Stat>, new: &Option<Stat>) -> bool { | |
if let &Some(ref new_stat) = new { | |
if let &Some(old_stat) = old { | |
return new_stat.size < old_stat.size | |
} | |
} | |
false | |
} | |
fn watch(includes: &Vec<String>, known: &mut HashSet<String>, created: &mut HashSet<String>, deleted: &mut HashSet<String>) { | |
let mut now_known: HashSet<String> = HashSet::new(); | |
for include in includes.iter() { | |
if let Ok(entries) = glob(include) { | |
for entry in entries { | |
if let Ok(path) = entry { | |
if let Ok(full_path) = fs::canonicalize(path) { | |
now_known.insert(full_path.to_string_lossy().into_owned()); | |
} | |
} | |
} | |
} | |
} | |
*created = now_known.difference(known).cloned().collect(); | |
*deleted = known.difference(&now_known).cloned().collect(); | |
*known = now_known; | |
} | |
fn report(known: &HashSet<String>, created: &HashSet<String>, deleted: &HashSet<String>, stats: &mut HashMap<String,Stat>) { | |
for delete in deleted.iter() { | |
println!("deleted:{:?} stat:{:?}", delete, stats.get(delete)); | |
} | |
for create in created.iter() { | |
println!("created:{:?} stat:{:?}", create, stats.get(create)); | |
} | |
let old_stats = stats.clone(); | |
stats.clear(); | |
for file in known.iter() { | |
let old_stat = old_stats.get(file); | |
let new_stat = stat(file); | |
if was_appended(&old_stat, &new_stat) { | |
println!("appended:{:?} stat:{:?}", file, new_stat); | |
} else if was_replaced(&old_stat, &new_stat) { | |
println!("replaced:{:?} stat:{:?}", file, new_stat); | |
} else if was_truncated(&old_stat, &new_stat) { | |
println!("truncated:{:?} stat:{:?}", file, new_stat); | |
} | |
if let Some(actual_stat) = new_stat { | |
stats.insert(file.clone(), actual_stat); | |
} | |
} | |
} | |
fn tail(interval: u64, includes: &Vec<String>) { | |
let delay = Duration::new(interval,0); | |
let mut known = HashSet::new(); | |
let mut created = HashSet::new(); | |
let mut deleted = HashSet::new(); | |
let mut stats = HashMap::new(); | |
loop { | |
watch(includes, &mut known, &mut created, &mut deleted); | |
report(&known, &created, &deleted, &mut stats); | |
sleep(delay); | |
} | |
} | |
fn main() { | |
let args: Args = Docopt::new(USAGE) | |
.and_then(|d| d.decode()) | |
.unwrap_or_else(|e| e.exit()); | |
if args.flag_version { | |
println!("{}", VERSION); | |
} else { | |
let interval = if args.flag_interval < 1 { | |
println!("WARNING: Invalid <interval> provided, falling back to default..."); | |
1 | |
} else { args.flag_interval }; | |
tail(interval, &args.arg_includes); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment