1use crate::Result;
32use serde::{Deserialize, Serialize};
33use std::path::PathBuf;
34use std::time::{SystemTime, UNIX_EPOCH};
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Session {
39 pub id: String,
41 pub name: Option<String>,
43 pub job_ids: Vec<String>,
45 pub created_at: u64,
47 pub updated_at: u64,
49 pub status: SessionStatus,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum SessionStatus {
57 Active,
59 Paused,
61 Completed,
63 Terminated,
65}
66
67pub struct SessionManager {
69 sessions_dir: PathBuf,
70}
71
72impl SessionManager {
73 pub fn new(sessions_dir: PathBuf) -> Result<Self> {
75 std::fs::create_dir_all(&sessions_dir).map_err(|e| {
76 crate::Error::BackupError(format!("Failed to create sessions dir: {}", e))
77 })?;
78 Ok(Self { sessions_dir })
79 }
80
81 pub fn create_session(&mut self, name: Option<&str>) -> Result<Session> {
83 let now = SystemTime::now()
84 .duration_since(UNIX_EPOCH)
85 .unwrap_or_default();
86 let ts = now.as_secs();
87 let id = crate::utils::generate_id();
88
89 let session = Session {
90 id: id.clone(),
91 name: name.map(String::from),
92 job_ids: Vec::new(),
93 created_at: ts,
94 updated_at: ts,
95 status: SessionStatus::Active,
96 };
97
98 self.save_session(&session)?;
99 Ok(session)
100 }
101
102 pub fn load_session(&self, session_id: &str) -> Result<Session> {
104 let path = self.session_path(session_id);
105 let content = std::fs::read_to_string(&path)
106 .map_err(|_| crate::Error::BackupError(format!("Session not found: {}", session_id)))?;
107 serde_json::from_str(&content)
108 .map_err(|e| crate::Error::BackupError(format!("Failed to parse session: {}", e)))
109 }
110
111 pub fn add_job(&mut self, session_id: &str, job_id: &str) -> Result<()> {
113 let mut session = self.load_session(session_id)?;
114 session.job_ids.push(job_id.to_string());
115 session.updated_at = SystemTime::now()
116 .duration_since(UNIX_EPOCH)
117 .unwrap_or_default()
118 .as_secs();
119 self.save_session(&session)
120 }
121
122 pub fn list_sessions(&self) -> Result<Vec<Session>> {
124 let mut sessions = Vec::new();
125 if !self.sessions_dir.exists() {
126 return Ok(sessions);
127 }
128
129 for entry in std::fs::read_dir(&self.sessions_dir)
130 .map_err(|e| crate::Error::BackupError(format!("Failed to read sessions: {}", e)))?
131 {
132 let entry = entry
133 .map_err(|e| crate::Error::BackupError(format!("Failed to read entry: {}", e)))?;
134 let path = entry.path();
135 if path.extension().is_some_and(|ext| ext == "json") {
136 if let Ok(content) = std::fs::read_to_string(&path) {
137 if let Ok(session) = serde_json::from_str(&content) {
138 sessions.push(session);
139 }
140 }
141 }
142 }
143
144 sessions.sort_by_key(|s: &Session| s.updated_at);
145 sessions.reverse();
146 Ok(sessions)
147 }
148
149 fn session_path(&self, session_id: &str) -> PathBuf {
150 self.sessions_dir.join(format!("{}.json", session_id))
151 }
152
153 fn save_session(&self, session: &Session) -> Result<()> {
154 let path = self.session_path(&session.id);
155 let content = serde_json::to_string_pretty(session).map_err(|e| {
156 crate::Error::BackupError(format!("Failed to serialize session: {}", e))
157 })?;
158 std::fs::write(&path, content)
159 .map_err(|e| crate::Error::BackupError(format!("Failed to write session: {}", e)))?;
160 Ok(())
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167 use std::fs;
168
169 fn tmp_dir(name: &str) -> PathBuf {
170 let dir = std::env::temp_dir().join(format!("runtimo_test_sessions_{}", name));
171 let _ = fs::remove_dir_all(&dir);
172 fs::create_dir_all(&dir).unwrap();
173 dir
174 }
175
176 #[test]
177 fn creates_session() {
178 let dir = tmp_dir("creates");
179 let mut mgr = SessionManager::new(dir).unwrap();
180 let session = mgr.create_session(Some("test")).unwrap();
181 assert!(!session.id.is_empty());
182 assert_eq!(session.name, Some("test".to_string()));
183 assert_eq!(session.job_ids.len(), 0);
184 }
185
186 #[test]
187 fn adds_job_to_session() {
188 let dir = tmp_dir("adds_job");
189 let mut mgr = SessionManager::new(dir).unwrap();
190 let session = mgr.create_session(None).unwrap();
191 mgr.add_job(&session.id, "job-123").unwrap();
192
193 let loaded = mgr.load_session(&session.id).unwrap();
194 assert_eq!(loaded.job_ids.len(), 1);
195 assert_eq!(loaded.job_ids[0], "job-123");
196 }
197
198 #[test]
199 fn lists_sessions() {
200 let dir = tmp_dir("lists");
201 let mut mgr = SessionManager::new(dir.clone()).unwrap();
202 let _ = mgr.create_session(Some("first")).unwrap();
203 let _ = mgr.create_session(Some("second")).unwrap();
204
205 let sessions = mgr.list_sessions().unwrap();
206 assert_eq!(sessions.len(), 2);
207 }
208}