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_cleanup() {
203 let tmp = TempDir::new().unwrap();
204 let cache_dir = tmp.path().join("cache");
205 let box_dir = tmp.path().join("box");
206 std::fs::create_dir_all(&cache_dir).unwrap();
207 std::fs::create_dir_all(&box_dir).unwrap();
208 make_sample_rootfs(&cache_dir);
209
210 let provider = CopyProvider;
211 let rootfs = provider.prepare(&box_dir, &cache_dir).unwrap();
212 assert!(rootfs.exists());
213
214 provider.cleanup(&box_dir, false).unwrap();
215 assert!(!rootfs.exists());
216 }
217
218 #[test]
219 fn test_copy_provider_cleanup_nonexistent() {
220 let tmp = TempDir::new().unwrap();
221 let provider = CopyProvider;
222 provider.cleanup(tmp.path(), false).unwrap();
224 }
225
226 #[test]
227 fn test_copy_provider_name() {
228 assert_eq!(CopyProvider.name(), "copy");
229 }
230
231 #[test]
232 fn test_overlay_provider_name() {
233 assert_eq!(OverlayProvider.name(), "overlay");
234 }
235
236 #[test]
237 fn test_default_provider_returns_something() {
238 let provider = default_provider();
239 assert!(!provider.name().is_empty());
241 }
242
243 #[cfg(target_os = "linux")]
244 #[test]
245 fn test_overlay_provider_prepare_and_cleanup() {
246 if !super::super::overlay::is_overlay_supported() {
247 return;
249 }
250
251 let tmp = TempDir::new().unwrap();
252 let cache_dir = tmp.path().join("cache");
253 let box_dir = tmp.path().join("box");
254 std::fs::create_dir_all(&cache_dir).unwrap();
255 std::fs::create_dir_all(&box_dir).unwrap();
256 make_sample_rootfs(&cache_dir);
257
258 let provider = OverlayProvider;
259 let merged = provider.prepare(&box_dir, &cache_dir).unwrap();
260
261 assert_eq!(merged, box_dir.join("merged"));
262 assert!(merged.join("etc/hostname").exists());
263 assert_eq!(
264 std::fs::read_to_string(merged.join("etc/hostname")).unwrap(),
265 "testbox"
266 );
267
268 std::fs::write(merged.join("etc/newfile"), "overlay write").unwrap();
270 assert!(box_dir.join("upper/etc/newfile").exists());
271
272 provider.cleanup(&box_dir, false).unwrap();
273 assert!(!box_dir.join("merged").exists());
274 assert!(!box_dir.join("upper").exists());
275 assert!(!box_dir.join("work").exists());
276 }
277}