gist.github.com/Andrew-Shay/566c18fc0e0e9ce721b7b85cdfd0c3ec
use axum::{ body::Body, http::{header, StatusCode}, response::IntoResponse, routing::get, Router, }; use tokio::{fs::File, io::AsyncSeekExt}; use tokio_util::io::ReaderStream; /// How to serve a file with axum that supports resuming if interrupted /// https://andrewshay.me/digital-garden/rust/axum-download-file-resume.html /// /// HOW TO USE /// Create input.zip in your current working directory. Likely the same dir as Cargo.toml /// $ cargo run /// Note: This doesn't seem to work in Chrome. If you can solve it, let me know. /// In Firefox go to 127.0.0.1:3000 /// You should see Hello, World! /// Go to 127.0.0.1:3000/download /// File should start downloading /// Try pausing/resuming the download. And turning off the server mid download. /// When you resume the download, it should continue from where it left off. It should NOT start over. /// This is accomplished through CONTENT_RANGE and PARTIAL_CONTENT /// /// Cargo.toml /// [dependencies] /// axum = "0.8.1" /// tokio = { version = "1.43.0", features = ["full"] } /// tokio-util = { version = "0.7.13", features = ["io"] } #[tokio::main] async fn main() { let app = Router::new() .route("/", get(root)) // Hello World test .route("/download", get(download)); // Downloads the file let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap(); println!("listening..."); axum::serve(listener, app).await.unwrap(); } async fn root() -> &'static str { "Hello, World!" } // https://docs.rs/axum/latest/axum/extract/index.html async fn download(headers: header::HeaderMap) -> impl IntoResponse { println!("\n\n######\n"); println!("Request Headers\n{:?}\n", headers); // Get existing bytes downloaded. // The browser sends this information. // It may not exist when the download is first started. // Sample from Firefox "range": "bytes=715808768-" let range: u64 = match headers.get("range") { Some(range) => match range.to_str() { Ok(range) => { let mut split = range.split("="); match split.nth(1) { Some(range) => { let range = range.strip_suffix("-").unwrap_or(range); let range: u64 = range.parse().unwrap_or(0); range } None => 0, } } Err(_) => 0, }, None => 0, }; println!("Parsed range: {:?}\n", range); // Open file and seek into how much the browser has already downloaded let mut file = File::open("input.zip").await.unwrap(); let file_len: u64 = file.metadata().await.unwrap().len(); let bytes_left = (file_len - range).to_string(); file.seek(std::io::SeekFrom::Start(range)).await.unwrap(); // Response headers telling browser we have an attachment with a given range already into the file let reponse_headers = [ (header::ACCEPT_RANGES, "bytes".to_string()), (header::CONTENT_TYPE, "application/octet-stream".to_string()), (header::CONTENT_LENGTH, bytes_left), ( header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}\"", "output.zip"), // attachment tells browser to download the file and not just display it ), ( header::CONTENT_RANGE, format!("bytes {}-{}/{}", range, file_len - 1, file_len), // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range ), ]; let stream = ReaderStream::new(file); println!("Response Headers\n{:?}", reponse_headers); ( StatusCode::PARTIAL_CONTENT, reponse_headers, Body::from_stream(stream), ) }