文件下载-流式下载

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"),
    }
}

 

posted on 2024-06-14 13:59  书源  阅读(60)  评论(0编辑  收藏  举报

导航