use std::collections::BTreeSet;
use std::fs;
use std::path::Path;
use std::path::PathBuf;
use serde::Deserialize;
use serde::Serialize;
use crate::error::Result;
use crate::error::SnapshotError;
pub const DECLARED_WINDOW_TURNS: u64 = 100;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct Declaration {
turn: u64,
path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct DeclaredFile {
version: u32,
turns: Vec<String>,
entries: Vec<Declaration>,
}
impl Default for DeclaredFile {
fn default() -> Self {
Self {
version: crate::workspace::FORMAT_VERSION,
turns: Vec::new(),
entries: Vec::new(),
}
}
}
pub struct DeclaredStore {
root: PathBuf,
}
impl DeclaredStore {
pub fn open(root: impl Into<PathBuf>) -> Result<Self> {
let root = root.into();
fs::create_dir_all(&root).map_err(|e| SnapshotError::io(&root, e))?;
Ok(Self { root })
}
pub fn declare(&self, session_id: &str, turn_id: &str, paths: &[PathBuf]) -> Result<()> {
if paths.is_empty() {
return Ok(());
}
let mut file = self.load(session_id)?;
let turn = match file.turns.iter().position(|t| t == turn_id) {
Some(index) => index as u64,
None => {
file.turns.push(turn_id.to_string());
(file.turns.len() - 1) as u64
}
};
for path in paths {
file.entries.retain(|entry| &entry.path != path);
file.entries.push(Declaration {
turn,
path: path.clone(),
});
}
self.save(session_id, &file)
}
pub fn note_turn(&self, session_id: &str, turn_id: &str) -> Result<()> {
let mut file = self.load(session_id)?;
if file.turns.iter().any(|t| t == turn_id) {
return Ok(());
}
if file.entries.is_empty() {
return Ok(());
}
file.turns.push(turn_id.to_string());
self.save(session_id, &file)
}
pub fn active(&self, session_id: &str) -> Result<BTreeSet<PathBuf>> {
let file = self.load(session_id)?;
let latest = file.turns.len() as u64;
let cutoff = latest.saturating_sub(DECLARED_WINDOW_TURNS);
Ok(file
.entries
.into_iter()
.filter(|entry| entry.turn + 1 > cutoff)
.map(|entry| entry.path)
.collect())
}
pub fn all(&self, session_id: &str) -> Result<BTreeSet<PathBuf>> {
Ok(self
.load(session_id)?
.entries
.into_iter()
.map(|entry| entry.path)
.collect())
}
pub fn remove(&self, session_id: &str) -> Result<()> {
let path = self.path_for(session_id)?;
match fs::remove_file(&path) {
Ok(()) => Ok(()),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(e) => Err(SnapshotError::io(&path, e)),
}
}
pub fn session_ids(&self) -> Result<Vec<String>> {
let entries = fs::read_dir(&self.root).map_err(|e| SnapshotError::io(&self.root, e))?;
let mut out = Vec::new();
for entry in entries.flatten() {
let name = entry.file_name().to_string_lossy().into_owned();
if let Some(id) = name.strip_suffix(".json") {
out.push(id.to_string());
}
}
Ok(out)
}
pub fn settled(&self, session_id: &str) -> bool {
self.path_for(session_id)
.is_ok_and(|path| crate::sweep::settled(&path))
}
fn load(&self, session_id: &str) -> Result<DeclaredFile> {
let path = self.path_for(session_id)?;
match fs::read(&path) {
Ok(bytes) => {
let file: DeclaredFile = serde_json::from_slice(&bytes)?;
if file.version != crate::workspace::FORMAT_VERSION {
return Err(SnapshotError::UnknownRecordVersion {
kind: "declared set",
id: session_id.to_string(),
found: file.version,
supported: crate::workspace::FORMAT_VERSION,
});
}
Ok(file)
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(DeclaredFile::default()),
Err(e) => Err(SnapshotError::io(&path, e)),
}
}
fn save(&self, session_id: &str, file: &DeclaredFile) -> Result<()> {
let path = self.path_for(session_id)?;
let tmp = crate::sweep::tmp_name(&path);
fs::write(&tmp, serde_json::to_vec_pretty(file)?)
.map_err(|e| SnapshotError::io(&tmp, e))?;
fs::rename(&tmp, &path).map_err(|e| SnapshotError::io(&path, e))
}
fn path_for(&self, session_id: &str) -> Result<PathBuf> {
crate::id::validate_stored("session id", session_id)?;
Ok(self.root.join(format!("{session_id}.json")))
}
}
pub(crate) fn dir_in(partition: &Path) -> PathBuf {
partition.join("declared")
}
#[cfg(test)]
#[path = "declared_tests.rs"]
mod tests;