zero4rs 2.0.0

zero4rs is a powerful, pragmatic, and extremely fast web framework for Rust
Documentation
use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;

use actix_http::header;
use actix_web::{web, HttpRequest, HttpResponse, Responder};

use futures_util::stream::{self};

use crate::core::R;
use crate::prelude2::Render;

// # Rust crate vips 是 libvips 的 FFI 封装,
// # 但它不自带 libvips,本机必须安装系统库
// brew install vips
// brew install expat
// brew install libffi
// brew reinstall bzip2
// export PKG_CONFIG_PATH="/opt/homebrew/opt/libffi/lib/pkgconfig:/opt/homebrew/opt/expat/lib/pkgconfig:$PKG_CONFIG_PATH"
// export PKG_CONFIG_PATH="/opt/homebrew/opt/libffi/lib/pkgconfig:/opt/homebrew/opt/expat/lib/pkgconfig:/opt/homebrew/opt/bzip2/lib/pkgconfig:$PKG_CONFIG_PATH"
// sudo apt install libvips-dev
// # 验证系统 vips 版本
// vips --version
// # 验证 pkg-config
// pkg-config --cflags --libs vips
// # 输出类似:
// -I/opt/homebrew/include -L/opt/homebrew/lib -lvips
// # 如果还是找不到,可在编译时指定:
// export LIBRARY_PATH="/opt/homebrew/lib:$LIBRARY_PATH"
// export PKG_CONFIG_PATH="/opt/homebrew/lib/pkgconfig:$PKG_CONFIG_PATH"
// # 然后重新编译:
// cargo clean
// cargo build
// ## 生成渐进式(progressive)图片
// vips copy input.png output.jpg[Q=90,interlace]
// ## 可以用 exiftool 或 identify 验证:
// exiftool output-progressive.jpg | grep "Progressive"
// File Type Extension              : jpg
// JFIF Version                     : 1.02
// Progressive Scans                : Yes ✅

// ## Actix Web 多 worker
// JPEG -> turbojpeg
// JPEG -> mozjpeg
// PNG  -> oxipng
// WebP -> webp crate
// 其他 -> image crate
#[cfg(target_os = "linux")]
pub async fn progressive_image(file_path: String, request: HttpRequest) -> impl Responder {
    use anyhow::anyhow;
    use anyhow::Context;

    // 加载图片 JPEG、PNG、GIF、WEBP、TIFF
    let image = libvips::VipsImage::new_from_file(&file_path)
        .context(format!("加载文件异常: {}", file_path))
        .map_err(|e| anyhow!(e))?;

    let path = Path::new(&file_path);

    if let Some(filename) = path.file_name() {
        let output_file = format!(
            "{}/{}",
            crate::commons::temp_dir(),
            filename.to_string_lossy()
        );

        if let Some(ext) = path.extension() {
            let _ext = ext.to_string_lossy().trim().to_lowercase();

            return if _ext == "jpg" || _ext == "jpeg" {
                // 保存图片
                libvips::ops::jpegsave_with_opts(
                    &image,
                    &output_file,
                    &libvips::ops::JpegsaveOptions {
                        q: 100,
                        interlace: true, // 👈 渐进式输出
                        ..Default::default()
                    },
                )
                .context(format!("保存文件异常: {}", output_file))
                .map_err(|e| anyhow!(e))?;

                request.json(200, R::ok(output_file))
            } else if _ext == "png" {
                // 保存图片
                libvips::ops::pngsave_with_opts(
                    &image,
                    &output_file,
                    &libvips::ops::PngsaveOptions {
                        q: 100,
                        interlace: true, // 👈 渐进式输出
                        ..Default::default()
                    },
                )
                .context(format!("保存文件异常: {}", output_file))
                .map_err(|e| anyhow!(e))?;

                request.json(200, R::ok(output_file))
            } else {
                request.json(200, R::failed(400, "不支持的文件类型, 仅支持: jpeg, png"))
            };
        }
    }

    request.json(200, R::failed(400, "未能读取文件名"))
}

