use anyhow::Result;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
const RESOURCE_MANIFEST_FILE: &str = "resource-manifest.json";
pub fn copy_resources_with_extensions(
src_dir: &Path,
output_dir: &Path,
custom_extensions: Option<&[String]>,
exclude_patterns: Option<&[String]>,
) -> Result<Vec<PathBuf>> {
let mut copied: Vec<PathBuf> = Vec::new();
if !src_dir.exists() {
return Ok(copied);
}
let exclude_regexes = if custom_extensions.is_none() {
match exclude_patterns {
Some(patterns) => {
let mut regexes = Vec::with_capacity(patterns.len());
for p in patterns {
regexes.push(regex::Regex::new(p)?);
}
Some(regexes)
}
None => None,
}
} else {
None
};
for entry in walkdir::WalkDir::new(src_dir) {
let entry = entry?;
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("java") {
continue;
}
let should_copy = match custom_extensions {
Some(exts) => {
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
exts.iter().any(|e| {
let e = e.strip_prefix('.').unwrap_or(e);
e == ext
})
}
None => {
if let Some(ref regexes) = exclude_regexes {
let rel = path.strip_prefix(src_dir).unwrap_or(path);
let rel_str = rel.to_string_lossy();
!regexes.iter().any(|re| re.is_match(&rel_str))
} else {
true
}
}
};
if !should_copy {
continue;
}
let rel = path.strip_prefix(src_dir)?;
let dest = output_dir.join(rel);
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
let needs_copy = if dest.exists() {
let src_mtime = std::fs::metadata(path)?.modified()?;
let dst_mtime = std::fs::metadata(&dest)?.modified()?;
src_mtime > dst_mtime
} else {
true
};
if needs_copy {
std::fs::copy(path, &dest)?;
}
copied.push(rel.to_path_buf());
}
Ok(copied)
}
pub fn sync_resources(
resource_roots: &[PathBuf],
output_dir: &Path,
manifest_dir: &Path,
custom_extensions: Option<&[String]>,
exclude_patterns: Option<&[String]>,
) -> Result<()> {
let mut current: BTreeSet<String> = BTreeSet::new();
for root in resource_roots {
for rel in copy_resources_with_extensions(root, output_dir, custom_extensions, exclude_patterns)? {
current.insert(rel.to_string_lossy().to_string());
}
}
let manifest_path = manifest_dir.join(RESOURCE_MANIFEST_FILE);
for rel in load_resource_manifest(&manifest_path) {
if !current.contains(&rel) {
let _ = std::fs::remove_file(output_dir.join(&rel));
}
}
write_resource_manifest(&manifest_path, ¤t)?;
Ok(())
}
fn load_resource_manifest(path: &Path) -> Vec<String> {
std::fs::read_to_string(path)
.ok()
.and_then(|s| serde_json::from_str::<Vec<String>>(&s).ok())
.unwrap_or_default()
}
fn write_resource_manifest(path: &Path, entries: &BTreeSet<String>) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let list: Vec<&String> = entries.iter().collect();
std::fs::write(path, serde_json::to_string(&list)?)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sync_resources_prunes_orphan_keeps_live_and_untracked() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let res_dir = root.join("src/main/resources");
let out = root.join("out/classes");
let manifest_dir = root.join("cache/fp");
std::fs::create_dir_all(res_dir.join("graphql")).unwrap();
std::fs::write(res_dir.join("graphql/Issue.graphqls"), "type Issue").unwrap();
std::fs::write(res_dir.join("graphql/StandardIssue.graphqls"), "type StandardIssue").unwrap();
sync_resources(&[res_dir.clone()], &out, &manifest_dir, None, None).unwrap();
assert!(out.join("graphql/Issue.graphqls").exists());
assert!(out.join("graphql/StandardIssue.graphqls").exists());
std::fs::remove_file(res_dir.join("graphql/StandardIssue.graphqls")).unwrap();
sync_resources(&[res_dir.clone()], &out, &manifest_dir, None, None).unwrap();
assert!(out.join("graphql/Issue.graphqls").exists(), "live resource kept");
assert!(
!out.join("graphql/StandardIssue.graphqls").exists(),
"orphan resource must be pruned"
);
std::fs::write(out.join("generated.json"), "{}").unwrap();
sync_resources(&[res_dir.clone()], &out, &manifest_dir, None, None).unwrap();
assert!(
out.join("generated.json").exists(),
"untracked (AP-generated) file must not be pruned"
);
}
}