use std::path::{Path, PathBuf};
use crate::{StorageError, StorageResult};
pub const GENERATION_SEP: &str = "__g";
pub fn logical_name(name: &str) -> &str {
if let Some(pos) = name.rfind(GENERATION_SEP) {
let suffix = &name[pos + GENERATION_SEP.len()..];
if !suffix.is_empty() && suffix.bytes().all(|b| b.is_ascii_digit()) {
return &name[..pos];
}
}
name
}
pub fn generation_name(logical: &str, generation: u64) -> String {
format!("{logical}{GENERATION_SEP}{generation}")
}
pub fn parse_generation(name: &str) -> Option<u64> {
let pos = name.rfind(GENERATION_SEP)?;
let suffix = &name[pos + GENERATION_SEP.len()..];
if suffix.is_empty() || !suffix.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
suffix.parse().ok()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GenerationInfo {
pub generation: u64,
pub metadata_path: PathBuf,
}
pub async fn list_generations(base: &Path, logical: &str) -> StorageResult<Vec<GenerationInfo>> {
let mut out = Vec::new();
for (generation, metadata_path) in scan_generation_paths(base, logical).await? {
if metadata_path.is_file() {
out.push(GenerationInfo {
generation,
metadata_path,
});
}
}
out.sort_by_key(|g| g.generation);
Ok(out)
}
pub async fn list_artifact_generations(base: &Path, logical: &str) -> StorageResult<Vec<u64>> {
let mut gens = std::collections::BTreeSet::new();
let mut rd = tokio::fs::read_dir(base)
.await
.map_err(|e| StorageError::Io(e.to_string()))?;
let artifact_prefix = format!("{logical}{GENERATION_SEP}");
while let Some(entry) = rd
.next_entry()
.await
.map_err(|e| StorageError::Io(e.to_string()))?
{
let name = entry.file_name();
let name = name.to_string_lossy();
let Some(rest) = name.strip_prefix(&artifact_prefix) else {
continue;
};
let Some(num) = rest.split('_').next() else {
continue;
};
if !num.is_empty() && num.bytes().all(|b| b.is_ascii_digit()) {
gens.insert(num.parse::<u64>().unwrap_or(u64::MAX));
}
}
Ok(gens.into_iter().collect())
}
pub async fn delete_generation(base: &Path, logical: &str, generation: u64) -> StorageResult<()> {
let prefix = format!("{logical}{GENERATION_SEP}{generation}_");
let mut rd = tokio::fs::read_dir(base)
.await
.map_err(|e| StorageError::Io(e.to_string()))?;
while let Some(entry) = rd
.next_entry()
.await
.map_err(|e| StorageError::Io(e.to_string()))?
{
if !entry.file_name().to_string_lossy().starts_with(&prefix) {
continue;
}
let path = entry.path();
if path.is_dir() {
tokio::fs::remove_dir_all(&path)
.await
.map_err(|e| StorageError::Io(e.to_string()))?;
} else {
tokio::fs::remove_file(&path)
.await
.map_err(|e| StorageError::Io(e.to_string()))?;
}
}
Ok(())
}
pub fn write_json_atomic(path: &Path, contents: &str) -> StorageResult<()> {
use std::io::Write;
let tmp = path.with_extension("json.tmp");
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(|e| StorageError::Io(e.to_string()))?;
}
{
let mut f = std::fs::File::create(&tmp).map_err(|e| StorageError::Io(e.to_string()))?;
f.write_all(contents.as_bytes())
.map_err(|e| StorageError::Io(e.to_string()))?;
f.sync_all().map_err(|e| StorageError::Io(e.to_string()))?;
}
std::fs::rename(&tmp, path).map_err(|e| StorageError::Io(e.to_string()))?;
Ok(())
}
async fn scan_generation_paths(base: &Path, logical: &str) -> StorageResult<Vec<(u64, PathBuf)>> {
let mut out = Vec::new();
let mut rd = tokio::fs::read_dir(base)
.await
.map_err(|e| StorageError::Io(e.to_string()))?;
let prefix = format!("{logical}{GENERATION_SEP}");
while let Some(entry) = rd
.next_entry()
.await
.map_err(|e| StorageError::Io(e.to_string()))?
{
let name = entry.file_name();
let name = name.to_string_lossy().into_owned();
let Some(rest) = name.strip_prefix(&prefix) else {
continue;
};
let Some(meta) = rest.strip_suffix("_metadata.json") else {
continue;
};
if !meta.is_empty() && meta.bytes().all(|b| b.is_ascii_digit()) {
out.push((meta.parse().unwrap_or(u64::MAX), entry.path()));
}
}
Ok(out)
}