zero4rs 2.0.0

zero4rs is a powerful, pragmatic, and extremely fast web framework for Rust
Documentation
use lazy_static::lazy_static;

use crate::prelude2::*;

use std::str::FromStr;

use actix_web::http::header;
use futures_util::TryStreamExt as _;
use mongodb::bson::doc;

use mime::Mime;
use mime::IMAGE_GIF;
use mime::IMAGE_JPEG;
use mime::IMAGE_PNG;

use crate::core::auth0::Requestor;
use crate::core::auth0::UserId;

use crate::sitepages::upload0::clear_files;
use crate::sitepages::upload0::UploadType;
use crate::sitepages::upload0::GRID_FS_DATABASE_NAME;

lazy_static! {
    static ref LIMIT_IMAGE_FILE_COUNT: usize = 1; // 支持的文件上传数量
    static ref LIMIT_FILE_SIZE: usize = 1_048_576 * 150; // 上传文件大小限制 150MiB * n
    static ref LEGAL_IMAGE_FILETYPES: [Mime; 3] = [IMAGE_PNG, IMAGE_JPEG, IMAGE_GIF];
}

// 上传文件
// curl -v -F 'file0=@\"C:/myfile.txt\"' 'http://localhost:8000/upload_images'
pub async fn upload_images(
    mut payload: actix_multipart::Multipart,
    request: HttpRequest,
    app_state: web::Data<AppContext>,
    user_id: web::ReqData<UserId>,
    requestor: web::ReqData<Requestor>,
) -> impl Responder {
    // 1. limit the file size
    // 2. limit the file count
    // 3. limit the file type
    // 4. save the file
    // 5. convert to .gif

    let user_id = user_id.into_inner();
    let user_details = requestor.user();

    let content_length = match request.headers().get(header::CONTENT_LENGTH) {
        Some(val) => val.to_str().unwrap_or("0").parse().unwrap(),
        None => 0,
    };

    if content_length == 0 {
        return request.json(200, R::failed(400, "上传图片为空!".to_string()));
    }

    // 上传文件大小限制 50MiB
    if content_length > *LIMIT_FILE_SIZE {
        // fixed bug!!! release the stream
        while let Ok(Some(_)) = payload.try_next().await {
            // while let Ok(Some(_chunk)) = field.try_next().await {}
        }

        return request.json(
            200,
            R::failed(
                400,
                format!(
                    "图片大小超过限制: {}",
                    crate::commons::format_bytes_size(*LIMIT_FILE_SIZE)
                ),
            ),
        );
    }

    let mut current_count = 0;
    let mut upload_type: Option<String> = None;
    let mut is_invalid = false;
    let mut failed_message = String::new();
    let mut failed_code = 0;
    let mut upload_id = None;
    let mut file_list = vec![];
    let mut field_file0_found = false;

    loop {
        if let Ok(Some(mut field)) = payload.try_next().await {
            if is_invalid {
                continue;
            }

            if current_count > *LIMIT_IMAGE_FILE_COUNT {
                failed_code = 400;
                failed_message =
                    format!("最大上传图片数量: {}", *LIMIT_IMAGE_FILE_COUNT).to_string();
                is_invalid = true;
                continue;
            }

            if field.name().is_none() {
                continue;
            }

            let field_name = field.name().unwrap();

            if field_name == "upload_type" {
                while let Ok(Some(_chunk)) = field.try_next().await {
                    if let Ok(value) = std::str::from_utf8(&_chunk) {
                        upload_type = Some(value.to_string());
                        break;
                    }
                }
                continue;
            }

            if field_name != "file0" {
                continue;
            }

            field_file0_found = true;

            let content_disposition = field.content_disposition();
            let mut filename = "unknown";

            if content_disposition.is_some() {
                if let Some(f) = content_disposition {
                    filename = f.get_filename().unwrap_or("unknown");
                }
            }

            if let Some(file_type) = field.content_type() {
                if !LEGAL_IMAGE_FILETYPES.contains(file_type) {
                    failed_code = 400;
                    failed_message =
                        format!("不支持的图片类型: {}, file_name={}", &file_type, filename);
                    log::warn!("{}", failed_message);
                    is_invalid = true;
                    continue;
                }
            } else {
                continue;
            }

            let (file_name, destination, _) = crate::commons::save_upload(field).await?;

            file_list.push((file_name, destination));

            // resize the image
            // let l =
            //     web::block(
            //         move || async move { crate::commons::gif(200, 200, &destination) },
            //     )
            //     .await
            //     .unwrap()
            //     .await;

            // log::info!(
            //     "upload_file: _upload_type={}, file_name={}, destination={}",
            //     upload_type.clone().unwrap_or("N/a".to_string()),
            //     &file_name,
            //     l.to_str().unwrap()
            // );
        } else {
            break;
        }

        current_count += 1;
    }

    if is_invalid || !field_file0_found {
        clear_files(
            file_list
                .into_iter()
                .map(|(_, path)| path.clone())
                .collect(),
        );

        if !field_file0_found {
            return request.json(200, R::failed(400, "file0 is required"));
        } else {
            return request.json(200, R::failed(failed_code, failed_message));
        }
    }

    if let Some(upload_type) = upload_type {
        match UploadType::from_str(&upload_type) {
            Ok(upload_type) => {
                for (file_name, destination) in file_list {
                    let mime_type = mime_guess::MimeGuess::from_path(&destination)
                        .first()
                        .map(|mime| mime.to_string());

                    log::info!(
                        "upload_file: _upload_type={}, destination={}",
                        upload_type.as_str(),
                        destination.to_str().unwrap()
                    );

                    if upload_type == UploadType::ProfilePicture {
                        // 压缩图片
                    }

                    let metadata = doc! {
                        "user_id": &user_id.0,
                        "resource_id": &user_details.resource_id,
                        "upload_type": upload_type.as_str(),
                        "mime_type": mime_type,
                        "created_at": crate::commons::timestamp_millis()
                    };

                    upload_id = app_state
                        .mongo()
                        .save_file_with_opts(
                            &GRID_FS_DATABASE_NAME,
                            &file_name,
                            &destination,
                            metadata,
                        )
                        .await?;

                    std::fs::remove_file(destination).map_err(|e| anyhow::anyhow!(e))?;
                }

                request.json(200, R::ok(upload_id))
            }
            Err(_) => {
                clear_files(
                    file_list
                        .into_iter()
                        .map(|(_, path)| path.clone())
                        .collect(),
                );

                request.json(
                    200,
                    R::failed(
                        failed_code,
                        format!("不支持的图片上传类型: {}", upload_type),
                    ),
                )
            }
        }
    } else {
        clear_files(
            file_list
                .into_iter()
                .map(|(_, path)| path.clone())
                .collect(),
        );

        request.json(
            200,
            R::failed(failed_code, "upload_type is required!".to_string()),
        )
    }
}