diff --git a/axum-extra/src/response/file_stream.rs b/axum-extra/src/response/file_stream.rs index a779787780..03271410c5 100644 --- a/axum-extra/src/response/file_stream.rs +++ b/axum-extra/src/response/file_stream.rs @@ -545,7 +545,7 @@ mod tests { .unwrap() .to_str() .unwrap(), - format!("bytes 20-1000/{}", content_length) + format!("bytes 20-1000/{content_length}") ); Ok(()) } diff --git a/examples/stream-to-file/Cargo.toml b/examples/stream-to-file/Cargo.toml index fee5e0ebbe..534324f846 100644 --- a/examples/stream-to-file/Cargo.toml +++ b/examples/stream-to-file/Cargo.toml @@ -5,11 +5,9 @@ edition = "2021" publish = false [dependencies] -async-stream = "0.3" axum = { path = "../../axum", features = ["multipart"] } -axum-extra = { path = "../../axum-extra", features = ["file-stream"] } futures = "0.3" tokio = { version = "1.0", features = ["full"] } tokio-util = { version = "0.7", features = ["io"] } tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } \ No newline at end of file diff --git a/examples/stream-to-file/src/main.rs b/examples/stream-to-file/src/main.rs index ded7c0e68c..46272cf98f 100644 --- a/examples/stream-to-file/src/main.rs +++ b/examples/stream-to-file/src/main.rs @@ -4,26 +4,22 @@ //! cargo run -p example-stream-to-file //! ``` -use async_stream::try_stream; use axum::{ body::Bytes, extract::{Multipart, Path, Request}, - http::{header, HeaderMap, StatusCode}, - response::{Html, IntoResponse, Redirect, Response}, + http::StatusCode, + response::{Html, Redirect}, routing::{get, post}, BoxError, Router, }; -use axum_extra::response::file_stream::FileStream; use futures::{Stream, TryStreamExt}; -use std::{io, path::PathBuf}; -use tokio::{ - fs::File, - io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, BufWriter}, -}; -use tokio_util::io::{ReaderStream, StreamReader}; +use std::io; +use tokio::{fs::File, io::BufWriter}; +use tokio_util::io::StreamReader; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + const UPLOADS_DIRECTORY: &str = "uploads"; -const DOWNLOAD_DIRECTORY: &str = "downloads"; + #[tokio::main] async fn main() { tracing_subscriber::registry() @@ -39,23 +35,9 @@ async fn main() { .await .expect("failed to create `uploads` directory"); - tokio::fs::create_dir(DOWNLOAD_DIRECTORY) - .await - .expect("failed to create `downloads` directory"); - - //create a file to download - create_test_file(std::path::Path::new(DOWNLOAD_DIRECTORY).join("test.txt")) - .await - .expect("failed to create test file"); - let app = Router::new() - .route("/upload", get(show_form).post(accept_form)) - .route("/", get(show_form2).post(accept_form)) - .route("/file/{file_name}", post(save_request_body)) - .route("/file_download", get(file_download_handler)) - .route("/simpler_file_download", get(simpler_file_download_handler)) - .route("/range_file", get(file_range_handler)) - .route("/range_file_stream", get(try_file_range_handler)); + .route("/", get(show_form).post(accept_form)) + .route("/file/{file_name}", post(save_request_body)); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await @@ -64,19 +46,6 @@ async fn main() { axum::serve(listener, app).await.unwrap(); } -async fn create_test_file(path: PathBuf) -> io::Result<()> { - let mut file = File::create(path).await?; - for i in 1..=30 { - let line = format!( - "Hello, this is the simulated file content! This is line {}\n", - i - ); - file.write_all(line.as_bytes()).await?; - } - file.flush().await?; - Ok(()) -} - // Handler that streams the request body to a file. // // POST'ing to `/file/foo.txt` will create a file called `foo.txt`. @@ -115,249 +84,6 @@ async fn show_form() -> Html<&'static str> { ) } -// Handler that returns HTML for a multipart form. -async fn show_form2() -> Html<&'static str> { - Html( - r#" - - - - Upload and Download! - - -

Upload and Download Files

