use std::collections::HashMap;
use std::io::{self, Error, ErrorKind, Read, Write};
use std::fs::{self, File};
use std::path::PathBuf;
use sha2::{Sha256, Digest};
use dirs::cache_dir;
use std::time::{Duration, SystemTime};
use std::env;
use crate::hint_file::{HintFile, Dependency};
use crate::artifact::{ArtifactManager, ArtifactType};
pub struct CacheEntry {
pub command: String,
pub output: String,
pub timestamp: SystemTime,
}
pub struct CommandCache {
cache: HashMap<String, String>,
cache_dir: PathBuf,
hint_file: Option<HintFile>,
current_dir: PathBuf,
artifact_manager: ArtifactManager,
}
impl CommandCache {
pub fn new() -> Self {
let mut cache_dir = cache_dir().unwrap_or_else(|| PathBuf::from("."));
cache_dir.push("cacher");
let _ = fs::create_dir_all(&cache_dir);
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let hint_file = HintFile::find_hint_file(¤t_dir);
let artifact_manager = ArtifactManager::new(cache_dir.clone());
CommandCache {
cache: HashMap::new(),
cache_dir,
hint_file,
current_dir,
artifact_manager,
}
}
pub fn store(&mut self, command: &str, output: &str) {
self.cache.insert(command.to_string(), output.to_string());
}
pub fn get(&self, command: &str) -> Option<&String> {
self.cache.get(command)
}
pub fn generate_id(&self, command: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(command.as_bytes());
if let Some(hint_file) = &self.hint_file {
if let Some(command_hint) = hint_file.find_matching_command(command) {
for env_var in &command_hint.include_env {
if let Ok(value) = env::var(env_var) {
hasher.update(format!("{}={}", env_var, value).as_bytes());
}
}
for dependency in &command_hint.depends_on {
match dependency {
Dependency::File { file } => {
let path = self.current_dir.join(file);
if path.exists() {
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
if let Ok(duration) = modified.duration_since(SystemTime::UNIX_EPOCH) {
hasher.update(format!("{}={}", file, duration.as_secs()).as_bytes());
}
}
}
}
},
Dependency::Files { files } => {
if let Ok(entries) = glob::glob(&format!("{}/{}", self.current_dir.display(), files)) {
for entry in entries {
if let Ok(path) = entry {
if let Ok(metadata) = fs::metadata(&path) {
if let Ok(modified) = metadata.modified() {
if let Ok(duration) = modified.duration_since(SystemTime::UNIX_EPOCH) {
if let Some(path_str) = path.to_str() {
hasher.update(format!("{}={}", path_str, duration.as_secs()).as_bytes());
}
}
}
}
}
}
}
},
Dependency::Lines { lines } => {
let path = self.current_dir.join(&lines.file);
if path.exists() {
if let Ok(content) = fs::read_to_string(&path) {
if let Ok(regex) = regex::Regex::new(&lines.pattern) {
let mut matching_lines = String::new();
for line in content.lines() {
if regex.is_match(line) {
matching_lines.push_str(line);
matching_lines.push('\n');
}
}
hasher.update(matching_lines.as_bytes());
}
}
}
}
}
}
} else {
for env_var in &hint_file.default.include_env {
if let Ok(value) = env::var(env_var) {
hasher.update(format!("{}={}", env_var, value).as_bytes());
}
}
}
}
format!("{:x}", hasher.finalize())
}
pub fn get_cache_path(&self, id: &str) -> PathBuf {
let cache_dir = self.cache_dir.join(id);
fs::create_dir_all(&cache_dir).unwrap_or_else(|_| {});
cache_dir
}
pub fn get_stdout_path(&self, id: &str) -> PathBuf {
self.get_cache_path(id).join("stdout")
}
pub fn get_metadata_path(&self, id: &str) -> PathBuf {
self.get_cache_path(id).join("metadata.json")
}
pub fn save_to_disk(&self, command: &str, output: &str) -> io::Result<()> {
let id = self.generate_id(command);
let _ = self.get_cache_path(&id);
let stdout_path = self.get_stdout_path(&id);
let mut stdout_file = File::create(stdout_path)?;
stdout_file.write_all(output.as_bytes())?;
let metadata_path = self.get_metadata_path(&id);
let metadata = format!(
"{{\"command\":\"{}\",\"timestamp\":{}}}",
command.replace("\"", "\\\""),
SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()
);
let mut metadata_file = File::create(metadata_path)?;
metadata_file.write_all(metadata.as_bytes())?;
Ok(())
}
pub fn load_from_disk(&self, command: &str) -> io::Result<Option<String>> {
let id = self.generate_id(command);
let stdout_path = self.get_stdout_path(&id);
if !stdout_path.exists() {
return Ok(None);
}
let mut file = File::open(stdout_path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(Some(contents))
}
pub fn execute_command(&self, command: &str) -> io::Result<String> {
let mut parts = command.split_whitespace();
let program = parts.next().ok_or_else(|| {
Error::new(ErrorKind::InvalidInput, "Empty command")
})?;
let args: Vec<&str> = parts.collect();
let output = std::process::Command::new(program)
.args(&args)
.output()
.map_err(|e| {
Error::new(ErrorKind::Other, format!("Failed to execute command: {}", e))
})?;
if !output.status.success() {
return Err(Error::new(
ErrorKind::Other,
format!(
"Command failed with exit code {}: {}",
output.status.code().unwrap_or(-1),
String::from_utf8_lossy(&output.stderr)
)
));
}
let output_str = String::from_utf8_lossy(&output.stdout).to_string();
Ok(output_str)
}
pub fn execute_and_cache(&mut self, command: &str, ttl: Option<Duration>, force: bool) -> io::Result<String> {
if !force {
if let Some(output) = self.get(command) {
return Ok(output.clone());
}
if let Ok(Some((output, timestamp))) = self.load_from_disk_with_timestamp(command) {
let effective_ttl = self.get_effective_ttl(command, ttl);
if let Some(ttl_duration) = effective_ttl {
if let Ok(age) = SystemTime::now().duration_since(timestamp) {
if age > ttl_duration {
} else {
self.store(command, &output);
return Ok(output);
}
}
} else {
self.store(command, &output);
return Ok(output);
}
}
}
let output = self.execute_command(command)?;
self.store(command, &output);
self.save_to_disk(command, &output)?;
Ok(output)
}
pub fn get_effective_ttl(&self, command: &str, default_ttl: Option<Duration>) -> Option<Duration> {
if let Some(hint_file) = &self.hint_file {
if let Some(command_hint) = hint_file.find_matching_command(command) {
if let Some(ttl_seconds) = command_hint.ttl {
return Some(Duration::from_secs(ttl_seconds));
}
}
if let Some(ttl_seconds) = hint_file.default.ttl {
return Some(Duration::from_secs(ttl_seconds));
}
}
default_ttl
}
pub fn load_from_disk_with_timestamp(&self, command: &str) -> io::Result<Option<(String, SystemTime)>> {
let id = self.generate_id(command);
let stdout_path = self.get_stdout_path(&id);
let metadata_path = self.get_metadata_path(&id);
if !stdout_path.exists() || !metadata_path.exists() {
return Ok(None);
}
let mut stdout_file = File::open(stdout_path)?;
let mut stdout_content = String::new();
stdout_file.read_to_string(&mut stdout_content)?;
let mut metadata_file = File::open(metadata_path)?;
let mut metadata_content = String::new();
metadata_file.read_to_string(&mut metadata_content)?;
let mut timestamp = SystemTime::UNIX_EPOCH;
if let Some(start) = metadata_content.find("\"timestamp\":") {
if let Some(end) = metadata_content[start + 12..].find("}") {
if let Ok(secs) = metadata_content[start + 12..start + 12 + end].trim().parse::<u64>() {
timestamp = SystemTime::UNIX_EPOCH + Duration::from_secs(secs);
}
}
}
Ok(Some((stdout_content, timestamp)))
}
pub fn list_cached_commands(&self) -> io::Result<Vec<(String, SystemTime)>> {
let mut entries = Vec::new();
if !self.cache_dir.exists() {
return Ok(entries);
}
for entry in fs::read_dir(&self.cache_dir)? {
let entry = entry?;
let cache_dir = entry.path();
if cache_dir.is_dir() {
let metadata_path = cache_dir.join("metadata.json");
if metadata_path.exists() {
if let Ok(mut file) = File::open(&metadata_path) {
let mut contents = String::new();
if file.read_to_string(&mut contents).is_ok() {
let mut command = String::new();
let mut timestamp = SystemTime::UNIX_EPOCH;
if let Some(start) = contents.find("\"command\":\"") {
if let Some(end) = contents[start + 11..].find("\"") {
command = contents[start + 11..start + 11 + end]
.replace("\\\"", "\"")
.to_string();
}
}
if let Some(start) = contents.find("\"timestamp\":") {
if let Some(end) = contents[start + 12..].find("}") {
if let Ok(secs) = contents[start + 12..start + 12 + end].trim().parse::<u64>() {
timestamp = SystemTime::UNIX_EPOCH + Duration::from_secs(secs);
}
}
}
if !command.is_empty() {
entries.push((command, timestamp));
}
}
}
}
}
}
entries.sort_by(|a, b| b.1.cmp(&a.1)); Ok(entries)
}
pub fn clear_cache(&mut self, command: Option<&str>) -> io::Result<()> {
if !self.cache_dir.exists() {
return Ok(());
}
match command {
Some(cmd) => {
let id = self.generate_id(cmd);
let cache_dir = self.get_cache_path(&id);
if cache_dir.exists() {
fs::remove_dir_all(cache_dir)?;
}
self.cache.remove(cmd);
},
None => {
fs::remove_dir_all(&self.cache_dir)?;
fs::create_dir_all(&self.cache_dir)?;
self.cache.clear();
}
}
Ok(())
}
pub fn get_command_artifacts(&self, command: &str) -> Option<Vec<ArtifactType>> {
if let Some(hint_file) = &self.hint_file {
if let Some(command_hint) = hint_file.find_matching_command(command) {
if !command_hint.artifacts.is_empty() {
return Some(command_hint.artifacts.clone());
}
}
}
None
}
pub fn cache_artifacts(&self, cache_id: String, _command: &str, artifacts: Vec<ArtifactType>) -> io::Result<()> {
for artifact in artifacts {
self.artifact_manager.cache_artifact(&artifact, &cache_id, &self.current_dir)?;
}
Ok(())
}
pub fn restore_artifacts(&self, cache_id: String, artifacts: Vec<ArtifactType>) -> io::Result<bool> {
let mut all_restored = true;
println!("Restoring artifacts for cache ID: {}", cache_id);
for artifact in artifacts {
println!("Restoring artifact: {:?}", artifact);
if !self.artifact_manager.restore_artifact(&artifact, &cache_id, &self.current_dir)? {
println!("Failed to restore artifact");
all_restored = false;
}
}
println!("All artifacts restored: {}", all_restored);
Ok(all_restored)
}
pub fn execute_and_cache_with_artifacts(&mut self, command: &str, ttl: Option<Duration>, force: bool) -> io::Result<String> {
let id = self.generate_id(command);
if !force {
if let Some(artifacts) = self.get_command_artifacts(command) {
if self.restore_artifacts(id.clone(), artifacts.clone()).is_ok() {
if let Ok(Some((output, timestamp))) = self.load_from_disk_with_timestamp(command) {
let effective_ttl = self.get_effective_ttl(command, ttl);
let use_cache = if let Some(ttl_duration) = effective_ttl {
if let Ok(age) = SystemTime::now().duration_since(timestamp) {
age <= ttl_duration
} else {
true
}
} else {
true
};
if use_cache {
self.store(command, &output);
return Ok(output);
}
}
}
}
}
let output = self.execute_command(command)?;
self.store(command, &output);
self.save_to_disk(command, &output)?;
if let Some(artifacts) = self.get_command_artifacts(command) {
self.cache_artifacts(id, command, artifacts)?;
}
Ok(output)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_store_and_retrieve() {
let mut cache = CommandCache::new();
let command = "echo hello";
let output = "hello\n";
cache.store(command, output);
assert_eq!(cache.get(command), Some(&output.to_string()));
}
#[test]
fn test_retrieve_nonexistent() {
let cache = CommandCache::new();
let command = "echo nonexistent";
assert_eq!(cache.get(command), None);
}
#[test]
fn test_generate_id() {
let cache = CommandCache::new();
let command = "echo hello";
let id1 = cache.generate_id(command);
let id2 = cache.generate_id(command);
assert_eq!(id1, id2);
assert!(!id1.is_empty());
}
#[test]
fn test_disk_cache() {
let cache = CommandCache::new();
let command = "test_disk_cache_command";
let output = "test output";
cache.save_to_disk(command, output).unwrap();
let loaded = cache.load_from_disk(command).unwrap();
assert_eq!(loaded, Some(output.to_string()));
}
#[test]
fn test_execute_and_cache() {
let mut cache = CommandCache::new();
let command = "echo test_execute";
let result = cache.execute_and_cache(command, None, false);
assert!(result.is_ok());
assert!(cache.get(command).is_some());
let loaded = cache.load_from_disk(command).unwrap();
assert!(loaded.is_some());
}
#[test]
fn test_ttl_and_force() {
let mut cache = CommandCache::new();
let command = "echo ttl_test";
let result1 = cache.execute_and_cache(command, Some(Duration::from_secs(1)), false).unwrap();
std::thread::sleep(Duration::from_secs(2));
let result2 = cache.execute_and_cache(command, Some(Duration::from_secs(1)), false).unwrap();
assert_eq!(result1, result2);
let result3 = cache.execute_and_cache(command, None, true).unwrap();
assert_eq!(result2, result3);
}
#[test]
fn test_list_and_clear_cache() {
let mut cache = CommandCache::new();
let command = "echo list_test";
let _ = cache.execute_and_cache(command, None, false);
let entries = cache.list_cached_commands().unwrap();
assert!(!entries.is_empty());
let _ = cache.clear_cache(Some(command));
}
}
pub mod hint_file;
pub mod artifact;
impl CommandCache {
pub fn reload_hint_file(&mut self) {
let current_dir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
self.current_dir = current_dir;
self.hint_file = HintFile::find_hint_file(&self.current_dir);
}
pub fn get_hint_file(&self) -> Option<&HintFile> {
self.hint_file.as_ref()
}
}