use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum EntryKind {
Base,
Override,
Template,
}
#[derive(Debug)]
pub struct FileAction {
pub source: PathBuf,
pub target_rel_path: PathBuf,
pub kind: EntryKind,
}
pub fn scan_package(pkg_dir: &Path, hostname: &str, roles: &[&str]) -> Result<Vec<FileAction>> {
let mut files: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();
collect_files(pkg_dir, pkg_dir, &mut files)
.with_context(|| format!("failed to scan package directory: {}", pkg_dir.display()))?;
let mut actions = Vec::new();
for (target_path, variants) in &files {
let action = resolve_variant(target_path, variants, hostname, roles);
actions.push(action);
}
actions.sort_by(|a, b| a.target_rel_path.cmp(&b.target_rel_path));
Ok(actions)
}
fn collect_files(
base: &Path,
dir: &Path,
files: &mut HashMap<PathBuf, Vec<PathBuf>>,
) -> Result<()> {
for entry in
std::fs::read_dir(dir).with_context(|| format!("failed to read directory: {}", dir.display()))?
{
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_files(base, &path, files)?;
} else {
let rel_path = path
.strip_prefix(base)
.expect("collected path must be under base directory")
.to_path_buf();
let canonical = canonical_target_path(&rel_path);
files.entry(canonical).or_default().push(path);
}
}
Ok(())
}
fn file_name_str(path: &Path) -> &str {
path.file_name()
.expect("path has no filename")
.to_str()
.expect("filename is not valid UTF-8")
}
fn canonical_target_path(rel_path: &Path) -> PathBuf {
let file_name = file_name_str(rel_path);
let base_name = if let Some(idx) = file_name.find("##") {
&file_name[..idx]
} else {
file_name
};
let base_name = base_name.strip_suffix(".tera").unwrap_or(base_name);
if let Some(parent) = rel_path.parent() {
if parent == Path::new("") {
PathBuf::from(base_name)
} else {
parent.join(base_name)
}
} else {
PathBuf::from(base_name)
}
}
fn resolve_variant(
target_path: &Path,
variants: &[PathBuf],
hostname: &str,
roles: &[&str],
) -> FileAction {
let host_suffix = format!("##host.{hostname}");
if let Some(source) = variants
.iter()
.find(|v| file_name_str(v).contains(&host_suffix))
{
return FileAction {
source: source.clone(),
target_rel_path: target_path.to_path_buf(),
kind: EntryKind::Override,
};
}
for role in roles.iter().rev() {
let role_suffix = format!("##role.{role}");
if let Some(source) = variants
.iter()
.find(|v| file_name_str(v).contains(&role_suffix))
{
return FileAction {
source: source.clone(),
target_rel_path: target_path.to_path_buf(),
kind: EntryKind::Override,
};
}
}
if let Some(source) = variants.iter().find(|v| {
let name = file_name_str(v);
name.ends_with(".tera") && !name.contains("##")
}) {
return FileAction {
source: source.clone(),
target_rel_path: target_path.to_path_buf(),
kind: EntryKind::Template,
};
}
let source = variants
.iter()
.find(|v| {
let name = file_name_str(v);
!name.contains("##") && !name.ends_with(".tera")
})
.unwrap_or(&variants[0]);
FileAction {
source: source.clone(),
target_rel_path: target_path.to_path_buf(),
kind: EntryKind::Base,
}
}