rsincron 0.0.4

Rust rewrite of the incredibly useful but abandoned incron software
Documentation
use std::{collections::HashMap, fs::read_to_string, path::PathBuf, process::Command};

use futures::TryStreamExt;
use inotify::{EventMask, Inotify, WatchDescriptor, WatchMask};
use rsincronlib::{get_user_table_path, EVENT_TYPES};
use walkdir::WalkDir;

type Config<'a> = HashMap<WatchDescriptor, (String, WatchMask, &'a str)>;

fn expand_variables(
    input: &str,
    filename: &str,
    path: &str,
    mask_text: &str,
    mask_bits: &str,
) -> String {
    let mut formatted = String::new();
    let mut dollar = false;
    for c in input.chars() {
        if c == '$' {
            if !dollar {
                dollar = true;
            } else {
                formatted.push(c);
                dollar = false;
            }
        } else {
            if dollar {
                match c {
                    '#' => formatted.push_str(filename),
                    '@' => formatted.push_str(path),
                    '%' => formatted.push_str(mask_text),
                    '&' => formatted.push_str(mask_bits),
                    _ => formatted.push(c),
                }
                dollar = false;
            } else {
                formatted.push(c);
            }
        }
    }
    formatted
}

fn runtime_add_watch<'a>(
    mut inotify: Inotify,
    mut configs: Config<'a>,
    (path, mask, command): (String, WatchMask, &'a str),
    filename: Option<&str>,
) -> (Config<'a>, Inotify) {
    let mut pathbuf = vec![path];

    if let Some(filename) = filename {
        pathbuf.push(filename.to_string());
    };

    let pathbuf = PathBuf::from_iter(pathbuf);
    let Some(path) = pathbuf.to_str() else {
        return (configs, inotify)
    };

    let Ok(descriptor) = inotify.add_watch(&path.to_string(), mask) else {
        return (configs, inotify)
    };

    println!("setup watch: {} for masks {:?}", path, mask);
    configs
        .entry(descriptor)
        .or_insert((path.to_string(), mask, command));

    (configs, inotify)
}

pub fn process_table<'a>(mut inotify: Inotify, table: &'a str) -> (Config<'a>, Inotify) {
    let mut configs = HashMap::new();
    let types = HashMap::from(EVENT_TYPES);
    for line in table.lines() {
        if line.clone().chars().nth(0) == Some('#') {
            continue;
        };

        let mut fields = line.split('\t');
        let Some(path) = fields.next() else {
            continue;
        };

        let mask = {
            let Some(masks) = fields.next() else {
                continue;
            };

            masks.split(',').fold(WatchMask::empty(), |mut mask, new| {
                mask.insert(*types.get(new).unwrap());
                return mask;
            })
        };

        let Some(command) = fields.next() else {
            continue;
        };

        (configs, inotify) =
            runtime_add_watch(inotify, configs, (path.to_string(), mask, command), None);

        for entry in WalkDir::new(path)
            .min_depth(1)
            .into_iter()
            .filter_entry(|e| e.file_type().is_dir())
        {
            let Ok(entry) = entry else {
                continue;
            };

            let Some(path) = entry.path().to_str() else {
                continue;
            };

            (configs, inotify) =
                runtime_add_watch(inotify, configs, (path.to_string(), mask, command), None);
        }
    }

    (configs, inotify)
}

#[async_std::main]
async fn main() {
    let mut inotify = Inotify::init().expect("Error while initializing inotify instance");
    let user = std::env::var("USER").expect("USER is not set: exiting");

    let table = read_to_string(get_user_table_path().join(user))
        .expect("failed to read user table: exiting");

    let buffer = [0; 1024];
    let mut stream = inotify.event_stream(buffer).unwrap();

    let (mut configs, mut inotify) = process_table(inotify, &table);
    while let Ok(event) = stream.try_next().await {
        let Some(event) = event else {
            continue;
        };

        let (path, mask, command) = configs.get(&event.wd).unwrap().clone();
        let filename = match event.name {
            Some(string) => string.to_str().unwrap_or_default().to_owned(),
            _ => String::default(),
        };

        if event.mask == EventMask::CREATE | EventMask::ISDIR {
            (configs, inotify) = runtime_add_watch(
                inotify,
                configs,
                (path.to_owned(), mask.to_owned(), command),
                Some(&filename),
            );
        }

        let masks = format!("{:?}", event.mask).replace(" | ", ",");
        let command = expand_variables(
            command,
            &filename,
            &path.to_string(),
            &masks,
            &event.mask.bits().to_string(),
        );

        let _ = Command::new("bash").arg("-c").arg(command).spawn();
    }
}