hardcpy 0.2.1

Simple backup tool written in Rust
use crate::{BackupEntry, FileEntry, _copy, _pb_update};
use colored::Colorize;
use indicatif::{HumanCount, MultiProgress, ProgressBar, ProgressStyle};
use indicatif_log_bridge::LogWrapper;
use log::{error, info};
use rusqlite::{Result, Transaction};
use sha2::{Digest, Sha256};
use std::fs::{self, File};
use std::io::Read;

pub fn verify(conn: &Transaction, id: u64) {
    let mut error_list = Vec::new();
    let mut verified = 0;
    let mut real_count = 0;
    let mut copied = 0;
    let mut stmt = conn
        .prepare("SELECT source, dest, sha256 FROM Files WHERE backup_id = ?1")
        .unwrap();
    let iter = stmt
        .query_map([id as i64], |row| {
            Ok(FileEntry {
                backup_id: id,
                from: row.get::<usize, String>(0).unwrap().into(),
                to: row.get::<usize, String>(1).unwrap().into(),
                sha256: row.get_unwrap(2),
            })
        })
        .unwrap();
    let multi = MultiProgress::new();
    let logger = colog::default_builder().build();
    LogWrapper::new(multi.clone(), logger).try_init().unwrap();

    let pb = multi.add(ProgressBar::new(
        _count_matches(conn, id as i64).unwrap() as u64
    ));

    pb.set_style(
        ProgressStyle::with_template(
            "{spinner:.green} {msg:.blue.bold} [{bar:50.cyan/blue}] {human_pos}/{human_len} [{elapsed_precise}] ({eta})",
        )
        .unwrap()
        .progress_chars("#>-"),
    );
    pb.set_message("Verifying");

    pb.set_position(0);

    let pb_clone = pb.clone();
    let t = _pb_update(pb_clone);

    for entry in iter {
        real_count += 1;
        let entry = entry.unwrap();
        info!(
            "{} \"{}\"",
            "Verifying".green().bold(),
            entry.to.display().to_string()
        );
        let mut read_from = match File::open(&entry.to) {
            Ok(v) => v,
            Err(_) => {
                info!(
                    "\n{} {}",
                    "Copying".blue().bold(),
                    entry.from.display().to_string()
                );
                match fs::copy(&entry.from, &entry.to) {
                    Ok(v) => v,
                    Err(e) => {
                        error!("{e}");
                        error_list.push(e);
                        continue;
                    }
                };
                copied += 1;
                File::open(&entry.to).unwrap()
            }
        };
        let mut hasher = Sha256::new();

        let file_size = read_from.metadata().unwrap().len();
        let max_buf_size = 1024 * 1024 * 1024 * 4;
        let buf_size = file_size.min(max_buf_size);
        let mut buf = Vec::with_capacity(buf_size as usize);
        while read_from.read_to_end(&mut buf).unwrap() > 0 {
            hasher.update(&buf);
        }

        let hash = format!("{:x}", hasher.finalize());
        if hash != entry.sha256 {
            info!(
                "\n{} \"{}\"",
                "Copying".green().bold(),
                entry.to.display().to_string()
            );
            match fs::copy(entry.from, entry.to) {
                Ok(v) => v,
                Err(e) => {
                    error!("{e}");
                    error_list.push(e);
                    continue;
                }
            };
            copied += 1;
        }
        verified += 1;
        pb.inc(1);
    }
    pb.finish();
    t.join().unwrap();
    multi.remove(&pb);

    println!(
        "{} {} out of {} files. Copied {} files. ({} errors occured)",
        "Verified".green().bold(),
        HumanCount(verified).to_string(),
        HumanCount(real_count).to_string(),
        HumanCount(copied).to_string(),
        HumanCount(error_list.len() as u64).to_string(),
    );
}

fn _count_matches(conn: &Transaction, id: i64) -> Result<usize> {
    let mut stmt = conn.prepare("SELECT COUNT(*) FROM Files WHERE backup_id = ?1")?;
    let count: i64 = stmt.query_row([id], |row| row.get(0))?;
    Ok(count as usize)
}

pub fn revert(conn: &Transaction, id: u64, multithread: bool) {
    let mut stmt = conn
        .prepare("SELECT source, dest FROM Backups WHERE id = ?1")
        .unwrap();
    let mut iter = stmt
        .query_map([id as i64], |row| {
            Ok((row.get(0).unwrap(), row.get(1).unwrap()))
        })
        .unwrap();

    let source_str: String;
    let dest_str: String;
    match iter.next() {
        Some(v) => {
            let v = v.unwrap();
            source_str = v.1;
            dest_str = v.0;
        }
        None => {
            eprintln!("Couldn't find {id}");
            return;
        }
    }
    drop(iter);
    drop(stmt);

    _copy(conn, multithread, source_str.into(), dest_str.into());
}

pub fn delete(conn: &Transaction, id: u64) {
    let mut stmt = conn
        .prepare("SELECT dest FROM Backups WHERE id = ?1")
        .unwrap();
    let mut iter = stmt
        .query_map([id as i64], |row| Ok(row.get(0).unwrap()))
        .unwrap();

    let dest_str: String;
    match iter.next() {
        Some(v) => {
            let v = v.unwrap();
            dest_str = v;
        }
        None => {
            eprintln!("Couldn't find {id}");
            return;
        }
    }
    drop(iter);
    drop(stmt);

    match fs::remove_dir_all(&dest_str) {
        Ok(_) => {}
        Err(e) => {
            eprintln!("{} {}", "Error:".red().bold(), e);
        }
    };
    println!("Deleted {}", dest_str);
    _delete_entry(conn, id);
}

pub fn soft_delete(conn: &Transaction, id: u64) {
    if _delete_entry(conn, id) {
        println!("Deleted {}", id);
        return;
    }
    eprintln!("Couldn't find \"{}\".", id);
}

pub fn list(conn: &Transaction) {
    let mut stmt = conn
        .prepare("SELECT id, source, dest, compression FROM Backups")
        .unwrap();
    let iter = stmt
        .query_map((), |row| {
            Ok(BackupEntry {
                id: row.get::<usize, i64>(0).unwrap() as u64,
                from: row.get::<usize, String>(1).unwrap().into(),
                to: row.get::<usize, String>(2).unwrap().into(),
                compression: row.get(3).unwrap_or(None),
            })
        })
        .unwrap();

    for entry in iter {
        let entry = entry.unwrap();

        println!(
            "{}: {}\n    {}: {}\n    {}: {}",
            "ID".bold(),
            entry.id,
            "Source".bold(),
            entry.from.display().to_string(),
            "Destination".bold(),
            entry.to.display().to_string()
        );
    }
}

fn _delete_entry(conn: &Transaction, id: u64) -> bool {
    match conn
        .execute("DELETE FROM Backups WHERE id = ?1", [id as i64])
        .unwrap()
    {
        0 => false,
        _ => true,
    }
}