use std::fs::{self, File}; use std::io::Read; use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::{anyhow, Context, Result}; use ed2k::digest::Digest; use ed2k::Ed2k; use md5::Md5; use serde::{Deserialize, Serialize}; use sha1::Sha1; use sha2::Sha256; use xxhash_rust::xxh3::Xxh3; use crate::constants::{DEFAULT_BUFFER_SIZE, HEAD_115_BYTES, HEAD_BAIDU_BYTES}; use crate::head_hash::{calc_head_115, calc_head_baidu, HeadChunk}; use crate::utils::{basename, friendly_size, hex_upper}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FileMeta { pub basename: String, pub size: u64, pub friendly_size: String, pub mtime: i64, pub head_115: String, pub head_baidu: String, pub ed2k: String, pub md5: String, pub sha1: String, pub sha256: String, pub xxh128: String, } impl FileMeta { pub fn from_path_with_callback( path: &Path, mut on_bytes_read: F1, mut on_iop: F2, ) -> Result where F1: FnMut(u64), F2: FnMut(), { let info = fs::metadata(path).with_context(|| format!("无法读取文件信息: {}", path.display()))?; if !info.is_file() { return Err(anyhow!("{} 不是文件", path.display())); } let basename_str = basename( path.file_name() .ok_or_else(|| anyhow!("{} 缺少文件名", path.display()))?, ); let size = info.len(); let friendly = friendly_size(size); let mtime = info .modified() .unwrap_or(SystemTime::UNIX_EPOCH) .duration_since(UNIX_EPOCH) .map(|d| d.as_secs() as i64) .unwrap_or(0); let mut file = File::open(path).with_context(|| format!("无法打开文件: {}", path.display()))?; let mut buffer = vec![0u8; DEFAULT_BUFFER_SIZE]; let mut md5_hasher = Md5::new(); let mut sha1_hasher = Sha1::new(); let mut sha256_hasher = Sha256::new(); let mut xxh_hasher = Xxh3::new(); let mut ed2k_hasher = Ed2k::new(); let mut head115 = HeadChunk::new(HEAD_115_BYTES); let mut head_baidu = HeadChunk::new(HEAD_BAIDU_BYTES); loop { let read_len = file.read(&mut buffer)?; if read_len == 0 { break; } let chunk = &buffer[..read_len]; md5_hasher.update(chunk); sha1_hasher.update(chunk); sha256_hasher.update(chunk); xxh_hasher.update(chunk); ed2k_hasher.update(chunk); 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()); let head_baidu = calc_head_baidu(head_baidu.as_slice()); let md5_hex = hex_upper(md5_hasher.finalize()); let sha1_hex = hex_upper(sha1_hasher.finalize()); let sha256_hex = hex_upper(sha256_hasher.finalize()); let xxh_hex = hex_upper(xxh_hasher.digest128().to_be_bytes()); let ed2k_hex = hex_upper(ed2k_hasher.finalize()); Ok(Self { basename: basename_str, size, friendly_size: friendly, mtime, head_115, head_baidu, ed2k: ed2k_hex, md5: md5_hex, sha1: sha1_hex, sha256: sha256_hex, xxh128: xxh_hex, }) } pub fn from_reader(reader: R) -> Result { Ok(serde_json::from_reader(reader)?) } pub fn to_pretty_json(&self) -> Result { Ok(serde_json::to_string_pretty(self)?) } } pub fn calc_xxh128_with_callback( path: &Path, mut on_bytes_read: F1, mut on_iop: F2, ) -> Result 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(); loop { let read_len = file.read(&mut buffer)?; if read_len == 0 { 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())) }