aster_forge_utils/
raii.rs

1//! RAII cleanup guards for short-lived runtime resources.
2//!
3//! The guards remove temporary files or directories when they leave scope, covering early returns
4//! and panic unwinding. They are deliberately small and best-effort; startup cleanup should still
5//! handle resources left behind by process termination.
6
7use std::path::{Path, PathBuf};
8
9/// RAII guard for short-lived runtime temporary files.
10///
11/// It prevents cleanup from being skipped on early returns or panic unwinding. Files left behind
12/// after process termination should still be handled by startup runtime-temp cleanup.
13pub struct TempFileGuard {
14    path: PathBuf,
15    cleanup_label: &'static str,
16}
17
18impl TempFileGuard {
19    /// Creates a guard that removes `path` on drop.
20    #[must_use]
21    pub fn new(path: PathBuf, cleanup_label: &'static str) -> Self {
22        Self {
23            path,
24            cleanup_label,
25        }
26    }
27
28    /// Returns the guarded path.
29    #[must_use]
30    pub fn path(&self) -> &Path {
31        &self.path
32    }
33}
34
35impl Drop for TempFileGuard {
36    fn drop(&mut self) {
37        if let Err(error) = std::fs::remove_file(&self.path)
38            && error.kind() != std::io::ErrorKind::NotFound
39        {
40            tracing::warn!(
41                path = ?self.path,
42                cleanup = self.cleanup_label,
43                "failed to cleanup temp file: {error}"
44            );
45        }
46    }
47}
48
49/// RAII guard for short-lived runtime temporary directories.
50///
51/// Directories left behind after process termination should still be handled by startup
52/// runtime-temp cleanup.
53pub struct TempDirGuard {
54    path: PathBuf,
55    cleanup_label: &'static str,
56}
57
58impl TempDirGuard {
59    /// Creates a guard that removes `path` recursively on drop.
60    #[must_use]
61    pub fn new(path: PathBuf, cleanup_label: &'static str) -> Self {
62        Self {
63            path,
64            cleanup_label,
65        }
66    }
67
68    /// Returns the guarded path.
69    #[must_use]
70    pub fn path(&self) -> &Path {
71        &self.path
72    }
73}
74
75impl Drop for TempDirGuard {
76    fn drop(&mut self) {
77        if let Err(error) = std::fs::remove_dir_all(&self.path)
78            && error.kind() != std::io::ErrorKind::NotFound
79        {
80            tracing::warn!(
81                path = %self.path.display(),
82                cleanup = self.cleanup_label,
83                "failed to cleanup temp dir: {error}"
84            );
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::{TempDirGuard, TempFileGuard};
92    use std::path::PathBuf;
93
94    fn temp_path(name: &str) -> PathBuf {
95        std::env::temp_dir().join(format!("aster-forge-{name}-{}", uuid::Uuid::new_v4()))
96    }
97
98    #[test]
99    fn temp_file_guard_removes_file_on_drop() {
100        let path = temp_path("file-guard");
101        std::fs::write(&path, b"temporary").expect("temp file should be created");
102
103        {
104            let guard = TempFileGuard::new(path.clone(), "test-temp-file");
105            assert_eq!(guard.path(), path.as_path());
106            assert!(path.exists());
107        }
108
109        assert!(!path.exists());
110    }
111
112    #[test]
113    fn temp_file_guard_ignores_missing_file() {
114        let path = temp_path("missing-file-guard");
115        {
116            let guard = TempFileGuard::new(path.clone(), "test-missing-temp-file");
117            assert_eq!(guard.path(), path.as_path());
118        }
119
120        assert!(!path.exists());
121    }
122
123    #[test]
124    fn temp_dir_guard_removes_directory_tree_on_drop() {
125        let path = temp_path("dir-guard");
126        let nested = path.join("nested");
127        std::fs::create_dir_all(&nested).expect("nested temp dir should be created");
128        std::fs::write(nested.join("file.txt"), b"temporary")
129            .expect("nested temp file should be created");
130
131        {
132            let guard = TempDirGuard::new(path.clone(), "test-temp-dir");
133            assert_eq!(guard.path(), path.as_path());
134            assert!(nested.exists());
135        }
136
137        assert!(!path.exists());
138    }
139
140    #[test]
141    fn temp_dir_guard_ignores_missing_directory() {
142        let path = temp_path("missing-dir-guard");
143        {
144            let guard = TempDirGuard::new(path.clone(), "test-missing-temp-dir");
145            assert_eq!(guard.path(), path.as_path());
146        }
147
148        assert!(!path.exists());
149    }
150}