Để xử lý tải xuống tệp một cách linh hoạt — vừa hỗ trợ lưu trực tiếp vào thiết bị người dùng, vừa cho phép hiển thị nội dung (như ảnh, PDF) trong trình duyệt — cần phối hợp đồng bộ giữa frontend và backend theo cơ chế luồng (streaming).
1. Xử lý phía client
1.1 Tải xuống tự động qua tab mới
Khi mục tiêu là kích hoạt hành vi lưu tệp ngay lập tức (ví dụ: file nén, tài liệu), ta sử dụng window.open() để mở URL tải xuống trong tab/tabs riêng. Cách này tránh các hạn chế từ chính sách cùng nguồn (CORS) và không yêu cầu xử lý phản hồi thủ công:
window.open('/api/v1/files/archive.zip');
1.2 Tải và hiển thị nội dung trong DOM
Với các định dạng có thể render trực tiếp (ảnh, SVG, PDF…), cần lấy dữ liệu dưới dạng Blob, sau đó tạo URL đối tượng để gán vào phần tử đích:
<img id="preview" width="200" height="150" alt="Đang tải..." />
this.http.get('/api/v1/files/photo.jpg', {
responseType: 'blob'
}).subscribe(blob => {
const url = URL.createObjectURL(blob);
const img = document.getElementById('preview') as HTMLImageElement;
if (img) img.src = url;
});
2. Triển khai phía server với Actix Web và Tokio
Phản hồi HTTP chuẩn cho tải xuống phải bao gồm ba header quan trọng:
Content-Type: Khai báo kiểu MIME (ví dụ:image/jpeg,application/zip)Content-Disposition: Chỉ định hành vi "attachment" hoặc "inline", kèm tên tệp gốcContent-Length: Độ dài byte của nội dung — giúp trình duyệt hiển thị tiến trình và quản lý bộ nhớ hiệu quả
Dưới đây là triển khai Rust sử dụng actix-web phiên bản 4, tích hợp streaming không chặn với tokio::fs và async-stream:
[dependencies]
actix-web = { version = "4.4", features = ["openssl"] }
tokio = { version = "1.37", features = ["full"] }
async-stream = "0.3"
futures-util = "0.3"
openssl = { version = "0.10", features = ["v111"] }
use actix_web::{
http::header::{ContentDisposition, ContentType, DispositionParam, DispositionType},
get, App, HttpResponse, HttpServer, Responder, web,
};
use std::path::PathBuf;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use async_stream::stream;
#[get("/api/v1/files/{name}")]
async fn stream_file(name: web::Path<String>) -> impl Responder {
let filepath = PathBuf::from("data/uploads/").join(&*name);
if !filepath.exists() {
return HttpResponse::NotFound().body("Tệp không tồn tại");
}
let metadata = match File::open(&filepath).await {
Ok(file) => file.metadata().await.ok(),
_ => None,
};
let file_size = match metadata {
Some(m) => m.len(),
None => return HttpResponse::InternalServerError().body("Không đọc được thông tin tệp"),
};
let stream = stream! {
let mut file = match File::open(&filepath).await {
Ok(f) => f,
Err(e) => {
eprintln!("Lỗi mở tệp: {}", e);
return;
}
};
let mut buffer = [0u8; 8192];
loop {
match file.read(&mut buffer).await {
Ok(0) => break,
Ok(n) => yield Ok(web::Bytes::copy_from_slice(&buffer[..n])),
Err(e) => {
eprintln!("Lỗi đọc tệp: {}", e);
break;
}
}
}
};
HttpResponse::Ok()
.content_type("application/octet-stream")
.insert_header(("Content-Length", file_size.to_string()))
.insert_header(ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::Filename(name.into_inner())],
})
.streaming(stream)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("Khởi động máy chủ tại http://127.0.0.1:8080");
HttpServer::new(|| App::new().service(stream_file))
.bind("127.0.0.1:8080")?
.run()
.await
}