l-s 0.5.0

Summary any file‘s meta.
mod cli;
mod constants;
mod head_hash;
mod meta;
mod utils;

use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::Instant;

use anyhow::{Context, Result};
use clap::Parser;
use meta::{calc_xxh128_with_callback, scan_dir_xxh128, DirSnapshot, FileMeta, ProgressTracker};

fn main() -> Result<()> {
    let started = Instant::now();
    let cli = cli::Cli::parse();
    let target = cli.resolve_path()?;
    println!("目标: {}", target.display());

    if target.is_dir() {
        process_dir(&target)?;
    } else {
        process_file(&target)?;
    }

    println!("耗时: {:?}", started.elapsed());
    Ok(())
}

fn process_file(path: &Path) -> Result<()> {
    let meta_dir = path
        .parent()
        .map(Path::to_path_buf)
        .unwrap_or_else(|| PathBuf::from("."))
        .join("meta");
    fs::create_dir_all(&meta_dir)
        .with_context(|| format!("无法创建目录: {}", meta_dir.display()))?;

    let basename = path
        .file_name()
        .map(|n| n.to_string_lossy().to_string())
        .unwrap_or_else(|| "unknown".to_string());
    let save_path = meta_dir.join(format!("{basename}.json"));

    // 获取文件大小
    let file_size = fs::metadata(path)
        .with_context(|| format!("无法读取文件信息: {}", path.display()))?
        .len();

    if !save_path.exists() {
        let tracker = ProgressTracker::new_single_file(file_size, &basename);
        let on_bytes = tracker.bytes_callback();
        let on_iop = tracker.iop_callback();
        let meta = FileMeta::from_path_with_callback(path, on_bytes, on_iop)?;
        tracker.finish("处理完成");
        let json = meta.to_pretty_json()?;
        println!("{}", json);
        fs::write(&save_path, json)?;
        return Ok(());
    }

    let existing = File::open(&save_path)
        .with_context(|| format!("无法读取历史元数据: {}", save_path.display()))?;
    let old_meta = FileMeta::from_reader(existing)?;

    // 使用进度条计算快速哈希
    let tracker = ProgressTracker::new_single_file(file_size, &basename);
    let on_bytes = tracker.bytes_callback();
    let on_iop = tracker.iop_callback();
    let fast_hash = calc_xxh128_with_callback(path, on_bytes, on_iop)?;
    tracker.finish("校验完成");

    if fast_hash == old_meta.xxh128 {
        println!("校验通过.");
        return Ok(());
    }

    println!("校验失败!");
    println!("现校验文件:");
    let tracker = ProgressTracker::new_single_file(file_size, &basename);
    let on_bytes = tracker.bytes_callback();
    let on_iop = tracker.iop_callback();
    let meta = FileMeta::from_path_with_callback(path, on_bytes, on_iop)?;
    tracker.finish("处理完成");
    println!("{}", meta.to_pretty_json()?);
    println!("原校验文件:");
    println!("{}", old_meta.to_pretty_json()?);

    Ok(())
}

fn process_dir(path: &Path) -> Result<()> {
    let meta_path = path.join("meta.json");
    let backup_path = path.join("meta-old.json");

    if !meta_path.exists() {
        let snapshot = DirSnapshot::build_root(path)?;
        let json = serde_json::to_string_pretty(&snapshot)?;
        let mut file = File::create(&meta_path)
            .with_context(|| format!("无法写入: {}", meta_path.display()))?;
        file.write_all(json.as_bytes())?;
        return Ok(());
    }

    if backup_path.exists() {
        fs::remove_file(&backup_path)?;
    }

    fs::rename(&meta_path, &backup_path)
        .with_context(|| format!("无法重命名旧meta: {}", meta_path.display()))?;
    println!("发现旧元数据,已暂存为 meta-old.json,开始校验...");

    let meta_file =
        File::open(&backup_path).with_context(|| format!("无法读取: {}", backup_path.display()))?;
    let snapshot = DirSnapshot::from_reader(meta_file)?;
    let mut stored = snapshot.collect_file_map(path);
    let current = scan_dir_xxh128(path)?;
    let mut issues = false;

    for (file_path, hash) in current {
        if let Some(meta) = stored.remove(&file_path) {
            if hash != meta.xxh128 {
                println!(
                    "校验失败: {}\n  期望: {}\n  当前: {}",
                    file_path.display(),
                    meta.xxh128,
                    hash
                );
                issues = true;
            }
        } else {
            println!("文件新增: {}", file_path.display());
            issues = true;
        }
    }

    for (missing_path, _) in stored {
        println!("文件缺失: {}", missing_path.display());
        issues = true;
    }

    if issues {
        println!("校验存在异常,已保留 meta-old.json 供排查。");
    } else {
        println!("校验通过.");
        fs::rename(&backup_path, &meta_path)
            .with_context(|| format!("无法恢复meta: {}", meta_path.display()))?;
    }

    Ok(())
}