conduit_static/
lib.rs

1use conduit::{box_error, header, Body, Handler, HandlerResult, RequestExt, Response, StatusCode};
2use conduit_mime_types as mime;
3use filetime::FileTime;
4use std::fs::File;
5use std::path::{Path, PathBuf};
6use time::OffsetDateTime;
7
8pub struct Static {
9    path: PathBuf,
10}
11
12impl Static {
13    pub fn new<P: AsRef<Path>>(path: P) -> Static {
14        Static {
15            path: path.as_ref().to_path_buf(),
16        }
17    }
18
19    pub fn lookup(&self, request_path: &str) -> HandlerResult {
20        let request_path = request_path.strip_prefix('/').unwrap_or(request_path);
21        if request_path.contains("..") {
22            return Ok(not_found());
23        }
24
25        let path = self.path.join(request_path);
26        let mime = mime::mime_for_path(&path).unwrap_or("application/octet-stream");
27        let file = match File::open(&path) {
28            Ok(f) => f,
29            Err(..) => return Ok(not_found()),
30        };
31        let data = file.metadata().map_err(box_error)?;
32        if data.is_dir() {
33            return Ok(not_found());
34        }
35        let mtime = FileTime::from_last_modification_time(&data);
36        let mtime = OffsetDateTime::from_unix_timestamp(mtime.unix_seconds());
37
38        Response::builder()
39            .header(header::CONTENT_TYPE, mime)
40            .header(header::CONTENT_LENGTH, data.len())
41            .header(header::LAST_MODIFIED, mtime.format("%a, %d %b %Y %T GMT"))
42            .body(Body::File(file))
43            .map_err(box_error)
44    }
45}
46
47impl Handler for Static {
48    fn call(&self, request: &mut dyn RequestExt) -> HandlerResult {
49        self.lookup(request.path())
50    }
51}
52
53fn not_found() -> Response<Body> {
54    Response::builder()
55        .status(StatusCode::NOT_FOUND)
56        .header(header::CONTENT_LENGTH, 0)
57        .header(header::CONTENT_TYPE, "text/plain")
58        .body(Body::empty())
59        .unwrap()
60}
61
62#[cfg(test)]
63mod tests {
64    use std::fs::{self, File};
65    use std::io::prelude::*;
66    use tempdir::TempDir;
67
68    use crate::Static;
69    use conduit::{header, Handler, Method, StatusCode};
70    use conduit_test::{MockRequest, ResponseExt};
71
72    #[test]
73    fn test_static() {
74        let td = TempDir::new("conduit-static").unwrap();
75        let root = td.path();
76        let handler = Static::new(root);
77        File::create(&root.join("Cargo.toml"))
78            .unwrap()
79            .write_all(b"[package]")
80            .unwrap();
81        let mut req = MockRequest::new(Method::GET, "/Cargo.toml");
82        let res = handler.call(&mut req).expect("No response");
83        assert_eq!(
84            res.headers().get(header::CONTENT_TYPE).unwrap(),
85            "application/toml"
86        );
87        assert_eq!(res.headers().get(header::CONTENT_LENGTH).unwrap(), "9");
88        assert_eq!(*res.into_cow(), b"[package]"[..]);
89    }
90
91    #[test]
92    fn test_lookup() {
93        let td = TempDir::new("conduit-static").unwrap();
94        let root = td.path();
95        let handler = Static::new(root);
96        File::create(&root.join("Cargo.toml"))
97            .unwrap()
98            .write_all(b"[package]")
99            .unwrap();
100
101        let res = handler.lookup("Cargo.toml").expect("No response");
102        assert_eq!(
103            res.headers().get(header::CONTENT_TYPE).unwrap(),
104            "application/toml"
105        );
106        assert_eq!(res.headers().get(header::CONTENT_LENGTH).unwrap(), "9");
107        assert_eq!(*res.into_cow(), b"[package]"[..]);
108    }
109
110    #[test]
111    fn test_mime_types() {
112        let td = TempDir::new("conduit-static").unwrap();
113        let root = td.path();
114        fs::create_dir(&root.join("src")).unwrap();
115        File::create(&root.join("src/fixture.css")).unwrap();
116
117        let handler = Static::new(root);
118        let mut req = MockRequest::new(Method::GET, "/src/fixture.css");
119        let res = handler.call(&mut req).expect("No response");
120        assert_eq!(res.headers().get(header::CONTENT_TYPE).unwrap(), "text/css");
121        assert_eq!(res.headers().get(header::CONTENT_LENGTH).unwrap(), "0");
122    }
123
124    #[test]
125    fn test_missing() {
126        let td = TempDir::new("conduit-static").unwrap();
127        let root = td.path();
128
129        let handler = Static::new(root);
130        let mut req = MockRequest::new(Method::GET, "/nope");
131        let res = handler.call(&mut req).expect("No response");
132        assert_eq!(res.status(), StatusCode::NOT_FOUND);
133    }
134
135    #[test]
136    fn test_dir() {
137        let td = TempDir::new("conduit-static").unwrap();
138        let root = td.path();
139
140        fs::create_dir(&root.join("foo")).unwrap();
141
142        let handler = Static::new(root);
143        let mut req = MockRequest::new(Method::GET, "/foo");
144        let res = handler.call(&mut req).expect("No response");
145        assert_eq!(res.status(), StatusCode::NOT_FOUND);
146    }
147
148    #[test]
149    fn last_modified() {
150        let td = TempDir::new("conduit-static").unwrap();
151        let root = td.path();
152        File::create(&root.join("test")).unwrap();
153        let handler = Static::new(root);
154        let mut req = MockRequest::new(Method::GET, "/test");
155        let res = handler.call(&mut req).expect("No response");
156        assert_eq!(res.status(), StatusCode::OK);
157        assert!(res.headers().get(header::LAST_MODIFIED).is_some());
158    }
159
160    #[test]
161    fn emoji_path() {
162        let td = TempDir::new("conduit-static").unwrap();
163        let root = td.path();
164        File::create(&root.join("test")).unwrap();
165        let handler = Static::new(root);
166        let mut req = MockRequest::new(Method::GET, "🎉");
167        let res = handler.call(&mut req).expect("No response");
168        assert_eq!(res.status(), StatusCode::NOT_FOUND);
169    }
170}