use super::{ResolvedSession, SESSION_SCRATCH_ROOT, SessionSpec, keeps_scratch_outside_working_directory};
use crate::snippets::error::{Error, Result};
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
pub(super) fn purge_stale_session_scratch(resolved: &[ResolvedSession<'_>]) {
let mut live: BTreeMap<&Path, BTreeSet<&str>> = BTreeMap::new();
for (_, spec, session) in resolved {
let fingerprints = live.entry(spec.working_directory.as_path()).or_default();
if !keeps_scratch_outside_working_directory(spec.language) {
fingerprints.insert(session.fingerprint.as_str());
}
}
for (working_directory, fingerprints) in live {
if let Err(error) = purge_session_scratch_root(working_directory, &fingerprints) {
tracing::warn!(
working_directory = %working_directory.display(),
error = %error,
"could not purge stale snippet session scratch"
);
}
}
}
fn purge_session_scratch_root(working_directory: &Path, live_fingerprints: &BTreeSet<&str>) -> Result<()> {
let root = working_directory.join(SESSION_SCRATCH_ROOT);
let entries = match std::fs::read_dir(&root) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => {
return Err(Error::Other(format!(
"reading snippet session scratch root {}: {error}",
root.display()
)));
}
};
for entry in entries {
let entry = entry.map_err(|error| {
Error::Other(format!(
"reading an entry in snippet session scratch root {}: {error}",
root.display()
))
})?;
let path = entry.path();
let name = entry.file_name();
let is_directory = entry.file_type().is_ok_and(|file_type| file_type.is_dir());
let is_live = name.to_str().is_some_and(|name| live_fingerprints.contains(name));
if !is_directory {
remove_scratch(std::fs::remove_file(&path), &path)?;
} else if is_live {
purge_stale_workspace_scratch_files(&path)?;
} else {
remove_scratch(std::fs::remove_dir_all(&path), &path)?;
}
}
Ok(())
}
fn remove_scratch(outcome: std::io::Result<()>, path: &Path) -> Result<()> {
match outcome {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(Error::Other(format!(
"removing stale snippet session scratch {}: {error}",
path.display()
))),
}
}
fn purge_stale_workspace_scratch_files(directory: &Path) -> Result<()> {
let entries = match std::fs::read_dir(directory) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => {
return Err(Error::Other(format!(
"reading snippet workspace directory {}: {error}",
directory.display()
)));
}
};
for entry in entries {
let entry = entry.map_err(|error| {
Error::Other(format!(
"reading an entry in snippet workspace directory {}: {error}",
directory.display()
))
})?;
let is_stale_file = entry.file_type().is_ok_and(|file_type| file_type.is_file());
if !is_stale_file {
continue;
}
if let Err(error) = std::fs::remove_file(entry.path())
&& error.kind() != std::io::ErrorKind::NotFound
{
return Err(Error::Other(format!(
"removing stale snippet scratch file {}: {error}",
entry.path().display()
)));
}
}
Ok(())
}
pub(super) fn purge_abandoned_scratch(spec: &SessionSpec, timeout_secs: u64) {
let root = crate::snippets::scratch::scratch_root(spec.language, &spec.working_directory, spec.manifest.as_deref());
if let Err(error) = crate::snippets::scratch::purge_stale_scratch_root(&root, timeout_secs) {
tracing::warn!(
scratch_root = %root.display(),
language = %spec.language,
error = %error,
"could not purge abandoned snippet scratch"
);
}
}
pub(super) fn cleanup_legacy_scratch_directories(working_directory: &Path, timeout_secs: u64) -> Result<()> {
let stale_after = std::time::Duration::from_secs(timeout_secs.saturating_add(60));
let entries = std::fs::read_dir(working_directory).map_err(|error| {
Error::Other(format!(
"reading snippet working directory {}: {error}",
working_directory.display()
))
})?;
for entry in entries {
let entry = match entry {
Ok(entry) => entry,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => {
return Err(Error::Other(format!(
"reading an entry in snippet working directory {}: {error}",
working_directory.display()
)));
}
};
let entry_type = match entry.file_type() {
Ok(entry_type) => entry_type,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => {
return Err(Error::Other(format!(
"reading snippet scratch entry type {}: {error}",
entry.path().display()
)));
}
};
if !entry_type.is_dir() || !entry.file_name().to_string_lossy().starts_with(".alef-snippet-") {
continue;
}
let modified = match entry.metadata() {
Ok(metadata) => metadata.modified().map_err(|error| {
Error::Other(format!(
"reading snippet scratch modification time {}: {error}",
entry.path().display()
))
})?,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => {
return Err(Error::Other(format!(
"reading snippet scratch metadata {}: {error}",
entry.path().display()
)));
}
};
if modified.elapsed().is_ok_and(|age| age >= stale_after)
&& let Err(error) = std::fs::remove_dir_all(entry.path())
&& error.kind() != std::io::ErrorKind::NotFound
{
return Err(Error::Other(format!(
"removing stale snippet scratch directory {}: {error}",
entry.path().display()
)));
}
}
Ok(())
}