文件下载-流式下载
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"),
}
}
浙公网安备 33010602011771号