|
1 | 1 | mod service; |
2 | 2 | mod utils; |
3 | 3 |
|
4 | | -use std::fs::read_dir; |
5 | | -use std::path::{Component, Path, PathBuf}; |
6 | | - |
7 | | -use anyhow::{Context, Result}; |
| 4 | +use anyhow::Result; |
8 | 5 | use bytes::Bytes; |
9 | | -use http::{header::CONTENT_TYPE, request::Parts, HeaderValue, Method, Response, StatusCode, Uri}; |
10 | | -use http_body_util::{BodyExt, Full}; |
11 | | -use multer::Multipart; |
12 | | -use percent_encoding::{percent_decode_str, utf8_percent_encode}; |
13 | | -use tokio::io::AsyncWriteExt; |
| 6 | +use http::{Method, Response, StatusCode}; |
| 7 | +use http_body_util::Full; |
14 | 8 |
|
| 9 | +use crate::handler::file_server::service::FileServerConfig; |
15 | 10 | use crate::server::{HttpRequest, HttpResponse}; |
16 | 11 |
|
17 | 12 | use self::service::FileServer as FileServerService; |
18 | 13 |
|
19 | 14 | pub struct FileServer { |
20 | 15 | file_service: FileServerService, |
21 | | - path: PathBuf, |
22 | 16 | } |
23 | 17 |
|
24 | 18 | impl FileServer { |
25 | | - pub fn new(path: PathBuf) -> Self { |
| 19 | + pub fn new(config: FileServerConfig) -> Self { |
26 | 20 | Self { |
27 | | - file_service: FileServerService::new(path.clone()), |
28 | | - path, |
| 21 | + file_service: FileServerService::new(config), |
29 | 22 | } |
30 | 23 | } |
31 | 24 |
|
32 | 25 | pub async fn handle(&self, req: HttpRequest) -> Result<HttpResponse> { |
33 | | - let (parts, body) = req.into_parts(); |
34 | | - let body = body.collect().await.unwrap().to_bytes(); |
| 26 | + let (parts, _) = req.into_parts(); |
35 | 27 |
|
36 | 28 | if parts.uri.path().starts_with("/api/v1") { |
37 | | - return self.handle_api(parts, body).await; |
38 | | - } |
39 | | - |
40 | | - let path = parts.uri.path(); |
41 | | - let path = path.strip_prefix('/').unwrap_or(path); |
42 | | - |
43 | | - if let Some(file) = FileExplorerAssets::get(path) { |
44 | | - let content_type = mime_guess::from_path(path).first_or_octet_stream(); |
45 | | - let content_type = HeaderValue::from_str(content_type.as_ref()).unwrap(); |
46 | | - let body = Full::new(Bytes::from(file.data.to_vec())); |
47 | | - let mut response = Response::new(body); |
48 | | - let mut headers = response.headers().clone(); |
49 | | - |
50 | | - headers.append(CONTENT_TYPE, content_type); |
51 | | - *response.headers_mut() = headers; |
52 | | - |
| 29 | + let mut response = Response::new(Full::new(Bytes::from("Method Not Allowed"))); |
| 30 | + *response.status_mut() = StatusCode::METHOD_NOT_ALLOWED; |
53 | 31 | return Ok(response); |
54 | 32 | } |
55 | 33 |
|
56 | | - let index = FileExplorerAssets::get("index.html").unwrap(); |
57 | | - let body = Full::new(Bytes::from(index.data.to_vec())); |
58 | | - let mut response = Response::new(body); |
59 | | - let mut headers = response.headers().clone(); |
60 | | - |
61 | | - headers.append(CONTENT_TYPE, "text/html".try_into().unwrap()); |
62 | | - *response.headers_mut() = headers; |
63 | | - |
64 | | - Ok(response) |
65 | | - } |
66 | | - |
67 | | - async fn handle_api(&self, parts: Parts, body: Bytes) -> Result<HttpResponse> { |
68 | | - let path = Self::parse_req_uri(parts.uri.clone()).unwrap(); |
69 | | - |
70 | | - match parts.method { |
71 | | - Method::GET => match self.file_explorer.peek(path).await { |
72 | | - Ok(entry) => match entry { |
73 | | - Entry::Directory(dir) => { |
74 | | - let directory_index = |
75 | | - self.marshall_directory_index(dir.path()).await.unwrap(); |
76 | | - let json = serde_json::to_string(&directory_index).unwrap(); |
77 | | - let body = Full::new(Bytes::from(json)); |
78 | | - let mut response = Response::new(body); |
79 | | - let mut headers = response.headers().clone(); |
80 | | - |
81 | | - headers.append(CONTENT_TYPE, "application/json".try_into().unwrap()); |
82 | | - *response.headers_mut() = headers; |
83 | | - |
84 | | - Ok(response) |
85 | | - } |
86 | | - Entry::File(mut file) => { |
87 | | - let body = Full::new(Bytes::from(file.bytes().await.unwrap())); |
88 | | - let mut response = Response::new(body); |
89 | | - let mut headers = response.headers().clone(); |
90 | | - |
91 | | - headers.append(CONTENT_TYPE, file.mime().to_string().try_into().unwrap()); |
92 | | - *response.headers_mut() = headers; |
93 | | - |
94 | | - Ok(response) |
95 | | - } |
96 | | - }, |
97 | | - Err(err) => { |
98 | | - let message = format!("Failed to resolve path: {err}"); |
99 | | - Ok(Response::new(Full::new(Bytes::from(message)))) |
100 | | - } |
101 | | - }, |
102 | | - Method::POST => { |
103 | | - self.handle_file_upload(parts, body).await?; |
104 | | - Ok(Response::new(Full::new(Bytes::from( |
105 | | - "POST method is not supported", |
106 | | - )))) |
107 | | - } |
108 | | - _ => Ok(Response::new(Full::new(Bytes::from("Unsupported method")))), |
| 34 | + if parts.method == Method::GET { |
| 35 | + return self.file_service.resolve(parts.uri.to_string()).await; |
109 | 36 | } |
110 | | - } |
111 | | - |
112 | | - async fn handle_file_upload(&self, parts: Parts, body: Bytes) -> Result<HttpResponse> { |
113 | | - // Extract the `multipart/form-data` boundary from the headers. |
114 | | - let boundary = parts |
115 | | - .headers |
116 | | - .get(CONTENT_TYPE) |
117 | | - .and_then(|ct| ct.to_str().ok()) |
118 | | - .and_then(|ct| multer::parse_boundary(ct).ok()); |
119 | | - |
120 | | - // Send `BAD_REQUEST` status if the content-type is not multipart/form-data. |
121 | | - if boundary.is_none() { |
122 | | - return Ok(Response::builder() |
123 | | - .status(StatusCode::BAD_REQUEST) |
124 | | - .body(Full::from("BAD REQUEST")) |
125 | | - .unwrap()); |
126 | | - } |
127 | | - |
128 | | - // Process the multipart e.g. you can store them in files. |
129 | | - if let Err(err) = self.process_multipart(body, boundary.unwrap()).await { |
130 | | - return Ok(Response::builder() |
131 | | - .status(StatusCode::INTERNAL_SERVER_ERROR) |
132 | | - .body(Full::from(format!("INTERNAL SERVER ERROR: {err}"))) |
133 | | - .unwrap()); |
134 | | - } |
135 | | - |
136 | | - Ok(Response::new(Full::from("Success"))) |
137 | | - } |
138 | | - |
139 | | - async fn process_multipart(&self, bytes: Bytes, boundary: String) -> multer::Result<()> { |
140 | | - let cursor = std::io::Cursor::new(bytes); |
141 | | - let bytes_stream = tokio_util::io::ReaderStream::new(cursor); |
142 | | - let mut multipart = Multipart::new(bytes_stream, boundary); |
143 | | - |
144 | | - // Iterate over the fields, `next_field` method will return the next field if |
145 | | - // available. |
146 | | - while let Some(mut field) = multipart.next_field().await? { |
147 | | - // Get the field name. |
148 | | - let name = field.name(); |
149 | | - |
150 | | - // Get the field's filename if provided in "Content-Disposition" header. |
151 | | - let file_name = field.file_name().to_owned().unwrap_or("default.png"); |
152 | | - |
153 | | - // Get the "Content-Type" header as `mime::Mime` type. |
154 | | - let content_type = field.content_type(); |
155 | | - |
156 | | - let mut file = tokio::fs::File::create(file_name).await.unwrap(); |
157 | | - |
158 | | - println!( |
159 | | - "\n\nName: {name:?}, FileName: {file_name:?}, Content-Type: {content_type:?}\n\n" |
160 | | - ); |
161 | | - |
162 | | - // Process the field data chunks e.g. store them in a file. |
163 | | - let mut field_bytes_len = 0; |
164 | | - while let Some(field_chunk) = field.chunk().await? { |
165 | | - // Do something with field chunk. |
166 | | - field_bytes_len += field_chunk.len(); |
167 | | - file.write_all(&field_chunk).await.unwrap(); |
168 | | - } |
169 | 37 |
|
170 | | - println!("Field Bytes Length: {field_bytes_len:?}"); |
171 | | - } |
172 | | - |
173 | | - Ok(()) |
174 | | - } |
175 | | - |
176 | | - fn parse_req_uri(uri: Uri) -> Result<PathBuf> { |
177 | | - let parts: Vec<&str> = uri.path().split('/').collect(); |
178 | | - let path = &parts[3..].join("/"); |
179 | | - |
180 | | - Ok(decode_uri(path)) |
181 | | - } |
182 | | - |
183 | | - /// Encodes a `PathBuf` component using `PercentEncode` with UTF-8 charset. |
184 | | - /// |
185 | | - /// # Panics |
186 | | - /// |
187 | | - /// If the component's `OsStr` representation doesn't belong to valid UTF-8 |
188 | | - /// this function panics. |
189 | | - fn encode_component(comp: Component) -> String { |
190 | | - let component = comp |
191 | | - .as_os_str() |
192 | | - .to_str() |
193 | | - .expect("The provided OsStr doesn't belong to the UTF-8 charset."); |
194 | | - |
195 | | - utf8_percent_encode(component, PERCENT_ENCODE_SET).to_string() |
196 | | - } |
197 | | - |
198 | | - fn breadcrumbs_from_path(root_dir: &Path, path: &Path) -> Result<Vec<BreadcrumbItem>> { |
199 | | - let root_dir_name = root_dir |
200 | | - .components() |
201 | | - .next_back() |
202 | | - .unwrap() |
203 | | - .as_os_str() |
204 | | - .to_str() |
205 | | - .expect("The first path component is not UTF-8 charset compliant."); |
206 | | - let stripped = path |
207 | | - .strip_prefix(root_dir)? |
208 | | - .components() |
209 | | - .map(Self::encode_component) |
210 | | - .collect::<Vec<String>>(); |
211 | | - |
212 | | - let mut breadcrumbs = stripped |
213 | | - .iter() |
214 | | - .enumerate() |
215 | | - .map(|(idx, entry_name)| BreadcrumbItem { |
216 | | - depth: (idx + 1) as u8, |
217 | | - entry_name: percent_decode_str(entry_name) |
218 | | - .decode_utf8() |
219 | | - .expect("The path name is not UTF-8 compliant") |
220 | | - .to_string(), |
221 | | - entry_link: format!("/{}", stripped[0..=idx].join("/")), |
222 | | - }) |
223 | | - .collect::<Vec<BreadcrumbItem>>(); |
224 | | - |
225 | | - breadcrumbs.insert( |
226 | | - 0, |
227 | | - BreadcrumbItem { |
228 | | - depth: 0, |
229 | | - entry_name: String::from(root_dir_name), |
230 | | - entry_link: String::from("/"), |
231 | | - }, |
232 | | - ); |
233 | | - |
234 | | - Ok(breadcrumbs) |
235 | | - } |
236 | | - |
237 | | - /// Creates entry's relative path. Used by Handlebars template engine to |
238 | | - /// provide navigation through `FileExplorer` |
239 | | - /// |
240 | | - /// If the root_dir is: `https-server/src` |
241 | | - /// The entry path is: `https-server/src/server/service/file_explorer.rs` |
242 | | - /// |
243 | | - /// Then the resulting path from this function is the absolute path to |
244 | | - /// the "entry path" in relation to the "root_dir" path. |
245 | | - /// |
246 | | - /// This happens because links should behave relative to the `/` path |
247 | | - /// which in this case is `http-server/src` instead of system's root path. |
248 | | - fn make_dir_entry_link(root_dir: &Path, entry_path: &Path) -> String { |
249 | | - let path = entry_path.strip_prefix(root_dir).unwrap(); |
250 | | - |
251 | | - encode_uri(path) |
252 | | - } |
253 | | - |
254 | | - /// Creates a `DirectoryIndex` with the provided `root_dir` and `path` |
255 | | - /// (HTTP Request URI) |
256 | | - fn index_directory(root_dir: PathBuf, path: PathBuf) -> Result<DirectoryIndex> { |
257 | | - let breadcrumbs = Self::breadcrumbs_from_path(&root_dir, &path)?; |
258 | | - let entries = read_dir(path).context("Unable to read directory")?; |
259 | | - let mut directory_entries: Vec<DirectoryEntry> = Vec::new(); |
260 | | - |
261 | | - for entry in entries { |
262 | | - let entry = entry.context("Unable to read entry")?; |
263 | | - let metadata = entry.metadata()?; |
264 | | - |
265 | | - let display_name = entry |
266 | | - .file_name() |
267 | | - .to_str() |
268 | | - .context("Unable to gather file name into a String")? |
269 | | - .to_string(); |
270 | | - |
271 | | - let date_created = if let Ok(time) = metadata.created() { |
272 | | - Some(time.into()) |
273 | | - } else { |
274 | | - None |
275 | | - }; |
276 | | - |
277 | | - let date_modified = if let Ok(time) = metadata.modified() { |
278 | | - Some(time.into()) |
279 | | - } else { |
280 | | - None |
281 | | - }; |
282 | | - |
283 | | - let entry_type = if metadata.file_type().is_dir() { |
284 | | - EntryType::Directory |
285 | | - } else if let Some(ext) = display_name.split(".").last() { |
286 | | - match ext.to_ascii_lowercase().as_str() { |
287 | | - "gitignore" | "gitkeep" => EntryType::Git, |
288 | | - "justfile" => EntryType::Justfile, |
289 | | - "md" => EntryType::Markdown, |
290 | | - "rs" => EntryType::Rust, |
291 | | - "toml" => EntryType::Toml, |
292 | | - _ => EntryType::File, |
293 | | - } |
294 | | - } else { |
295 | | - EntryType::File |
296 | | - }; |
297 | | - |
298 | | - directory_entries.push(DirectoryEntry { |
299 | | - is_dir: metadata.is_dir(), |
300 | | - size_bytes: metadata.len(), |
301 | | - entry_path: Self::make_dir_entry_link(&root_dir, &entry.path()), |
302 | | - display_name, |
303 | | - entry_type, |
304 | | - date_created, |
305 | | - date_modified, |
306 | | - }); |
307 | | - } |
308 | | - |
309 | | - directory_entries.sort(); |
310 | | - |
311 | | - Ok(DirectoryIndex { |
312 | | - entries: directory_entries, |
313 | | - breadcrumbs, |
314 | | - sort: Sort::Directory, |
315 | | - }) |
316 | | - } |
317 | | - |
318 | | - async fn marshall_directory_index(&self, path: PathBuf) -> Result<DirectoryIndex> { |
319 | | - Self::index_directory(self.path.clone(), path) |
| 38 | + let mut response = Response::new(Full::new(Bytes::from("Method Not Allowed"))); |
| 39 | + *response.status_mut() = StatusCode::METHOD_NOT_ALLOWED; |
| 40 | + Ok(response) |
320 | 41 | } |
321 | 42 | } |
0 commit comments