opendev_runtime/
plan_index.rs1use chrono::Utc;
9use serde::{Deserialize, Serialize};
10use std::io::Write;
11use std::path::PathBuf;
12use tracing::warn;
13
14const INDEX_FILE: &str = "plans-index.json";
15const VERSION: u32 = 1;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct PlanEntry {
20 pub name: String,
21 #[serde(rename = "sessionId", skip_serializing_if = "Option::is_none")]
22 pub session_id: Option<String>,
23 #[serde(rename = "projectPath", skip_serializing_if = "Option::is_none")]
24 pub project_path: Option<String>,
25 pub created: String,
26}
27
28#[derive(Debug, Serialize, Deserialize)]
30struct IndexData {
31 version: u32,
32 entries: Vec<PlanEntry>,
33}
34
35impl Default for IndexData {
36 fn default() -> Self {
37 Self {
38 version: VERSION,
39 entries: Vec::new(),
40 }
41 }
42}
43
44pub struct PlanIndex {
46 plans_dir: PathBuf,
47 index_path: PathBuf,
48}
49
50impl PlanIndex {
51 pub fn new(plans_dir: impl Into<PathBuf>) -> Self {
56 let dir = plans_dir.into();
57 let index_path = dir.join(INDEX_FILE);
58 Self {
59 plans_dir: dir,
60 index_path,
61 }
62 }
63
64 fn read_index(&self) -> IndexData {
66 if !self.index_path.exists() {
67 return IndexData::default();
68 }
69 match std::fs::read_to_string(&self.index_path) {
70 Ok(content) => serde_json::from_str::<IndexData>(&content).unwrap_or_default(),
71 Err(_) => IndexData::default(),
72 }
73 }
74
75 fn write_index(&self, data: &IndexData) -> std::io::Result<()> {
77 std::fs::create_dir_all(&self.plans_dir)?;
78
79 let tmp_path = self.plans_dir.join(".plans-idx-tmp");
80 {
81 let mut f = std::fs::File::create(&tmp_path)?;
82 let json = serde_json::to_string_pretty(data).map_err(std::io::Error::other)?;
83 f.write_all(json.as_bytes())?;
84 f.write_all(b"\n")?;
85 f.sync_all()?;
86 }
87
88 std::fs::rename(&tmp_path, &self.index_path).inspect_err(|_| {
89 let _ = std::fs::remove_file(&tmp_path);
90 })
91 }
92
93 pub fn add_entry(&self, name: &str, session_id: Option<&str>, project_path: Option<&str>) {
97 let mut data = self.read_index();
98
99 data.entries.retain(|e| e.name != name);
101
102 data.entries.push(PlanEntry {
103 name: name.to_string(),
104 session_id: session_id.map(|s| s.to_string()),
105 project_path: project_path.map(|s| s.to_string()),
106 created: Utc::now().to_rfc3339(),
107 });
108
109 if let Err(e) = self.write_index(&data) {
110 warn!("Failed to write plan index: {}", e);
111 }
112 }
113
114 pub fn get_by_session(&self, session_id: &str) -> Option<PlanEntry> {
116 self.read_index()
117 .entries
118 .into_iter()
119 .find(|e| e.session_id.as_deref() == Some(session_id))
120 }
121
122 pub fn get_by_project(&self, project_path: &str) -> Vec<PlanEntry> {
124 self.read_index()
125 .entries
126 .into_iter()
127 .filter(|e| e.project_path.as_deref() == Some(project_path))
128 .collect()
129 }
130
131 pub fn remove_entry(&self, name: &str) {
133 let mut data = self.read_index();
134 data.entries.retain(|e| e.name != name);
135 if let Err(e) = self.write_index(&data) {
136 warn!("Failed to write plan index: {}", e);
137 }
138 }
139
140 pub fn list_all(&self) -> Vec<PlanEntry> {
142 self.read_index().entries
143 }
144}
145
146#[cfg(test)]
147#[path = "plan_index_tests.rs"]
148mod tests;