1use anyhow::Result;
2use colored::Colorize;
3use std::path::PathBuf;
4use std::process::Command;
5use std::time::Duration;
6use tokio::time::interval;
7
8pub struct PostDeleteWatcher {
9 pub removed_paths: Vec<PathBuf>,
10 pub duration: Duration,
11 pub poll_interval: Duration,
12}
13
14impl PostDeleteWatcher {
15 pub fn new(removed_paths: Vec<PathBuf>) -> Self {
16 Self {
17 removed_paths,
18 duration: Duration::from_secs(24 * 3600),
19 poll_interval: Duration::from_secs(300),
20 }
21 }
22
23 pub async fn run(self) -> Result<()> {
24 let elapsed_budget = self.duration;
25 let started = tokio::time::Instant::now();
26 let mut ticker = interval(self.poll_interval);
27 let needles: Vec<String> = self.removed_paths.iter()
28 .filter_map(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
29 .collect();
30
31 println!("{}", "post-delete watcher started, monitoring for 24h".cyan());
32
33 while started.elapsed() < elapsed_budget {
34 ticker.tick().await;
35 if let Some(hits) = check_journal(&needles).await {
36 for h in hits {
37 println!("{} {}", "possible fallout (log):".yellow().bold(), h);
38 println!(" run `diskr restore <name>` to bring it back from trash");
39 }
40 }
41 if let Some(hits) = check_lsof(&needles).await {
42 for h in hits {
43 println!("{} {}", "possible fallout (open handle):".red().bold(), h);
44 println!(" a running process still has this file open, deleting it may have broken something");
45 }
46 }
47 }
48 println!("{}", "post-delete watcher finished, no further monitoring".green());
49 Ok(())
50 }
51}
52
53async fn check_journal(needles: &[String]) -> Option<Vec<String>> {
54 if needles.is_empty() {
55 return None;
56 }
57 let output = Command::new("journalctl")
58 .arg("--since").arg("-6min")
59 .arg("-p").arg("err")
60 .arg("--no-pager")
61 .output().ok()?;
62 let text = String::from_utf8_lossy(&output.stdout);
63 let hits: Vec<String> = text.lines()
64 .filter(|line| needles.iter().any(|n| line.contains(n.as_str())))
65 .map(|l| l.to_string())
66 .collect();
67 if hits.is_empty() { None } else { Some(hits) }
68}
69
70async fn check_lsof(needles: &[String]) -> Option<Vec<String>> {
71 if needles.is_empty() {
72 return None;
73 }
74 let output = Command::new("lsof").arg("+L1").output().ok()?;
75 let text = String::from_utf8_lossy(&output.stdout);
76 let hits: Vec<String> = text.lines()
77 .filter(|line| line.contains("(deleted)") && needles.iter().any(|n| line.contains(n.as_str())))
78 .map(|l| l.to_string())
79 .collect();
80 if hits.is_empty() { None } else { Some(hits) }
81}