1use std::io::ErrorKind;
4use std::path::{Path, PathBuf};
5
6use serde::{Deserialize, Serialize};
7
8use crate::network::AdapterSnapshot;
9
10use super::{LoadResult, StateError, StateStore};
11
12const STATE_FILE_VERSION: u32 = 1;
16
17#[derive(Debug, Serialize, Deserialize)]
23struct StateFile {
24 version: u32,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
30 saved_at: Option<String>,
31
32 snapshots: Vec<AdapterSnapshot>,
34}
35
36impl StateFile {
37 fn new(snapshots: &[AdapterSnapshot]) -> Self {
39 Self {
40 version: STATE_FILE_VERSION,
41 saved_at: Some(unix_timestamp_now()),
42 snapshots: snapshots.to_vec(),
43 }
44 }
45}
46
47fn unix_timestamp_now() -> String {
51 use std::time::SystemTime;
52
53 let now = SystemTime::now();
54 let duration = now
55 .duration_since(SystemTime::UNIX_EPOCH)
56 .unwrap_or_default();
57
58 format!("{}", duration.as_secs())
59}
60
61#[derive(Debug, Clone)]
73pub struct FileStateStore {
74 path: PathBuf,
75}
76
77impl FileStateStore {
78 #[must_use]
80 pub fn new(path: impl Into<PathBuf>) -> Self {
81 Self { path: path.into() }
82 }
83
84 #[must_use]
86 pub fn path(&self) -> &Path {
87 &self.path
88 }
89
90 fn save_blocking(path: &Path, state: &StateFile) -> Result<(), StateError> {
94 let content = serde_json::to_string_pretty(state).map_err(StateError::Serialize)?;
95
96 if let Some(parent) = path.parent() {
98 if !parent.as_os_str().is_empty() {
99 std::fs::create_dir_all(parent).map_err(StateError::Write)?;
100 }
101 }
102
103 let temp_path = PathBuf::from(format!("{}.tmp", path.display()));
106
107 std::fs::write(&temp_path, content).map_err(StateError::Write)?;
109
110 std::fs::rename(&temp_path, path).map_err(StateError::Write)?;
112
113 Ok(())
114 }
115}
116
117impl StateStore for FileStateStore {
118 fn load(&self) -> LoadResult {
119 let content = match std::fs::read_to_string(&self.path) {
120 Ok(c) => c,
121 Err(e) if e.kind() == ErrorKind::NotFound => return LoadResult::NotFound,
122 Err(e) => {
123 return LoadResult::Corrupted {
124 reason: format!("Failed to read file: {e}"),
125 };
126 }
127 };
128
129 match serde_json::from_str::<StateFile>(&content) {
130 Ok(state) => {
131 if state.version != STATE_FILE_VERSION {
133 return LoadResult::Corrupted {
134 reason: format!(
135 "Incompatible version: expected {STATE_FILE_VERSION}, got {}",
136 state.version
137 ),
138 };
139 }
140 LoadResult::Loaded(state.snapshots)
141 }
142 Err(e) => LoadResult::Corrupted {
143 reason: format!("Invalid JSON: {e}"),
144 },
145 }
146 }
147
148 async fn save(&self, snapshots: &[AdapterSnapshot]) -> Result<(), StateError> {
149 let path = self.path.clone();
150 let state = StateFile::new(snapshots);
151
152 tokio::task::spawn_blocking(move || Self::save_blocking(&path, &state))
154 .await
155 .expect("spawn_blocking task panicked")
156 }
157}