use std::fs;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::bundle::list_sessions;
use crate::storage::now_ms;
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RetentionPolicy {
pub max_age_days: Option<u32>,
pub max_sessions: Option<u32>,
}
impl RetentionPolicy {
pub fn is_empty(&self) -> bool {
self.max_age_days.is_none() && self.max_sessions.is_none()
}
}
pub fn enforce_retention(root: &Path, policy: &RetentionPolicy) -> Result<usize, String> {
if policy.is_empty() {
return Ok(0);
}
let mut sessions = list_sessions(root);
let now = now_ms();
let mut removed = 0usize;
if let Some(days) = policy.max_age_days {
let cutoff = now - (days as i64) * 86_400_000;
sessions.retain(|s| {
let keep = s["endedAt"].as_i64().map_or(true, |t| t >= cutoff);
if !keep {
if let Some(id) = s["id"].as_str() {
let _ = fs::remove_dir_all(root.join(id));
removed += 1;
}
}
keep
});
}
if let Some(max) = policy.max_sessions {
sessions.sort_by(|a, b| b["startedAt"].as_i64().cmp(&a["startedAt"].as_i64()));
for s in sessions.iter().skip(max as usize) {
if let Some(id) = s["id"].as_str() {
let _ = fs::remove_dir_all(root.join(id));
removed += 1;
}
}
}
Ok(removed)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use tempfile::tempdir;
fn make_session(root: &Path, id: &str, started: i64, ended: Option<i64>) {
let dir = root.join(id);
fs::create_dir_all(dir.join("segments")).unwrap();
let mut s = json!({ "id": id, "startedAt": started });
if let Some(e) = ended {
s["endedAt"] = json!(e);
}
fs::write(dir.join("session.json"), s.to_string()).unwrap();
}
#[test]
fn enforces_max_age() {
let root = tempdir().unwrap();
let now = now_ms();
make_session(root.path(), "old", now - 86_400_000 * 40, Some(now - 86_400_000 * 40));
make_session(root.path(), "fresh", now - 1000, Some(now - 1000));
make_session(root.path(), "active", now - 2000, None);
let removed = enforce_retention(
root.path(),
&RetentionPolicy {
max_age_days: Some(30),
max_sessions: None,
},
)
.unwrap();
assert_eq!(removed, 1);
assert!(!root.path().join("old").exists());
assert!(root.path().join("fresh").exists());
assert!(root.path().join("active").exists());
}
#[test]
fn enforces_max_sessions() {
let root = tempdir().unwrap();
make_session(root.path(), "s1", 1000, Some(1100));
make_session(root.path(), "s2", 2000, Some(2100));
make_session(root.path(), "s3", 3000, Some(3100));
let removed = enforce_retention(
root.path(),
&RetentionPolicy {
max_age_days: None,
max_sessions: Some(2),
},
)
.unwrap();
assert_eq!(removed, 1);
assert!(!root.path().join("s1").exists());
assert!(root.path().join("s2").exists());
assert!(root.path().join("s3").exists());
}
#[test]
fn empty_policy_noop() {
let root = tempdir().unwrap();
make_session(root.path(), "s1", 1000, Some(1100));
let removed = enforce_retention(root.path(), &RetentionPolicy::default()).unwrap();
assert_eq!(removed, 0);
assert!(root.path().join("s1").exists());
}
}