use serde_json::Value;
use std::{
ffi::OsStr,
fs,
io::{BufRead, BufReader, Read},
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
use super::progress::Progress;
use crate::error::{AppError, io_err};
pub fn home_dir() -> Result<PathBuf, AppError> {
std::env::home_dir().ok_or_else(|| AppError::Io {
operation: "resolve home directory",
path: "$HOME".to_string(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "home directory not found"),
})
}
pub fn env_abs_path(var: &str, raw: Option<&OsStr>, home: &Path) -> Option<PathBuf> {
let s = raw?.to_string_lossy().trim().to_string();
if s.is_empty() {
return None;
}
let path = if s == "~" {
home.to_path_buf()
} else if let Some(suffix) = s.strip_prefix("~/").or_else(|| s.strip_prefix("~\\")) {
home.join(suffix)
} else {
PathBuf::from(&s)
};
if path.is_absolute() {
Some(path)
} else {
eprintln!("> {var}='{s}' 不是绝对路径, 已忽略(回退默认路径)");
None
}
}
pub fn xdg_data_dir(home: &Path, raw: Option<&OsStr>) -> PathBuf {
match raw {
Some(v) if !v.to_string_lossy().trim().is_empty() => {
PathBuf::from(v.to_string_lossy().trim())
}
_ => home.join(".local").join("share"),
}
}
pub fn now_epoch() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
pub fn discover_files(base: &Path, extension: &str, max_depth: usize) -> Vec<PathBuf> {
let mut files = Vec::new();
collect_dir(base, extension, 0, max_depth, &mut files);
files.sort();
files
}
fn collect_dir(
dir: &Path,
extension: &str,
depth: usize,
max_depth: usize,
files: &mut Vec<PathBuf>,
) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
let path = entry.path();
if file_type.is_dir() {
if depth < max_depth {
collect_dir(&path, extension, depth + 1, max_depth, files);
}
} else if file_type.is_file()
&& path
.extension()
.and_then(|e| e.to_str())
.is_some_and(|e| e.eq_ignore_ascii_case(extension))
{
files.push(path);
}
}
}
const MAX_LINE_BYTES: usize = 16 * 1024 * 1024;
pub fn for_each_jsonl_progress(
path: &Path,
needles: &[&str],
progress: &mut Progress,
on_line: impl FnMut(Value) -> bool,
) -> Result<(), AppError> {
for_each_jsonl_impl(path, needles, MAX_LINE_BYTES, Some(progress), on_line)
}
fn for_each_jsonl_impl(
path: &Path,
needles: &[&str],
max_line: usize,
mut progress: Option<&mut Progress>,
mut on_line: impl FnMut(Value) -> bool,
) -> Result<(), AppError> {
let file = fs::File::open(path).map_err(|e| io_err("open", path, e))?;
let mut reader = BufReader::with_capacity(64 * 1024, file);
let mut line: Vec<u8> = Vec::new();
let mut warned = false;
loop {
line.clear();
let mut oversized = false;
let mut eof = true; let mut any = false;
loop {
let buf = match reader.fill_buf() {
Ok(buf) => buf,
Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(io_err("read", path, e)),
};
if buf.is_empty() {
break;
}
eof = false;
any = true;
match buf.iter().position(|&b| b == b'\n') {
Some(nl) => {
if !oversized && line.len() + nl <= max_line {
line.extend_from_slice(&buf[..nl]);
} else {
oversized = true;
}
reader.consume(nl + 1);
if let Some(p) = &mut progress {
p.add((nl + 1) as u64);
}
}
None => {
if !oversized && line.len() + buf.len() <= max_line {
line.extend_from_slice(buf);
} else {
oversized = true;
}
let len = buf.len();
reader.consume(len);
if let Some(p) = &mut progress {
p.add(len as u64);
}
continue; }
}
break; }
if eof && !any {
return Ok(()); }
if oversized {
if !warned {
warned = true;
eprintln!(
"> {}: line(s) over {max_line} bytes skipped",
path.display()
);
}
continue;
}
if !needles.is_empty() && !line_contains(&line, needles) {
continue;
}
if let Ok(value) = serde_json::from_slice::<Value>(&line)
&& !on_line(value)
{
return Ok(());
}
}
}
fn line_contains(line: &[u8], needles: &[&str]) -> bool {
needles
.iter()
.filter(|n| !n.is_empty())
.any(|n| contains_needle(line, n.as_bytes()))
}
fn contains_needle(line: &[u8], needle: &[u8]) -> bool {
let n = needle.len();
if n == 0 || n > line.len() {
return false;
}
if n == 1 {
return line.iter().any(|&b| b == needle[0]);
}
let (a0, a1) = (needle[0], needle[1]);
let last = line.len() - n; for i in 0..=last {
if line[i] == a0 && line[i + 1] == a1 && &line[i..i + n] == needle {
return true;
}
}
false
}
pub fn total_bytes(files: &[PathBuf]) -> u64 {
files
.iter()
.map(|f| fs::metadata(f).map(|m| m.len()).unwrap_or(0))
.sum()
}
pub fn file_name_str(path: &Path) -> &str {
path.file_name().and_then(|n| n.to_str()).unwrap_or("")
}
pub fn warn_file(err: &AppError) {
eprintln!("> {err}");
}
pub struct ProgressReader<'a, R: Read> {
inner: R,
progress: &'a mut Progress,
}
impl<'a, R: Read> ProgressReader<'a, R> {
pub fn new(inner: R, progress: &'a mut Progress) -> ProgressReader<'a, R> {
ProgressReader { inner, progress }
}
}
impl<R: Read> Read for ProgressReader<'_, R> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let n = self.inner.read(buf)?;
self.progress.add(n as u64);
Ok(n)
}
}
pub fn u64_get(value: &Value, keys: &[&str]) -> u64 {
match get_nested(value, keys) {
Some(v) => v
.as_u64()
.unwrap_or_else(|| v.as_f64().map(|f| f.max(0.0) as u64).unwrap_or(0)),
None => 0,
}
}
pub fn str_get<'a>(value: &'a Value, keys: &[&str]) -> Option<&'a str> {
get_nested(value, keys).and_then(Value::as_str)
}
pub fn str_get_nonempty<'a>(value: &'a Value, keys: &[&str]) -> Option<&'a str> {
str_get(value, keys).filter(|s| !s.trim().is_empty())
}
pub fn cost_get(value: &Value, keys: &[&str]) -> Option<f64> {
let raw = get_nested(value, keys)?;
let cost = raw.as_f64()?;
(cost > 0.0 && cost.is_finite()).then_some(cost)
}
pub fn bool_get(value: &Value, keys: &[&str]) -> bool {
get_nested(value, keys)
.and_then(Value::as_bool)
.unwrap_or(false)
}
pub fn timestamp_to_epoch(value: &Value) -> Option<i64> {
if let Some(n) = value.as_i64() {
return Some(if n > 100_000_000_000 { n / 1000 } else { n });
}
let s = value.as_str()?;
if let Ok(n) = s.parse::<i64>() {
return Some(if n > 100_000_000_000 { n / 1000 } else { n });
}
chrono::DateTime::parse_from_rfc3339(s)
.ok()
.map(|dt| dt.timestamp())
}
fn get_nested<'a>(value: &'a Value, keys: &[&str]) -> Option<&'a Value> {
let mut current = value;
for key in keys {
current = current.get(key)?;
}
Some(current)
}
#[cfg(test)]
#[path = "../tests/io_load_test.rs"]
mod tests;