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 chksum_md5 as md5;
use futures_util::TryStreamExt as _;

use actix_web::http::header;

use mongodb::bson::doc;

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_VIDEO_FILE_COUNT: usize = 1; // 支持的文件上传数量
    static ref LIMIT_VIDEO_FILE_SIZE: usize = 1_048_576 * 9500; // 上传文件大小限制 9500MiB * n
    static ref LEGAL_VIDEO_IMAGE_FILETYPES: [String; 1] = ["video/mp4".to_string()];
}

// 上传文件
// curl -v -F 'file0=@\"C:/myfile.txt\"' 'http://localhost:8000/upload_videos'
pub async fn upload_videos(
    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()));
    }

    // 上传文件大小限制
    if content_length > *LIMIT_VIDEO_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_VIDEO_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![];

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

            if current_count > *LIMIT_VIDEO_FILE_COUNT {
                failed_code = 400;
                failed_message =
                    format!("最大上传视频数量: {}", *LIMIT_VIDEO_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;
            }

            if let Some(file_type) = field.content_type() {
                if !LEGAL_VIDEO_IMAGE_FILETYPES.contains(&file_type.to_string()) {
                    failed_code = 400;
                    failed_message = format!("不支持的视频类型: {}", &file_type);
                    log::warn!("{}", failed_message);
                    is_invalid = true;
                    continue;
                }
            } else {
                continue;
            }

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

            log::info!("save_upload: destination={}", destination.to_str().unwrap());

            file_list.push((file_name, destination));
        } else {
            break;
        }

        current_count += 1;
    }

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

        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());

                    let file = std::fs::File::open(&destination).unwrap();
                    let file_md5 = md5::chksum(&file).unwrap().to_hex_lowercase();
                    let curr_fsize = file.metadata().unwrap().len();

                    if let Some((_id, flength)) = app_state
                        .mongo()
                        .get_file_by_md5(&GRID_FS_DATABASE_NAME, &file_md5)
                        .await?
                    {
                        if curr_fsize == flength {
                            log::info!(
                                "upload_file: _upload_type={}, file_md5={}, flength={}, destination={}",
                                upload_type.as_str(),
                                file_md5,
                                flength,
                                destination.to_str().unwrap()
                            );

                            if let Err(err) = std::fs::remove_file(&destination) {
                                log::error!("clear-files-error: error={:?}", err)
                            }

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

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

                    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()),
        )
    }
}