fs-librarian 0.4.7

Librarian runs pre-configured commands against a group of files that match a set of filters
mod config;
mod fs_notify;
mod library;
mod mime_type;
mod template;
use clap::{Args, Parser, Subcommand};
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::channel;
use std::thread;
use std::time::Duration;

#[macro_use]
extern crate bitflags;

static GLOBAL_THREAD_COUNT: AtomicUsize = AtomicUsize::new(0);
static GLOBAL_FAILED_TREADS: AtomicUsize = AtomicUsize::new(0);

/// Runs pre-configured commands against a group of files that match a set of filters
#[derive(Debug, Parser)]
#[clap(name = "fs-librarian")]
#[clap(author, version, about = "Goes through file types inside directories and does with them as you wish", long_about = None)]
struct Cli {
    #[clap(subcommand)]
    command: Commands,
}

#[derive(Debug, Subcommand)]
enum Commands {
    /// Use OS specific mechanisms, like Linux's inotify, to watch for changes in directories that will trigger Librarian when new files are added or changed
    #[clap(arg_required_else_help = true)]
    Watch {
        /// Path to the configuration file to use
        #[clap(required = true)]
        config_path: String,
        /// Print the file Librarian got a notification for, instead of running the pre-configured command against it
        #[clap(short, long)]
        dry_run: bool,
    },

    /// Run Librarian once
    #[clap(arg_required_else_help = true)]
    SingleShot {
        /// Path to the configuration file to use
        #[clap(required = true)]
        config_path: String,
        /// Print the filtered files, instead of running the pre-configured commands against them
        #[clap(short, long)]
        dry_run: bool,
    },

    /// Debugging tools to help you to better work with Librarian
    Test(Test),
}

#[derive(Debug, Args)]
#[clap(args_conflicts_with_subcommands = true)]
struct Test {
    #[clap(subcommand)]
    command: TestCommands,
}

#[derive(Debug, Subcommand)]
enum TestCommands {
    /// Check a file's MIME type
    Mime {
        #[clap(value_parser)]
        file_path: String,
    },
}

fn main() {
    let args = Cli::parse();
    match args.command {
        Commands::Watch {
            config_path,
            dry_run,
        } => {
            watch(&config_path, dry_run);
        }
        Commands::SingleShot {
            config_path,
            dry_run,
        } => {
            single_shot(&config_path, dry_run);
        }
        Commands::Test(t) => {
            test(&t);
        }
    }
}

fn get_config(config_path: &String) -> config::Config {
    match config::Config::new(config_path) {
        Ok(c) => c,
        Err(e) => {
            eprintln!("{}", e);
            std::process::exit(exitcode::CONFIG);
        }
    }
}

fn watch(config_path: &String, dry_run: bool) {
    let conf = get_config(config_path);

    let mut paths: HashSet<PathBuf> = HashSet::new();
    // let mut libraries = Vec::new();
    for cur_lib_config in conf.libraries {
        for cur_dir in cur_lib_config.1.filter.directories {
            paths.insert(PathBuf::from(cur_dir));
        }
    }

    let (on_event_sender, on_event_receiver) = channel();
    let (mut notify_obj, _) =
        fs_notify::Notify::new(&conf.fs_watch, paths, on_event_sender).unwrap();
    let config_path_clone = config_path.clone();
    thread::spawn(move || loop {
        let path = match on_event_receiver.recv() {
            Ok(p) => p,
            Err(e) => {
                eprintln!("{}", e);
                continue;
            }
        };

        let conf = get_config(&config_path_clone);
        for cur_lib_config in conf.libraries {
            let cur_lib = library::Library::new(&cur_lib_config.1, &dry_run);
            if !cur_lib.contains_path(Path::new(&path)) {
                continue;
            }

            let number = match cur_lib.process(Some(Path::new(&path))) {
                Ok(n) => n,
                Err(e) => {
                    eprintln!("{}", e);
                    0
                }
            };

            if number > 0 {
                println!(
                    "Processed '{}' as part of the {} library",
                    path, cur_lib_config.0
                );
            }
        }
    });

    single_shot(config_path, dry_run);
    if let Err(e) = notify_obj.watch() {
        eprint!(
            "An error was thrown while attempting to watch a library: {:?}",
            e
        );
    }
}

fn single_shot(config_path: &String, dry_run: bool) {
    let conf = get_config(config_path);

    for cur_conf in conf.libraries {
        GLOBAL_THREAD_COUNT.fetch_add(1, Ordering::SeqCst);
        thread::spawn(move || {
            match library::Library::new(&cur_conf.1, &dry_run).process(None) {
                Ok(k) => {
                    println!("Processed {} files in the {} library", k, cur_conf.0);
                }
                Err(e) => {
                    eprintln!("{}", e);
                    GLOBAL_FAILED_TREADS.fetch_add(1, Ordering::SeqCst);
                }
            }

            GLOBAL_THREAD_COUNT.fetch_sub(1, Ordering::SeqCst);
        });
    }

    while GLOBAL_THREAD_COUNT.load(Ordering::SeqCst) != 0 {
        thread::sleep(Duration::from_millis(1));
    }

    if GLOBAL_FAILED_TREADS.load(Ordering::SeqCst) > 0 {
        std::process::exit(exitcode::DATAERR);
    }
}

fn test(test: &Test) {
    match &test.command {
        TestCommands::Mime { file_path } => mime(file_path),
    }
}

fn mime(file_path: &str) {
    match mime_type::File::new(Path::new(file_path)).get_mime_type() {
        Err(e) => {
            eprintln!("{}", e);
            std::process::exit(exitcode::DATAERR);
        }
        Ok(m) => {
            println!("{}", m)
        }
    };
}