use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::network::AdapterSnapshot;
use super::{LoadResult, StateError, StateStore};
const STATE_FILE_VERSION: u32 = 1;
#[derive(Debug, Serialize, Deserialize)]
struct StateFile {
version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
saved_at: Option<String>,
snapshots: Vec<AdapterSnapshot>,
}
impl StateFile {
fn new(snapshots: &[AdapterSnapshot]) -> Self {
Self {
version: STATE_FILE_VERSION,
saved_at: Some(unix_timestamp_now()),
snapshots: snapshots.to_vec(),
}
}
}
fn unix_timestamp_now() -> String {
use std::time::SystemTime;
let now = SystemTime::now();
let duration = now
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
format!("{}", duration.as_secs())
}
#[derive(Debug, Clone)]
pub struct FileStateStore {
path: PathBuf,
}
impl FileStateStore {
#[must_use]
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
fn save_blocking(path: &Path, state: &StateFile) -> Result<(), StateError> {
let content = serde_json::to_string_pretty(state).map_err(StateError::Serialize)?;
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() {
std::fs::create_dir_all(parent).map_err(StateError::Write)?;
}
}
let temp_path = PathBuf::from(format!("{}.tmp", path.display()));
std::fs::write(&temp_path, content).map_err(StateError::Write)?;
std::fs::rename(&temp_path, path).map_err(StateError::Write)?;
Ok(())
}
}
impl StateStore for FileStateStore {
fn load(&self) -> LoadResult {
let content = match std::fs::read_to_string(&self.path) {
Ok(c) => c,
Err(e) if e.kind() == ErrorKind::NotFound => return LoadResult::NotFound,
Err(e) => {
return LoadResult::Corrupted {
reason: format!("Failed to read file: {e}"),
};
}
};
match serde_json::from_str::<StateFile>(&content) {
Ok(state) => {
if state.version != STATE_FILE_VERSION {
return LoadResult::Corrupted {
reason: format!(
"Incompatible version: expected {STATE_FILE_VERSION}, got {}",
state.version
),
};
}
LoadResult::Loaded(state.snapshots)
}
Err(e) => LoadResult::Corrupted {
reason: format!("Invalid JSON: {e}"),
},
}
}
async fn save(&self, snapshots: &[AdapterSnapshot]) -> Result<(), StateError> {
let path = self.path.clone();
let state = StateFile::new(snapshots);
tokio::task::spawn_blocking(move || Self::save_blocking(&path, &state))
.await
.expect("spawn_blocking task panicked")
}
}