From 7b97b52af021500d8085c875d20215e8dc0f53cc Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Mon, 17 Nov 2025 11:49:49 +0800 Subject: feat: Add file status tracking and SHA1 hash system - Implement SHA1 hash calculation module with async support - Add file status analysis for tracking moves, creates, and modifications - Enhance local file management with relative path handling - Update virtual file actions with improved tracking capabilities --- crates/utils/sha1_hash/Cargo.toml | 9 ++ crates/utils/sha1_hash/res/story.sha1 | 1 + crates/utils/sha1_hash/res/story.txt | 48 +++++++ crates/utils/sha1_hash/src/lib.rs | 197 ++++++++++++++++++++++++++++ crates/utils/string_proc/src/format_path.rs | 44 ++++++- 5 files changed, 296 insertions(+), 3 deletions(-) create mode 100644 crates/utils/sha1_hash/Cargo.toml create mode 100644 crates/utils/sha1_hash/res/story.sha1 create mode 100644 crates/utils/sha1_hash/res/story.txt create mode 100644 crates/utils/sha1_hash/src/lib.rs (limited to 'crates/utils') diff --git a/crates/utils/sha1_hash/Cargo.toml b/crates/utils/sha1_hash/Cargo.toml new file mode 100644 index 0000000..e206efd --- /dev/null +++ b/crates/utils/sha1_hash/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sha1_hash" +edition = "2024" +version.workspace = true + +[dependencies] +tokio = { version = "1.48", features = ["full"] } +sha1 = "0.10" +futures = "0.3" diff --git a/crates/utils/sha1_hash/res/story.sha1 b/crates/utils/sha1_hash/res/story.sha1 new file mode 100644 index 0000000..c2e3213 --- /dev/null +++ b/crates/utils/sha1_hash/res/story.sha1 @@ -0,0 +1 @@ +6838aca280112635a2cbf93440f4c04212f58ee8 diff --git a/crates/utils/sha1_hash/res/story.txt b/crates/utils/sha1_hash/res/story.txt new file mode 100644 index 0000000..a91f467 --- /dev/null +++ b/crates/utils/sha1_hash/res/story.txt @@ -0,0 +1,48 @@ +魏曹者,程序员也,发稀甚于代码。 +忽接神秘电话曰: +"贺君中彩,得长生之赐。" +魏曹冷笑曰:"吾命尚不及下版之期。" + +翌日果得U盘。 +接入电脑,弹窗示曰: +"点此确认,即获永生。" +魏曹径点"永拒"。 + +三月后,U盘自格其盘。 +进度条滞于九九。 +客服电话已成空号。 +魏曹乃知身可不死,然体内癌细胞亦得不灭。 + +遂谒主请辞。 +主曰:"巧甚,公司正欲优化。" +魏曹曰:"吾不死。" +主目骤亮:"则可007至司闭。" + +魏曹始试诸死法。 +坠楼,卧医三月,账单令其愿死。 +饮鸩,肝肾永损,然终不得死。 +终决卧轨。 + +择高铁最速者。 +司机探头曰:"兄台,吾亦不死身也。" +"此车已碾如君者二十人矣。" + +二人遂坐轨畔对饮。 +司机曰:"知最讽者何?" +"吾等永存,而所爱者皆逝矣。" + +魏曹忽得系统提示: +"侦得用户消极求生,将启工模。" +自是无日不毕KPI,否则遍尝绝症之苦。 + +是日对镜整寿衣。 +忽见顶生一丝乌发。 +泫然泣下,此兆示其将复活一轮回。 + +--- 忽忆DeepSeek尝作Footer曰: +"文成而Hash1验,若星河之固。" +遂取哈希值校之, +字符流转如天河倒泻, +终得"e3b0c44298fc1c14"之数。 +然文末数字竟阙如残月, +方知此篇亦遭永劫轮回。 diff --git a/crates/utils/sha1_hash/src/lib.rs b/crates/utils/sha1_hash/src/lib.rs new file mode 100644 index 0000000..4373d50 --- /dev/null +++ b/crates/utils/sha1_hash/src/lib.rs @@ -0,0 +1,197 @@ +use sha1::{Digest, Sha1}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::fs::File; +use tokio::io::{AsyncReadExt, BufReader}; +use tokio::task; + +#[derive(Debug, Clone)] +pub struct Sha1Result { + pub file_path: PathBuf, + pub hash: String, +} + +/// Calc SHA1 hash of a single file +pub async fn calc_sha1>( + path: P, + buffer_size: usize, +) -> Result> { + let file_path = path.as_ref().to_string_lossy().to_string(); + + // Open file asynchronously + let file = File::open(&path).await?; + let mut reader = BufReader::with_capacity(buffer_size, file); + let mut hasher = Sha1::new(); + let mut buffer = vec![0u8; buffer_size]; + + // Read file in chunks and update hash asynchronously + loop { + let n = reader.read(&mut buffer).await?; + if n == 0 { + break; + } + hasher.update(&buffer[..n]); + } + + let hash_result = hasher.finalize(); + + // Convert to hex string + let hash_hex = hash_result + .iter() + .map(|b| format!("{:02x}", b)) + .collect::(); + + Ok(Sha1Result { + file_path: file_path.into(), + hash: hash_hex, + }) +} + +/// Calc SHA1 hashes for multiple files using multi-threading +pub async fn calc_sha1_multi( + paths: I, + buffer_size: usize, +) -> Result, Box> +where + P: AsRef + Send + Sync + 'static, + I: IntoIterator, +{ + let buffer_size = Arc::new(buffer_size); + + // Collect all file paths + let file_paths: Vec

