螃蟹要清蒸 發表於 2025-8-3 09:49:40

使用Rust语言搞定图片上传功能的示例详解

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>1、下载引入</li><ul class="second_class_ul"><li>Cargo.toml安装依赖</li></ul><li>2、使用</li><ul class="second_class_ul"><li>入口申明模块</li><li>申明模块入口</li><li>routes.rs模块之中添加接口</li><li>测试接口逻辑</li></ul><li>3、功能实现</li><ul class="second_class_ul"><li>上传文件逻辑</li><li>测试上传图片接口</li><li>文件归位</li><li>文件静态路径</li></ul></ul></div><p class="maodian"></p><h2>1、下载引入</h2>
<p class="maodian"></p><h3>Cargo.toml安装依赖</h3>
<p>已经有的就不需要额外添加了</p>
<p>这里我额外移入了uuid 生成唯一文件名</p>
<div class="jb51code"><pre class="brush:asm;">
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"# 静态文件服务
</pre></div>
<p>uuid最新版本地址:https://crates.io/crates/uuid</p>
<p class="maodian"></p><h2>2、使用</h2>
<p class="maodian"></p><h3>入口申明模块</h3>
<p>路由入口,我们新建一个<code>upload</code>模块</p>
<p><code>main.rs</code>文件之中申明模块</p>
<div class="jb51code"><pre class="brush:js;">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))
    })
</pre></div>
<p class="maodian"></p><h3>申明模块入口</h3>
<p>这里需要申明外层模块和子模块两个部分</p>
<div class="jb51code"><pre class="brush:js;">src\modules\mod.rs

pub mod upload;
pub mod user;


src\modules\upload\mod.rs
pub mod handlers;
pub mod routes; // 必须有这一行,否则无法使用路由
</pre></div>
<p class="maodian"></p><h3>routes.rs模块之中添加接口</h3>
<p><code>routes.rs</code>模块添加接口</p>
<div class="jb51code"><pre class="brush:js;">use actix_web::web;
pub fn config(cfg: &amp;mut web::ServiceConfig) {
cfg.route("/upload/image", web::post().to(crate::modules::upload::handlers::uploadimg));
}

</pre></div>
<p class="maodian"></p><h3>测试接口逻辑</h3>
<p><code>handlers.rs</code>之中处理方法逻辑,编写我们的上传,这里我们先测试一下</p>
<div class="jb51code"><pre class="brush:js;">handlers.rs

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

// 上传图片接口
pub async fn uploadimg() -&gt; HttpResponse {
    HttpResponse::Ok().json(ApiResponse {
      code: 200,
      msg: "接口信息",
      data:None::&lt;()&gt;,
    })
}
</pre></div>
<p>测试我们的接口,返回如下</p>
<div class="jb51code"><pre class="brush:json;">{
    "code": 200,
    "msg": "接口信息"
}
</pre></div>
<p class="maodian"></p><h2>3、功能实现</h2>
<p class="maodian"></p><h3>上传文件逻辑</h3>
<p>接下来我们参考我们之前的接口部分,返回上传图片成功以后的数据,这里我们先以实现功能为主</p>
<div class="jb51code"><pre class="brush:js;">// // # 处理函数(可选)
// use actix_web::{web, HttpRequest, HttpResponse, Responder};
// use sqlx::{MySqlPool,MySql, Pool};

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


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;

// 定义响应数据结构
#
struct UploadResponse {
    fullPath: String,
    relativePath: String,
    size: u64,
    fileName: String,
    fileType: String,
    fileUid: String,
}

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

