feat: progress bar.

This commit is contained in:
2025-12-18 23:52:50 +08:00
parent 13083ed92e
commit 09f2452bc4
4 changed files with 386 additions and 16 deletions

View File

@@ -33,6 +33,14 @@ pub struct FileMeta {
impl FileMeta {
pub fn from_path(path: &Path) -> Result<Self> {
Self::from_path_with_callback(path, |_| {}, || {})
}
pub fn from_path_with_callback<F1, F2>(path: &Path, mut on_bytes_read: F1, mut on_iop: F2) -> Result<Self>
where
F1: FnMut(u64),
F2: FnMut(),
{
let info =
fs::metadata(path).with_context(|| format!("无法读取文件信息: {}", path.display()))?;
if !info.is_file() {
@@ -78,6 +86,9 @@ impl FileMeta {
head115.feed(chunk);
head_baidu.feed(chunk);
on_bytes_read(read_len as u64);
on_iop(); // 每次 read 调用算一次 IOPS
}
let head_115 = calc_head_115(head115.as_slice());
@@ -114,6 +125,14 @@ impl FileMeta {
}
pub fn calc_xxh128(path: &Path) -> Result<String> {
calc_xxh128_with_callback(path, |_| {}, || {})
}
pub fn calc_xxh128_with_callback<F1, F2>(path: &Path, mut on_bytes_read: F1, mut on_iop: F2) -> Result<String>
where
F1: FnMut(u64),
F2: FnMut(),
{
let mut file = File::open(path).with_context(|| format!("无法打开文件: {}", path.display()))?;
let mut buffer = vec![0u8; DEFAULT_BUFFER_SIZE];
let mut hasher = Xxh3::new();
@@ -124,6 +143,8 @@ pub fn calc_xxh128(path: &Path) -> Result<String> {
break;
}
hasher.update(&buffer[..read_len]);
on_bytes_read(read_len as u64);
on_iop(); // 每次 read 调用算一次 IOPS
}
Ok(hex_upper(hasher.digest128().to_be_bytes()))

View File

@@ -1,13 +1,16 @@
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::Instant;
use anyhow::{Context, Result};
use indicatif::{ProgressBar, ProgressStyle};
use serde::{Deserialize, Serialize};
use super::file::{calc_xxh128, FileMeta};
use super::file::{calc_xxh128_with_callback, FileMeta};
use crate::constants::META_VERSION;
use crate::utils::{basename, should_skip_dir, should_skip_file};
use crate::utils::{basename, friendly_size, should_skip_dir, should_skip_file};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirSnapshot {
@@ -20,8 +23,34 @@ pub struct DirSnapshot {
impl DirSnapshot {
pub fn build_root(path: &Path) -> Result<Self> {
let mut node = Self::build_node(path)?;
// 先统计总文件数
let total_files = count_files(path)?;
// 创建进度条
let pb = if total_files > 0 {
let pb = ProgressBar::new(total_files);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("#>-"),
);
pb.set_message("构建中...");
Some(pb)
} else {
None
};
let start = Instant::now();
let total_bytes_read = Arc::new(Mutex::new(0u64));
let total_iops = Arc::new(Mutex::new(0u64));
let mut node = Self::build_node(path, &pb, &start, &total_bytes_read, &total_iops)?;
node.v = Some(META_VERSION.to_string());
if let Some(pb) = &pb {
pb.finish_with_message("构建完成");
}
Ok(node)
}
@@ -29,7 +58,13 @@ impl DirSnapshot {
Ok(serde_json::from_reader(reader)?)
}
fn build_node(path: &Path) -> Result<Self> {
fn build_node(
path: &Path,
pb: &Option<ProgressBar>,
start: &Instant,
total_bytes_read: &Arc<Mutex<u64>>,
total_iops: &Arc<Mutex<u64>>,
) -> Result<Self> {
let dir_name = path
.file_name()
.map(basename)
@@ -57,8 +92,7 @@ impl DirSnapshot {
if should_skip_dir(&name) {
continue;
}
println!("目录: {}", full_path.display());
dirs.push(Self::build_node(&full_path)?);
dirs.push(Self::build_node(&full_path, pb, start, total_bytes_read, total_iops)?);
continue;
}
@@ -66,9 +100,28 @@ impl DirSnapshot {
continue;
}
let meta = FileMeta::from_path(&full_path)?;
println!("文件: {} {}", meta.friendly_size, full_path.display());
let total_bytes_read_clone = total_bytes_read.clone();
let total_iops_clone = total_iops.clone();
let meta = FileMeta::from_path_with_callback(&full_path, move |bytes| {
*total_bytes_read_clone.lock().unwrap() += bytes;
}, move || {
*total_iops_clone.lock().unwrap() += 1;
})?;
files.push(meta);
// 更新进度条
if let Some(pb) = pb {
pb.inc(1);
let elapsed = start.elapsed().as_secs_f64();
let total_bytes = *total_bytes_read.lock().unwrap();
let total_ops = *total_iops.lock().unwrap();
if total_bytes > 0 && elapsed > 0.0 {
let speed_bytes_per_sec = total_bytes as f64 / elapsed;
let speed_str = friendly_size(speed_bytes_per_sec as u64);
let iops = total_ops as f64 / elapsed;
pb.set_message(format!("IO速度: {}/s | IOPS: {:.0}", speed_str, iops));
}
}
}
Ok(Self {
@@ -98,12 +151,80 @@ impl DirSnapshot {
}
pub fn scan_dir_xxh128(path: &Path) -> Result<BTreeMap<PathBuf, String>> {
// 先统计总文件数
let total_files = count_files(path)?;
// 创建进度条
let pb = if total_files > 0 {
let pb = ProgressBar::new(total_files);
pb.set_style(
ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({percent}%) {msg}")
.unwrap()
.progress_chars("#>-"),
);
pb.set_message("扫描中...");
Some(pb)
} else {
None
};
let mut map = BTreeMap::new();
walk_dir(path, &mut map)?;
let start = Instant::now();
let total_bytes_read = Arc::new(Mutex::new(0u64));
let total_iops = Arc::new(Mutex::new(0u64));
walk_dir_with_progress(path, &mut map, &pb, &start, &total_bytes_read, &total_iops)?;
if let Some(pb) = &pb {
pb.finish_with_message("扫描完成");
}
Ok(map)
}
fn walk_dir(path: &Path, map: &mut BTreeMap<PathBuf, String>) -> Result<()> {
fn count_files(path: &Path) -> Result<u64> {
let mut count = 0u64;
count_files_recursive(path, &mut count)?;
Ok(count)
}
fn count_files_recursive(path: &Path, count: &mut u64) -> Result<()> {
let entries = fs::read_dir(path)
.with_context(|| format!("无法遍历目录: {}", path.display()))?
.collect::<Result<Vec<_>, _>>()
.with_context(|| format!("读取目录失败: {}", path.display()))?;
for entry in entries {
let file_name = entry.file_name();
let name = file_name.to_string_lossy().to_string();
let full_path = entry.path();
let file_type = entry
.file_type()
.with_context(|| format!("无法读取类型: {}", full_path.display()))?;
if file_type.is_dir() {
if should_skip_dir(&name) {
continue;
}
count_files_recursive(&full_path, count)?;
} else {
if !should_skip_file(&name) {
*count += 1;
}
}
}
Ok(())
}
fn walk_dir_with_progress(
path: &Path,
map: &mut BTreeMap<PathBuf, String>,
pb: &Option<ProgressBar>,
start: &Instant,
total_bytes_read: &Arc<Mutex<u64>>,
total_iops: &Arc<Mutex<u64>>,
) -> Result<()> {
let mut entries = fs::read_dir(path)
.with_context(|| format!("无法遍历目录: {}", path.display()))?
.collect::<Result<Vec<_>, _>>()
@@ -122,7 +243,7 @@ fn walk_dir(path: &Path, map: &mut BTreeMap<PathBuf, String>) -> Result<()> {
if should_skip_dir(&name) {
continue;
}
walk_dir(&full_path, map)?;
walk_dir_with_progress(&full_path, map, pb, start, total_bytes_read, total_iops)?;
continue;
}
@@ -130,8 +251,28 @@ fn walk_dir(path: &Path, map: &mut BTreeMap<PathBuf, String>) -> Result<()> {
continue;
}
let hash = calc_xxh128(&full_path)?;
let total_bytes_read_clone = total_bytes_read.clone();
let total_iops_clone = total_iops.clone();
let hash = calc_xxh128_with_callback(&full_path, move |bytes| {
*total_bytes_read_clone.lock().unwrap() += bytes;
}, move || {
*total_iops_clone.lock().unwrap() += 1;
})?;
map.insert(full_path, hash);
// 更新进度条
if let Some(pb) = pb {
pb.inc(1);
let elapsed = start.elapsed().as_secs_f64();
let total_bytes = *total_bytes_read.lock().unwrap();
let total_ops = *total_iops.lock().unwrap();
if total_bytes > 0 && elapsed > 0.0 {
let speed_bytes_per_sec = total_bytes as f64 / elapsed;
let speed_str = friendly_size(speed_bytes_per_sec as u64);
let iops = total_ops as f64 / elapsed;
pb.set_message(format!("IO速度: {}/s | IOPS: {:.0}", speed_str, iops));
}
}
}
Ok(())