feat: [#1932] l-s v0.5.1 原子写入保护及文档更新
This commit is contained in:
+44
-6
@@ -4,10 +4,10 @@ mod head_hash;
|
||||
mod meta;
|
||||
mod utils;
|
||||
|
||||
use std::fs::{self, File};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
use std::time::{Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::Parser;
|
||||
@@ -57,7 +57,7 @@ fn process_file(path: &Path) -> Result<()> {
|
||||
tracker.finish("处理完成");
|
||||
let json = meta.to_pretty_json()?;
|
||||
println!("{}", json);
|
||||
fs::write(&save_path, json)?;
|
||||
write_atomic(&save_path, &json)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -98,9 +98,7 @@ fn process_dir(path: &Path) -> Result<()> {
|
||||
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())?;
|
||||
write_atomic(&meta_path, &json)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -151,3 +149,43 @@ fn process_dir(path: &Path) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_atomic(path: &Path, contents: &str) -> Result<()> {
|
||||
let parent = path
|
||||
.parent()
|
||||
.map(Path::to_path_buf)
|
||||
.unwrap_or_else(|| PathBuf::from("."));
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map(|duration| duration.as_nanos())
|
||||
.unwrap_or(0);
|
||||
let tmp_path = parent.join(format!(".l-s-tmp-{}-{nanos}", std::process::id()));
|
||||
|
||||
let result = (|| -> Result<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(&tmp_path)
|
||||
.with_context(|| format!("无法创建临时文件: {}", tmp_path.display()))?;
|
||||
file.write_all(contents.as_bytes())
|
||||
.with_context(|| format!("无法写入临时文件: {}", tmp_path.display()))?;
|
||||
file.sync_all()
|
||||
.with_context(|| format!("无法同步临时文件: {}", tmp_path.display()))?;
|
||||
drop(file);
|
||||
|
||||
fs::rename(&tmp_path, path).with_context(|| {
|
||||
format!(
|
||||
"无法将临时文件重命名为目标文件: {} -> {}",
|
||||
tmp_path.display(),
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
if result.is_err() {
|
||||
let _ = fs::remove_file(&tmp_path);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
+10
-2
@@ -32,7 +32,11 @@ pub struct FileMeta {
|
||||
}
|
||||
|
||||
impl FileMeta {
|
||||
pub fn from_path_with_callback<F1, F2>(path: &Path, mut on_bytes_read: F1, mut on_iop: F2) -> Result<Self>
|
||||
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(),
|
||||
@@ -120,7 +124,11 @@ impl FileMeta {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calc_xxh128_with_callback<F1, F2>(path: &Path, mut on_bytes_read: F1, mut on_iop: F2) -> Result<String>
|
||||
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(),
|
||||
|
||||
+13
-7
@@ -9,11 +9,11 @@ use crate::utils::friendly_size;
|
||||
/// 进度跟踪器,封装进度条和 IO 统计信息
|
||||
pub struct ProgressTracker {
|
||||
multi: Option<MultiProgress>,
|
||||
file_progress_bar: Option<ProgressBar>, // 文件数量进度条
|
||||
current_file_bar: Option<ProgressBar>, // 当前文件进度条
|
||||
file_progress_bar: Option<ProgressBar>, // 文件数量进度条
|
||||
current_file_bar: Option<ProgressBar>, // 当前文件进度条
|
||||
bytes_read: Arc<AtomicU64>,
|
||||
current_file_bytes: Arc<AtomicU64>, // 当前文件已读字节数
|
||||
current_file_size: Arc<AtomicU64>, // 当前文件总大小
|
||||
current_file_bytes: Arc<AtomicU64>, // 当前文件已读字节数
|
||||
current_file_size: Arc<AtomicU64>, // 当前文件总大小
|
||||
iops: Arc<AtomicU64>,
|
||||
start_time: Instant,
|
||||
last_update: Arc<AtomicU64>,
|
||||
@@ -44,7 +44,7 @@ impl ProgressTracker {
|
||||
.unwrap()
|
||||
.progress_chars("=>-"),
|
||||
);
|
||||
current_pb.set_length(0); // 设置为0长度来隐藏
|
||||
current_pb.set_length(0); // 设置为0长度来隐藏
|
||||
|
||||
(Some(multi), Some(file_pb), Some(current_pb))
|
||||
} else {
|
||||
@@ -163,7 +163,10 @@ impl ProgressTracker {
|
||||
String::new()
|
||||
};
|
||||
|
||||
pb.set_message(format!("IO速度: {}/s | IOPS: {:.0} | {}", speed_str, iops, eta_str));
|
||||
pb.set_message(format!(
|
||||
"IO速度: {}/s | IOPS: {:.0} | {}",
|
||||
speed_str, iops, eta_str
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +249,10 @@ impl ProgressTracker {
|
||||
String::new()
|
||||
};
|
||||
|
||||
pb.set_message(format!("IO速度: {}/s | IOPS: {:.0} | {}", speed_str, iops_value, eta_str));
|
||||
pb.set_message(format!(
|
||||
"IO速度: {}/s | IOPS: {:.0} | {}",
|
||||
speed_str, iops_value, eta_str
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-9
@@ -83,9 +83,7 @@ impl DirSnapshot {
|
||||
}
|
||||
|
||||
// 获取文件大小并开始跟踪
|
||||
let file_size = entry.metadata()
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
let file_size = entry.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
tracker.start_file(file_size, &name);
|
||||
|
||||
let on_bytes = tracker.bytes_callback();
|
||||
@@ -124,9 +122,9 @@ impl DirSnapshot {
|
||||
/// 校验通过则返回已有的 DirSnapshot,否则返回 Err 终止流程。
|
||||
fn verify_and_load(path: &Path, tracker: &ProgressTracker) -> Result<Self> {
|
||||
let meta_path = path.join("meta.json");
|
||||
let meta_file = File::open(&meta_path)
|
||||
.with_context(|| format!("无法读取: {}", meta_path.display()))?;
|
||||
let snapshot: Self = serde_json::from_reader(meta_file)
|
||||
let meta_file =
|
||||
File::open(&meta_path).with_context(|| format!("无法读取: {}", meta_path.display()))?;
|
||||
let mut snapshot: Self = serde_json::from_reader(meta_file)
|
||||
.with_context(|| format!("无法解析: {}", meta_path.display()))?;
|
||||
|
||||
let mut stored = snapshot.collect_file_map(path);
|
||||
@@ -162,6 +160,11 @@ impl DirSnapshot {
|
||||
} else {
|
||||
eprintln!("{msg}");
|
||||
}
|
||||
snapshot.dir_name = path
|
||||
.file_name()
|
||||
.map(basename)
|
||||
.unwrap_or_else(|| path.to_string_lossy().to_string());
|
||||
snapshot.v = None;
|
||||
Ok(snapshot)
|
||||
}
|
||||
}
|
||||
@@ -252,9 +255,7 @@ fn walk_dir_with_progress(
|
||||
}
|
||||
|
||||
// 获取文件大小并开始跟踪
|
||||
let file_size = entry.metadata()
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
let file_size = entry.metadata().map(|m| m.len()).unwrap_or(0);
|
||||
tracker.start_file(file_size, &name);
|
||||
|
||||
let on_bytes = tracker.bytes_callback();
|
||||
|
||||
+7
-1
@@ -44,6 +44,7 @@ pub fn should_skip_file(name: &str) -> bool {
|
||||
.any(|item| item.eq_ignore_ascii_case(name))
|
||||
|| name.starts_with("._")
|
||||
|| name.starts_with("Thumb_")
|
||||
|| name.starts_with(".l-s-tmp-")
|
||||
}
|
||||
|
||||
pub fn hex_upper(bytes: impl AsRef<[u8]>) -> String {
|
||||
@@ -56,7 +57,7 @@ pub fn hex_upper(bytes: impl AsRef<[u8]>) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::friendly_size;
|
||||
use super::{friendly_size, should_skip_file};
|
||||
|
||||
#[test]
|
||||
fn friendly_size_formats_units() {
|
||||
@@ -65,4 +66,9 @@ mod tests {
|
||||
assert_eq!(friendly_size(1024), "1.00KB");
|
||||
assert_eq!(friendly_size(1024 * 1024), "1.00MB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_skip_atomic_write_temp_files() {
|
||||
assert!(should_skip_file(".l-s-tmp-123-456"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user