rsincron 0.0.8

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

use inotify::{Inotify, WatchMask};
use serde::Deserialize;

use crate::{
    events::EVENT_TYPES,
    handler::{Handler, Watch, WatchConfig},
};

#[derive(Deserialize, Clone)]
#[serde(default)]
pub struct LoggingConfig {
    pub file: PathBuf,
    pub stdout: bool,
    pub level: log::LevelFilter,
}

impl Default for LoggingConfig {
    fn default() -> Self {
        Self {
            file: PathBuf::from("/var/log/rsincron.log"),
            stdout: true,
            level: log::LevelFilter::Warn,
        }
    }
}

#[derive(Deserialize, Clone)]
#[serde(default)]
pub struct HandlerConfig {
    pub current_user: String,
    pub home_directory: PathBuf,
    pub watch_table: PathBuf,
    pub poll_time: u64,
    pub logging: LoggingConfig,
}

impl Default for HandlerConfig {
    fn default() -> Self {
        let current_user = std::env::var("USER").expect("USER envvar is not set: exiting");
        let home_directory = std::env::var("HOME")
            .map(|home| PathBuf::from(home))
            .expect("HOME envvar is not set: exiting");

        Self {
            current_user,
            home_directory: home_directory.clone(),
            watch_table: home_directory.join(".local/share/rsincron.table"),
            poll_time: 1000,
            logging: LoggingConfig::default(),
        }
    }
}

impl HandlerConfig {
    pub fn setup(self) -> Result<Handler, Box<dyn error::Error>> {
        let table = fs::read_to_string(&self.watch_table)?;
        let event_types = HashMap::from(EVENT_TYPES);
        let mut handler = Handler {
            inotify: Inotify::init()?,
            active_watches: HashMap::new(),
            failed_watches: HashSet::new(),
            handler_config: self,
        };

        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 mut mask = WatchMask::empty();
            let mut config = WatchConfig::default();
            let Some(masks) = fields.next() else {
                continue;
            };

            for m in masks.split(',') {
                match event_types.get(m) {
                    Some(m) => mask.insert(*m),
                    _ => match m.split_once('=') {
                        Some(("recursive", value)) => config.recursive = value.parse().unwrap(),
                        _ => continue,
                    },
                }
            }

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

            let mut watch = Watch {
                path: PathBuf::from(path),
                mask,
                command: command.to_string(),
                config,
            };

            match handler.add_watch(watch.clone(), true, None) {
                Err(_) => {
                    watch.config.table_watch = true;
                    handler.failed_watches.insert(watch.clone())
                }
                _ => true,
            };

            if config.recursive {
                handler.recursive_add_watch(watch);
            }
        }

        Ok(handler)
    }

    pub fn dispatch_log(&self) -> Result<(), Box<dyn error::Error>> {
        let mut dispatch = fern::Dispatch::new()
            .format(|out, message, record| {
                out.finish(format_args!(
                    "{}[{}][{}] {}",
                    chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
                    record.target(),
                    record.level(),
                    message
                ))
            })
            .level(self.logging.level);

        if self.logging.stdout {
            dispatch = dispatch.chain(std::io::stdout());
        }

        if let Ok(log_file) = fern::log_file(&self.logging.file) {
            dispatch = dispatch.chain(log_file);
        }

        Ok(dispatch.apply()?)
    }
}