Skip to main content

ralph/fsutil/
temp.rs

1//! Purpose: Ralph temp-root, temp-file, and stale-cleanup helpers.
2//!
3//! Responsibilities:
4//! - Resolve Ralph's temp root directory.
5//! - Create Ralph-scoped temp directories and files.
6//! - Remove stale temp entries by prefix and retention window.
7//!
8//! Scope:
9//! - Temp-path creation and cleanup only; atomic writes and safeguard dump gating live elsewhere.
10//!
11//! Usage:
12//! - Used by cleanup flows, runner prompts, plugin IO, issue publishing, and safeguard dump persistence.
13//!
14//! Invariants/Assumptions:
15//! - Ralph temp artifacts live under `std::env::temp_dir()/ralph`.
16//! - Cleanup is prefix-based and best-effort on per-entry metadata or deletion failures.
17//! - Ralph-created temp files use the `ralph_` prefix so cleanup can discover them.
18
19use crate::constants::paths::{LEGACY_PROMPT_PREFIX, RALPH_TEMP_DIR_NAME, RALPH_TEMP_PREFIX};
20use anyhow::{Context, Result};
21use std::fs;
22use std::path::{Path, PathBuf};
23use std::time::{Duration, SystemTime};
24
25pub fn ralph_temp_root() -> PathBuf {
26    std::env::temp_dir().join(RALPH_TEMP_DIR_NAME)
27}
28
29pub fn cleanup_stale_temp_entries(
30    base: &Path,
31    prefixes: &[&str],
32    retention: Duration,
33) -> Result<usize> {
34    if !base.exists() {
35        return Ok(0);
36    }
37
38    let now = SystemTime::now();
39    let mut removed = 0usize;
40
41    for entry in fs::read_dir(base).with_context(|| format!("read temp dir {}", base.display()))? {
42        let entry = entry.with_context(|| format!("read temp dir entry in {}", base.display()))?;
43        let path = entry.path();
44        let name = entry.file_name();
45        let name = name.to_string_lossy();
46
47        if !prefixes.iter().any(|prefix| name.starts_with(prefix)) {
48            continue;
49        }
50
51        let metadata = match entry.metadata() {
52            Ok(metadata) => metadata,
53            Err(err) => {
54                log::warn!(
55                    "unable to read temp metadata for {}: {}",
56                    path.display(),
57                    err
58                );
59                continue;
60            }
61        };
62
63        let modified = match metadata.modified() {
64            Ok(time) => time,
65            Err(err) => {
66                log::warn!(
67                    "unable to read temp modified time for {}: {}",
68                    path.display(),
69                    err
70                );
71                continue;
72            }
73        };
74
75        let age = match now.duration_since(modified) {
76            Ok(age) => age,
77            Err(_) => continue,
78        };
79
80        if age < retention {
81            continue;
82        }
83
84        if metadata.is_dir() {
85            if fs::remove_dir_all(&path).is_ok() {
86                removed += 1;
87            } else {
88                log::warn!("failed to remove temp dir {}", path.display());
89            }
90        } else if fs::remove_file(&path).is_ok() {
91            removed += 1;
92        } else {
93            log::warn!("failed to remove temp file {}", path.display());
94        }
95    }
96
97    Ok(removed)
98}
99
100pub fn cleanup_stale_temp_dirs(base: &Path, retention: Duration) -> Result<usize> {
101    cleanup_stale_temp_entries(base, &[RALPH_TEMP_PREFIX], retention)
102}
103
104pub fn cleanup_default_temp_dirs(retention: Duration) -> Result<usize> {
105    let mut removed = 0usize;
106    removed += cleanup_stale_temp_dirs(&ralph_temp_root(), retention)?;
107    removed +=
108        cleanup_stale_temp_entries(&std::env::temp_dir(), &[LEGACY_PROMPT_PREFIX], retention)?;
109    Ok(removed)
110}
111
112pub fn create_ralph_temp_dir(label: &str) -> Result<tempfile::TempDir> {
113    let base = ralph_temp_root();
114    fs::create_dir_all(&base).with_context(|| format!("create temp dir {}", base.display()))?;
115    let prefix = format!(
116        "{prefix}{label}_",
117        prefix = RALPH_TEMP_PREFIX,
118        label = label.trim()
119    );
120    let dir = tempfile::Builder::new()
121        .prefix(&prefix)
122        .tempdir_in(&base)
123        .with_context(|| format!("create temp dir in {}", base.display()))?;
124    Ok(dir)
125}
126
127/// Creates a NamedTempFile in the ralph temp directory with the ralph_ prefix.
128/// This ensures the file will be caught by cleanup_default_temp_dirs().
129pub fn create_ralph_temp_file(label: &str) -> Result<tempfile::NamedTempFile> {
130    let base = ralph_temp_root();
131    fs::create_dir_all(&base).with_context(|| format!("create temp dir {}", base.display()))?;
132    let prefix = format!(
133        "{prefix}{label}_",
134        prefix = RALPH_TEMP_PREFIX,
135        label = label.trim()
136    );
137    tempfile::Builder::new()
138        .prefix(&prefix)
139        .suffix(".tmp")
140        .tempfile_in(&base)
141        .with_context(|| format!("create temp file in {}", base.display()))
142}