a3s_box_runtime/rootfs/
provider.rs1use std::path::{Path, PathBuf};
8
9use a3s_box_core::error::{BoxError, Result};
10
11pub trait RootfsProvider: Send + Sync {
13 fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf>;
16
17 fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()>;
23
24 fn name(&self) -> &'static str;
26}
27
28pub struct CopyProvider;
33
34impl RootfsProvider for CopyProvider {
35 fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
36 let rootfs = box_dir.join("rootfs");
37 if rootfs.exists() {
39 tracing::info!(path = %rootfs.display(), "Reusing persistent rootfs");
40 return Ok(rootfs);
41 }
42 crate::cache::layer_cache::copy_dir_recursive(cache_dir, &rootfs)?;
43 Ok(rootfs)
44 }
45
46 fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()> {
47 if persistent {
48 tracing::info!("Persistent box: keeping rootfs on disk");
49 return Ok(());
50 }
51 let rootfs = box_dir.join("rootfs");
52 if rootfs.exists() {
53 std::fs::remove_dir_all(&rootfs).map_err(|e| {
54 BoxError::BuildError(format!(
55 "Failed to remove rootfs {}: {}",
56 rootfs.display(),
57 e
58 ))
59 })?;
60 }
61 Ok(())
62 }
63
64 fn name(&self) -> &'static str {
65 "copy"
66 }
67}
68
69pub struct OverlayProvider;
79
80impl RootfsProvider for OverlayProvider {
81 fn prepare(&self, box_dir: &Path, cache_dir: &Path) -> Result<PathBuf> {
82 let upper = box_dir.join("upper");
83 let work = box_dir.join("work");
84 let merged = box_dir.join("merged");
85
86 for dir in [&upper, &work, &merged] {
87 std::fs::create_dir_all(dir).map_err(|e| {
88 BoxError::BuildError(format!(
89 "Failed to create overlay dir {}: {}",
90 dir.display(),
91 e
92 ))
93 })?;
94 }
95
96 if super::is_mountpoint(&merged) {
99 tracing::debug!(merged = %merged.display(), "Overlay already mounted; reusing");
100 return Ok(merged);
101 }
102
103 super::overlay::overlay_mount(cache_dir, &upper, &work, &merged)?;
104
105 tracing::info!(
106 lower = %cache_dir.display(),
107 merged = %merged.display(),
108 "Overlay mount ready"
109 );
110
111 Ok(merged)
112 }
113
114 fn cleanup(&self, box_dir: &Path, persistent: bool) -> Result<()> {
115 let merged = box_dir.join("merged");
116 super::unmount_box_overlay(&merged);
121
122 if persistent {
123 tracing::info!("Persistent box: keeping overlay upper layer on disk");
125 for dir_name in &["merged", "work"] {
126 let dir = box_dir.join(dir_name);
127 if dir.exists() {
128 if let Err(e) = std::fs::remove_dir_all(&dir) {
129 tracing::warn!(path = %dir.display(), error = %e, "Failed to remove overlay dir");
130 }
131 }
132 }
133 return Ok(());
134 }
135
136 for dir_name in &["upper", "work", "merged"] {
137 let dir = box_dir.join(dir_name);
138 if dir.exists() {
139 if let Err(e) = std::fs::remove_dir_all(&dir) {
140 tracing::warn!(
141 path = %dir.display(),
142 error = %e,
143 "Failed to remove overlay dir"
144 );
145 }
146 }
147 }
148
149 Ok(())
150 }
151
152 fn name(&self) -> &'static str {
153 "overlay"
154 }
155}
156
157pub fn default_provider() -> Box<dyn RootfsProvider> {
159 if super::overlay::is_overlay_supported() {
160 tracing::info!("Using overlayfs rootfs provider");
161 return Box::new(OverlayProvider);
162 }
163
164 tracing::info!("Overlayfs not available, using copy provider");
165 Box::new(CopyProvider)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use tempfile::TempDir;
172
173 fn make_sample_rootfs(dir: &Path) {
174 std::fs::create_dir_all(dir.join("etc")).unwrap();
175 std::fs::create_dir_all(dir.join("bin")).unwrap();
176 std::fs::write(dir.join("etc/hostname"), "testbox").unwrap();
177 std::fs::write(dir.join("bin/hello"), "#!/bin/sh\necho hi").unwrap();
178 }
179
180 #[test]
181 fn test_copy_provider_prepare() {
182 let tmp = TempDir::new().unwrap();
183 let cache_dir = tmp.path().join("cache");
184 let box_dir = tmp.path().join("box");
185 std::fs::create_dir_all(&cache_dir).unwrap();
186 std::fs::create_dir_all(&box_dir).unwrap();
187 make_sample_rootfs(&cache_dir);
188
189 let provider = CopyProvider;
190 let rootfs = provider.prepare(&box_dir, &cache_dir).unwrap();
191
192 assert_eq!(rootfs, box_dir.join("rootfs"));
193 assert!(rootfs.join("etc/hostname").exists());
194 assert_eq!(
195 std::fs::read_to_string(rootfs.join("etc/hostname")).unwrap(),
196 "testbox"
197 );
198 assert!(rootfs.join("bin/hello").exists());
199 }
200
201 #[test]
202 fn test_copy_provider_prepare_reuses_existing_rootfs_without_overwriting() {
203 let tmp = TempDir::new().unwrap();
204 let cache_dir = tmp.path().join("cache");
205 let box_dir = tmp.path().join("box");
206 let rootfs = box_dir.join("rootfs");
207 std::fs::create_dir_all(&cache_dir).unwrap();
208 std::fs::create_dir_all(rootfs.join("etc")).unwrap();
209 make_sample_rootfs(&cache_dir);
210 std::fs::write(rootfs.join("etc/hostname"), "persistent-host").unwrap();
211
212 let provider = CopyProvider;
213 let prepared = provider.prepare(&box_dir, &cache_dir).unwrap();
214
215 assert_eq!(prepared, rootfs);
216 assert_eq!(
217 std::fs::read_to_string(prepared.join("etc/hostname")).unwrap(),
218 "persistent-host"
219 );
220 assert!(
221 !prepared.join("bin/hello").exists(),
222 "existing persistent rootfs must not be overwritten from cache"
223 );
224 }
225
226 #[test]
227 fn test_copy_provider_cleanup() {
228 let tmp = TempDir::new().unwrap();
229 let cache_dir = tmp.path().join("cache");
230 let box_dir = tmp.path().join("box");
231 std::fs::create_dir_all(&cache_dir).unwrap();
232 std::fs::create_dir_all(&box_dir).unwrap();
233 make_sample_rootfs(&cache_dir);
234
235 let provider = CopyProvider;
236 let rootfs = provider.prepare(&box_dir, &cache_dir).unwrap();
237 assert!(rootfs.exists());
238
239 provider.cleanup(&box_dir, false).unwrap();
240 assert!(!rootfs.exists());
241 }
242
243 #[test]
244 fn test_copy_provider_cleanup_persistent_keeps_rootfs() {
245 let tmp = TempDir::new().unwrap();
246 let box_dir = tmp.path().join("box");
247 let rootfs = box_dir.join("rootfs");
248 std::fs::create_dir_all(rootfs.join("etc")).unwrap();
249 std::fs::write(rootfs.join("etc/hostname"), "kept").unwrap();
250
251 CopyProvider.cleanup(&box_dir, true).unwrap();
252
253 assert_eq!(
254 std::fs::read_to_string(rootfs.join("etc/hostname")).unwrap(),
255 "kept"
256 );
257 }
258
259 #[test]
260 fn test_copy_provider_cleanup_nonexistent() {
261 let tmp = TempDir::new().unwrap();
262 let provider = CopyProvider;
263 provider.cleanup(tmp.path(), false).unwrap();
265 }
266
267 #[test]
268 fn test_copy_provider_name() {
269 assert_eq!(CopyProvider.name(), "copy");
270 }
271
272 #[test]
273 fn test_overlay_provider_name() {
274 assert_eq!(OverlayProvider.name(), "overlay");
275 }
276
277 #[test]
278 fn test_overlay_provider_cleanup_persistent_keeps_upper_only() {
279 let tmp = TempDir::new().unwrap();
280 let box_dir = tmp.path().join("box");
281 for dir in ["upper", "work", "merged"] {
282 std::fs::create_dir_all(box_dir.join(dir)).unwrap();
283 }
284 std::fs::write(box_dir.join("upper/data.txt"), "state").unwrap();
285 std::fs::write(box_dir.join("work/scratch.txt"), "work").unwrap();
286 std::fs::write(box_dir.join("merged/view.txt"), "merged").unwrap();
287
288 OverlayProvider.cleanup(&box_dir, true).unwrap();
289
290 assert_eq!(
291 std::fs::read_to_string(box_dir.join("upper/data.txt")).unwrap(),
292 "state"
293 );
294 assert!(!box_dir.join("work").exists());
295 assert!(!box_dir.join("merged").exists());
296 }
297
298 #[test]
299 fn test_overlay_provider_cleanup_nonpersistent_removes_all_overlay_dirs() {
300 let tmp = TempDir::new().unwrap();
301 let box_dir = tmp.path().join("box");
302 for dir in ["upper", "work", "merged"] {
303 std::fs::create_dir_all(box_dir.join(dir)).unwrap();
304 std::fs::write(box_dir.join(dir).join("file.txt"), "data").unwrap();
305 }
306
307 OverlayProvider.cleanup(&box_dir, false).unwrap();
308
309 assert!(!box_dir.join("upper").exists());
310 assert!(!box_dir.join("work").exists());
311 assert!(!box_dir.join("merged").exists());
312 }
313
314 #[test]
315 fn test_default_provider_returns_something() {
316 let provider = default_provider();
317 assert!(!provider.name().is_empty());
319 }
320
321 #[cfg(target_os = "linux")]
322 #[test]
323 fn test_overlay_provider_prepare_and_cleanup() {
324 if !super::super::overlay::is_overlay_supported() {
325 return;
327 }
328
329 let tmp = TempDir::new().unwrap();
330 let cache_dir = tmp.path().join("cache");
331 let box_dir = tmp.path().join("box");
332 std::fs::create_dir_all(&cache_dir).unwrap();
333 std::fs::create_dir_all(&box_dir).unwrap();
334 make_sample_rootfs(&cache_dir);
335
336 let provider = OverlayProvider;
337 let merged = provider.prepare(&box_dir, &cache_dir).unwrap();
338
339 assert_eq!(merged, box_dir.join("merged"));
340 assert!(merged.join("etc/hostname").exists());
341 assert_eq!(
342 std::fs::read_to_string(merged.join("etc/hostname")).unwrap(),
343 "testbox"
344 );
345
346 std::fs::write(merged.join("etc/newfile"), "overlay write").unwrap();
348 assert!(box_dir.join("upper/etc/newfile").exists());
349
350 provider.cleanup(&box_dir, false).unwrap();
351 assert!(!box_dir.join("merged").exists());
352 assert!(!box_dir.join("upper").exists());
353 assert!(!box_dir.join("work").exists());
354 }
355}