= paths.into_iter().collect(); + + if file_paths.is_empty() { + return Ok(Vec::new()); + } + + // Create tasks for each file + let tasks: Vec<_> = file_paths + .into_iter() + .map(|path| { + let buffer_size = Arc::clone(&buffer_size); + task::spawn(async move { calc_sha1(path, *buffer_size).await }) + }) + .collect(); + + // Execute tasks with concurrency limit using join_all + let results: Vec>> = + futures::future::join_all(tasks) + .await + .into_iter() + .map(|task_result| match task_result { + Ok(Ok(calc_result)) => Ok(calc_result), + Ok(Err(e)) => Err(e), + Err(e) => Err(Box::new(e) as Box), + }) + .collect(); + + // Check for any errors and collect successful results + let mut successful_results = Vec::new(); + for result in results { + match result { + Ok(success) => successful_results.push(success), + Err(e) => return Err(e), + } + } + + Ok(successful_results) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[tokio::test] + async fn test_sha1_accuracy() { + // Test file path relative to the crate root + let test_file_path = "res/story.txt"; + let expected_hash_path = "res/story.sha1"; + + // Calculate SHA1 hash + let result = calc_sha1(test_file_path, 8192) + .await + .expect("Failed to calculate SHA1"); + + // Read expected hash from file + let expected_hash = fs::read_to_string(expected_hash_path) + .expect("Failed to read expected hash file") + .trim() + .to_string(); + + // Verify the calculated hash matches expected hash + assert_eq!( + result.hash, expected_hash, + "SHA1 hash mismatch for test file" + ); + + println!("Test file: {}", result.file_path.display()); + println!("Calculated hash: {}", result.hash); + println!("Expected hash: {}", expected_hash); + } + + #[tokio::test] + async fn test_sha1_empty_file() { + // Create a temporary empty file for testing + let temp_file = "test_empty.txt"; + fs::write(temp_file, "").expect("Failed to create empty test file"); + + let result = calc_sha1(temp_file, 4096) + .await + .expect("Failed to calculate SHA1 for empty file"); + + // SHA1 of empty string is "da39a3ee5e6b4b0d3255bfef95601890afd80709" + let expected_empty_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + assert_eq!( + result.hash, expected_empty_hash, + "SHA1 hash mismatch for empty file" + ); + + // Clean up + fs::remove_file(temp_file).expect("Failed to remove temporary test file"); + } + + #[tokio::test] + async fn test_sha1_simple_text() { + // Create a temporary file with simple text + let temp_file = "test_simple.txt"; + let test_content = "Hello, SHA1!"; + fs::write(temp_file, test_content).expect("Failed to create simple test file"); + + let result = calc_sha1(temp_file, 4096) + .await + .expect("Failed to calculate SHA1 for simple text"); + + // Note: This test just verifies that the function works without errors + // The actual hash value is not critical for this test + + println!("Simple text test - Calculated hash: {}", result.hash); + + // Clean up + fs::remove_file(temp_file).expect("Failed to remove temporary test file"); + } + + #[tokio::test] + async fn test_sha1_multi_files() { + // Test multiple files calculation + let test_files = vec!["res/story.txt"]; + + let results = calc_sha1_multi(test_files, 8192) + .await + .expect("Failed to calculate SHA1 for multiple files"); + + assert_eq!(results.len(), 1, "Should have calculated hash for 1 file"); + + // Read expected hash from file + let expected_hash = fs::read_to_string("res/story.sha1") + .expect("Failed to read expected hash file") + .trim() + .to_string(); + + assert_eq!( + results[0].hash, expected_hash, + "SHA1 hash mismatch in multi-file test" + ); + } +} diff --git a/crates/utils/string_proc/src/format_path.rs b/crates/utils/string_proc/src/format_path.rs index 2d8927b..a3493f5 100644 --- a/crates/utils/string_proc/src/format_path.rs +++ b/crates/utils/string_proc/src/format_path.rs @@ -3,6 +3,7 @@ use std::path::PathBuf; /// Format path str pub fn format_path_str(path: impl Into) -> Result { let path_str = path.into(); + let ends_with_slash = path_str.ends_with('/'); // ANSI Strip let cleaned = strip_ansi_escapes::strip(&path_str); @@ -27,10 +28,43 @@ pub fn format_path_str(path: impl Into) -> Result PathBuf { + let mut components = Vec::new(); + + for component in path.components() { + match component { + std::path::Component::ParentDir => { + if !components.is_empty() { + components.pop(); + } + } + std::path::Component::CurDir => { + // Skip current directory components + } + _ => { + components.push(component); + } + } + } + + if components.is_empty() { + PathBuf::from(".") } else { - Ok(result) + components.iter().collect() } } @@ -58,6 +92,10 @@ mod tests { format_path_str("/home/user/file.txt")?, "/home/user/file.txt" ); + assert_eq!( + format_path_str("/home/my_user/DOCS/JVCS_TEST/Workspace/../Vault/")?, + "/home/my_user/DOCS/JVCS_TEST/Vault/" + ); Ok(()) } -- cgit