Rust-搞定图片上传功能

Rust-搞定图片上传功能

1、下载引入

🍎Cargo.toml安装依赖

已经有的就不需要额外添加了

这里我额外移入了uuid 生成唯一文件名

javascript 复制代码
[dependencies]
actix-web = "4.0"  # 开发 RESTful API接口
actix-multipart = "0.4" # 处理文件上传
tokio = { version = "1", features = ["full"] } # 异步运行时,网络、文件I/O异步任务
futures = "0.3" # 异步编程 
serde = { version = "1.0", features = ["derive"] } # 序列化和反序列化
serde_json = "1.0" # 帮我们生成文件名
uuid = "1.0"   # 帮我们生成文件名
mime_guess = "2.0" # 猜测文件类型


更换为下面的 2025-08-01
uuid = { version = "1.17.0", features = ["v4"] }


// 添加文件依赖
  actix-files = "0.6.2"  # 静态文件服务
javascript 复制代码
// uuid最新版本地址

https://crates.io/crates/uuid

2、使用

🍎入口申明模块

路由入口,我们新建一个upload模块

main.rs文件之中申明模块

javascript 复制代码
HttpServer::new(move || {
        let cors = Cors::default()
            .allow_any_origin()
            .allow_any_method()
            .allow_any_header(); // 允许所有来源
        App::new()
            // 添加 CORS 中间件
            .wrap(cors)
            // 2. 注入数据库连接池
            .app_data(web::Data::new(pool.clone()))
            // 3. 注册模块路由加前缀
            .service(
                web::scope("/api") // 这里加上 /api 前缀
                    .configure(modules::user::routes::config),
                    .configure(modules::upload::routes::config),
            )
            // 3. 注册路由
            .route("/", web::get().to(welcome))
    })

🍎申明模块入口

这里需要申明外层模块和子模块两个部分

javascript 复制代码
src\modules\mod.rs

pub mod upload;
pub mod user;


src\modules\upload\mod.rs
pub mod handlers;
pub mod routes; // 必须有这一行,否则无法使用路由

🍎routes.rs模块之中添加接口

routes.rs模块添加接口

javascript 复制代码
use actix_web::web;
pub fn config(cfg: &mut web::ServiceConfig) {
  cfg.route("/upload/image", web::post().to(crate::modules::upload::handlers::uploadimg));
}

🍎测试接口逻辑

handlers.rs之中处理方法逻辑,编写我们的上传,这里我们先测试一下

javascript 复制代码
handlers.rs

// # 处理函数(可选)
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use sqlx::{MySqlPool,MySql, Pool};
use crate::common::response::ApiResponse;// 导入 ApiResponse 模型

// 上传图片接口
pub async fn uploadimg() -> HttpResponse {
    HttpResponse::Ok().json(ApiResponse {
        code: 200,
        msg: "接口信息",
        data:  None::<()>,
    })
}

测试我们的接口,返回如下

javascript 复制代码
{
    "code": 200,
    "msg": "接口信息"
}

3、功能实现

🍎上传文件逻辑

接下来我们参考我们之前的接口部分,返回上传图片成功以后的数据,这里我们先以实现功能为主

javascript 复制代码
// // # 处理函数(可选)
// use actix_web::{web, HttpRequest, HttpResponse, Responder};
// use sqlx::{MySqlPool,MySql, Pool};

// // 上传图片接口
// pub async fn uploadimg() -> HttpResponse {
//     HttpResponse::Ok().json(ApiResponse {
//         code: 200,
//         msg: "接口信息",
//         data:  None::<()>,
//     })
// }


use actix_web::{web, HttpResponse, Responder};
use actix_multipart::Multipart;
use futures::StreamExt;
use std::fs::{create_dir_all, File};
use std::io::Write;
use uuid::Uuid;
use crate::common::response::ApiResponse;
use std::env;
use std::path::Path;

// 定义响应数据结构
#[derive(serde::Serialize)]
struct UploadResponse {
    fullPath: String,
    relativePath: String,
    size: u64,
    fileName: String,
    fileType: String,
    fileUid: String,
}