pub async fn upload_img(mut payload: Multipart) -&gt; impl Responder {
    // 创建上传目录(如果不存在)
    if let Err(e) = create_dir_all(UPLOAD_DIR) {
      return internal_server_error(&amp;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) =&gt; f,
            Err(e) =&gt; return bad_request(&amp;format!("字段解析失败: {}", e)),
      };

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

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

      // 生成唯一文件名和路径
      let extension = get_extension(&amp;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(&amp;mut field, &amp;file_path).await {
            Ok(size) =&gt; size,
            Err(e) =&gt; {
                return internal_server_error(&amp;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: &amp;str) -&gt; &amp;str {
    match mime_type {
      "image/jpeg" =&gt; "jpg",
      "image/png" =&gt; "png",
      "image/gif" =&gt; "gif",
      _ =&gt; "bin", // 不会发生(前面已验证)
    }
}

/// 保存上传的文件并返回文件大小
async fn save_file(field: &amp;mut actix_multipart::Field, path: &amp;str) -&gt; std::io::Result&lt;u64&gt; {
    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(&amp;chunk)?;
      total_size += chunk.len() as u64;
    }
   
    file.flush()?;
    Ok(total_size)
}

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

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

</pre></div>
<p class="maodian"></p><h3>测试上传图片接口</h3>
<p>测试接口这个时候给我们返回的数据如下</p>
<div class="jb51code"><pre class="brush:json;">{
    "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"
    }
}
</pre></div>
<p class="maodian"></p><h3>文件归位</h3>
<p>现在可以看到我们传入的文件都在upload下,我们分配一下,图片和视频区别后面</p>
<div class="jb51code"><pre class="brush:js;">pub async fn upload_img(mut payload: Multipart) -&gt; impl Responder {
    // 创建图片存储目录(如果不存在)
    let image_dir = format!("{}/{}", BASE_UPLOAD_DIR, IMAGE_SUBDIR);
    if let Err(e) = create_dir_all(&amp;image_dir) {
      return internal_server_error(&amp;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) =&gt; f,
            Err(e) =&gt; return bad_request(&amp;format!("字段解析失败: {}", e)),
      };

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

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

      // 生成唯一文件名和路径
      let extension = get_extension(&amp;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(&amp;mut field, &amp;file_path).await {
            Ok(size) =&gt; size,
            Err(e) =&gt; {
               return internal_server_error(&amp;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("未检测到上传的文件")
}
</pre></div>
<p>这个时候返回的接口地址,已经成为我们想要的路径了</p>
<div class="jb51code"><pre class="brush:json;">{
    "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"
    }
}
</pre></div>
<p class="maodian"></p><h3>文件静态路径</h3>
<p>但是访问我们的图片地址,却无法访问,这是为什么呢?</p>
<p>这是因为我们服务器上还没有装静态文件服务,在我们跟入口文件之中配置</p>
<p>依赖前提必须安装这个依赖</p>
<div class="jb51code"><pre class="brush:js;">// 依赖前提
actix-files = "0.6.2"# 静态文件服务
</pre></div>
<p>在主文件之中引入</p>
<div class="jb51code"><pre class="brush:js;">// 文件服务
use actix_files as fs;
use std::fs::create_dir_all; // 创建目录
</pre></div>
<p>入口文件之中添加我们的静态文件地址</p>
<div class="jb51code"><pre class="brush:js;">async fn main() -&gt; std::io::Result&lt;()&gt; {
    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 &amp;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(&amp;database_url).await.expect("连接数据库失败");

    let database_url = env::var("DATABASE_URL").unwrap(); // 获取数据库连接字符串
    let pool = MySqlPool::connect(&amp;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
}
</pre></div>
<p>静态资源目录搭建好了以后,再次访问,我们的图片可以完美展示啦</p>
<p>快来跟我一起体验Rust之美吧,最近几天都写的很难,所幸都攻克了 简单但是可能我是小白 很多问题总算踩过去了</p>
<p>到此这篇关于使用Rust语言搞定图片上传功能的示例详解的文章就介绍到这了,更多相关Rust图片上传内容请搜索琼殿技术社区以前的文章或继续浏览下面的相关文章希望大家以后多多支持琼殿技术社区!</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>通过rust实现自己的web登录图片验证码功能</li><li>前端基于Rust实现的Wasm进行图片压缩的技术文档(实现方案)</li><li>Rust语言实现图像编码转换</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: 使用Rust语言搞定图片上传功能的示例详解