use std::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};
use fake::Fake;
use super::LiveFolderConfig;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EntryKind {
File,
Folder,
}
#[derive(Clone, Debug)]
pub struct Entry {
pub name: String,
pub kind: EntryKind,
pub size: u64,
pub created: SystemTime,
pub modified: SystemTime,
pub children: BTreeMap<String, Entry>,
}
impl Entry {
pub fn folder(name: String, now: SystemTime) -> Self {
Self {
name,
kind: EntryKind::Folder,
size: 0,
created: now,
modified: now,
children: BTreeMap::new(),
}
}
pub fn file(name: String, size: u64, now: SystemTime) -> Self {
Self {
name,
kind: EntryKind::File,
size,
created: now,
modified: now,
children: BTreeMap::new(),
}
}
}
#[derive(Debug)]
pub struct Tree {
pub root: Entry,
}
impl Tree {
pub fn new(now: SystemTime) -> Self {
Self {
root: Entry::folder(String::new(), now),
}
}
pub fn get(&self, path: &str) -> Option<&Entry> {
if path.is_empty() {
return Some(&self.root);
}
let mut cur = &self.root;
for seg in path.split('/') {
cur = cur.children.get(seg)?;
}
Some(cur)
}
pub fn get_mut(&mut self, path: &str) -> Option<&mut Entry> {
if path.is_empty() {
return Some(&mut self.root);
}
let mut cur = &mut self.root;
for seg in path.split('/') {
cur = cur.children.get_mut(seg)?;
}
Some(cur)
}
pub fn touch(&mut self, path: &str, when: SystemTime) {
self.root.modified = when;
if path.is_empty() {
return;
}
let mut cur = &mut self.root;
for seg in path.split('/') {
cur.modified = when;
cur = match cur.children.get_mut(seg) {
Some(c) => c,
None => return,
};
}
cur.modified = when;
}
}
pub fn simulate_second(tree: &mut Tree, cfg: &LiveFolderConfig, when: SystemTime) {
let (date_str, hour, hms) = split_time(when);
let access_folder_name = format!("access_logs_{hour:02}");
let access_folder_path = format!("{date_str}/{access_folder_name}");
ensure_path(tree, &[&date_str, &access_folder_name], when);
let line_len = rand_range(cfg.bytes_per_request.0, cfg.bytes_per_request.1 + 1);
let delta = line_len * cfg.requests_per_sec;
let chunk_name = bump_active_chunk(tree, &access_folder_path, cfg.chunk_threshold, delta, when);
tree.touch(&format!("{access_folder_path}/{chunk_name}"), when);
let roll: f64 = (0..10_000).fake::<u64>() as f64 / 100.0;
if roll < cfg.error_pct_per_sec {
let err_folder_path = format!("{date_str}/error_logs");
let err_file = format!("{hms}-errors.log");
let err_size = rand_range(cfg.error_size.0, cfg.error_size.1 + 1);
ensure_path(tree, &[&date_str, "error_logs"], when);
if let Some(folder) = tree.get_mut(&err_folder_path) {
folder.children.insert(
err_file.clone(),
Entry::file(err_file.clone(), err_size, when),
);
}
tree.touch(&format!("{err_folder_path}/{err_file}"), when);
}
for (etype, pct) in super::EVENT_TYPES {
if rand_range(0, 100) < *pct as u64 {
let ev_folder_path = format!("{date_str}/events");
let file_name = format!("{etype}.log");
let bump = rand_range(cfg.event_bump.0, cfg.event_bump.1 + 1);
ensure_path(tree, &[&date_str, "events"], when);
if let Some(folder) = tree.get_mut(&ev_folder_path) {
let file = folder
.children
.entry(file_name.clone())
.or_insert_with(|| Entry::file(file_name.clone(), 0, when));
file.size += bump;
file.modified = when;
}
tree.touch(&format!("{ev_folder_path}/{file_name}"), when);
}
}
}
fn ensure_path(tree: &mut Tree, segments: &[&str], now: SystemTime) {
let mut cur = &mut tree.root;
for seg in segments {
cur = cur
.children
.entry((*seg).to_string())
.or_insert_with(|| Entry::folder((*seg).to_string(), now));
}
}
fn bump_active_chunk(
tree: &mut Tree,
folder_path: &str,
threshold: u64,
delta: u64,
now: SystemTime,
) -> String {
let Some(folder) = tree.get_mut(folder_path) else {
return String::new();
};
let max_idx = folder
.children
.keys()
.filter_map(|k| {
k.strip_prefix("chunk_")
.and_then(|s| s.strip_suffix(".log"))
.and_then(|s| s.parse::<usize>().ok())
})
.max();
let idx = match max_idx {
None => 1,
Some(i) => {
let name = format!("chunk_{i:02}.log");
let size = folder.children.get(&name).map(|e| e.size).unwrap_or(0);
if size < threshold { i } else { i + 1 }
}
};
let name = format!("chunk_{idx:02}.log");
let chunk = folder
.children
.entry(name.clone())
.or_insert_with(|| Entry::file(name.clone(), 0, now));
chunk.size += delta;
chunk.modified = now;
if chunk.size >= threshold {
let next_name = format!("chunk_{:02}.log", idx + 1);
folder
.children
.entry(next_name.clone())
.or_insert_with(|| Entry::file(next_name, 0, now));
}
name
}
pub fn folder_size(entry: &Entry) -> (u64, u64) {
let mut size = 0u64;
let mut files = 0u64;
fn walk(e: &Entry, size: &mut u64, files: &mut u64) {
match e.kind {
EntryKind::File => {
*size += e.size;
*files += 1;
}
EntryKind::Folder => {
for c in e.children.values() {
walk(c, size, files);
}
}
}
}
walk(entry, &mut size, &mut files);
(size, files)
}
fn split_time(t: SystemTime) -> (String, u32, String) {
let secs = t
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let s = (secs % 60) as u32;
let m = ((secs / 60) % 60) as u32;
let h = ((secs / 3600) % 24) as u32;
let days = secs / 86400;
let (y, mo, d) = days_to_ymd(days);
(
format!("{y:04}-{mo:02}-{d:02}"),
h,
format!("{h:02}:{m:02}:{s:02}"),
)
}
pub fn format_ts(t: SystemTime) -> String {
let secs = t
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let s = (secs % 60) as u32;
let m = ((secs / 60) % 60) as u32;
let h = ((secs / 3600) % 24) as u32;
let days = secs / 86400;
let (y, mo, d) = days_to_ymd(days);
format!("{y:04}-{mo:02}-{d:02} {h:02}:{m:02}:{s:02}")
}
fn days_to_ymd(days: u64) -> (u32, u32, u32) {
let z = days as i64 + 719468;
let era = z.div_euclid(146097);
let doe = (z - era * 146097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
(y as u32, m as u32, d as u32)
}
pub fn rand_range(lo: u64, hi: u64) -> u64 {
if hi <= lo {
return lo;
}
(lo..hi).fake()
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn cfg() -> LiveFolderConfig {
LiveFolderConfig {
requests_per_sec: 100,
bytes_per_request: (60, 100),
chunk_threshold: 288_000,
error_pct_per_sec: 100.0, error_size: (500, 500),
event_bump: (2000, 2000),
backfill: Duration::ZERO,
}
}
#[test]
fn touch_propagates_modified_up_to_root() {
let mut tree = Tree::new(SystemTime::UNIX_EPOCH);
ensure_path(
&mut tree,
&["2026-06-03", "access_logs_11"],
SystemTime::UNIX_EPOCH,
);
let later = SystemTime::UNIX_EPOCH + Duration::from_secs(100);
tree.touch("2026-06-03/access_logs_11/chunk_01.log", later);
assert_eq!(tree.root.modified, later);
assert_eq!(tree.get("2026-06-03").unwrap().modified, later);
assert_eq!(
tree.get("2026-06-03/access_logs_11").unwrap().modified,
later
);
}
#[test]
fn chunk_rolls_when_threshold_met() {
let mut tree = Tree::new(SystemTime::UNIX_EPOCH);
let mut c = cfg();
c.chunk_threshold = 200; c.requests_per_sec = 1;
c.bytes_per_request = (100, 100);
simulate_second(&mut tree, &c, SystemTime::UNIX_EPOCH);
let f = tree.get("1970-01-01/access_logs_00").unwrap();
assert!(f.children.contains_key("chunk_01.log"));
assert_eq!(f.children["chunk_01.log"].size, 100);
simulate_second(
&mut tree,
&c,
SystemTime::UNIX_EPOCH + Duration::from_secs(1),
);
let f = tree.get("1970-01-01/access_logs_00").unwrap();
assert_eq!(f.children["chunk_01.log"].size, 200);
assert!(f.children.contains_key("chunk_02.log"));
assert_eq!(f.children["chunk_02.log"].size, 0);
}
#[test]
fn folder_size_walks_recursively() {
let now = SystemTime::UNIX_EPOCH;
let mut tree = Tree::new(now);
ensure_path(&mut tree, &["d", "access_logs_00"], now);
let f = tree.get_mut("d/access_logs_00").unwrap();
f.children.insert(
"chunk_01.log".into(),
Entry::file("chunk_01.log".into(), 100, now),
);
f.children.insert(
"chunk_02.log".into(),
Entry::file("chunk_02.log".into(), 250, now),
);
let (size, files) = folder_size(tree.get("d").unwrap());
assert_eq!(size, 350);
assert_eq!(files, 2);
}
}