foxtive_supervisor/persistence/
fs.rs1use super::{PersistedTaskState, TaskStateStore};
2use std::path::{Path, PathBuf};
3use tokio::fs;
4use tokio::io::{AsyncReadExt, AsyncWriteExt};
5
6pub struct FsStateStore {
9 base_path: PathBuf,
10}
11
12impl FsStateStore {
13 pub async fn new(base_path: impl AsRef<Path>) -> anyhow::Result<Self> {
14 let path = base_path.as_ref().to_path_buf();
15 fs::create_dir_all(&path).await?;
16 Ok(Self { base_path: path })
17 }
18
19 fn state_file_path(&self, task_id: &str) -> PathBuf {
20 self.base_path.join(format!("{}.json", task_id))
21 }
22}
23
24#[async_trait::async_trait]
25impl TaskStateStore for FsStateStore {
26 async fn save_state(&self, state: PersistedTaskState) -> anyhow::Result<()> {
27 let file_path = self.state_file_path(&state.task_id);
28 let json = serde_json::to_string_pretty(&state)?;
29 let mut file = fs::File::create(&file_path).await?;
30 file.write_all(json.as_bytes()).await?;
31 file.sync_all().await?; Ok(())
33 }
34
35 async fn load_state(&self, task_id: &str) -> anyhow::Result<Option<PersistedTaskState>> {
36 let file_path = self.state_file_path(task_id);
37 if !file_path.exists() {
38 return Ok(None);
39 }
40
41 let mut file = fs::File::open(&file_path).await?;
42 let mut json = String::new();
43 file.read_to_string(&mut json).await?;
44
45 if json.is_empty() {
46 return Ok(None);
47 }
48
49 let state: PersistedTaskState = serde_json::from_str(&json)?;
50 Ok(Some(state))
51 }
52
53 async fn load_all_states(&self) -> anyhow::Result<Vec<PersistedTaskState>> {
54 let mut states = Vec::new();
55 let mut entries = fs::read_dir(&self.base_path).await?;
56
57 while let Some(entry) = entries.next_entry().await? {
58 let path = entry.path();
59 if path.is_file() && path.extension().is_some_and(|ext| ext == "json") {
60 let mut file = fs::File::open(&path).await?;
61 let mut json = String::new();
62 file.read_to_string(&mut json).await?;
63
64 if json.is_empty() {
65 continue;
66 }
67
68 if let Ok(state) = serde_json::from_str::<PersistedTaskState>(&json) {
69 states.push(state);
70 } else {
71 eprintln!("Warning: Could not parse state file: {:?}", path);
73 }
74 }
75 }
76 Ok(states)
77 }
78
79 async fn delete_state(&self, task_id: &str) -> anyhow::Result<()> {
80 let file_path = self.state_file_path(task_id);
81 if file_path.exists() {
82 fs::remove_file(&file_path).await?;
83 }
84 Ok(())
85 }
86}