文件下载-流式下载
1. 前端:
1.1 流式下载:
为了避免浏览器同源策略影响, 我们通过打开一个新的标签页来实现下载:
这种方式会直接触发浏览器保存, 将文件保存在浏览器默认下载文件中. (当然想要触发文件保存, 仍然需要后端配合实现)
window.open('/api/download/test.tar');
1.2 为了实现某些文件的预览我们需要将文件先加载到内存中, 而不是自动触发保存
<img id="image" style="vertical-align: middle; " alt="图片加载中..." width="160px" height="60px"/>
constructor(private http: HttpClient) {} this.http.get('/api/download/test.png', { responseType: 'blob', }).subscribe(res=>{ const blob = new Blob([res], { type: 'image/png' }); const image = document.getElementById('image',) as HTMLImageElement; if (image) { image.src = window.URL.createObjectURL(blob); } else { throw new Error('Failed to obtain verification code element'); } });
2. 后端
我们先看下载的响应协议
HTTP/1.1 200 OK Content-Type: application/octet-stream Content-Disposition: attachment; filename="test.png" Content-Length: [文件大小] [其他可选的响应头] [文件的数据]
可以看到文件下载响应头需要设置下面三个头, 浏览器收到协议后会自动进行一些处理:
设置 Content-Type
设置 Content-Disposition
设置 Content-Length
Cargo.toml:
[dependencies] actix-web = { version = "4", features = ["openssl"] } openssl = { version = "0.10", features = ["v110"] } tokio = { version = "1.38.0", features = ["full"] } async-stream = "0.3" futures-util = "0.3.30" actix-multipart = "0.4"
rust:
use actix_web::{ http::header::{ContentDisposition, ContentType, DispositionParam, DispositionType, HeaderValue}, get, post, web, Error, App, HttpRequest, HttpResponse, HttpServer, Responder, }; use std::path::Path; use tokio::io::AsyncReadExt; use openssl::ssl::{SslFiletype, SslAcceptor, SslMethod}; use actix_multipart::Multipart; use tokio::io::AsyncWriteExt; use futures_util::StreamExt; use tokio::fs; #[actix_web::main] async fn main() -> std::io::Result<()> { let addr = "127.0.0.1:8888"; println!("准备监听: {}", addr); let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); builder .set_private_key_file( "D:\\server.key", SslFiletype::PEM, ) .unwrap(); builder .set_certificate_chain_file("D:\\server.crt") .unwrap(); HttpServer::new(|| { App::new() .service(my_download) .service(my_upload) }) .bind_openssl(addr, builder)? .run() .await } #[get("/api/download/{filename}/")] async fn my_download(filename: web::Path<(String)>) -> impl Responder { println!("get / 接收到请求"); filename = &filename; println!("filename: {}", filename); let file_full_path = format!("F:\\{}", &filename); let path = Path::new(&file_full_path); if !path.exists() { return HttpResponse::NotFound().body("File not found"); } match tokio::fs::File::open(&path).await { Ok(mut file) => { let file_len = file.metadata().await.unwrap().len(); println!(" {} ", file_len); println!(" {} ", filename); let stream = async_stream::stream! { let mut buffer = [0; 8192]; // 8KB buffer loop { match file.read(&mut buffer).await { Ok(0) => break, // EOF Ok(n) => yield Ok::<_, std::io::Error>(web::Bytes::copy_from_slice(&buffer[..n])), Err(e) => { eprintln!("Error reading file: {}", e); break; } } } }; HttpResponse::Ok() .content_type("application/octet-stream") .insert_header(( "Content-Length", HeaderValue::from_str(&file_len.to_string()).unwrap() )) .insert_header(ContentDisposition { disposition: DispositionType::Attachment, parameters: vec! [DispositionParam::Filename(filename.to_string())], }) .streaming(stream) }, Err(_) => HttpResponse::InternalServerError().body("Error opening file"), } }