Skip to main content

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