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}