tina-core 0.0.2

Tina platform
Documentation
//! 文件工具
use crate::app_system_error;
use crate::tina::data::binary::StoredFileData;
use crate::tina::data::number::RoundNumber;
use crate::tina::data::AppResult;
use crate::tina::file::{FileContent, FileData};
use crate::tina::server::application::Application;
use crate::tina::util::not_empty::INotEmpty;
use crate::tina::util::string::{AsStr, IntoStr};
use bytes::Bytes;
use chrono::{Datelike, Local};
use image::EncodableLayout;
use once_cell::sync::Lazy;
use regex::{Regex, RegexBuilder};
use std::fs::File;
use std::io::Write;
use std::path::Path;
use tracing::error;
use uuid::Uuid;

static FILENAME_PATTERN: Lazy<Regex> =
    Lazy::new(|| RegexBuilder::new("[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+").build().expect("build FILENAME_PATTERN failed"));

/// 文件工具
pub struct FileUtil;

impl FileUtil {
    /// KB
    pub const KB: u64 = 1024;
    /// MB
    pub const MB: u64 = Self::KB * 1024;
    /// GB
    pub const GB: u64 = Self::MB * 1024;
    /// TB
    pub const TB: u64 = Self::GB * 1024;
    const DEFAULT_ALLOWED_EXTENSION: [&str; 22] = [
        "bmp", "gif", "jpg", "jpeg", "png", // 图片
        "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt", // word excel powerpoint
        "rar", "zip", "gz", "bz2", // 压缩文件
        "mp4", "avi", "rmvb", // 视频格式
        "pdf",  // pdf
    ];
    /// 写数据到文件中
    pub async fn write_data_to_local(application: &Application, prefix: impl AsRef<str>, data: &mut FileData) -> AppResult<(String, u64)> {
        let prefix = prefix.as_ref();
        let extension = Self::get_file_ext_name(data);
        let time = Local::now();
        let file_path = format!("{}/{}/{}/{}/{}.{}", prefix, time.year(), time.month(), time.day(), Uuid::new_v4(), extension);
        let path = format!("{}/{}", application.get_server_config()?.file_base_path, file_path);
        let path = Path::new(path.as_str());
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)
                .map_err(|v| app_system_error!("创建目录失败: {}, reason: {:?}", parent.to_str().as_str(), v))?;
        }
        let mut file_size = 0;
        let mut file = File::create(path).map_err(|v| app_system_error!("创建文件失败: {}, reason: {:?}", file_path, v))?;
        while let Some(r) = futures::StreamExt::next(data).await {
            let buf: Bytes = r?.into();
            let buf_bytes = buf.as_bytes();
            file.write(buf_bytes).map_err(|v| app_system_error!("写入文件内容失败: {}, reason: {:?}", file_path.as_str(), v))?;
            file_size += buf_bytes.len() as u64;
        }
        Ok((file_path, file_size))
    }
    /// 读取文件数据
    pub async fn read_local_data(application: &Application, related_path: impl AsRef<str>) -> AppResult<Option<FileContent>> {
        let file_path = related_path.as_ref();
        let path = format!("{}/{}", application.get_server_config()?.file_base_path, file_path);
        let path = Path::new(path.as_str());
        match path.exists() {
            true => {
                let file_data = StoredFileData::new(path, false);
                Ok(Some(FileContent::from_stored_file_data(file_data)))
            }
            false => Ok(None),
        }
    }
    /// 获取文件名
    pub fn get_file_name(file: &FileData) -> &str {
        match file.get_original_filename() {
            None => file.get_name(),
            Some(v) => v,
        }
    }
    /// 获取图像后缀
    pub fn get_file_ext_name(file: &FileData) -> String {
        let content_type = file.get_content_type();
        let mut ext = content_type.split_once('/').map(|v| v.1).into_str();
        if ext.is_empty() {
            let name = file.get_original_filename().into_str();
            if name.not_empty() && name.contains('.') {
                ext = name.rsplit_once('.').map(|v| v.1).into_str();
            }
        }
        if ext.is_empty() {
            let name = file.get_name();
            if name.not_empty() && name.contains('.') {
                ext = name.rsplit_once('.').map(|v| v.1).into_str();
            }
        }
        while ext.starts_with('.') {
            ext = ext.split_once('.').map(|v| v.1).into_str();
        }
        ext.to_lowercase()
    }
    /// 删除文件
    pub fn delete_file(application: &Application, relative_path: impl AsRef<str>) -> AppResult<bool> {
        let path = format!("{}/{}", application.get_server_config()?.file_base_path, relative_path.as_ref());
        let path = Path::new(path.as_str());
        match std::fs::remove_file(path) {
            Ok(_) => Ok(true),
            Err(err) => {
                error!("删除文件失败: {:?}, reason: {:?}", path, err);
                Ok(false)
            }
        }
    }
    /// 获取文件类型
    pub fn get_file_type(filename: &str) -> &str {
        filename.rsplit_once('.').map(|v| v.1).unwrap_or_default()
    }
    /// 文件名称验证
    pub fn is_valid_filename(filename: impl AsRef<str>) -> bool {
        let filename = filename.as_ref();
        FILENAME_PATTERN.is_match(filename)
    }
    /// 检查文件是否可下载
    pub fn check_allow_download(resource: impl AsRef<str>) -> bool {
        let resource = resource.as_ref();
        // 禁止目录上跳级别
        if resource.contains("..") {
            return false;
        }
        let file_type = Self::get_file_type(resource);
        // 检查允许下载的文件规则
        if Self::DEFAULT_ALLOWED_EXTENSION.contains(&file_type) {
            return true;
        }
        // 不在允许下载的文件规则
        false
    }
    /// 字节转换
    pub fn format_file_size(size: u64) -> String {
        if size >= Self::TB {
            return format!("{} TB", ((size as f64) / (Self::TB as f64)).round_num(2, true));
        }
        if size >= Self::GB {
            return format!("{} GB", ((size as f64) / (Self::GB as f64)).round_num(2, true));
        }
        if size >= Self::MB {
            return format!("{} MB", ((size as f64) / (Self::MB as f64)).round_num(2, true));
        }
        if size >= Self::KB {
            return format!("{} KB", ((size as f64) / (Self::KB as f64)).round_num(2, true));
        }
        format!("{} B", size)
    }
}