// Actix 会自动从 post body 中读取文本并转换成 String,但如果内容不是 UTF-8,会报错。
#[cfg(not(target_os = "linux"))]
pub async fn progressive_image(file_path: String, request: HttpRequest) -> impl Responder {
    let path = Path::new(&file_path);

    if let Some(filename) = path.file_name() {
        let output_file = format!(
            "{}/{}",
            crate::commons::temp_dir(),
            filename.to_string_lossy()
        );

        return request.json(200, R::ok(output_file));
    }

    request.json(200, R::ok(crate::commons::temp_dir()))
}

// | 格式   | Progressive / Interlaced | 浏览器效果      |
// | ----  | ------------------------ | -------------- |
// | JPEG  | Progressive              | 先模糊后清晰     |
// | PNG   | Adam7 Interlaced         | 先粗略后清晰     |
// | WebP  | Progressive              | 先模糊后清晰,体积小 |

// 渐进式渲染图片: Actix Web 渐进式图片加载 + Range 支持
// ✅ 支持浏览器 <img>、视频播放器、断点续传,
// ✅ 自动识别文件类型(jpg/png/gif/webp/mp4/mp3 等),
// ✅ 后端分块流式读取文件,节省内存。
pub async fn progressive_render(
    _query: actix_web::web::Query<HashMap<String, String>>,
    req: HttpRequest,
) -> actix_web::Result<HttpResponse, actix_web::Error> {
    let file_path = _query.get("file_path").cloned().unwrap_or_default();

    let mut file = File::open(&file_path)?;

    let metadata = file.metadata()?;
    let file_size = metadata.len();

    // ----------------------------
    // 解析 Range 请求头
    // ----------------------------
    let range_header = req
        .headers()
        .get(header::RANGE)
        .and_then(|v| v.to_str().ok());

    let (start, end) = if let Some(range_str) = range_header {
        if let Some(range_bytes) = range_str.strip_prefix("bytes=") {
            let parts: Vec<&str> = range_bytes.split('-').collect();
            let start = parts[0].parse::<u64>().unwrap_or(0);

            let end = if parts.len() > 1 && !parts[1].is_empty() {
                parts[1].parse::<u64>().unwrap_or(file_size - 1)
            } else {
                file_size - 1
            };

            // Range 边界检查
            if start >= file_size {
                return Ok(HttpResponse::RangeNotSatisfiable()
                    .append_header((header::CONTENT_RANGE, format!("bytes */{}", file_size)))
                    .finish());
            }

            (start, end.min(file_size - 1))
        } else {
            (0, file_size - 1)
        }
    } else {
        (0, file_size - 1)
    };

    // 分块, 流式返回
    let chunk_size = 8 * 1024;

    file.seek(SeekFrom::Start(start))?;

    let total_bytes = end - start + 1;

    let stream = stream::unfold((file, start), move |(mut f, mut pos)| async move {
        let mut buf = vec![0; chunk_size];
        if pos > end {
            return None;
        }

        let remaining = end - pos + 1;
        let read_len = remaining.min(chunk_size as u64) as usize;

        match f.read(&mut buf[..read_len]) {
            Ok(0) => None,
            Ok(n) => {
                pos += n as u64;
                Some((
                    Ok::<_, std::io::Error>(web::Bytes::copy_from_slice(&buf[..n])),
                    (f, pos),
                ))
            }
            Err(_) => None,
        }
    });

    // ----------------------------
    // Partial Content 支持
    // ----------------------------
    let mut response = if range_header.is_some() {
        HttpResponse::PartialContent()
    } else {
        HttpResponse::Ok()
    };

    let content_type = mime_guess::from_path(&file_path)
        .first_or_octet_stream()
        .to_string();

    Ok(response
        .append_header((header::CONTENT_TYPE, content_type))
        .append_header((header::ACCEPT_RANGES, "bytes"))
        .append_header((
            header::CONTENT_RANGE,
            format!("bytes {}-{}/{}", start, end, file_size),
        ))
        .append_header((header::CONTENT_LENGTH, total_bytes.to_string()))
        .streaming(stream))
}