1use std::io::ErrorKind;
9use std::path::Path;
10use std::time::Duration;
11
12const CLEANUP_RETRY_ATTEMPTS: usize = 3;
13const CLEANUP_RETRY_DELAY: Duration = Duration::from_millis(50);
14
15pub async fn cleanup_temp_file(path: impl AsRef<Path>) {
17 let path = path.as_ref();
18 if let Err(error) = tokio::fs::remove_file(path).await
19 && error.kind() != ErrorKind::NotFound
20 {
21 tracing::warn!(
22 path = %path.display(),
23 error = %error,
24 "failed to cleanup temp file"
25 );
26 }
27}
28
29pub async fn cleanup_temp_dir(path: impl AsRef<Path>) {
34 let path = path.as_ref();
35 for _ in 0..CLEANUP_RETRY_ATTEMPTS {
36 match tokio::fs::remove_dir_all(path).await {
37 Ok(()) => return,
38 Err(error) if error.kind() == ErrorKind::NotFound => return,
39 Err(error) if error.kind() == ErrorKind::DirectoryNotEmpty => {
40 tokio::time::sleep(CLEANUP_RETRY_DELAY).await;
41 }
42 Err(error) => {
43 tracing::warn!(
44 path = %path.display(),
45 error = %error,
46 "failed to cleanup temp dir"
47 );
48 return;
49 }
50 }
51 }
52
53 if let Err(error) = tokio::fs::remove_dir_all(path).await
54 && error.kind() != ErrorKind::NotFound
55 {
56 tracing::warn!(
57 path = %path.display(),
58 error = %error,
59 "failed to cleanup temp dir"
60 );
61 }
62}
63
64pub async fn cleanup_runtime_temp_root(temp_root: &str) {
66 cleanup_temp_dir(crate::paths::runtime_temp_dir(temp_root)).await;
67}
68
69#[cfg(test)]
70mod tests {
71 use std::path::PathBuf;
72 use std::sync::atomic::{AtomicU64, Ordering};
73
74 use super::{cleanup_runtime_temp_root, cleanup_temp_dir, cleanup_temp_file};
75
76 static TEMP_ID: AtomicU64 = AtomicU64::new(0);
77
78 fn unique_temp_path(label: &str) -> PathBuf {
79 std::env::temp_dir().join(format!(
80 "aster-forge-utils-{label}-{}-{}",
81 std::process::id(),
82 TEMP_ID.fetch_add(1, Ordering::Relaxed)
83 ))
84 }
85
86 #[tokio::test]
87 async fn cleanup_temp_file_removes_file() {
88 let path = unique_temp_path("file-cleanup");
89 tokio::fs::write(&path, b"temporary")
90 .await
91 .expect("temp file should be created");
92
93 cleanup_temp_file(&path).await;
94
95 assert!(!path.exists());
96 }
97
98 #[tokio::test]
99 async fn cleanup_temp_file_tolerates_missing_file() {
100 let path = unique_temp_path("missing-file-cleanup");
101
102 cleanup_temp_file(&path).await;
103
104 assert!(!path.exists());
105 }
106
107 #[tokio::test]
108 async fn cleanup_temp_dir_removes_directory_tree() {
109 let path = unique_temp_path("dir-cleanup");
110 let nested = path.join("nested");
111 tokio::fs::create_dir_all(&nested)
112 .await
113 .expect("nested temp dir should be created");
114 tokio::fs::write(nested.join("payload.txt"), b"temporary")
115 .await
116 .expect("nested temp file should be created");
117
118 cleanup_temp_dir(&path).await;
119
120 assert!(!path.exists());
121 }
122
123 #[tokio::test]
124 async fn cleanup_temp_dir_tolerates_missing_directory() {
125 let path = unique_temp_path("missing-dir-cleanup");
126
127 cleanup_temp_dir(&path).await;
128
129 assert!(!path.exists());
130 }
131
132 #[tokio::test]
133 async fn cleanup_runtime_temp_root_removes_runtime_namespace_only() {
134 let root = unique_temp_path("runtime-cleanup");
135 let runtime = crate::paths::runtime_temp_dir(root.to_str().expect("path should be utf-8"));
136 let keep = root.join("tasks");
137 tokio::fs::create_dir_all(&runtime)
138 .await
139 .expect("runtime temp dir should be created");
140 tokio::fs::create_dir_all(&keep)
141 .await
142 .expect("task temp dir should be created");
143
144 cleanup_runtime_temp_root(root.to_str().expect("path should be utf-8")).await;
145
146 assert!(!PathBuf::from(runtime).exists());
147 assert!(keep.is_dir());
148 cleanup_temp_dir(root).await;
149 }
150}