Skip to content

Commit ce69b2b

Browse files
committed
feat: update legacy handler to use new types
1 parent 55935a6 commit ce69b2b

File tree

11 files changed

+57
-341
lines changed

11 files changed

+57
-341
lines changed

src/http-server/src/config.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::net::IpAddr;
22
use std::str::FromStr;
33

4-
use anyhow::{bail, Error, Result};
4+
use anyhow::{Error, Result, bail};
55

66
#[derive(Clone, Debug)]
77
pub struct Config {
@@ -28,7 +28,9 @@ impl FromStr for BasicAuth {
2828
let parts = s.split(":").collect::<Vec<&str>>();
2929

3030
if parts.len() != 2 {
31-
bail!("Expected a string with a colon to separe username and password for Basic Authentication.");
31+
bail!(
32+
"Expected a string with a colon to separe username and password for Basic Authentication."
33+
);
3234
}
3335

3436
Ok(BasicAuth {

src/http-server/src/handler/file_explorer/core/fs/file.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::fs::Metadata;
22
use std::path::PathBuf;
33

44
use anyhow::Result;
5-
use mime_guess::{from_path, Mime};
5+
use mime_guess::{Mime, from_path};
66
use tokio::io::AsyncReadExt;
77

88
/// Wrapper around `tokio::fs::File` built from a OS ScopedFileSystem file

src/http-server/src/handler/file_explorer/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use std::path::{Component, Path, PathBuf};
88

99
use anyhow::{Context, Result};
1010
use bytes::Bytes;
11-
use http::{header::CONTENT_TYPE, request::Parts, HeaderValue, Method, Response, StatusCode, Uri};
11+
use http::{HeaderValue, Method, Response, StatusCode, Uri, header::CONTENT_TYPE, request::Parts};
1212
use http_body_util::{BodyExt, Full};
1313
use multer::Multipart;
1414
use percent_encoding::{percent_decode_str, utf8_percent_encode};
@@ -19,7 +19,7 @@ use tokio::io::AsyncWriteExt;
1919
use crate::server::{HttpRequest, HttpResponse};
2020

2121
use self::proto::BreadcrumbItem;
22-
use self::utils::{decode_uri, encode_uri, PERCENT_ENCODE_SET};
22+
use self::utils::{PERCENT_ENCODE_SET, decode_uri, encode_uri};
2323

2424
#[derive(Embed)]
2525
#[folder = "./ui"]

src/http-server/src/handler/file_explorer/utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::{Path, PathBuf};
22

3-
use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC};
3+
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode, utf8_percent_encode};
44

55
pub const PERCENT_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC
66
.remove(b'-')
Lines changed: 14 additions & 293 deletions
Original file line numberDiff line numberDiff line change
@@ -1,321 +1,42 @@
11
mod service;
22
mod utils;
33

4-
use std::fs::read_dir;
5-
use std::path::{Component, Path, PathBuf};
6-
7-
use anyhow::{Context, Result};
4+
use anyhow::Result;
85
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;
148

9+
use crate::handler::file_server::service::FileServerConfig;
1510
use crate::server::{HttpRequest, HttpResponse};
1611

1712
use self::service::FileServer as FileServerService;
1813

1914
pub struct FileServer {
2015
file_service: FileServerService,
21-
path: PathBuf,
2216
}
2317

2418
impl FileServer {
25-
pub fn new(path: PathBuf) -> Self {
19+
pub fn new(config: FileServerConfig) -> Self {
2620
Self {
27-
file_service: FileServerService::new(path.clone()),
28-
path,
21+
file_service: FileServerService::new(config),
2922
}
3023
}
3124

3225
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();
3527

3628
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;
5331
return Ok(response);
5432
}
5533

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;
10936
}
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-
}
16937

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)
32041
}
32142
}

src/http-server/src/handler/file_server/service/file.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use anyhow::{Context, Result};
88
use chrono::{DateTime, Local};
99
use futures::Stream;
1010
use hyper::body::Bytes;
11-
use mime_guess::{from_path, Mime};
11+
use mime_guess::{Mime, from_path};
1212
use tokio::io::{AsyncRead, ReadBuf};
1313

1414
pub const FILE_BUFFER_SIZE: usize = 8 * 1024;

0 commit comments

Comments
 (0)