1use crate::container_utils::require_ostree_container;
6use crate::mountutil::is_mountpoint;
7use anyhow::Context;
8use anyhow::Result;
9use cap_std::fs::Dir;
10use cap_std::fs::MetadataExt;
11use cap_std_ext::cap_std;
12use cap_std_ext::dirext::CapStdExtDirExt;
13use std::path::Path;
14use std::path::PathBuf;
15use tokio::task;
16
17const FORCE_CLEAN_PATHS: &[&str] = &["run", "tmp", "var/tmp", "var/cache"];
19
20fn remove_all_on_mount_recurse(root: &Dir, rootdev: u64, path: &Path) -> Result<bool> {
22 let mut skipped = false;
23 for entry in root
24 .read_dir(path)
25 .with_context(|| format!("Reading {path:?}"))?
26 {
27 let entry = entry?;
28 let metadata = entry.metadata()?;
29 if metadata.dev() != rootdev {
30 skipped = true;
31 continue;
32 }
33 let name = entry.file_name();
34 let path = &path.join(name);
35
36 if metadata.is_dir() {
37 skipped |= remove_all_on_mount_recurse(root, rootdev, path.as_path())?;
38 } else {
39 root.remove_file(path)
40 .with_context(|| format!("Removing {path:?}"))?;
41 }
42 }
43 if !skipped {
44 root.remove_dir(path)
45 .with_context(|| format!("Removing {path:?}"))?;
46 }
47 Ok(skipped)
48}
49
50fn clean_subdir(root: &Dir, rootdev: u64) -> Result<()> {
51 for entry in root.entries()? {
52 let entry = entry?;
53 let metadata = entry.metadata()?;
54 let dev = metadata.dev();
55 let path = PathBuf::from(entry.file_name());
56 if dev != rootdev {
58 tracing::trace!("Skipping entry in foreign dev {path:?}");
59 continue;
60 }
61 if is_mountpoint(root, &path)?.unwrap_or_default() {
64 tracing::trace!("Skipping mount point {path:?}");
65 continue;
66 }
67 if metadata.is_dir() {
68 remove_all_on_mount_recurse(root, rootdev, &path)?;
69 } else {
70 root.remove_file(&path)
71 .with_context(|| format!("Removing {path:?}"))?;
72 }
73 }
74 Ok(())
75}
76
77fn clean_paths_in(root: &Dir, rootdev: u64) -> Result<()> {
78 for path in FORCE_CLEAN_PATHS {
79 let subdir = if let Some(subdir) = root.open_dir_optional(path)? {
80 subdir
81 } else {
82 continue;
83 };
84 clean_subdir(&subdir, rootdev).with_context(|| format!("Cleaning {path}"))?;
85 }
86 Ok(())
87}
88
89pub fn prepare_ostree_commit_in(root: &Dir) -> Result<()> {
92 let rootdev = root.dir_metadata()?.dev();
93 clean_paths_in(root, rootdev)
94}
95
96pub fn prepare_ostree_commit_in_nonstrict(root: &Dir) -> Result<()> {
99 let rootdev = root.dir_metadata()?.dev();
100 clean_paths_in(root, rootdev)
101}
102
103pub(crate) async fn container_commit() -> Result<()> {
106 task::spawn_blocking(move || {
107 require_ostree_container()?;
108 let rootdir = Dir::open_ambient_dir("/", cap_std::ambient_authority())?;
109 prepare_ostree_commit_in(&rootdir)
110 })
111 .await?
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use camino::Utf8Path;
118
119 use cap_std_ext::cap_tempfile;
120
121 #[test]
122 fn commit() -> Result<()> {
123 let td = &cap_tempfile::tempdir(cap_std::ambient_authority())?;
124
125 prepare_ostree_commit_in(td).unwrap();
127 prepare_ostree_commit_in_nonstrict(td).unwrap();
128
129 let var = Utf8Path::new("var");
130 let run = Utf8Path::new("run");
131 let tmp = Utf8Path::new("tmp");
132 let vartmp_foobar = &var.join("tmp/foo/bar");
133 let runsystemd = &run.join("systemd");
134 let resolvstub = &runsystemd.join("resolv.conf");
135
136 for p in [var, run, tmp] {
137 td.create_dir(p)?;
138 }
139
140 td.create_dir_all(vartmp_foobar)?;
141 td.write(vartmp_foobar.join("a"), "somefile")?;
142 td.write(vartmp_foobar.join("b"), "somefile2")?;
143 td.create_dir_all(runsystemd)?;
144 td.write(resolvstub, "stub resolv")?;
145 prepare_ostree_commit_in(td).unwrap();
146 assert!(td.try_exists(var)?);
147 assert!(td.try_exists(var.join("tmp"))?);
148 assert!(!td.try_exists(vartmp_foobar)?);
149 assert!(td.try_exists(run)?);
150 assert!(!td.try_exists(runsystemd)?);
151
152 let systemd = run.join("systemd");
153 td.create_dir_all(&systemd)?;
154 prepare_ostree_commit_in(td).unwrap();
155 assert!(td.try_exists(var)?);
156 assert!(!td.try_exists(&systemd)?);
157
158 td.remove_dir_all(var)?;
159 td.create_dir(var)?;
160 td.write(var.join("foo"), "somefile")?;
161 prepare_ostree_commit_in(td).unwrap();
162 assert!(!td.try_exists(var.join("tmp"))?);
165 assert!(td.try_exists(var)?);
166
167 td.write(var.join("foo"), "somefile")?;
168 prepare_ostree_commit_in_nonstrict(td).unwrap();
169 assert!(td.try_exists(var)?);
170
171 let nested = Utf8Path::new("var/lib/nested");
172 td.create_dir_all(nested)?;
173 td.write(nested.join("foo"), "test1")?;
174 td.write(nested.join("foo2"), "test2")?;
175 prepare_ostree_commit_in(td).unwrap();
176 assert!(td.try_exists(var)?);
177 assert!(td.try_exists(nested)?);
178
179 Ok(())
180 }
181}