Skip to content

Commit

Permalink
Add a from_path function to make it easier to return a stream of file…
Browse files Browse the repository at this point in the history
…s with file name and content size.
  • Loading branch information
YanHeDoki committed Nov 25, 2024
1 parent 6967a97 commit 1f2d314
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 48 deletions.
85 changes: 64 additions & 21 deletions axum-extra/src/response/file_stream.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::{io, path::PathBuf};

use axum::{
body,
response::{IntoResponse, Response},
Expand All @@ -6,6 +8,11 @@ use axum::{
use bytes::Bytes;
use futures_util::TryStream;
use http::{header, StatusCode};
use tokio::fs::File;
use tokio_util::io::ReaderStream;

/// Alias for `tokio_util::io::ReaderStream<File>`.
pub type AsyncReaderStream = ReaderStream<File>;

/// Encapsulate the file stream.
/// The encapsulated file stream construct requires passing in a stream
Expand All @@ -20,7 +27,7 @@ use http::{header, StatusCode};
/// };
/// use axum_extra::response::file_stream::FileStream;
/// use tokio::fs::File;
/// use tokio_util::io::ReaderStream ;
/// use tokio_util::io::ReaderStream;
/// async fn file_stream() -> Result<Response, (StatusCode, String)> {
/// let stream=ReaderStream::new(File::open("test.txt").await.map_err(|e| (StatusCode::NOT_FOUND, format!("File not found: {e}")))?);
/// let file_stream_resp = FileStream::new(stream)
Expand Down Expand Up @@ -61,6 +68,54 @@ where
}
}

/// Create a file stream from a file path.
/// # Examples
/// ```
/// use axum::{
/// http::StatusCode,
/// response::{Response, IntoResponse},
/// Router,
/// routing::get
/// };
/// use axum_extra::response::file_stream::FileStream;
/// use std::path::PathBuf;
/// use tokio_util::io::ReaderStream;
/// use tokio::fs::File;
/// async fn file_stream() -> Result<Response, (StatusCode, String)> {
/// Ok(FileStream::<ReaderStream<File>>::from_path(PathBuf::from("test.txt"))
/// .await
/// .map_err(|e| (StatusCode::NOT_FOUND, format!("File not found: {e}")))?
/// .into_response())
/// }
/// let app = Router::new().route("/FileStreamDownload", get(file_stream));
/// # let _: Router = app;
/// ```
pub async fn from_path(path: PathBuf) -> io::Result<FileStream<AsyncReaderStream>> {
// open file
let file = File::open(&path).await?;
let mut content_size = None;
let mut file_name = None;

// get file metadata length
if let Ok(metadata) = file.metadata().await {
content_size = Some(metadata.len());
}

// get file name
if let Some(file_name_os) = path.file_name() {
if let Some(file_name_str) = file_name_os.to_str() {
file_name = Some(file_name_str.to_owned());
}
}

// return FileStream
Ok(FileStream {
stream: ReaderStream::new(file),
file_name,
content_size,
})
}

/// Set the file name of the file.
pub fn file_name<T: Into<String>>(mut self, file_name: T) -> Self {
self.file_name = Some(file_name.into());
Expand Down Expand Up @@ -111,8 +166,7 @@ mod tests {
use axum::{extract::Request, routing::get, Router};
use body::Body;
use http_body_util::BodyExt;
use std::io::{Cursor, SeekFrom};
use tokio::io::AsyncSeekExt;
use std::io::Cursor;
use tokio_util::io::ReaderStream;
use tower::ServiceExt;

Expand Down Expand Up @@ -164,24 +218,13 @@ mod tests {
}

#[tokio::test]
async fn response_half_file() -> Result<(), Box<dyn std::error::Error>> {
async fn response_from_path() -> Result<(), Box<dyn std::error::Error>> {
let app = Router::new().route(
"/half_file",
"/from_path",
get(move || async move {
let mut file = tokio::fs::File::open("CHANGELOG.md").await.unwrap();

// get file size
let file_size = file.metadata().await.unwrap().len();

// seek to the middle of the file
let mid_position = file_size / 2;
file.seek(SeekFrom::Start(mid_position)).await.unwrap();

// response file stream
let stream = ReaderStream::new(file);
FileStream::new(stream)
.file_name("CHANGELOG.md")
.content_size(mid_position)
FileStream::<AsyncReaderStream>::from_path("CHANGELOG.md".into())
.await
.unwrap()
.into_response()
}),
);
Expand All @@ -190,7 +233,7 @@ mod tests {
let response = app
.oneshot(
Request::builder()
.uri("/half_file")
.uri("/from_path")
.body(Body::empty())
.unwrap(),
)
Expand All @@ -212,7 +255,7 @@ mod tests {

let file = tokio::fs::File::open("CHANGELOG.md").await.unwrap();
// get file size
let content_length = file.metadata().await.unwrap().len() / 2;
let content_length = file.metadata().await.unwrap().len();

assert_eq!(
response
Expand Down
35 changes: 8 additions & 27 deletions examples/stream-to-file/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,14 @@ use axum::{
routing::{get, post},
BoxError, Router,
};
use axum_extra::response::file_stream::FileStream;
use axum_extra::response::file_stream::{AsyncReaderStream, FileStream};
use futures::{Stream, TryStreamExt};
use std::io;
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncSeekExt, BufWriter},
};
use tokio_util::io::{ReaderStream, StreamReader};
use tokio_util::io::StreamReader;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
const UPLOADS_DIRECTORY: &str = "uploads";

Expand Down Expand Up @@ -133,31 +133,12 @@ async fn show_form2() -> Html<&'static str> {
/// A simpler file download handler that uses the `FileStream` response.
/// Returns the entire file as a stream.
async fn simpler_file_download_handler() -> Response {
let Ok(file) = File::open("./CHANGELOG.md").await else {
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to open file").into_response();
};

let Ok(file_metadata) = file.metadata().await else {
return (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to get file metadata",
)
.into_response();
};

// Constructing a Stream with ReaderStream
let stream = ReaderStream::new(file);

// Use FileStream to return and set some information.
// Will set application/octet-stream in the header.
let file_stream_resp = FileStream::new(stream)
.file_name("test.txt")
.content_size(file_metadata.len());

//It is also possible to set only the stream FileStream will be automatically set on the http header.
//let file_stream_resp = FileStream::new(stream);

file_stream_resp.into_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::<AsyncReaderStream>::from_path("./CHANGELOG.md".into())
.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
Expand Down

0 comments on commit 1f2d314

Please sign in to comment.