ryra_core/data/
classify.rs1use std::path::{Path, PathBuf};
2
3use crate::error::{Error, Result};
4use crate::manifest;
5
6pub fn classify_home_dir(home_dir: &Path) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
27 let mut data = Vec::new();
28 let mut ephemeral = Vec::new();
29 if !home_dir.exists() {
30 return Ok((data, ephemeral));
31 }
32
33 let manifest_path = home_dir.join(manifest::MANIFEST_FILENAME);
34 let manifest_entries: Vec<PathBuf> = if manifest_path.exists() {
35 let content =
36 std::fs::read_to_string(&manifest_path).map_err(|source| Error::FileRead {
37 path: manifest_path.clone(),
38 source,
39 })?;
40 let (entries, _envs) = manifest::parse(&content)?;
41 entries.into_iter().map(|e| e.path).collect()
42 } else {
43 Vec::new()
44 };
45 let have_manifest = manifest_path.exists();
46
47 let entries = std::fs::read_dir(home_dir).map_err(|source| Error::FileRead {
48 path: home_dir.to_path_buf(),
49 source,
50 })?;
51 for entry in entries {
52 let entry = entry.map_err(|source| Error::FileRead {
53 path: home_dir.to_path_buf(),
54 source,
55 })?;
56 let path = entry.path();
57
58 if !have_manifest {
59 data.push(path);
62 continue;
63 }
64
65 if path == manifest_path || path.file_name().and_then(|n| n.to_str()) == Some(".env") {
70 ephemeral.push(path);
71 continue;
72 }
73
74 let is_ephemeral = manifest_entries
78 .iter()
79 .any(|m| m == &path || m.starts_with(&path));
80 if is_ephemeral {
81 ephemeral.push(path);
82 } else {
83 data.push(path);
84 }
85 }
86 data.sort();
87 ephemeral.sort();
88 Ok((data, ephemeral))
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::manifest::{ManifestEntry, format};
95 use std::fs;
96
97 fn write_manifest(home: &Path, paths: &[&Path]) {
98 let entries: Vec<ManifestEntry> = paths
99 .iter()
100 .map(|p| ManifestEntry {
101 path: p.to_path_buf(),
102 sha256: "0".repeat(64),
103 })
104 .collect();
105 fs::write(
106 home.join(manifest::MANIFEST_FILENAME),
107 format(&entries, &[]),
108 )
109 .unwrap();
110 }
111
112 #[test]
113 fn classifies_manifest_listed_files_as_ephemeral() {
114 let dir = tempfile::tempdir().unwrap();
115 let home = dir.path();
116
117 fs::write(home.join("svc.container"), "[Container]").unwrap();
119 fs::write(home.join("svc.network"), "[Network]").unwrap();
120 fs::write(home.join("metadata.toml"), "").unwrap();
121 fs::create_dir(home.join("configs")).unwrap();
122 fs::write(home.join("configs").join("nginx.conf"), "").unwrap();
123 fs::write(home.join(".env"), "FOO=bar").unwrap();
125
126 fs::create_dir(home.join("db-data")).unwrap();
128 fs::create_dir(home.join("storage-data")).unwrap();
129
130 write_manifest(
131 home,
132 &[
133 &home.join("svc.container"),
134 &home.join("svc.network"),
135 &home.join("metadata.toml"),
136 &home.join("configs").join("nginx.conf"),
137 ],
138 );
139
140 let (data, eph) = classify_home_dir(home).unwrap();
141 let eph_names: Vec<String> = eph
142 .iter()
143 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
144 .collect();
145 let data_names: Vec<String> = data
146 .iter()
147 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
148 .collect();
149 assert_eq!(
150 eph_names,
151 vec![
152 ".env".to_string(),
153 "configs".to_string(),
154 "metadata.toml".to_string(),
155 "service.manifest".to_string(),
156 "svc.container".to_string(),
157 "svc.network".to_string(),
158 ]
159 );
160 assert_eq!(
161 data_names,
162 vec!["db-data".to_string(), "storage-data".to_string()]
163 );
164 }
165
166 #[test]
167 fn user_dropped_files_are_preserved_as_data() {
168 let dir = tempfile::tempdir().unwrap();
171 let home = dir.path();
172 fs::write(home.join("svc.container"), "").unwrap();
173 fs::write(home.join("my-notes.txt"), "remember to back this up").unwrap();
174 write_manifest(home, &[&home.join("svc.container")]);
175
176 let (data, eph) = classify_home_dir(home).unwrap();
177 let data_names: Vec<String> = data
178 .iter()
179 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
180 .collect();
181 let eph_names: Vec<String> = eph
182 .iter()
183 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
184 .collect();
185 assert_eq!(data_names, vec!["my-notes.txt".to_string()]);
186 assert_eq!(
187 eph_names,
188 vec!["service.manifest".to_string(), "svc.container".to_string()]
189 );
190 }
191
192 #[test]
193 fn missing_manifest_classifies_everything_as_data() {
194 let dir = tempfile::tempdir().unwrap();
197 let home = dir.path();
198 fs::create_dir(home.join("db-data")).unwrap();
199 fs::write(home.join("leftover.txt"), "").unwrap();
200
201 let (data, eph) = classify_home_dir(home).unwrap();
202 assert!(eph.is_empty());
203 let data_names: Vec<String> = data
204 .iter()
205 .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
206 .collect();
207 assert_eq!(
208 data_names,
209 vec!["db-data".to_string(), "leftover.txt".to_string()]
210 );
211 }
212
213 #[test]
214 fn missing_home_dir_returns_empty() {
215 let (data, eph) = classify_home_dir(Path::new("/nonexistent-xyz-123")).unwrap();
216 assert!(data.is_empty());
217 assert!(eph.is_empty());
218 }
219}