- - -
-
- -
- -
- -
-
- -
- -
-
- -
-
- - -
-
- - -
-
- - - - "#, - ) -} - -/// A simpler file download handler that uses the `FileStream` response. -/// Returns the entire file as a stream. -async fn simpler_file_download_handler() -> Response { - //If you want to simply return a file as a stream - // you can use the from_path method directly, passing in the path of the file to construct a stream with a header and length. - FileStream::>::from_path( - &std::path::Path::new(DOWNLOAD_DIRECTORY).join("test.txt"), - ) - .await - .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Failed to open file").into_response()) - .into_response() -} - -/// If you want to control the returned files in more detail you can implement a Stream -/// For example, use the try_stream! macro to construct a file stream and set which parts are needed. -async fn file_download_handler() -> Response { - let file_path = format!("{DOWNLOAD_DIRECTORY}/test.txt"); - let file_stream = match try_stream(&file_path, 5, 25, 10).await { - Ok(file_stream) => file_stream, - Err(e) => { - println!("{e}"); - return (StatusCode::INTERNAL_SERVER_ERROR, "Failed try stream!").into_response(); - } - }; - - // Use FileStream to return and set some information. - // Will set application/octet-stream in the header. - let file_stream_resp = FileStream::new(Box::pin(file_stream)) - .file_name("test.txt") - .content_size(20_u64); - - file_stream_resp.into_response() -} - -/// More complex manipulation of files and conversion to a stream -async fn try_stream( - file_path: &str, - start: u64, - mut end: u64, - buffer_size: usize, -) -> Result, std::io::Error>>, String> { - let mut file = File::open(file_path) - .await - .map_err(|e| format!("open file:{file_path} err:{e}"))?; - - file.seek(std::io::SeekFrom::Start(start)) - .await - .map_err(|e| format!("file:{file_path} seek err:{e}"))?; - - if end == 0 { - let metadata = file - .metadata() - .await - .map_err(|e| format!("file:{file_path} get metadata err:{e}"))?; - end = metadata.len(); - } - - let mut buffer = vec![0; buffer_size]; - - let stream = try_stream! { - let mut total_read = 0; - - while total_read < end { - let bytes_to_read = std::cmp::min(buffer_size as u64, end - total_read); - let n = file.read(&mut buffer[..bytes_to_read as usize]).await.map_err(|e| { - std::io::Error::new(std::io::ErrorKind::Other, e) - })?; - if n == 0 { - break; // EOF - } - total_read += n as u64; - yield buffer[..n].to_vec(); - - } - }; - Ok(stream) -} - -async fn try_stream2( - mut file: File, - start: u64, - mut end: u64, - buffer_size: usize, -) -> Result, std::io::Error>>, String> { - file.seek(std::io::SeekFrom::Start(start)) - .await - .map_err(|e| format!("file seek err:{e}"))?; - - if end == 0 { - let metadata = file - .metadata() - .await - .map_err(|e| format!("file get metadata err:{e}"))?; - end = metadata.len(); - } - - let mut buffer = vec![0; buffer_size]; - - let stream = try_stream! { - let mut total_read = 0; - - while total_read < end { - let bytes_to_read = std::cmp::min(buffer_size as u64, end - total_read); - let n = file.read(&mut buffer[..bytes_to_read as usize]).await.map_err(|e| { - std::io::Error::new(std::io::ErrorKind::Other, e) - })?; - if n == 0 { - break; // EOF - } - total_read += n as u64; - yield buffer[..n].to_vec(); - - } - }; - Ok(stream) -} - -/// A file download handler that accepts a range header and returns a partial file as a stream. -/// You can return directly from the path -/// But you can't download this stream directly from your browser, you need to use a tool like curl or Postman. -async fn try_file_range_handler(headers: HeaderMap) -> Response { - let range_header = headers - .get(header::RANGE) - .and_then(|value| value.to_str().ok()); - - let (start, end) = if let Some(range) = range_header { - if let Some(range) = parse_range_header(range) { - range - } else { - return (StatusCode::RANGE_NOT_SATISFIABLE, "Invalid Range").into_response(); - } - } else { - (0, 0) // default range end = 0, if end = 0 end == file size - 1 - }; - - let file_path = format!("{DOWNLOAD_DIRECTORY}/test.txt"); - FileStream::>::try_range_response( - std::path::Path::new(&file_path), - start, - end, - ) - .await - .unwrap() -} - -/// If you want to control the stream yourself -async fn file_range_handler(headers: HeaderMap) -> Response { - // Parse the range header to get the start and end values. - let range_header = headers - .get(header::RANGE) - .and_then(|value| value.to_str().ok()); - - // If the range header is invalid, return a 416 Range Not Satisfiable response. - let (start, end) = if let Some(range) = range_header { - if let Some(range) = parse_range_header(range) { - range - } else { - return (StatusCode::RANGE_NOT_SATISFIABLE, "Invalid Range").into_response(); - } - } else { - (0, 0) // default range end = 0, if end = 0 end == file size - 1 - }; - - let file_path = format!("{DOWNLOAD_DIRECTORY}/test.txt"); - - let file = File::open(file_path).await.unwrap(); - - let file_size = file.metadata().await.unwrap().len(); - - let file_stream = match try_stream2(file, start, end, 256).await { - Ok(file_stream) => file_stream, - Err(e) => { - println!("{e}"); - return (StatusCode::INTERNAL_SERVER_ERROR, "Failed try stream!").into_response(); - } - }; - - FileStream::new(Box::pin(file_stream)).into_range_response(start, end, file_size) -} - -/// Parse the range header and return the start and end values. -fn parse_range_header(range: &str) -> Option<(u64, u64)> { - let range = range.strip_prefix("bytes=")?; - let mut parts = range.split('-'); - let start = parts.next()?.parse::().ok()?; - let end = parts - .next() - .and_then(|s| s.parse::().ok()) - .unwrap_or(0); - if start > end { - return None; - } - Some((start, end)) -} - // Handler that accepts a multipart form upload and streams each field to a file. async fn accept_form(mut multipart: Multipart) -> Result { while let Ok(Some(field)) = multipart.next_field().await { @@ -415,4 +141,4 @@ fn path_is_valid(path: &str) -> bool { } components.count() == 1 -} +} \ No newline at end of file