use crate::Result;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: String,
pub name: Option<String>,
pub job_ids: Vec<String>,
pub created_at: u64,
pub updated_at: u64,
pub status: SessionStatus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SessionStatus {
Active,
Paused,
Completed,
Terminated,
}
pub struct SessionManager {
sessions_dir: PathBuf,
}
impl SessionManager {
pub fn new(sessions_dir: PathBuf) -> Result<Self> {
std::fs::create_dir_all(&sessions_dir).map_err(|e| {
crate::Error::BackupError(format!("Failed to create sessions dir: {}", e))
})?;
Ok(Self { sessions_dir })
}
pub fn create_session(&mut self, name: Option<&str>) -> Result<Session> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let ts = now.as_secs();
let id = format!("{:x}{:x}", ts, now.subsec_nanos());
let session = Session {
id: id.clone(),
name: name.map(String::from),
job_ids: Vec::new(),
created_at: ts,
updated_at: ts,
status: SessionStatus::Active,
};
self.save_session(&session)?;
Ok(session)
}
pub fn load_session(&self, session_id: &str) -> Result<Session> {
let path = self.session_path(session_id);
let content = std::fs::read_to_string(&path)
.map_err(|_| crate::Error::BackupError(format!("Session not found: {}", session_id)))?;
serde_json::from_str(&content)
.map_err(|e| crate::Error::BackupError(format!("Failed to parse session: {}", e)))
}
pub fn add_job(&mut self, session_id: &str, job_id: &str) -> Result<()> {
let mut session = self.load_session(session_id)?;
session.job_ids.push(job_id.to_string());
session.updated_at = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
self.save_session(&session)
}
pub fn list_sessions(&self) -> Result<Vec<Session>> {
let mut sessions = Vec::new();
if !self.sessions_dir.exists() {
return Ok(sessions);
}
for entry in std::fs::read_dir(&self.sessions_dir)
.map_err(|e| crate::Error::BackupError(format!("Failed to read sessions: {}", e)))?
{
let entry = entry
.map_err(|e| crate::Error::BackupError(format!("Failed to read entry: {}", e)))?;
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "json") {
if let Ok(content) = std::fs::read_to_string(&path) {
if let Ok(session) = serde_json::from_str(&content) {
sessions.push(session);
}
}
}
}
sessions.sort_by_key(|s: &Session| s.updated_at);
sessions.reverse();
Ok(sessions)
}
fn session_path(&self, session_id: &str) -> PathBuf {
self.sessions_dir.join(format!("{}.json", session_id))
}
fn save_session(&self, session: &Session) -> Result<()> {
let path = self.session_path(&session.id);
let content = serde_json::to_string_pretty(session).map_err(|e| {
crate::Error::BackupError(format!("Failed to serialize session: {}", e))
})?;
std::fs::write(&path, content)
.map_err(|e| crate::Error::BackupError(format!("Failed to write session: {}", e)))?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn tmp_dir(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("runtimo_test_sessions_{}", name));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn creates_session() {
let dir = tmp_dir("creates");
let mut mgr = SessionManager::new(dir).unwrap();
let session = mgr.create_session(Some("test")).unwrap();
assert!(session.id.len() > 0);
assert_eq!(session.name, Some("test".to_string()));
assert_eq!(session.job_ids.len(), 0);
}
#[test]
fn adds_job_to_session() {
let dir = tmp_dir("adds_job");
let mut mgr = SessionManager::new(dir).unwrap();
let session = mgr.create_session(None).unwrap();
mgr.add_job(&session.id, "job-123").unwrap();
let loaded = mgr.load_session(&session.id).unwrap();
assert_eq!(loaded.job_ids.len(), 1);
assert_eq!(loaded.job_ids[0], "job-123");
}
#[test]
fn lists_sessions() {
let dir = tmp_dir("lists");
let mut mgr = SessionManager::new(dir.clone()).unwrap();
let _ = mgr.create_session(Some("first")).unwrap();
let _ = mgr.create_session(Some("second")).unwrap();
let sessions = mgr.list_sessions().unwrap();
assert_eq!(sessions.len(), 2);
}
}