use std::fs::File;
use std::io::{self, Write};
use std::os::unix::fs::FileExt;
use std::path::{Path, PathBuf};
use anyhow::{bail, Context};
use chrono::{DateTime, Local, LocalResult, NaiveDateTime, NaiveTime, SecondsFormat, TimeZone};
use crate::format::{self, ChunkRecord};
pub fn fmt_ms_rfc3339(ms: u64) -> String {
match Local.timestamp_millis_opt(ms as i64) {
LocalResult::Single(dt) => dt.to_rfc3339_opts(SecondsFormat::Millis, true),
_ => format!("@{ms}ms"),
}
}
pub fn fmt_ms(ms: u64) -> String {
match Local.timestamp_millis_opt(ms as i64) {
LocalResult::Single(dt) => dt.format("%Y-%m-%d %H:%M:%S%.3f").to_string(),
_ => format!("@{ms}ms"),
}
}
pub fn parse_time(s: &str) -> anyhow::Result<u64> {
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Ok(dt.timestamp_millis() as u64);
}
for fmt in [
"%Y-%m-%d %H:%M:%S",
"%Y-%m-%dT%H:%M:%S",
"%Y-%m-%d %H:%M",
"%Y-%m-%dT%H:%M",
] {
if let Ok(naive) = NaiveDateTime::parse_from_str(s, fmt) {
if let Some(dt) = Local.from_local_datetime(&naive).earliest() {
return Ok(dt.timestamp_millis() as u64);
}
}
}
for fmt in ["%H:%M:%S%.3f", "%H:%M:%S", "%H:%M"] {
if let Ok(t) = NaiveTime::parse_from_str(s, fmt) {
let naive = Local::now().date_naive().and_time(t);
if let Some(dt) = Local.from_local_datetime(&naive).earliest() {
return Ok(dt.timestamp_millis() as u64);
}
}
}
if let Ok(n) = s.parse::<u64>() {
return Ok(if n > 100_000_000_000 { n } else { n * 1000 });
}
bail!(
"unrecognized time {s:?} (try RFC3339, 'YYYY-MM-DD HH:MM:SS', 'HH:MM[:SS]', \
or unix seconds)"
)
}
pub fn resolve_backing(input: &Path) -> anyhow::Result<(PathBuf, String)> {
let file_name = input
.file_name()
.and_then(|s| s.to_str())
.with_context(|| format!("bad path {}", input.display()))?;
let base = file_name
.strip_suffix(&format!(".{}", format::TRUNK_EXT))
.or_else(|| file_name.strip_suffix(&format!(".{}", format::RINGS_EXT)))
.unwrap_or(file_name);
let dir = match input.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => PathBuf::from("."),
};
Ok((dir, base.to_string()))
}
fn backing_paths(input: &Path) -> anyhow::Result<(PathBuf, PathBuf)> {
let (dir, base) = resolve_backing(input)?;
let trunk = format::trunk_path(&dir, &base);
let rings = format::rings_path(&dir, &base);
if !rings.exists() {
bail!(
"no index file {} (expected a timberfs backing file or its logical name)",
rings.display()
);
}
Ok((trunk, rings))
}
pub fn cmd_query(file: &Path, from: Option<&str>, to: Option<&str>) -> anyhow::Result<()> {
let (trunk_path, rings_path) = backing_paths(file)?;
let chunks = format::read_index(&rings_path)
.with_context(|| format!("reading index {}", rings_path.display()))?;
let from_ms = from.map(parse_time).transpose()?.unwrap_or(0);
let to_ms = to.map(parse_time).transpose()?.unwrap_or(u64::MAX);
if from_ms > to_ms {
bail!("--from is after --to");
}
let start = chunks.partition_point(|c| c.last_write_ms < from_ms);
let end = chunks.partition_point(|c| c.first_write_ms <= to_ms);
let selected: &[ChunkRecord] = &chunks[start..end.max(start)];
let trunk = File::open(&trunk_path)?;
let stdout = io::stdout();
let mut out = stdout.lock();
let mut uncomp_total = 0u64;
for c in selected {
let mut comp = vec![0u8; c.comp_len as usize];
trunk.read_exact_at(&mut comp, c.comp_start)?;
let data = zstd::stream::decode_all(&comp[..])?;
out.write_all(&data)?;
uncomp_total += c.uncomp_len;
}
out.flush()?;
eprintln!(
"timberfs: {} of {} chunk(s), {} bytes (chunk granularity; unflushed tail not included)",
selected.len(),
chunks.len(),
uncomp_total
);
Ok(())
}
pub fn cmd_index(file: &Path) -> anyhow::Result<()> {
let (_trunk_path, rings_path) = backing_paths(file)?;
let chunks = format::read_index(&rings_path)
.with_context(|| format!("reading index {}", rings_path.display()))?;
println!(
"{:>5} {:>12} {:>10} {:>10} {:>6} {:<23} {:<23}",
"chunk", "uncomp@", "bytes", "comp", "ratio", "first write", "last write"
);
let mut total_uncomp = 0u64;
let mut total_comp = 0u64;
for (i, c) in chunks.iter().enumerate() {
println!(
"{:>5} {:>12} {:>10} {:>10} {:>5.1}x {:<23} {:<23}",
i,
c.uncomp_start,
c.uncomp_len,
c.comp_len,
c.uncomp_len as f64 / c.comp_len.max(1) as f64,
fmt_ms(c.first_write_ms),
fmt_ms(c.last_write_ms)
);
total_uncomp += c.uncomp_len;
total_comp += c.comp_len;
}
println!(
"total: {} chunk(s), {} bytes uncompressed, {} compressed ({:.1}x)",
chunks.len(),
total_uncomp,
total_comp,
total_uncomp as f64 / total_comp.max(1) as f64
);
Ok(())
}