Rust:axum学习笔记(4) 上传文件
接上一篇继续,上传文件是 web开发中的常用功能,本文将演示axum如何实现图片上传(注:其它类型的文件原理相同),一般来说要考虑以下几个因素:
1. 文件上传的大小限制
2. 文件上传的类型限制(仅限指定类型:比如图片)
3. 防止伪装mimetype进行攻击(比如:把.js文件改后缀变成.jpg伪装图片上传,早期有很多这类攻击)
另外,上传图片后,还可以让浏览器重定向到上传后的图片(当然,仅仅只是演示技术实现,实际应用中并非一定要这样)
先展示一个简单的上传文件的表单:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | // 上传表单 async fn show_upload() -> Html<&' static str> { Html( r#" <!doctype html> <html> <head> <meta charset= "utf-8" > <title>上传文件(仅支持图片上传)</title> </head> <body> <form action= "/save_image" method= "post" enctype= "multipart/form-data" > <label> 上传文件(仅支持图片上传): <input type= "file" name= "file" > </label> <button type= "submit" >上传文件</button> </form> </body> </html> "#, ) } |
上传后,用/save_image来处理图片上传
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // 上传图片 async fn save_image( ContentLengthLimit(mut multipart): ContentLengthLimit< Multipart, { 1024 * 1024 * 20 //20M }, >, ) -> Result<(StatusCode, HeaderMap), String> { if let Some(file) = multipart.next_field().await.unwrap() { //文件类型 let content_type = file.content_type().unwrap().to_string(); //校验是否为图片(出于安全考虑) if content_type.starts_with( "image/" ) { //根据文件类型生成随机文件名(出于安全考虑) let rnd = (random::<f32>() * 1000000000 as f32) as i32; //提取"/"的index位置 let index = content_type .find( "/" ) .map(|i| i) .unwrap_or(usize::max_value()); //文件扩展名 let mut ext_name = "xxx" ; if index != usize::max_value() { ext_name = &content_type[index + 1..]; } //最终保存在服务器上的文件名 let save_filename = format!( "{}/{}.{}" , SAVE_FILE_BASE_PATH, rnd, ext_name); //文件内容 let data = file.bytes().await.unwrap(); //辅助日志 println!( "filename:{},content_type:{}" , save_filename, content_type); //保存上传的文件 tokio::fs::write(&save_filename, &data) .await .map_err(|err| err.to_string())?; //上传成功后,显示上传后的图片 return redirect(format!( "/show_image/{}.{}" , rnd, ext_name)).await; } } //正常情况,走不到这里来 println!( "{}" , "没有上传文件或文件格式不对" ); //当上传的文件类型不对时,下面的重定向有时候会失败(感觉是axum的bug) return redirect(format!( "/upload" )).await; } |
上面的代码,如果上传成功,将自动跳转到/show_image来展示图片:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | /** * 显示图片 */ async fn show_image(Path(id): Path<String>) -> (HeaderMap, Vec<u8>) { let index = id.find( "." ).map(|i| i).unwrap_or(usize::max_value()); //文件扩展名 let mut ext_name = "xxx" ; if index != usize::max_value() { ext_name = &id[index + 1..]; } let content_type = format!( "image/{}" , ext_name); let mut headers = HeaderMap:: new (); headers.insert( HeaderName::from_static( "content-type" ), HeaderValue::from_str(&content_type).unwrap(), ); let file_name = format!( "{}/{}" , SAVE_FILE_BASE_PATH, id); (headers, read(&file_name).unwrap()) } /** * 重定向 */ async fn redirect(path: String) -> Result<(StatusCode, HeaderMap), String> { let mut headers = HeaderMap:: new (); //重设LOCATION,跳到新页面 headers.insert( axum::http::header::LOCATION, HeaderValue::from_str(&path).unwrap(), ); //302重定向 Ok((StatusCode::FOUND, headers)) } |
最后是路由设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #[tokio::main] async fn main() { // Set the RUST_LOG, if it hasn't been explicitly defined if std::env::var_os( "RUST_LOG" ).is_none() { std::env::set_var( "RUST_LOG" , "example_sse=debug,tower_http=debug" ) } tracing_subscriber::fmt::init(); // our router let app = Router:: new () .route( "/upload" , get(show_upload)) .route( "/save_image" ,post(save_image)) .route( "/show_image/:id" , get(show_image)) .layer(TraceLayer::new_for_http()); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); // run it with hyper on localhost:3000 axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } |
运行效果:
1. 初始上传表单:
2. 文件尺寸太大时
3.文件类型不对时
从输出日志上看
2022-01-23T03:56:33.381051Z DEBUG request{method=POST uri=/save_image version=HTTP/1.1}: tower_http::trace::on_request: started processing request
没有上传文件或文件格式不对
2022-01-23T03:56:33.381581Z DEBUG request{method=POST uri=/save_image version=HTTP/1.1}: tower_http::trace::on_response: finished processing request latency=0 ms status=302
已经正确处理,并发生了302重定向,但是浏览器里会报错connection_reset(不知道是不是axum的bug)
4. 成功上传后
最后附上完整代码:
cargo.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | [package] name = "uploadfile" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] axum = {version = "0.4.3", features = ["multipart","headers"] } tokio = { version = "1.0", features = ["full"]} rand = "0.7.3" tower-http = { version = "0.2.0", features = ["fs", "trace"] } futures = "0.3" tokio-stream = "0.1" headers = "0.3" tracing = "0.1" tracing-subscriber = { version="0.3", features = ["env-filter"] } |
main.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | use axum::{ extract::{ContentLengthLimit, Multipart, Path}, http::header::{HeaderMap, HeaderName, HeaderValue}, http::StatusCode, response::Html, routing::{get,post}, Router, }; use rand ::prelude::random; use std::fs::read; use std::net::SocketAddr; use tower_http::trace::TraceLayer; const SAVE_FILE_BASE_PATH: &str = "/Users/jimmy/Downloads/upload" ; // 上传表单 async fn show_upload() -> Html<&' static str> { Html( r#" <!doctype html> <html> <head> <meta charset= "utf-8" > <title>上传文件(仅支持图片上传)</title> </head> <body> <form action= "/save_image" method= "post" enctype= "multipart/form-data" > <label> 上传文件(仅支持图片上传): <input type= "file" name= "file" > </label> <button type= "submit" >上传文件</button> </form> </body> </html> "#, ) } // 上传图片 async fn save_image( ContentLengthLimit(mut multipart): ContentLengthLimit< Multipart, { 1024 * 1024 * 20 //20M }, >, ) -> Result<(StatusCode, HeaderMap), String> { if let Some(file) = multipart.next_field().await.unwrap() { //文件类型 let content_type = file.content_type().unwrap().to_string(); //校验是否为图片(出于安全考虑) if content_type.starts_with( "image/" ) { //根据文件类型生成随机文件名(出于安全考虑) let rnd = (random::<f32>() * 1000000000 as f32) as i32; //提取"/"的index位置 let index = content_type .find( "/" ) .map(|i| i) .unwrap_or(usize::max_value()); //文件扩展名 let mut ext_name = "xxx" ; if index != usize::max_value() { ext_name = &content_type[index + 1..]; } //最终保存在服务器上的文件名 let save_filename = format!( "{}/{}.{}" , SAVE_FILE_BASE_PATH, rnd, ext_name); //文件内容 let data = file.bytes().await.unwrap(); //辅助日志 println!( "filename:{},content_type:{}" , save_filename, content_type); //保存上传的文件 tokio::fs::write(&save_filename, &data) .await .map_err(|err| err.to_string())?; //上传成功后,显示上传后的图片 return redirect(format!( "/show_image/{}.{}" , rnd, ext_name)).await; } } //正常情况,走不到这里来 println!( "{}" , "没有上传文件或文件格式不对" ); //当上传的文件类型不对时,下面的重定向有时候会失败(感觉是axum的bug) return redirect(format!( "/upload" )).await; } /** * 显示图片 */ async fn show_image(Path(id): Path<String>) -> (HeaderMap, Vec<u8>) { let index = id.find( "." ).map(|i| i).unwrap_or(usize::max_value()); //文件扩展名 let mut ext_name = "xxx" ; if index != usize::max_value() { ext_name = &id[index + 1..]; } let content_type = format!( "image/{}" , ext_name); let mut headers = HeaderMap:: new (); headers.insert( HeaderName::from_static( "content-type" ), HeaderValue::from_str(&content_type).unwrap(), ); let file_name = format!( "{}/{}" , SAVE_FILE_BASE_PATH, id); (headers, read(&file_name).unwrap()) } /** * 重定向 */ async fn redirect(path: String) -> Result<(StatusCode, HeaderMap), String> { let mut headers = HeaderMap:: new (); //重设LOCATION,跳到新页面 headers.insert( axum::http::header::LOCATION, HeaderValue::from_str(&path).unwrap(), ); //302重定向 Ok((StatusCode::FOUND, headers)) } #[tokio::main] async fn main() { // Set the RUST_LOG, if it hasn't been explicitly defined if std::env::var_os( "RUST_LOG" ).is_none() { std::env::set_var( "RUST_LOG" , "example_sse=debug,tower_http=debug" ) } tracing_subscriber::fmt::init(); // our router let app = Router:: new () .route( "/upload" , get(show_upload)) .route( "/save_image" ,post(save_image)) .route( "/show_image/:id" , get(show_image)) .layer(TraceLayer::new_for_http()); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); // run it with hyper on localhost:3000 axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } |
出处:http://yjmyzz.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 上周热点回顾(3.3-3.9)
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
2016-01-23 docker学习(1) 安装
2016-01-23 maven/gradle 打包后自动上传到nexus仓库
2014-01-23 jboss上的soap web service开发示例
2014-01-23 jboss的时区问题
2014-01-23 JAVA JPA - 示例用法