const UPLOAD_DIR: &str = "./uploads";
const ALLOWED_MIME_TYPES: [&str; 3] = ["image/jpeg", "image/png", "image/gif"];

pub async fn upload_img(mut payload: Multipart) -> impl Responder {
    // 创建上传目录(如果不存在)
    if let Err(e) = create_dir_all(UPLOAD_DIR) {
        return internal_server_error(&format!("创建目录失败: {}", e));
    }

    // 获取基础URL(从环境变量或使用默认值)
    let base_url = env::var("BASE_URL")
        .unwrap_or_else(|_| "http://localhost:8080".to_string());

    // 遍历多部分表单字段
    while let Some(field_result) = payload.next().await {
        let mut field = match field_result {
            Ok(f) => f,
            Err(e) => return bad_request(&format!("字段解析失败: {}", e)),
        };

        // 获取内容处置头部
        let content_disposition = field.content_disposition();
        
        // 获取文件名
        let original_file_name = match content_disposition.get_filename() {
            Some(name) => name.to_string(),
            None => continue,  // 跳过非文件字段
        };

        // 验证文件类型
        let mime_type = field.content_type().to_string();
        if !ALLOWED_MIME_TYPES.contains(&mime_type.as_str()) {
            return bad_request("只允许 JPEG、PNG 或 GIF 图片");
        }

        // 生成唯一文件名和路径
        let extension = get_extension(&mime_type);
        let file_id = Uuid::new_v4().to_string();
        let unique_name = format!("{}.{}", file_id, extension);
        let file_path = format!("{}/{}", UPLOAD_DIR, unique_name);
        let relative_path = format!("/uploads/{}", unique_name);
        let absolute_path = format!("{}{}", base_url, relative_path);

        // 保存文件内容并获取文件大小
        let file_size = match save_file(&mut field, &file_path).await {
            Ok(size) => size,
            Err(e) => {
                return internal_server_error(&format!("文件保存失败: {}", e));
            }
        };

        // 创建响应数据
        let response_data = UploadResponse {
            fullPath: absolute_path,
            relativePath: relative_path,
            size: file_size,
            fileName: format!("图片-{}", original_file_name),
            fileType: mime_type,
            fileUid: file_id,
        };

        // 返回成功响应
        return HttpResponse::Ok().json(ApiResponse {
            code: 200,
            msg: "图片上传成功",
            data: Some(response_data),
        });
    }

    // 没有找到有效的文件字段
    bad_request("未检测到上传的文件")
}

/// 根据 MIME 类型获取文件扩展名
fn get_extension(mime_type: &str) -> &str {
    match mime_type {
        "image/jpeg" => "jpg",
        "image/png" => "png",
        "image/gif" => "gif",
        _ => "bin", // 不会发生(前面已验证)
    }
}

/// 保存上传的文件并返回文件大小
async fn save_file(field: &mut actix_multipart::Field, path: &str) -> std::io::Result<u64> {
    let mut file = File::create(path)?;
    let mut total_size = 0;
    
    // 处理每个数据块
    while let Some(chunk_result) = field.next().await {
        // 处理可能的 MultipartError
        let chunk = chunk_result.map_err(|e| {
            std::io::Error::new(
                std::io::ErrorKind::Other, 
                format!("读取数据块失败: {}", e)
            )
        })?;
        
        // 写入文件并更新大小
        file.write_all(&chunk)?;
        total_size += chunk.len() as u64;
    }
    
    file.flush()?;
    Ok(total_size)
}

/// 400 错误响应
fn bad_request(msg: &str) -> HttpResponse {
    HttpResponse::BadRequest().json(ApiResponse::<()> {
        code: 400,
        msg:"错误",
        data: None,
    })
}

/// 500 错误响应
fn internal_server_error(msg: &str) -> HttpResponse {
    HttpResponse::InternalServerError().json(ApiResponse::<()> {
        code: 500,
        msg:"错误",
        data: None,
    })
}

