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) }
}