use crate::error::{Result, StateError};
use crate::state::ReleaseState;
use serde_json;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
#[derive(Debug)]
pub struct StateManager {
state_file_path: PathBuf,
backup_file_path: PathBuf,
lock_file_path: PathBuf,
lock_handle: Option<FileLock>,
config: StateConfig,
}
#[derive(Debug, Clone)]
pub struct StateConfig {
pub backup_frequency: usize,
pub max_state_age_seconds: u64,
pub compress_state: bool,
pub lock_timeout_ms: u64,
pub validate_on_load: bool,
pub create_backups: bool,
}
impl Default for StateConfig {
fn default() -> Self {
Self {
backup_frequency: 5,
max_state_age_seconds: 86400 * 7, compress_state: false,
lock_timeout_ms: 5000, validate_on_load: true,
create_backups: true,
}
}
}
#[derive(Debug)]
struct FileLock {
lock_file: PathBuf,
pid: u32,
acquired_at: SystemTime,
}
impl Drop for FileLock {
fn drop(&mut self) {
let duration = self.acquired_at.elapsed().unwrap_or_default();
log::debug!(
"Releasing file lock (PID: {}, held for: {:?})",
self.pid,
duration
);
if self.lock_file.exists() {
let _ = std::fs::remove_file(&self.lock_file);
}
}
}
#[derive(Debug)]
pub struct LoadStateResult {
pub state: ReleaseState,
pub recovered_from_backup: bool,
pub warnings: Vec<String>,
}
#[derive(Debug)]
pub struct SaveStateResult {
pub success: bool,
pub file_size_bytes: u64,
pub save_duration: Duration,
pub backup_created: bool,
}
impl StateManager {
pub fn new<P: AsRef<Path>>(state_file_path: P) -> Result<Self> {
let state_file_path = state_file_path.as_ref().to_path_buf();
let backup_file_path = state_file_path.with_extension("backup.json");
let lock_file_path = state_file_path.with_extension("lock");
Ok(Self {
state_file_path,
backup_file_path,
lock_file_path,
lock_handle: None,
config: StateConfig::default(),
})
}
pub fn with_config<P: AsRef<Path>>(state_file_path: P, config: StateConfig) -> Result<Self> {
let state_file_path = state_file_path.as_ref().to_path_buf();
let backup_file_path = state_file_path.with_extension("backup.json");
let lock_file_path = state_file_path.with_extension("lock");
Ok(Self {
state_file_path,
backup_file_path,
lock_file_path,
lock_handle: None,
config,
})
}
pub fn save_state(&mut self, state: &ReleaseState) -> Result<SaveStateResult> {
let start_time = SystemTime::now();
self.acquire_lock()?;
if self.config.validate_on_load {
state.validate()?;
}
let serialized = serde_json::to_string_pretty(state)
.map_err(|e| StateError::SaveFailed {
reason: format!("Failed to serialize state: {}", e),
})?;
let backup_created = self.maybe_create_backup(&serialized)?;
let temp_file_path = self.state_file_path.with_extension("tmp");
{
let mut file = fs::File::create(&temp_file_path)
.map_err(|e| StateError::SaveFailed {
reason: format!("Failed to create temp file: {}", e),
})?;
file.write_all(serialized.as_bytes())
.map_err(|e| StateError::SaveFailed {
reason: format!("Failed to write state: {}", e),
})?;
file.sync_all()
.map_err(|e| StateError::SaveFailed {
reason: format!("Failed to sync file: {}", e),
})?;
}
fs::rename(&temp_file_path, &self.state_file_path)
.map_err(|e| StateError::SaveFailed {
reason: format!("Failed to rename temp file: {}", e),
})?;
let file_size_bytes = fs::metadata(&self.state_file_path)
.map(|m| m.len())
.unwrap_or(0);
let save_duration = start_time.elapsed().unwrap_or_default();
Ok(SaveStateResult {
success: true,
file_size_bytes,
save_duration,
backup_created,
})
}
pub fn load_state(&mut self) -> Result<LoadStateResult> {
self.acquire_lock()?;
let mut warnings = Vec::new();
let mut recovered_from_backup = false;
let state = match self.load_from_file(&self.state_file_path) {
Ok(state) => state,
Err(e) => {
warnings.push(format!("Failed to load main state file: {}", e));
match self.load_from_file(&self.backup_file_path) {
Ok(state) => {
warnings.push("Recovered state from backup file".to_string());
recovered_from_backup = true;
state
}
Err(backup_err) => {
return Err(StateError::LoadFailed {
reason: format!(
"Failed to load from both main ({}) and backup ({}) files",
e, backup_err
),
}.into());
}
}
}
};
if self.config.validate_on_load {
state.validate()?;
}
Ok(LoadStateResult {
state,
recovered_from_backup,
warnings,
})
}
pub fn state_exists(&self) -> bool {
self.state_file_path.exists()
}
pub fn backup_exists(&self) -> bool {
self.backup_file_path.exists()
}
pub fn cleanup_state(&self) -> Result<()> {
let mut errors = Vec::new();
if self.state_file_path.exists() {
if let Err(e) = fs::remove_file(&self.state_file_path) {
errors.push(format!("Failed to remove state file: {}", e));
}
}
if self.backup_file_path.exists() {
if let Err(e) = fs::remove_file(&self.backup_file_path) {
errors.push(format!("Failed to remove backup file: {}", e));
}
}
if self.lock_file_path.exists() {
if let Err(e) = fs::remove_file(&self.lock_file_path) {
errors.push(format!("Failed to remove lock file: {}", e));
}
}
if !errors.is_empty() {
return Err(StateError::SaveFailed {
reason: format!("Cleanup errors: {}", errors.join("; ")),
}.into());
}
Ok(())
}
pub fn create_backup(&self) -> Result<()> {
if !self.state_file_path.exists() {
return Err(StateError::NotFound.into());
}
fs::copy(&self.state_file_path, &self.backup_file_path)
.map_err(|e| StateError::SaveFailed {
reason: format!("Failed to create backup: {}", e),
})?;
Ok(())
}
pub fn restore_from_backup(&self) -> Result<()> {
if !self.backup_file_path.exists() {
return Err(StateError::NotFound.into());
}
fs::copy(&self.backup_file_path, &self.state_file_path)
.map_err(|e| StateError::LoadFailed {
reason: format!("Failed to restore from backup: {}", e),
})?;
Ok(())
}
pub fn get_state_info(&self) -> Result<StateFileInfo> {
let main_info = if self.state_file_path.exists() {
let metadata = fs::metadata(&self.state_file_path)
.map_err(|e| StateError::LoadFailed {
reason: format!("Failed to get state file metadata: {}", e),
})?;
Some(FileInfo {
size_bytes: metadata.len(),
modified_at: metadata.modified().ok(),
created_at: metadata.created().ok(),
})
} else {
None
};
let backup_info = if self.backup_file_path.exists() {
let metadata = fs::metadata(&self.backup_file_path)
.map_err(|e| StateError::LoadFailed {
reason: format!("Failed to get backup file metadata: {}", e),
})?;
Some(FileInfo {
size_bytes: metadata.len(),
modified_at: metadata.modified().ok(),
created_at: metadata.created().ok(),
})
} else {
None
};
let is_locked = self.lock_file_path.exists();
Ok(StateFileInfo {
state_file_path: self.state_file_path.clone(),
backup_file_path: self.backup_file_path.clone(),
main_file_info: main_info,
backup_file_info: backup_info,
is_locked,
})
}
pub fn is_locked_by_other_process(&self) -> bool {
if !self.lock_file_path.exists() {
return false;
}
match fs::read_to_string(&self.lock_file_path) {
Ok(content) => {
if let Ok(pid) = content.trim().parse::<u32>() {
pid != std::process::id()
} else {
false
}
}
Err(_) => false,
}
}
pub fn force_unlock(&mut self) -> Result<()> {
if self.lock_file_path.exists() {
fs::remove_file(&self.lock_file_path)
.map_err(|e| StateError::SaveFailed {
reason: format!("Failed to remove lock file: {}", e),
})?;
}
self.lock_handle = None;
Ok(())
}
fn load_from_file(&self, file_path: &Path) -> Result<ReleaseState> {
let mut file = fs::File::open(file_path)
.map_err(|e| StateError::LoadFailed {
reason: format!("Failed to open file {}: {}", file_path.display(), e),
})?;
let mut contents = String::new();
file.read_to_string(&mut contents)
.map_err(|e| StateError::LoadFailed {
reason: format!("Failed to read file {}: {}", file_path.display(), e),
})?;
let state: ReleaseState = serde_json::from_str(&contents)
.map_err(|e| StateError::Corrupted {
reason: format!("Failed to deserialize state: {}", e),
})?;
Ok(state)
}
fn maybe_create_backup(&self, serialized_state: &str) -> Result<bool> {
if !self.config.create_backups {
return Ok(false);
}
fs::write(&self.backup_file_path, serialized_state)
.map_err(|e| StateError::SaveFailed {
reason: format!("Failed to create backup: {}", e),
})?;
Ok(true)
}
fn acquire_lock(&mut self) -> Result<()> {
if self.lock_handle.is_some() {
return Ok(()); }
let start_time = SystemTime::now();
let timeout = Duration::from_millis(self.config.lock_timeout_ms);
while start_time.elapsed().unwrap_or_default() < timeout {
match fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&self.lock_file_path)
{
Ok(mut file) => {
let pid = std::process::id();
file.write_all(pid.to_string().as_bytes())
.map_err(|e| StateError::SaveFailed {
reason: format!("Failed to write lock file: {}", e),
})?;
self.lock_handle = Some(FileLock {
lock_file: self.lock_file_path.clone(),
pid,
acquired_at: SystemTime::now(),
});
return Ok(());
}
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
std::thread::sleep(Duration::from_millis(100));
}
Err(e) => {
return Err(StateError::SaveFailed {
reason: format!("Failed to create lock file: {}", e),
}.into());
}
}
}
Err(StateError::SaveFailed {
reason: "Timeout waiting for file lock".to_string(),
}.into())
}
pub fn set_config(&mut self, config: StateConfig) {
self.config = config;
}
pub fn config(&self) -> &StateConfig {
&self.config
}
}
impl Drop for StateManager {
fn drop(&mut self) {
if self.lock_handle.is_some() {
let _ = fs::remove_file(&self.lock_file_path);
}
}
}
#[derive(Debug, Clone)]
pub struct StateFileInfo {
pub state_file_path: PathBuf,
pub backup_file_path: PathBuf,
pub main_file_info: Option<FileInfo>,
pub backup_file_info: Option<FileInfo>,
pub is_locked: bool,
}
#[derive(Debug, Clone)]
pub struct FileInfo {
pub size_bytes: u64,
pub modified_at: Option<SystemTime>,
pub created_at: Option<SystemTime>,
}
impl StateFileInfo {
pub fn has_state(&self) -> bool {
self.main_file_info.is_some()
}
pub fn has_backup(&self) -> bool {
self.backup_file_info.is_some()
}
pub fn total_size_bytes(&self) -> u64 {
let main_size = self.main_file_info.as_ref().map(|f| f.size_bytes).unwrap_or(0);
let backup_size = self.backup_file_info.as_ref().map(|f| f.size_bytes).unwrap_or(0);
main_size + backup_size
}
pub fn format_info(&self) -> String {
let mut info = String::new();
if let Some(main_info) = &self.main_file_info {
info.push_str(&format!("Main state: {} bytes", main_info.size_bytes));
if let Some(modified) = main_info.modified_at {
if let Ok(elapsed) = modified.elapsed() {
info.push_str(&format!(" (modified {}s ago)", elapsed.as_secs()));
}
}
} else {
info.push_str("No main state file");
}
if let Some(backup_info) = &self.backup_file_info {
info.push_str(&format!(", Backup: {} bytes", backup_info.size_bytes));
}
if self.is_locked {
info.push_str(" [LOCKED]");
}
info
}
}
impl SaveStateResult {
pub fn format_result(&self) -> String {
if self.success {
let backup_info = if self.backup_created { " (backup created)" } else { "" };
format!(
"✅ State saved: {} bytes in {:.2}s{}",
self.file_size_bytes,
self.save_duration.as_secs_f64(),
backup_info
)
} else {
"❌ Failed to save state".to_string()
}
}
}
impl LoadStateResult {
pub fn format_result(&self) -> String {
let mut result = if self.recovered_from_backup {
"⚠️ State loaded from backup".to_string()
} else {
"✅ State loaded successfully".to_string()
};
if !self.warnings.is_empty() {
result.push_str(&format!(" ({} warnings)", self.warnings.len()));
}
result
}
}