🍎测试上传图片接口

测试接口这个时候给我们返回的数据如下

javascript 复制代码
{
    "code": 200,
    "msg": "图片上传成功",
    "data": {
        "fullPath": "http://localhost:8888/uploads/68007a03-497e-4982-8316-10881289cb1e.png",
        "relativePath": "/uploads/68007a03-497e-4982-8316-10881289cb1e.png",
        "size": 10739,
        "fileName": "图片-imgjiance2.png",
        "fileType": "image/png",
        "fileUid": "68007a03-497e-4982-8316-10881289cb1e"
    }
}

🍎文件归位

现在可以看到我们传入的文件都在upload下,我们分配一下,图片和视频区别后面

javascript 复制代码
pub async fn upload_img(mut payload: Multipart) -> impl Responder {
    // 创建图片存储目录(如果不存在)
    let image_dir = format!("{}/{}", BASE_UPLOAD_DIR, IMAGE_SUBDIR);
    if let Err(e) = create_dir_all(&image_dir) {
        return internal_server_error(&format!("创建目录失败: {}", e));
    }

    // 获取基础URL(从环境变量或使用默认值)
    let base_url = env::var("BASE_URL")
        .unwrap_or_else(|_| "http://localhost:3000".to_string());

    // 遍历多部分表单字段
    while let Some(field_result) = payload.next().await {
        let mut field = match field_result {
            Ok(f) => f,
            Err(e) => return bad_request(&format!("字段解析失败: {}", e)),
        };

        // 获取内容处置头部
        let content_disposition = field.content_disposition();
        
        // 获取文件名
        let original_file_name = match content_disposition.get_filename() {
            Some(name) => name.to_string(),
            None => continue,  // 跳过非文件字段
        };

        // 验证文件类型
        let mime_type = field.content_type().to_string();
        if !ALLOWED_MIME_TYPES.contains(&mime_type.as_str()) {
            return bad_request("只允许 JPEG、PNG 或 GIF 图片");
        }

        // 生成唯一文件名和路径
        let extension = get_extension(&mime_type);
        let file_id = Uuid::new_v4().to_string();
        let unique_name = format!("{}.{}", file_id, extension);
        
        // 文件存储路径(包含子目录)
        let file_path = format!("{}/{}", image_dir, unique_name);
        
        // URL 路径(包含子目录)
        let relative_path = format!("/uploads/{}/{}", IMAGE_SUBDIR, unique_name);
        let absolute_path = format!("{}{}", base_url, relative_path);

        // 保存文件内容并获取文件大小
        let file_size = match save_file(&mut field, &file_path).await {
            Ok(size) => size,
            Err(e) => {
               return internal_server_error(&format!("文件保存失败: {}", e));
            }
        };

        // 创建响应数据
        let response_data = UploadResponse {
            fullPath: absolute_path,
            relativePath: relative_path,
            size: file_size,
            fileName: format!("图片-{}", original_file_name),
            fileType: mime_type,
            fileUid: file_id,
        };

        // 返回成功响应
        return HttpResponse::Ok().json(ApiResponse {
            code: 200,
            msg: "图片上传成功".to_string(),
            data: Some(response_data),
        });
    }

    // 没有找到有效的文件字段
    bad_request("未检测到上传的文件")
}

这个时候返回的接口地址,已经成为我们想要的路径了

javascript 复制代码
{
    "code": 200,
    "msg": "图片上传成功",
    "data": {
        "fullPath": "http://localhost:8888/uploads/images/a8d23e18-7155-429e-aea2-5c0f70a545e5.png",
        "relativePath": "/uploads/images/a8d23e18-7155-429e-aea2-5c0f70a545e5.png",
        "size": 10739,
        "fileName": "图片-imgjiance2.png",
        "fileType": "image/png",
        "fileUid": "a8d23e18-7155-429e-aea2-5c0f70a545e5"
    }
}

🍎文件静态路径

