l-s 0.5.2

Summary any file‘s meta.
use std::fs::{File, OpenOptions};
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};

#[cfg(all(not(unix), not(windows)))]
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
#[cfg(windows)]
use std::os::windows::fs::{MetadataExt, OpenOptionsExt};

#[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 {
    #[cfg(not(unix))]
    pub fn from_path_with_callback<F1, F2>(
        path: &Path,
        on_bytes_read: F1,
        on_iop: F2,
    ) -> Result<Self>
    where
        F1: FnMut(u64),
        F2: FnMut(),
    {
        let file = open_regular_file_nofollow(path)?;
        Self::from_open_file_with_callback(path, file, on_bytes_read, on_iop)
    }

    pub(crate) fn from_open_file_with_callback<F1, F2>(
        path: &Path,
        mut file: File,
        mut on_bytes_read: F1,
        mut on_iop: F2,
    ) -> Result<Self>
    where
        F1: FnMut(u64),
        F2: FnMut(),
    {
        let info = file
            .metadata()
            .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 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<R: Read>(reader: R) -> Result<Self> {
        Ok(serde_json::from_reader(reader)?)
    }

    pub fn to_pretty_json(&self) -> Result<String> {
        Ok(serde_json::to_string_pretty(self)?)
    }
}

#[cfg(not(unix))]
pub fn calc_xxh128_with_callback<F1, F2>(
    path: &Path,
    on_bytes_read: F1,
    on_iop: F2,
) -> Result<String>
where
    F1: FnMut(u64),
    F2: FnMut(),
{
    let file = open_regular_file_nofollow(path)?;
    calc_xxh128_from_file_with_callback(path, file, on_bytes_read, on_iop)
}

pub(crate) fn calc_xxh128_from_file_with_callback<F1, F2>(
    path: &Path,
    mut file: File,
    mut on_bytes_read: F1,
    mut on_iop: F2,
) -> Result<String>
where
    F1: FnMut(u64),
    F2: FnMut(),
{
    let mut buffer = vec![0u8; DEFAULT_BUFFER_SIZE];
    let mut hasher = Xxh3::new();

    loop {
        let read_len = file
            .read(&mut buffer)
            .with_context(|| format!("无法读取文件: {}", path.display()))?;
        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()))
}

pub(crate) fn open_regular_file_nofollow(path: &Path) -> Result<File> {
    let file = open_file_nofollow(path)?;
    let info = file
        .metadata()
        .with_context(|| format!("无法读取文件信息: {}", path.display()))?;
    if !info.is_file() {
        return Err(anyhow!("{} 不是文件", path.display()));
    }
    Ok(file)
}

#[cfg(unix)]
fn open_file_nofollow(path: &Path) -> Result<File> {
    let mut options = OpenOptions::new();
    options.read(true);
    options.custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW);
    options
        .open(path)
        .with_context(|| format!("无法打开文件: {}", path.display()))
}

#[cfg(windows)]
fn open_file_nofollow(path: &Path) -> Result<File> {
    const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
    const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;

    let mut options = OpenOptions::new();
    options.read(true);
    options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
    let file = options
        .open(path)
        .with_context(|| format!("无法打开文件: {}", path.display()))?;

    let info = file
        .metadata()
        .with_context(|| format!("无法读取文件信息: {}", path.display()))?;
    if info.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
        return Err(anyhow!("不支持扫描符号链接或重解析点: {}", path.display()));
    }

    Ok(file)
}

#[cfg(all(not(unix), not(windows)))]
fn open_file_nofollow(path: &Path) -> Result<File> {
    let info = fs::symlink_metadata(path)
        .with_context(|| format!("无法读取文件信息: {}", path.display()))?;
    if info.file_type().is_symlink() {
        return Err(anyhow!("不支持扫描符号链接: {}", path.display()));
    }
    File::open(path).with_context(|| format!("无法打开文件: {}", path.display()))
}