datman 0.1.0

A chunked and deduplicated backup system using Yama
Documentation
/*
This file is part of Yama.

Yama is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Yama is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with Yama.  If not, see <https://www.gnu.org/licenses/>.
*/


use std::io;
use std::io::{StdinLock, Stdout, Write};
use std::path::Path;

use arc_interner::ArcIntern;
use byteorder::ReadBytesExt;
use termion::input::TermRead;
use termion::raw::{IntoRawMode, RawTerminal};

use crate::descriptor::{load_descriptor, Descriptor, SourceDescriptor};
use crate::labelling::State::{Excluded, Labelled, Split};
use crate::labelling::{
    load_labelling_rules, save_labelling_rules, GlobRule, Label, LabellingRules, State,
};
use crate::tree::{scan, FileTree, FileTree1};

use anyhow::{anyhow, bail};

pub fn interactive_label_node(
    path: String,
    current_state: Option<State>,
    node: &mut FileTree1<Option<State>>,
    labels: &Vec<Label>,
    rules: &mut LabellingRules,
    stdin: &mut StdinLock,
    stdout: &mut RawTerminal<Stdout>,
) -> anyhow::Result<()> {
    let mut next_state = current_state;
    if let Some(rule_state) = rules.apply(&path) {
        next_state = Some(rule_state.clone());
    } else if !next_state
        .as_ref()
        .map(|s| s.should_inherit())
        .unwrap_or(false)
    {
        if node.is_dir() {
            stdout.write_all(format!("\r{}/: _", path).as_bytes())?;
        } else if node.is_symlink() {
            stdout.write_all(format!("\r{} (symlink): _", path).as_bytes())?;
        } else {
            stdout.write_all(format!("\r{}: _", path).as_bytes())?;
        }
        stdout.flush()?;
        let user_input_state = loop {
            let next_char = stdin.read_u8()? as char;
            if next_char >= '1' && next_char <= '9' {
                let index = next_char as usize - '1' as usize;
                if let Some(label) = labels.get(index) {
                    rules
                        .position_based_rules
                        .insert(path.clone(), Labelled(label.clone()));
                    print!("\x08{}\r\n", label.0);
                    break Some(Labelled(label.clone()));
                }
            } else if next_char == 'x' {
                rules.position_based_rules.insert(path.clone(), Excluded);
                print!("\x08{}\r\n", next_char);
                break Some(Excluded);
            } else if next_char == 's' {
                if node.is_dir() {
                    rules.position_based_rules.insert(path.clone(), Split);
                    print!("\x08{}\r\n", next_char);
                    break Some(Split);
                } else {
                    print!("\x08!");
                    stdout.flush()?;
                }
            } else if next_char == 'p' {
                print!("\x08p\r\n\tPattern mode. Choose a label or other effect to apply to the pattern matches: _");
                stdout.flush()?;

                let rule_apply_state = loop {
                    let next_char = stdin.read_u8()? as char;
                    if next_char >= '1' && next_char <= '9' {
                        let index = next_char as usize - '1' as usize;
                        if let Some(label) = labels.get(index) {
                            print!("\x08{}\r\n", label.0);
                            break Labelled(label.clone());
                        }
                    } else if next_char == 'x' {
                        print!("\x08{}\r\n", next_char);
                        break Excluded;
                    } else if next_char == 's' {
                        print!("\x08{}\r\n", next_char);
                        break Split;
                    }
                };
                stdout.flush()?;

                stdout.suspend_raw_mode()?;
                print!("\tEnter a glob pattern to match on:\n\t");
                stdout.flush()?;
                let (pattern, glob) = loop {
                    let pattern = stdin
                        .read_line()?
                        .ok_or_else(|| anyhow!("EOT? when reading glob pattern"))?;

                    match glob::Pattern::new(&pattern) {
                        Ok(glob) => {
                            if !glob.matches(&path) {
                                println!("Doesn't match the path in question.");
                                continue;
                            }
                            break (pattern, glob);
                        }
                        Err(error) => {
                            println!("Error: {:?}. Try again.", error);
                        }
                    }
                };
                stdout.activate_raw_mode()?;

                rules.glob_based_rules.push(GlobRule {
                    pattern,
                    glob,
                    outcome: rule_apply_state.clone(),
                });
                break Some(rule_apply_state);
            }
        };
        next_state = user_input_state;
    }

    match node {
        FileTree::NormalFile { meta, .. } => {
            *meta = next_state;
        }
        FileTree::Directory { meta, children, .. } => {
            *meta = next_state.clone();

            for (child_name, child) in children.iter_mut() {
                let child_path = format!("{}/{}", path, child_name);
                interactive_label_node(
                    child_path,
                    next_state.clone(),
                    child,
                    labels,
                    rules,
                    stdin,
                    stdout,
                )?;
            }
        }
        FileTree::SymbolicLink { meta, .. } => {
            *meta = next_state;
        }
        FileTree::Other(_) => {
            panic!("Other() nodes shouldn't be present here.");
        }
    }

    Ok(())
}

pub fn interactive_labelling_session(path: &Path, source_name: String) -> anyhow::Result<()> {
    let descriptor: Descriptor = load_descriptor(path)?;

    let source = descriptor
        .source
        .get(&source_name)
        .ok_or_else(|| anyhow!("No source found by that name!"))?;

    if let SourceDescriptor::DirectorySource {
        hostname: _,
        directory,
    } = source
    {
        println!("Scanning source; this might take a little while...");
        let mut dir_scan = scan(directory)?
            .ok_or_else(|| anyhow!("Empty source."))?
            .replace_meta(&None);

        let mut rules = load_labelling_rules(path, &source_name)?;

        let labels: Vec<Label> = descriptor
            .labels
            .iter()
            .map(|label| Label(ArcIntern::new(label.clone())))
            .collect();

        println!("The following label mappings are available:");
        for (idx, label) in labels.iter().enumerate() {
            println!("\tFor {:?}, press {}!", label.0.as_ref(), idx + 1);
        }
        println!("\tTo split a directory, press 's'!");
        println!("\tTo exclude an entry, press 'x'!");
        println!("\tTo apply a pattern, press 'p'...");

        // Set terminal to raw mode to allow reading stdin one key at a time
        let mut stdout = io::stdout().into_raw_mode().unwrap();
        let stdin_unlocked = io::stdin();
        let mut stdin = stdin_unlocked.lock();
        interactive_label_node(
            "".to_owned(),
            None,
            &mut dir_scan,
            &labels,
            &mut rules,
            &mut stdin,
            &mut stdout,
        )?;
        drop(stdout);
        drop(stdin);

        println!("\nLabelling completed!");

        // save rules
        save_labelling_rules(path, &source_name, &rules)?;
    } else {
        bail!("Can't do interactive labelling on a non-directory source.");
    }

    Ok(())
}