use std::path::PathBuf;
use crate::error::{Error, Result};
pub(crate) fn resolve_artifact_path(pattern: &str) -> Result<PathBuf> {
if !contains_glob_chars(pattern) {
let path = PathBuf::from(pattern);
if path.exists() {
return Ok(path);
}
return Err(Error::validation_invalid_argument(
"build_artifact",
format!("Artifact not found: {}", pattern),
Some(pattern.to_string()),
None,
));
}
let entries: Vec<PathBuf> = glob::glob(pattern)
.map_err(|e| {
Error::validation_invalid_argument(
"build_artifact",
format!("Invalid glob pattern '{}': {}", pattern, e),
Some(pattern.to_string()),
None,
)
})?
.filter_map(|entry| entry.ok())
.filter(|p| p.is_file())
.collect();
if entries.is_empty() {
return Err(Error::validation_invalid_argument(
"build_artifact",
format!("No files match pattern: {}", pattern),
Some(pattern.to_string()),
None,
));
}
let newest = entries
.into_iter()
.max_by_key(|p| p.metadata().and_then(|m| m.modified()).ok());
match newest {
Some(path) => {
log_status!("deploy", "Resolved '{}' -> '{}'", pattern, path.display());
Ok(path)
}
None => Err(Error::validation_invalid_argument(
"build_artifact",
format!("No files match pattern: {}", pattern),
Some(pattern.to_string()),
None,
)),
}
}
fn contains_glob_chars(s: &str) -> bool {
s.contains('*') || s.contains('?') || s.contains('[') || s.contains(']')
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{self, File};
use std::io::Write;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
#[test]
fn test_literal_path_exists() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("artifact.zip");
File::create(&file_path).unwrap();
let result = resolve_artifact_path(file_path.to_str().unwrap());
assert!(result.is_ok());
assert_eq!(result.unwrap(), file_path);
}
#[test]
fn test_literal_path_not_exists() {
let result = resolve_artifact_path("/nonexistent/path/artifact.zip");
assert!(result.is_err());
let err = result.unwrap_err();
let details = err.details.to_string();
assert!(
details.contains("Artifact not found"),
"Expected error details to contain 'Artifact not found', got: {}",
details
);
}
#[test]
fn test_glob_pattern_single_match() {
let dir = TempDir::new().unwrap();
let file_path = dir.path().join("build-1.0.0.zip");
File::create(&file_path).unwrap();
let pattern = dir.path().join("build-*.zip");
let result = resolve_artifact_path(pattern.to_str().unwrap());
assert!(result.is_ok());
assert_eq!(result.unwrap(), file_path);
}
#[test]
fn test_glob_pattern_multiple_matches_returns_newest() {
let dir = TempDir::new().unwrap();
let old_file = dir.path().join("build-1.0.0.zip");
let mut f = File::create(&old_file).unwrap();
f.write_all(b"old").unwrap();
drop(f);
thread::sleep(Duration::from_millis(50));
let new_file = dir.path().join("build-1.0.1.zip");
let mut f = File::create(&new_file).unwrap();
f.write_all(b"new").unwrap();
drop(f);
let pattern = dir.path().join("build-*.zip");
let result = resolve_artifact_path(pattern.to_str().unwrap());
assert!(result.is_ok());
assert_eq!(result.unwrap(), new_file);
}
#[test]
fn test_glob_pattern_no_matches() {
let dir = TempDir::new().unwrap();
let pattern = dir.path().join("nonexistent-*.zip");
let result = resolve_artifact_path(pattern.to_str().unwrap());
assert!(result.is_err());
let err = result.unwrap_err();
let details = err.details.to_string();
assert!(
details.contains("No files match pattern"),
"Expected error details to contain 'No files match pattern', got: {}",
details
);
}
#[test]
fn test_glob_pattern_ignores_directories() {
let dir = TempDir::new().unwrap();
let subdir = dir.path().join("build-1.0.0.zip");
fs::create_dir(&subdir).unwrap();
let pattern = dir.path().join("build-*.zip");
let result = resolve_artifact_path(pattern.to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_contains_glob_chars() {
assert!(contains_glob_chars("dist/*.zip"));
assert!(contains_glob_chars("build-?.tar.gz"));
assert!(contains_glob_chars("file[0-9].txt"));
assert!(!contains_glob_chars("dist/artifact.zip"));
assert!(!contains_glob_chars("/path/to/file.tar.gz"));
}
}