mars_agents/platform/
fs.rs1use std::fs;
6use std::path::Path;
7
8use crate::error::MarsError;
9
10pub use crate::fs::{
11 FLAT_SKILL_EXCLUDED_TOP_LEVEL, atomic_install_dir, atomic_install_dir_filtered, atomic_write,
12 remove_item,
13};
14
15#[cfg(windows)]
16pub use crate::fs::clear_readonly;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum CachePublishResult {
21 Published,
23 AlreadyPresent,
25}
26
27pub fn replace_generated_dir(src: &Path, dest: &Path) -> Result<(), MarsError> {
29 let parent = dest.parent().unwrap_or(Path::new("."));
30 fs::create_dir_all(parent).map_err(|e| io_context("create generated parent", parent, e))?;
31
32 let old_path = parent.join(format!(
33 ".{}.old",
34 dest.file_name().unwrap_or_default().to_string_lossy()
35 ));
36
37 if old_path.symlink_metadata().is_ok() {
39 safe_remove(&old_path)?;
40 }
41
42 if dest.exists() {
43 #[cfg(windows)]
44 clear_readonly_recursive(dest)?;
45
46 fs::rename(dest, &old_path)
47 .map_err(|e| io_context("rename destination to backup", dest, e))?;
48
49 if let Err(e) = fs::rename(src, dest) {
50 let _ = fs::rename(&old_path, dest);
51 let _ = safe_remove(src);
52 return Err(io_context("rename source to destination", src, e));
53 }
54
55 let _ = safe_remove(&old_path);
56 } else {
57 fs::rename(src, dest).map_err(|e| io_context("rename source to destination", src, e))?;
58 }
59
60 Ok(())
61}
62
63pub fn publish_cache_dir_if_absent(
65 src: &Path,
66 dest: &Path,
67) -> Result<CachePublishResult, MarsError> {
68 if dest.exists() {
69 safe_remove(src)?;
70 return Ok(CachePublishResult::AlreadyPresent);
71 }
72
73 if let Some(parent) = dest.parent() {
74 fs::create_dir_all(parent).map_err(|e| io_context("create cache parent", parent, e))?;
75 }
76
77 match fs::rename(src, dest) {
78 Ok(()) => Ok(CachePublishResult::Published),
79 Err(_err) if dest.exists() => {
80 let _ = safe_remove(src);
81 Ok(CachePublishResult::AlreadyPresent)
82 }
83 Err(e) => Err(io_context("publish cache directory", src, e)),
84 }
85}
86
87pub fn safe_remove(path: &Path) -> Result<(), MarsError> {
89 let metadata = match path.symlink_metadata() {
90 Ok(metadata) => metadata,
91 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
92 Err(e) => return Err(io_context("read metadata for removal", path, e)),
93 };
94
95 #[cfg(windows)]
96 if metadata.is_dir() {
97 clear_readonly_recursive(path)?;
98 } else {
99 clear_readonly(path).map_err(|e| io_context("clear readonly bit", path, e))?;
100 }
101
102 if metadata.is_dir() {
103 fs::remove_dir_all(path).map_err(|e| io_context("remove directory", path, e))?;
104 } else {
105 fs::remove_file(path).map_err(|e| io_context("remove file", path, e))?;
106 }
107
108 Ok(())
109}
110
111#[cfg(windows)]
112fn clear_readonly_recursive(path: &Path) -> Result<(), MarsError> {
113 for entry in walkdir::WalkDir::new(path)
114 .into_iter()
115 .filter_map(|entry| entry.ok())
116 {
117 clear_readonly(entry.path())
118 .map_err(|e| io_context("clear readonly bit", entry.path(), e))?;
119 }
120 Ok(())
121}
122
123fn io_context(operation: &str, path: &Path, source: std::io::Error) -> MarsError {
124 MarsError::Io {
125 operation: operation.to_string(),
126 path: path.to_path_buf(),
127 source,
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use tempfile::TempDir;
135
136 #[test]
137 fn replace_generated_dir_basic() {
138 let tmp = TempDir::new().unwrap();
139 let src = tmp.path().join("src");
140 let dest = tmp.path().join("dest");
141
142 fs::create_dir(&src).unwrap();
143 fs::write(src.join("file.txt"), "content").unwrap();
144
145 replace_generated_dir(&src, &dest).unwrap();
146
147 assert!(!src.exists());
148 assert!(dest.join("file.txt").exists());
149 }
150
151 #[test]
152 fn replace_generated_dir_replaces_existing() {
153 let tmp = TempDir::new().unwrap();
154 let src = tmp.path().join("src");
155 let dest = tmp.path().join("dest");
156
157 fs::create_dir(&dest).unwrap();
158 fs::write(dest.join("old.txt"), "old").unwrap();
159
160 fs::create_dir(&src).unwrap();
161 fs::write(src.join("new.txt"), "new").unwrap();
162
163 replace_generated_dir(&src, &dest).unwrap();
164
165 assert!(!dest.join("old.txt").exists());
166 assert!(dest.join("new.txt").exists());
167 }
168
169 #[test]
170 fn publish_cache_dir_if_absent_publishes() {
171 let tmp = TempDir::new().unwrap();
172 let src = tmp.path().join("src");
173 let dest = tmp.path().join("dest");
174
175 fs::create_dir(&src).unwrap();
176 fs::write(src.join("file.txt"), "content").unwrap();
177
178 let result = publish_cache_dir_if_absent(&src, &dest).unwrap();
179
180 assert_eq!(result, CachePublishResult::Published);
181 assert!(!src.exists());
182 assert!(dest.join("file.txt").exists());
183 }
184
185 #[test]
186 fn publish_cache_dir_if_absent_accepts_existing() {
187 let tmp = TempDir::new().unwrap();
188 let src = tmp.path().join("src");
189 let dest = tmp.path().join("dest");
190
191 fs::create_dir(&dest).unwrap();
192 fs::write(dest.join("existing.txt"), "existing").unwrap();
193
194 fs::create_dir(&src).unwrap();
195 fs::write(src.join("new.txt"), "new").unwrap();
196
197 let result = publish_cache_dir_if_absent(&src, &dest).unwrap();
198
199 assert_eq!(result, CachePublishResult::AlreadyPresent);
200 assert!(!src.exists());
201 assert!(dest.join("existing.txt").exists());
202 assert!(!dest.join("new.txt").exists());
203 }
204
205 #[test]
206 fn safe_remove_handles_nonexistent() {
207 let tmp = TempDir::new().unwrap();
208 let path = tmp.path().join("nonexistent");
209
210 safe_remove(&path).unwrap();
211 }
212
213 #[test]
214 fn safe_remove_removes_file_and_directory_tree() {
215 let tmp = TempDir::new().unwrap();
216 let file = tmp.path().join("file.txt");
217 fs::write(&file, "content").unwrap();
218
219 safe_remove(&file).unwrap();
220 assert!(!file.exists());
221
222 let dir = tmp.path().join("dir");
223 fs::create_dir_all(dir.join("nested")).unwrap();
224 fs::write(dir.join("nested").join("file.txt"), "content").unwrap();
225
226 safe_remove(&dir).unwrap();
227 assert!(!dir.exists());
228 }
229
230 #[test]
231 fn replace_generated_dir_cleans_stale_backup_before_replace() {
232 let tmp = TempDir::new().unwrap();
233 let src = tmp.path().join("src");
234 let dest = tmp.path().join("dest");
235 let old = tmp.path().join(".dest.old");
236
237 fs::create_dir(&dest).unwrap();
238 fs::write(dest.join("old.txt"), "old").unwrap();
239 fs::create_dir(&old).unwrap();
240 fs::write(old.join("stale.txt"), "stale").unwrap();
241 fs::create_dir(&src).unwrap();
242 fs::write(src.join("new.txt"), "new").unwrap();
243
244 replace_generated_dir(&src, &dest).unwrap();
245
246 assert!(!old.exists());
247 assert!(!dest.join("old.txt").exists());
248 assert_eq!(fs::read_to_string(dest.join("new.txt")).unwrap(), "new");
249 }
250}