I have a React front-end application that has a download button on a page. When I click the download it hits my file serving URL and downloads the file to the browser's memory first and then it downloads to the system and I get to see the download indicator.
However, I don't want this functionality. I want the browser to show the download indicator as soon as my API responds with data.
I did some research and found that for Chrome or any browser to show a download indicator it needs to first get the content-length
header in the response header, but the file I am transferring is quite big about 3.7 GB so I choose Streams to send data.
But for streaming data it does not support the content-length
header, I tried passing it to my response but the 'content-length' header was just not showing up.
I did some more research and got to know that using content-range
might work but I did not get any luck even with that.
I use the async_streams
crate of Rust to create a stream of my file and then I send it over using actix_web
like so:
Ok(HttpResponse::Ok().content_type("application/x-zip-compressed").insert_header((header::CONTENT_RANGE, format!("bytes 0-1023/{}", file_size))).insert_header((header::CONTENT_DISPOSITION,format!("attachment; filename=\"USL.zip\""),)).streaming(my_data_stream))
Since this is a large file I am avoiding loading the entire file into the memory and sending it as my API is running on a VM with a specific size and restrictions.
Here is my entire code:
#[get("/api/v1/download/file.zip")]
pub async fn download_file_zip(req: HttpRequest, credential: BearerAuth) -> Result<HttpResponse> {
let file_path = "/path/to/file/Package.zip";
debug!("File path was set to {}", file_path);
if let Ok(file_metadata) = fs::metadata(&file_path) {
let file_size = file_metadata.len();
debug!("File size: {} bytes", file_size);
let mut chunk = vec![0u8; 10 * 1024 * 1024]; // Adjust the chunk size as needed
if let Ok(mut file) = File::open(file_path) {
debug!("File was opened successfully");
let my_data_stream = stream! {
let mut chunk = vec![0u8; 10 * 1024 * 1024]; // Adjust the chunk size as needed
loop {
match file.read(&mut chunk) {
Ok(n) => {
if n == 0 {
break;
}
info!("Read {} bytes from file", n);
yield Result::<Bytes, std::io::Error>::Ok(Bytes::from(chunk[..n].to_vec()));
}
Err(e) => {
eprintln!("Error reading file: {}", e);
yield Result::<Bytes, std::io::Error>::Err(e);
break;
}
}
}
};
debug!("Sending response...");
Ok(HttpResponse::Ok()
.content_type("application/x-zip-compressed")
.insert_header((header::CONTENT_RANGE, format!("bytes 0-1023/{}", file_size)))
.insert_header((
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"USL.zip\""),
))
.streaming(my_data_stream))
} else {
Ok(HttpResponse::NotFound().finish())
}
} else {
Ok(HttpResponse::NotFound().finish())
}
}
Here are some screenshots to help you understand better:
as you can see here I have pressed my download button 2 times and both times it downloads the file into the browser's memory first and then Chrome downloads it to my system.
Here are the list of headers I am passing:
Here is my frontend Receiving side of code:
fetch(apiUrl, requestOption)
.then((response) => {
if (response.ok) {
// If the response status is OK, initiate the download
const reader = response.body.getReader();
const stream = new ReadableStream({
start(controller) {
function push() {
reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
controller.enqueue(value);
push();
})
}
push();
}
});
return new Response(stream, { headers: { "Content-Type": "application/octet-stream" } });
} else {
console.error('Download failed with status:', response.status);
}
})
.then(response => response.blob())
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = 'file.zip'; // Provide a default filename
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
})
.catch((error) => {
console.error('Error while downloading:', error);
});
I am out of ideas please let me know if I can do something differently to send and receive large files.
I specifically want the download indicator to be shown when the Data is received at the front end normally how it happens in most cases.
Thank you!