Skip to content

Commit 5ba6340

Browse files
authored
Merge pull request #725 from mendelt/serve_file_endpoint
Add `serve_file` method to route
2 parents 0b2c5ec + 5ad0903 commit 5ba6340

File tree

6 files changed

+113
-11
lines changed

6 files changed

+113
-11
lines changed

examples/static_file.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<html>
2+
<head><title>Example</title></head>
3+
<body>
4+
<h1>Example</h1>
5+
</body>
6+
</html>

examples/static_file.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ async fn main() -> Result<(), std::io::Error> {
44
let mut app = tide::new();
55
app.at("/").get(|_| async { Ok("visit /src/*") });
66
app.at("/src").serve_dir("src/")?;
7+
app.at("/example").serve_file("examples/static_file.html")?;
78
app.listen("127.0.0.1:8080").await?;
89
Ok(())
910
}

src/fs/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
mod serve_dir;
2+
mod serve_file;
23

34
pub(crate) use serve_dir::ServeDir;
5+
pub(crate) use serve_file::ServeFile;

src/fs/serve_dir.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ use crate::{Body, Endpoint, Request, Response, Result, StatusCode};
33

44
use async_std::path::PathBuf as AsyncPathBuf;
55

6-
use std::ffi::OsStr;
76
use std::path::{Path, PathBuf};
7+
use std::{ffi::OsStr, io};
88

99
pub(crate) struct ServeDir {
1010
prefix: String,
@@ -43,16 +43,17 @@ where
4343
let file_path = AsyncPathBuf::from(file_path);
4444
if !file_path.starts_with(&self.dir) {
4545
log::warn!("Unauthorized attempt to read: {:?}", file_path);
46-
return Ok(Response::new(StatusCode::Forbidden));
47-
}
48-
if !file_path.exists().await {
49-
log::warn!("File not found: {:?}", file_path);
50-
return Ok(Response::new(StatusCode::NotFound));
46+
Ok(Response::new(StatusCode::Forbidden))
47+
} else {
48+
match Body::from_file(&file_path).await {
49+
Ok(body) => Ok(Response::builder(StatusCode::Ok).body(body).build()),
50+
Err(e) if e.kind() == io::ErrorKind::NotFound => {
51+
log::warn!("File not found: {:?}", &file_path);
52+
Ok(Response::new(StatusCode::NotFound))
53+
}
54+
Err(e) => Err(e.into()),
55+
}
5156
}
52-
let body = Body::from_file(&file_path).await?;
53-
let mut res = Response::new(StatusCode::Ok);
54-
res.set_body(body);
55-
Ok(res)
5657
}
5758
}
5859

src/fs/serve_file.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use crate::log;
2+
use crate::{Body, Endpoint, Request, Response, Result, StatusCode};
3+
use std::io;
4+
use std::path::Path;
5+
6+
use async_std::path::PathBuf as AsyncPathBuf;
7+
use async_trait::async_trait;
8+
9+
pub(crate) struct ServeFile {
10+
path: AsyncPathBuf,
11+
}
12+
13+
impl ServeFile {
14+
/// Create a new instance of `ServeFile`.
15+
pub(crate) fn init(path: impl AsRef<Path>) -> io::Result<Self> {
16+
let file = path.as_ref().to_owned().canonicalize()?;
17+
Ok(Self {
18+
path: AsyncPathBuf::from(file),
19+
})
20+
}
21+
}
22+
23+
#[async_trait]
24+
impl<State: Clone + Send + Sync + 'static> Endpoint<State> for ServeFile {
25+
async fn call(&self, _: Request<State>) -> Result {
26+
match Body::from_file(&self.path).await {
27+
Ok(body) => Ok(Response::builder(StatusCode::Ok).body(body).build()),
28+
Err(e) if e.kind() == io::ErrorKind::NotFound => {
29+
log::warn!("File not found: {:?}", &self.path);
30+
Ok(Response::new(StatusCode::NotFound))
31+
}
32+
Err(e) => Err(e.into()),
33+
}
34+
}
35+
}
36+
37+
#[cfg(test)]
38+
mod test {
39+
use super::*;
40+
41+
use crate::http::{Response, Url};
42+
use std::fs::{self, File};
43+
use std::io::Write;
44+
45+
fn serve_file(tempdir: &tempfile::TempDir) -> crate::Result<ServeFile> {
46+
let static_dir = tempdir.path().join("static");
47+
fs::create_dir(&static_dir)?;
48+
49+
let file_path = static_dir.join("foo");
50+
let mut file = File::create(&file_path)?;
51+
write!(file, "Foobar")?;
52+
53+
Ok(ServeFile::init(file_path)?)
54+
}
55+
56+
fn request(path: &str) -> crate::Request<()> {
57+
let request =
58+
crate::http::Request::get(Url::parse(&format!("http://localhost/{}", path)).unwrap());
59+
crate::Request::new((), request, vec![])
60+
}
61+
62+
#[async_std::test]
63+
async fn should_serve_file() {
64+
let tempdir = tempfile::tempdir().unwrap();
65+
let serve_file = serve_file(&tempdir).unwrap();
66+
67+
let mut res: Response = serve_file.call(request("static/foo")).await.unwrap().into();
68+
69+
assert_eq!(res.status(), 200);
70+
assert_eq!(res.body_string().await.unwrap(), "Foobar");
71+
}
72+
73+
#[async_std::test]
74+
async fn should_serve_404_when_file_missing() {
75+
let serve_file = ServeFile {
76+
path: AsyncPathBuf::from("gone/file"),
77+
};
78+
79+
let res: Response = serve_file.call(request("static/foo")).await.unwrap().into();
80+
81+
assert_eq!(res.status(), 404);
82+
}
83+
}

src/route.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use std::path::Path;
44
use std::sync::Arc;
55

66
use crate::endpoint::MiddlewareEndpoint;
7-
use crate::fs::ServeDir;
7+
use crate::fs::{ServeDir, ServeFile};
88
use crate::log;
99
use crate::{router::Router, Endpoint, Middleware};
1010

@@ -140,6 +140,15 @@ impl<'a, State: Clone + Send + Sync + 'static> Route<'a, State> {
140140
Ok(())
141141
}
142142

143+
/// Serve a static file.
144+
///
145+
/// The file will be streamed from disk, and a mime type will be determined
146+
/// based on magic bytes. Similar to serve_dir
147+
pub fn serve_file(&mut self, file: impl AsRef<Path>) -> io::Result<()> {
148+
self.get(ServeFile::init(file)?);
149+
Ok(())
150+
}
151+
143152
/// Add an endpoint for the given HTTP method
144153
pub fn method(&mut self, method: http_types::Method, ep: impl Endpoint<State>) -> &mut Self {
145154
if self.prefix {

0 commit comments

Comments
 (0)