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之美吧,最近几天都写的很难,所幸都攻克了 简单但是可能我是小白 很多问题总算踩过去了

相关推荐
杨DaB2 小时前
【SpringMVC】拦截器,实现小型登录验证
java·开发语言·后端·servlet·mvc
奕辰杰4 小时前
关于npm前端项目编译时栈溢出 Maximum call stack size exceeded的处理方案
前端·npm·node.js
JiaLin_Denny5 小时前
如何在NPM上发布自己的React组件(包)
前端·react.js·npm·npm包·npm发布组件·npm发布包
路光.6 小时前
触发事件,按钮loading状态,封装hooks
前端·typescript·vue3hooks
我爱996!6 小时前
SpringMVC——响应
java·服务器·前端
咔咔一顿操作7 小时前
Vue 3 入门教程7 - 状态管理工具 Pinia
前端·javascript·vue.js·vue3
kk爱闹7 小时前
用el-table实现的可编辑的动态表格组件
前端·vue.js
漂流瓶jz8 小时前
JavaScript语法树简介:AST/CST/词法/语法分析/ESTree/生成工具
前端·javascript·编译原理
换日线°8 小时前
css 不错的按钮动画
前端·css·微信小程序
风象南8 小时前
前端渲染三国杀:SSR、SPA、SSG
前端