但是访问我们的图片地址,却无法访问,这是为什么呢?

这是因为我们服务器上还没有装静态文件服务,在我们跟入口文件之中配置

依赖前提必须安装这个依赖

javascript 复制代码
// 依赖前提
actix-files = "0.6.2"  # 静态文件服务

在主文件之中引入

javascript 复制代码
// 文件服务
use actix_files as fs;
use std::fs::create_dir_all; // 创建目录

入口文件之中添加我们的静态文件地址

javascript 复制代码
async fn main() -> std::io::Result<()> {
    dotenv().ok(); // 一定要在读取环境变量之前调用

    // 显式设置日志级别和输出格式
    Builder::new()
        .parse_filters("info") // 设置日志级别为 info
        .init(); // 初始化日志记录器

    
    info!("日志系统已初始化!!!");
    
    // 确保上传目录存在
    let upload_dirs = [
        "./uploads",
        "./uploads/images",
        "./uploads/documents",
        "./uploads/videos",
        "./uploads/others"
    ];
    for dir in &upload_dirs {
        if let Err(e) = create_dir_all(dir) {
            eprintln!("创建目录 {} 失败: {}", dir, e);
            // 生产环境中可能需要更严格的处理
        }
    }
    info!("图片服务器已准备!");


    // 1. 初始化数据库连接池
    // let database_url = env::var("DATABASE_URL").expect("DATABASE_URL not set");
    // 创建 MySQL 异步连接池
    // let pool = MySqlPool::connect(&database_url).await.expect("连接数据库失败");

    let database_url = env::var("DATABASE_URL").unwrap(); // 获取数据库连接字符串
    let pool = MySqlPool::connect(&database_url).await.unwrap();
    
    HttpServer::new(move || {
        let cors = Cors::default()
            .allow_any_origin()
            .allow_any_method()
            .allow_any_header(); // 允许所有来源
        App::new()
            // 添加 CORS 中间件
            .wrap(cors)
            // 2. 注入数据库连接池
            .app_data(web::Data::new(pool.clone()))
            // 3. 注册模块路由加前缀
            .service(
                fs::Files::new("/uploads/images", "./uploads/images")
                    .prefer_utf8(true)
                    .show_files_listing() // 开发环境使用,生产环境应移除
            )
            // 可以添加其他静态文件目录
            .service(
                fs::Files::new("/uploads/documents", "./uploads/documents")
            )
            .service(
                web::scope("/api") // 这里加上 /api 前缀
                    .configure(modules::user::routes::config)
                    .configure(modules::upload::routes::config)
            )
            // 3. 注册路由
            .route("/", web::get().to(welcome))
    })
    .bind("0.0.0.0:8888")?
    .run()
    .await
}

静态资源目录搭建好了以后,再次访问,我们的图片可以完美展示啦

ok

快来跟我一起体验Rust之美吧,最近几天都写的很难,所幸都攻克了 简单但是可能我是小白 很多问题总算踩过去了

相关推荐
aiopencode4 小时前
APP 公钥与 MD5 信息在工程中的价值 一次签名排查过程带来的经验总结
后端
带带弟弟学爬虫__4 小时前
ks安卓—did注册
前端·javascript·vue.js·python·网络爬虫
维维酱4 小时前
使用 TRAE SOLO: 搭建前端项目脚手架
前端
恋猫de小郭5 小时前
Android Studio Otter 2 Feature 发布,最值得更新的 Android Studio
android·前端·flutter
小旭@5 小时前
vue3官方文档巩固
前端·javascript·vue.js
ServBay5 小时前
Django 6.0 发布,新增原生任务队列与 CSP 支持
后端·python·django
用户2190326527355 小时前
Spring Boot 4.0 整合 RabbitMQ 注解方式使用指南
后端
努力往上爬de蜗牛5 小时前
electron 打包
前端·javascript·electron
美自坚韧5 小时前
qiankun微前端
前端·vue.js
高桥留5 小时前
可编辑的span
前端·javascript·css