use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::manifest;
pub fn classify_home_dir(home_dir: &Path) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
let mut data = Vec::new();
let mut ephemeral = Vec::new();
if !home_dir.exists() {
return Ok((data, ephemeral));
}
let manifest_path = home_dir.join(manifest::MANIFEST_FILENAME);
let manifest_entries: Vec<PathBuf> = if manifest_path.exists() {
let content =
std::fs::read_to_string(&manifest_path).map_err(|source| Error::FileRead {
path: manifest_path.clone(),
source,
})?;
let (entries, _envs) = manifest::parse(&content)?;
entries.into_iter().map(|e| e.path).collect()
} else {
Vec::new()
};
let have_manifest = manifest_path.exists();
let entries = std::fs::read_dir(home_dir).map_err(|source| Error::FileRead {
path: home_dir.to_path_buf(),
source,
})?;
for entry in entries {
let entry = entry.map_err(|source| Error::FileRead {
path: home_dir.to_path_buf(),
source,
})?;
let path = entry.path();
if !have_manifest {
data.push(path);
continue;
}
if path == manifest_path || path.file_name().and_then(|n| n.to_str()) == Some(".env") {
ephemeral.push(path);
continue;
}
let is_ephemeral = manifest_entries
.iter()
.any(|m| m == &path || m.starts_with(&path));
if is_ephemeral {
ephemeral.push(path);
} else {
data.push(path);
}
}
data.sort();
ephemeral.sort();
Ok((data, ephemeral))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::{ManifestEntry, format};
use std::fs;
fn write_manifest(home: &Path, paths: &[&Path]) {
let entries: Vec<ManifestEntry> = paths
.iter()
.map(|p| ManifestEntry {
path: p.to_path_buf(),
sha256: "0".repeat(64),
})
.collect();
fs::write(
home.join(manifest::MANIFEST_FILENAME),
format(&entries, &[]),
)
.unwrap();
}
#[test]
fn classifies_manifest_listed_files_as_ephemeral() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
fs::write(home.join("svc.container"), "[Container]").unwrap();
fs::write(home.join("svc.network"), "[Network]").unwrap();
fs::write(home.join("metadata.toml"), "").unwrap();
fs::create_dir(home.join("configs")).unwrap();
fs::write(home.join("configs").join("nginx.conf"), "").unwrap();
fs::write(home.join(".env"), "FOO=bar").unwrap();
fs::create_dir(home.join("db-data")).unwrap();
fs::create_dir(home.join("storage-data")).unwrap();
write_manifest(
home,
&[
&home.join("svc.container"),
&home.join("svc.network"),
&home.join("metadata.toml"),
&home.join("configs").join("nginx.conf"),
],
);
let (data, eph) = classify_home_dir(home).unwrap();
let eph_names: Vec<String> = eph
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
let data_names: Vec<String> = data
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert_eq!(
eph_names,
vec![
".env".to_string(),
"configs".to_string(),
"metadata.toml".to_string(),
"service.manifest".to_string(),
"svc.container".to_string(),
"svc.network".to_string(),
]
);
assert_eq!(
data_names,
vec!["db-data".to_string(), "storage-data".to_string()]
);
}
#[test]
fn user_dropped_files_are_preserved_as_data() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
fs::write(home.join("svc.container"), "").unwrap();
fs::write(home.join("my-notes.txt"), "remember to back this up").unwrap();
write_manifest(home, &[&home.join("svc.container")]);
let (data, eph) = classify_home_dir(home).unwrap();
let data_names: Vec<String> = data
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
let eph_names: Vec<String> = eph
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert_eq!(data_names, vec!["my-notes.txt".to_string()]);
assert_eq!(
eph_names,
vec!["service.manifest".to_string(), "svc.container".to_string()]
);
}
#[test]
fn missing_manifest_classifies_everything_as_data() {
let dir = tempfile::tempdir().unwrap();
let home = dir.path();
fs::create_dir(home.join("db-data")).unwrap();
fs::write(home.join("leftover.txt"), "").unwrap();
let (data, eph) = classify_home_dir(home).unwrap();
assert!(eph.is_empty());
let data_names: Vec<String> = data
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert_eq!(
data_names,
vec!["db-data".to_string(), "leftover.txt".to_string()]
);
}
#[test]
fn missing_home_dir_returns_empty() {
let (data, eph) = classify_home_dir(Path::new("/nonexistent-xyz-123")).unwrap();
assert!(data.is_empty());
assert!(eph.is_empty());
}
}