use std::fs;
use std::path::{Path, PathBuf};
pub(super) const SEGMENT_MAX_BYTES: u64 = 64 * 1024 * 1024;
pub(super) const SEAL_EVENT: &str = "x-segment_sealed";
pub(super) fn dir(trail: &Path) -> PathBuf {
let stem = trail.file_stem().map_or_else(
|| "trail".into(),
|stem| stem.to_string_lossy().into_owned(),
);
trail.with_file_name(format!("{stem}.segments"))
}
pub(super) fn path(trail: &Path, seq: u64) -> PathBuf {
dir(trail).join(format!("{seq:06}.jsonl"))
}
pub(super) fn sealed(trail: &Path) -> Vec<(u64, PathBuf)> {
let Ok(entries) = fs::read_dir(dir(trail)) else {
return Vec::new();
};
let mut found: Vec<(u64, PathBuf)> = entries
.filter_map(Result::ok)
.filter_map(|entry| {
let name = entry.file_name();
let digits = name.to_str()?.strip_suffix(".jsonl")?;
if digits.len() < 6 || !digits.bytes().all(|b| b.is_ascii_digit()) {
return None;
}
Some((digits.parse().ok()?, entry.path()))
})
.collect();
found.sort_unstable_by_key(|(seq, _)| *seq);
found
}
pub(super) fn chain(trail: &Path) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = sealed(trail).into_iter().map(|(_, path)| path).collect();
files.push(trail.to_path_buf());
files
}
pub(super) fn seal_action(seq: u64) -> String {
format!("segment={seq}")
}
pub(super) fn sealed_seq(action: Option<&str>) -> Option<u64> {
action?.strip_prefix("segment=")?.parse().ok()
}
pub(super) fn due(len: u64, last: Option<&str>, now: chrono::DateTime<chrono::Utc>) -> bool {
if len == 0 {
return false;
}
if len >= SEGMENT_MAX_BYTES {
return true;
}
last.and_then(|stamp| chrono::DateTime::parse_from_rfc3339(stamp).ok())
.is_some_and(|last| last.with_timezone(&chrono::Utc).date_naive() < now.date_naive())
}
const BACK_OFF_SECS: i64 = 60 * 60;
static BACK_OFF_UNTIL: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
pub(super) fn back_off() {
BACK_OFF_UNTIL.store(
chrono::Utc::now().timestamp() + BACK_OFF_SECS,
std::sync::atomic::Ordering::Relaxed,
);
}
pub(super) fn backing_off() -> bool {
chrono::Utc::now().timestamp() < BACK_OFF_UNTIL.load(std::sync::atomic::Ordering::Relaxed)
}
pub(super) fn retire(trail: &Path, seq: u64) -> Result<(), String> {
let dir = dir(trail);
fs::create_dir_all(&dir).map_err(|e| format!("audit segments dir: {e}"))?;
crate::core::data_dir::ensure_dir_permissions(&dir);
let target = path(trail, seq);
if target.exists() {
return Err(format!("audit segment {seq} already exists"));
}
fs::rename(trail, &target).map_err(|e| format!("audit segment rename: {e}"))?;
#[cfg(unix)]
for synced in [
&dir,
&trail.parent().map_or_else(PathBuf::new, Path::to_path_buf),
] {
if let Ok(handle) = fs::File::open(synced) {
let _ = handle.sync_all();
}
}
Ok(())
}