Skip to main content

ralph/commands/
cleanup.rs

1//! Manual temp file cleanup command.
2//!
3//! Responsibilities:
4//! - Provide on-demand cleanup of ralph temporary files.
5//! - Support dry-run mode for safe preview of cleanup actions.
6//! - Support force mode for immediate cleanup regardless of age.
7//!
8//! Not handled here:
9//! - Automatic cleanup (see main.rs startup cleanup and fsutil cleanup triggers).
10//! - Tutorial sandbox cleanup (--keep-sandbox is intentional persistence).
11//!
12//! Invariants/assumptions:
13//! - Cleanup is best-effort and logs warnings on individual file errors.
14//! - Dry-run lists what would be deleted without making changes.
15
16use std::time::Duration;
17
18use anyhow::Result;
19
20use crate::cli::cleanup::CleanupArgs;
21use crate::fsutil;
22
23/// Run the cleanup command.
24pub fn run(args: &CleanupArgs) -> Result<()> {
25    let retention = if args.force {
26        Duration::ZERO
27    } else {
28        crate::constants::timeouts::TEMP_RETENTION
29    };
30
31    if args.dry_run {
32        return run_dry_run(retention);
33    }
34
35    let removed = fsutil::cleanup_default_temp_dirs(retention)?;
36    println!("Cleaned up {} temp entries", removed);
37    Ok(())
38}
39
40fn run_dry_run(retention: Duration) -> Result<()> {
41    let base = fsutil::ralph_temp_root();
42    println!(
43        "Dry run - would clean temp files older than {:?}",
44        retention
45    );
46    println!("Temp directory: {}", base.display());
47
48    if !base.exists() {
49        println!("Temp directory does not exist - nothing to clean.");
50        return Ok(());
51    }
52
53    let entries = list_entries_to_clean(
54        &base,
55        &[crate::constants::paths::RALPH_TEMP_PREFIX],
56        retention,
57    )?;
58
59    if entries.is_empty() {
60        println!("No temp files or directories would be cleaned.");
61    } else {
62        println!("Would delete {} entries:", entries.len());
63        for entry in entries {
64            let entry_type = if entry.is_dir() { "[dir]" } else { "[file]" };
65            println!("  {} {}", entry_type, entry.display());
66        }
67    }
68
69    // Also check legacy prefix
70    let legacy_entries = list_entries_to_clean(
71        &std::env::temp_dir(),
72        &[crate::constants::paths::LEGACY_PROMPT_PREFIX],
73        retention,
74    )?;
75
76    if !legacy_entries.is_empty() {
77        println!("\nWould delete {} legacy entries:", legacy_entries.len());
78        for entry in legacy_entries {
79            let entry_type = if entry.is_dir() { "[dir]" } else { "[file]" };
80            println!("  {} {}", entry_type, entry.display());
81        }
82    }
83
84    Ok(())
85}
86
87fn list_entries_to_clean(
88    base: &std::path::Path,
89    prefixes: &[&str],
90    retention: Duration,
91) -> Result<Vec<std::path::PathBuf>> {
92    use std::fs;
93    use std::time::SystemTime;
94
95    let mut entries = Vec::new();
96
97    if !base.exists() {
98        return Ok(entries);
99    }
100
101    let now = SystemTime::now();
102
103    for entry in fs::read_dir(base)? {
104        let entry = entry?;
105        let path = entry.path();
106        let name = entry.file_name();
107        let name = name.to_string_lossy();
108
109        if !prefixes.iter().any(|prefix| name.starts_with(prefix)) {
110            continue;
111        }
112
113        let metadata = match entry.metadata() {
114            Ok(metadata) => metadata,
115            Err(_) => continue,
116        };
117
118        let modified = match metadata.modified() {
119            Ok(time) => time,
120            Err(_) => continue,
121        };
122
123        let age = match now.duration_since(modified) {
124            Ok(age) => age,
125            Err(_) => continue,
126        };
127
128        if age >= retention {
129            entries.push(path);
130        }
131    }
132
133    Ok(entries)
134}