diskr-cli 1.0.0

Save your disk space, without fear of deleting the wrong thing.
Documentation
use anyhow::Result;
use colored::Colorize;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use tokio::time::interval;

pub struct PostDeleteWatcher {
    pub removed_paths: Vec<PathBuf>,
    pub duration: Duration,
    pub poll_interval: Duration,
}

impl PostDeleteWatcher {
    pub fn new(removed_paths: Vec<PathBuf>) -> Self {
        Self {
            removed_paths,
            duration: Duration::from_secs(24 * 3600),
            poll_interval: Duration::from_secs(300),
        }
    }

    pub async fn run(self) -> Result<()> {
        let elapsed_budget = self.duration;
        let started = tokio::time::Instant::now();
        let mut ticker = interval(self.poll_interval);
        let needles: Vec<String> = self.removed_paths.iter()
            .filter_map(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
            .collect();

        println!("{}", "post-delete watcher started, monitoring for 24h".cyan());

        while started.elapsed() < elapsed_budget {
            ticker.tick().await;
            if let Some(hits) = check_journal(&needles).await {
                for h in hits {
                    println!("{} {}", "possible fallout (log):".yellow().bold(), h);
                    println!("  run `diskr restore <name>` to bring it back from trash");
                }
            }
            if let Some(hits) = check_lsof(&needles).await {
                for h in hits {
                    println!("{} {}", "possible fallout (open handle):".red().bold(), h);
                    println!("  a running process still has this file open, deleting it may have broken something");
                }
            }
        }
        println!("{}", "post-delete watcher finished, no further monitoring".green());
        Ok(())
    }
}

async fn check_journal(needles: &[String]) -> Option<Vec<String>> {
    if needles.is_empty() {
        return None;
    }
    let output = Command::new("journalctl")
        .arg("--since").arg("-6min")
        .arg("-p").arg("err")
        .arg("--no-pager")
        .output().ok()?;
    let text = String::from_utf8_lossy(&output.stdout);
    let hits: Vec<String> = text.lines()
        .filter(|line| needles.iter().any(|n| line.contains(n.as_str())))
        .map(|l| l.to_string())
        .collect();
    if hits.is_empty() { None } else { Some(hits) }
}

async fn check_lsof(needles: &[String]) -> Option<Vec<String>> {
    if needles.is_empty() {
        return None;
    }
    let output = Command::new("lsof").arg("+L1").output().ok()?;
    let text = String::from_utf8_lossy(&output.stdout);
    let hits: Vec<String> = text.lines()
        .filter(|line| line.contains("(deleted)") && needles.iter().any(|n| line.contains(n.as_str())))
        .map(|l| l.to_string())
        .collect();
    if hits.is_empty() { None } else { Some(hits) }
}