From 7012881c28a02547a025340ef2105e39bde1a66a Mon Sep 17 00:00:00 2001 From: Fangdun Tsai Date: Fri, 15 Dec 2023 06:49:39 +0800 Subject: [PATCH] bump: v0.6.0 --- Cargo.toml | 14 +++---- viz-core/Cargo.toml | 2 + viz-core/src/body.rs | 6 +-- viz-core/src/response.rs | 78 +++++++++++++++++++++++++------------- viz-core/src/types.rs | 7 ++-- viz-macros/Cargo.toml | 4 +- viz-test/tests/response.rs | 21 ++++++++++ viz/Cargo.toml | 2 + 8 files changed, 93 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4355a3f1..7d11b4ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ members = [ ] [workspace.package] -version = "0.5.2" +version = "0.6.0" authors = ["Fangdun Tsai "] edition = "2021" homepage = "https://viz.rs" @@ -45,12 +45,12 @@ license = "MIT" rust-version = "1.63" # follows `tokio` and `hyper` [workspace.dependencies] -viz = { version = "0.5.2", path = "viz" } -viz-core = { version = "0.5.2", path = "viz-core" } -viz-router = { version = "0.5.2", path = "viz-router" } -viz-handlers = { version = "0.5.2", path = "viz-handlers", default-features = false } -viz-macros = { version = "0.1", path = "viz-macros" } -viz-test = { version = "0.1", path = "viz-test" } +viz = { version = "0.6.0", path = "viz" } +viz-core = { version = "0.6.0", path = "viz-core" } +viz-router = { version = "0.6.0", path = "viz-router" } +viz-handlers = { version = "0.6.0", path = "viz-handlers", default-features = false } +viz-macros = { version = "0.2.0", path = "viz-macros" } +viz-test = { version = "0.1.0", path = "viz-test" } async-trait = "0.1" dyn-clone = "1.0" diff --git a/viz-core/Cargo.toml b/viz-core/Cargo.toml index 4964d33b..91ed195c 100644 --- a/viz-core/Cargo.toml +++ b/viz-core/Cargo.toml @@ -25,6 +25,7 @@ default = [ "websocket", "cookie", "session", + "fs" ] state = [] @@ -37,6 +38,7 @@ query = ["dep:serde", "dep:serde_urlencoded"] multipart = ["dep:form-data"] websocket = ["dep:tokio-tungstenite", "tokio/rt"] sse = ["dep:tokio-stream", "tokio/time"] +fs = ["dep:tokio-util", "tokio/fs"] cookie = ["dep:cookie"] cookie-private = ["cookie", "cookie?/private"] diff --git a/viz-core/src/body.rs b/viz-core/src/body.rs index 4a4984e9..d00a40ef 100644 --- a/viz-core/src/body.rs +++ b/viz-core/src/body.rs @@ -7,7 +7,7 @@ use futures_util::{Stream, TryStreamExt}; use http_body_util::{combinators::BoxBody, BodyExt, Full, StreamBody}; use hyper::body::{Body, Frame, Incoming, SizeHint}; -use crate::{Bytes, Error, Result}; +use crate::{BoxError, Bytes, Error, Result}; /// The incoming body from HTTP [`Request`]. /// @@ -84,7 +84,7 @@ impl Body for IncomingBody { } impl Stream for IncomingBody { - type Item = Result>; + type Item = Result; #[inline] fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { @@ -138,7 +138,7 @@ impl OutgoingBody { StreamBody::new( stream .map_ok(Into::into) - .map_ok(Frame::::data) + .map_ok(Frame::data) .map_err(Into::into), ) .boxed(), diff --git a/viz-core/src/response.rs b/viz-core/src/response.rs index 1ee3f5ad..272b8246 100644 --- a/viz-core/src/response.rs +++ b/viz-core/src/response.rs @@ -1,9 +1,10 @@ use futures_util::Stream; use http_body_util::Full; -use crate::{header, Bytes, Error, OutgoingBody, Response, Result, StatusCode}; +use crate::{async_trait, header, Bytes, Error, OutgoingBody, Response, Result, StatusCode}; /// The [`Response`] Extension. +#[async_trait] pub trait ResponseExt: Sized { /// Get the size of this response's body. fn content_length(&self) -> Option; @@ -17,6 +18,11 @@ pub trait ResponseExt: Sized { K: header::AsHeaderName, T: std::str::FromStr; + /// The response was successful (status in the range [`200-299`][mdn]) or not. + /// + /// [mdn]: + fn ok(&self) -> bool; + /// The response with the specified [`Content-Type`][mdn]. /// /// [mdn]: @@ -24,12 +30,12 @@ pub trait ResponseExt: Sized { where B: Into, { - let mut res = Response::new(body.into()); - res.headers_mut().insert( + let mut resp = Response::new(body.into()); + resp.headers_mut().insert( header::CONTENT_TYPE, header::HeaderValue::from_static(content_type), ); - res + resp } /// The response with `text/plain; charset=utf-8` media type. @@ -82,20 +88,18 @@ pub trait ResponseExt: Sized { Response::new(OutgoingBody::streaming(stream)) } - // TODO: Download transfers the file from path as an attachment. - // fn download() -> Response - - /// The response was successful (status in the range [`200-299`][mdn]) or not. - /// - /// [mdn]: - fn ok(&self) -> bool; + /// Downloads transfers the file from path as an attachment. + #[cfg(feature = "fs")] + async fn download(path: T, name: Option<&str>) -> Result + where + T: AsRef + Send; /// The [`Content-Disposition`][mdn] header indicates if the content is expected to be /// displayed inline in the browser, that is, as a Web page or as part of a Web page, /// or as an attachment, that is downloaded and saved locally. /// /// [mdn]: - fn attachment(file: &str) -> Self; + fn attachment(value: &str) -> Self; /// The [`Content-Location`][mdn] header indicates an alternate location for the returned data. /// @@ -149,6 +153,7 @@ pub trait ResponseExt: Sized { } } +#[async_trait] impl ResponseExt for Response { fn content_length(&self) -> Option { self.headers() @@ -176,12 +181,33 @@ impl ResponseExt for Response { self.status().is_success() } - fn attachment(file: &str) -> Self { - let mut res = Self::default(); - let val = header::HeaderValue::from_str(file) + #[cfg(feature = "fs")] + async fn download(path: T, name: Option<&str>) -> Result + where + T: AsRef + Send, + { + let value = if let Some(filename) = name { + filename + } else if let Some(filename) = path.as_ref().file_name().and_then(std::ffi::OsStr::to_str) { + filename + } else { + "download" + } + .escape_default(); + + let mut resp = Self::attachment(&format!("attachment; filename=\"{value}\"")); + *resp.body_mut() = OutgoingBody::streaming(tokio_util::io::ReaderStream::new( + tokio::fs::File::open(path).await.map_err(Error::from)?, + )); + Ok(resp) + } + + fn attachment(value: &str) -> Self { + let val = header::HeaderValue::from_str(value) .expect("content-disposition is not the correct value"); - res.headers_mut().insert(header::CONTENT_DISPOSITION, val); - res + let mut resp = Self::default(); + resp.headers_mut().insert(header::CONTENT_DISPOSITION, val); + resp } fn location(location: T) -> Self @@ -190,9 +216,9 @@ impl ResponseExt for Response { { let val = header::HeaderValue::try_from(location.as_ref()) .expect("location is not the correct value"); - let mut res = Self::default(); - res.headers_mut().insert(header::CONTENT_LOCATION, val); - res + let mut resp = Self::default(); + resp.headers_mut().insert(header::CONTENT_LOCATION, val); + resp } fn redirect(url: T) -> Self @@ -201,9 +227,9 @@ impl ResponseExt for Response { { let val = header::HeaderValue::try_from(url.as_ref()).expect("url is not the correct value"); - let mut res = Self::default(); - res.headers_mut().insert(header::LOCATION, val); - res + let mut resp = Self::default(); + resp.headers_mut().insert(header::LOCATION, val); + resp } fn redirect_with_status(url: T, status: StatusCode) -> Self @@ -212,8 +238,8 @@ impl ResponseExt for Response { { assert!(status.is_redirection(), "not a redirection status code"); - let mut res = Self::redirect(url); - *res.status_mut() = status; - res + let mut resp = Self::redirect(url); + *resp.status_mut() = status; + resp } } diff --git a/viz-core/src/types.rs b/viz-core/src/types.rs index 06c438d2..aa115bf9 100644 --- a/viz-core/src/types.rs +++ b/viz-core/src/types.rs @@ -65,9 +65,10 @@ mod route_info; pub use route_info::RouteInfo; mod header; -mod payload; -mod realip; - pub use header::{Header, HeaderError}; + +mod payload; pub use payload::{Payload, PayloadError}; + +mod realip; pub use realip::RealIp; diff --git a/viz-macros/Cargo.toml b/viz-macros/Cargo.toml index 5e2676c5..060a384e 100644 --- a/viz-macros/Cargo.toml +++ b/viz-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "viz-macros" -version = "0.1.0" +version = "0.2.0" edition.workspace = true license.workspace = true authors.workspace = true @@ -17,7 +17,7 @@ categories = ["asynchronous", "network-programming", "web-programming"] proc-macro = true [dependencies] -syn = "2.0" +syn = { version = "2.0", features = ["full"] } quote = "1.0" [dev-dependencies] diff --git a/viz-test/tests/response.rs b/viz-test/tests/response.rs index 242738dd..2f2afb30 100644 --- a/viz-test/tests/response.rs +++ b/viz-test/tests/response.rs @@ -136,6 +136,12 @@ async fn response_ext_with_server() -> Result<()> { Full::new("".into()), mime::TEXT_XML.as_ref(), )) + }) + .get("/download", |_: Request| { + let dir = std::env::var("CARGO_MANIFEST_DIR") + .map(std::path::PathBuf::from) + .unwrap(); + Response::download(dir.join("README.md"), Some("file.txt")) }); let client = TestServer::new(router).await?; @@ -148,5 +154,20 @@ async fn response_ext_with_server() -> Result<()> { assert_eq!(resp.content_length(), Some(6)); assert_eq!(resp.text().await.map_err(Error::boxed)?, ""); + let resp = client.get("/download").send().await.map_err(Error::boxed)?; + assert_eq!(resp.content_length(), None); + assert_eq!( + resp.headers().get(http::header::CONTENT_DISPOSITION), + Some(http::header::HeaderValue::from_static( + "attachment; filename=\"file.txt\"" + )) + .as_ref() + ); + assert!(resp + .text() + .await + .map_err(Error::boxed)? + .starts_with("

")); + Ok(()) } diff --git a/viz/Cargo.toml b/viz/Cargo.toml index e1aaf52b..62f49b1b 100644 --- a/viz/Cargo.toml +++ b/viz/Cargo.toml @@ -24,6 +24,7 @@ default = [ "multipart", "cookie", "session", + "fs", ] http1 = ["hyper/http1"] @@ -38,6 +39,7 @@ params = ["viz-core/params"] multipart = ["viz-core/multipart"] websocket = ["viz-core/websocket"] sse = ["viz-core/sse"] +fs = ["viz-core/fs"] cookie = ["viz-core/cookie"] cookie-private = ["viz-core/cookie-private"]