tide_naive_static_files/
lib.rs

1//! Code heavily based on https://github.com/http-rs/tide/blob/4aec5fe2bb6b8202f7ae48e416eeb37345cf029f/backup/examples/staticfile.rs
2
3use async_std::{fs, future, io, task};
4use http::{
5    header::{self},
6    StatusCode,
7};
8use std::path::{Component, Path, PathBuf};
9use std::pin::Pin;
10use tide::{Endpoint, Request, Response, Result};
11
12/// A trait that provides a way to get a [`&Path`](std::path::Path) to your static
13/// assets directory from your tide app's state. Meant to be used with the
14/// [`serve_static_files`] function.
15///
16/// [`serve_static_files`]: tide_naive_static_files::serve_static_files
17///
18/// ```no_run
19/// use std::path::Path;
20/// use tide_naive_static_files::StaticRootDir;
21///
22/// struct MyState;
23///
24/// impl StaticRootDir for MyState {
25///     fn root_dir(&self) -> &Path {
26///         Path::new("./my-static-assets-dir")
27///     }
28/// }
29/// ```
30pub trait StaticRootDir {
31    fn root_dir(&self) -> &Path;
32}
33
34impl<T: StaticRootDir> StaticRootDir for &T {
35    fn root_dir(&self) -> &Path {
36        (*self).root_dir()
37    }
38}
39
40async fn stream_bytes(root: PathBuf, actual_path: &str) -> io::Result<Response> {
41    let mut path = get_path(&root, actual_path);
42
43    // Loop if the path points to a directory because we want to try looking for
44    // an "index.html" file within that directory.
45    let (meta, path): (fs::Metadata, PathBuf) = loop {
46        let meta = fs::metadata(&path).await.ok();
47
48        // If the file doesn't exist, then bail out.
49        if meta.is_none() {
50            return Ok(tide::Response::new(StatusCode::NOT_FOUND.as_u16())
51                .set_header(header::CONTENT_TYPE.as_str(), mime::TEXT_HTML.as_ref())
52                .body_string(format!("Couldn't locate requested file {:?}", actual_path)));
53        }
54        let meta = meta.unwrap();
55
56        // If the path points to a directory, look for an "index.html" file.
57        if !meta.is_file() {
58            path.push("index.html");
59            continue; // Try again.
60        } else {
61            break (meta, path);
62        }
63    };
64
65    let mime = mime_guess::from_path(&path).first_or_octet_stream();
66    let size = format!("{}", meta.len());
67
68    // We're done with the checks. Stream file!
69    let file = fs::File::open(PathBuf::from(&path)).await.unwrap();
70    let reader = io::BufReader::new(file);
71    Ok(tide::Response::new(StatusCode::OK.as_u16())
72        .body(reader)
73        .set_header(header::CONTENT_LENGTH.as_str(), size)
74        .set_mime(mime))
75}
76
77/// Percent-decode, normalize path components and return the final path joined with root.
78/// See https://github.com/iron/staticfile/blob/master/src/requested_path.rs
79fn get_path(root: &Path, path: &str) -> PathBuf {
80    let rel_path = Path::new(path)
81        .components()
82        .fold(PathBuf::new(), |mut result, p| {
83            match p {
84                Component::Normal(x) => result.push({
85                    let s = x.to_str().unwrap_or("");
86                    &*percent_encoding::percent_decode(s.as_bytes()).decode_utf8_lossy()
87                }),
88                Component::ParentDir => {
89                    result.pop();
90                }
91                _ => (), // ignore any other component
92            }
93
94            result
95        });
96    root.join(rel_path)
97}
98
99/// Use in a tide [`tide::Route::get`](tide::Route::get) handler to serve static files from an
100/// endpoint. In order to use this function, your tide app's state must
101/// implement the [`StaticRootDir`](tide_naive_static_files::StaticRootDir) trait.
102///
103/// The static assets will be served from the route provided to the `app.at`
104/// function. In the example below, the file `./my-static-asset-dir/foo.html`
105/// would be obtainable by making a GET request to
106/// `http://my.server.address/static/foo.html`.
107///
108/// ```no_run
109/// use std::path::Path;
110/// use tide_naive_static_files::{StaticRootDir, serve_static_files};
111///
112/// struct MyState;
113///
114/// impl StaticRootDir for MyState {
115///     fn root_dir(&self) -> &Path {
116///         Path::new("./my-static-asset-dir")
117///     }
118/// }
119///
120/// # fn main() {
121/// let state = MyState;
122/// let mut app = tide::with_state(state);
123/// app.at("static/*path")
124///     .get(|req| async { serve_static_files(req).await.unwrap() });
125/// # }
126/// ```
127pub async fn serve_static_files(ctx: Request<impl StaticRootDir>) -> Result {
128    let path: String = ctx.param("path").expect(
129        "`tide_naive_static_files::serve_static_files` requires a `*path` glob param at the end!",
130    );
131    let root = ctx.state();
132    let resp =
133        task::block_on(async move { stream_bytes(PathBuf::from(root.root_dir()), &path).await })
134            .unwrap_or_else(|e| {
135                eprintln!("tide-naive-static-files internal error: {}", e);
136                internal_server_error("Internal server error reading file")
137            });
138
139    Ok(resp)
140}
141
142/// A struct that holds a path to your app's static assets directory. This
143/// struct implements [`tide::Endpoint`](tide::Endpoint) so it can be passed directly to
144/// [`tide::Route::get`](tide::Route::get).
145///
146/// The static assets will be served from the route provided to the `app.at`. In
147/// the example below, the file `./my-static-asset-dir/foo.html` would be
148/// obtainable by making a GET request to
149/// `http://my.server.address/static/foo.html`.
150///
151/// ```no_run
152/// use tide_naive_static_files::StaticFilesEndpoint;
153///
154/// # fn main() {
155/// let mut app = tide::new();
156/// app.at("/static").strip_prefix().get(StaticFilesEndpoint {
157///     root: "./my-static-asset-dir/".into(),
158/// });
159/// # }
160/// ```
161pub struct StaticFilesEndpoint {
162    pub root: PathBuf,
163}
164
165type BoxFuture<T> = Pin<Box<dyn future::Future<Output = T> + Send>>;
166
167impl<State> Endpoint<State> for StaticFilesEndpoint {
168    type Fut = BoxFuture<Response>;
169
170    fn call(&self, ctx: Request<State>) -> Self::Fut {
171        let path = ctx.uri().to_string();
172        let root = self.root.clone();
173
174        Box::pin(async move {
175            stream_bytes(root, &path).await.unwrap_or_else(|e| {
176                eprintln!("tide-naive-static-files internal error: {}", e);
177                internal_server_error("Internal server error reading file")
178            })
179        })
180    }
181}
182
183fn internal_server_error(body: &'static str) -> Response {
184    tide::Response::new(StatusCode::INTERNAL_SERVER_ERROR.as_u16())
185        .set_header(header::CONTENT_TYPE.as_str(), mime::TEXT_HTML.as_ref())
186        .body_string(body.into())
187}