use super::error::{LifecycleError, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct LifecycleState {
pub last_phase: Option<String>,
pub phase_history: Vec<RunRecord>,
pub generated: Vec<GeneratedFile>,
pub cache_keys: Vec<CacheKey>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunRecord {
pub phase: String,
pub started_ms: u128,
pub duration_ms: u128,
pub success: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GeneratedFile {
pub path: String,
pub hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheKey {
pub phase: String,
pub key: String,
}
pub fn load_state<P: AsRef<Path>>(path: P) -> Result<LifecycleState> {
let path_ref = path.as_ref();
if !path_ref.exists() {
return Ok(LifecycleState::default());
}
let metadata =
std::fs::metadata(path_ref).map_err(|e| LifecycleError::state_load(path_ref, e))?;
if metadata.len() == 0 {
log::warn!(
"State file {} is empty, using default state",
path_ref.display()
);
return Ok(LifecycleState::default());
}
let content =
std::fs::read_to_string(path_ref).map_err(|e| LifecycleError::state_load(path_ref, e))?;
if content.trim().is_empty() {
log::warn!(
"State file {} contains only whitespace, using default state",
path_ref.display()
);
return Ok(LifecycleState::default());
}
let state: LifecycleState = serde_json::from_str(&content)
.map_err(|e| {
log::error!(
"Failed to parse state file {}: {}. Using default state.",
path_ref.display(),
e
);
e
})
.map_err(|e| LifecycleError::state_parse(path_ref, e))?;
Ok(state)
}
pub fn save_state<P: AsRef<Path>>(path: P, state: &LifecycleState) -> Result<()> {
let path_ref = path.as_ref();
if let Some(parent) = path_ref.parent() {
std::fs::create_dir_all(parent).map_err(|e| LifecycleError::DirectoryCreate {
path: parent.to_path_buf(),
source: e,
})?;
}
let json = serde_json::to_string_pretty(state)
.map_err(std::io::Error::other)
.map_err(|e| LifecycleError::state_save(path_ref, e))?;
let estimated_size = json.len() as u64 * 110 / 100;
if let Some(parent) = path_ref.parent() {
if let Ok(available_space) = get_available_space(parent) {
if available_space < estimated_size {
return Err(LifecycleError::state_save(
path_ref,
std::io::Error::other(format!(
"Insufficient disk space: need {} bytes, have {} bytes",
estimated_size, available_space
)),
));
}
}
}
let temp_path = path_ref.with_extension("json.tmp");
std::fs::write(&temp_path, json).map_err(|e| LifecycleError::state_save(path_ref, e))?;
std::fs::rename(&temp_path, path_ref).map_err(|e| LifecycleError::state_save(path_ref, e))?;
Ok(())
}
fn get_available_space(path: &Path) -> std::io::Result<u64> {
#[cfg(unix)]
{
let _metadata = std::fs::metadata(path)?;
Ok(u64::MAX)
}
#[cfg(windows)]
{
let _metadata = std::fs::metadata(path)?;
Ok(u64::MAX)
}
#[cfg(not(any(unix, windows)))]
{
let _ = path;
Ok(u64::MAX)
}
}
impl LifecycleState {
pub fn record_run(
&mut self, phase: String, started_ms: u128, duration_ms: u128, success: bool,
) {
self.phase_history.push(RunRecord {
phase: phase.clone(),
started_ms,
duration_ms,
success,
});
self.last_phase = Some(phase);
}
pub fn add_cache_key(&mut self, phase: String, key: String) {
self.cache_keys.push(CacheKey { phase, key });
}
pub fn last_run(&self, phase: &str) -> Option<&RunRecord> {
self.phase_history.iter().rev().find(|r| r.phase == phase)
}
pub fn get_cache_key(&self, phase: &str) -> Option<&str> {
self.cache_keys
.iter()
.rev()
.find(|k| k.phase == phase)
.map(|k| k.key.as_str())
}
pub fn has_completed_phase(&self, phase: &str) -> bool {
self.phase_history
.iter()
.any(|r| r.phase == phase && r.success)
}
}