Skip to content

Commit 5240fb3

Browse files
committed
feat: refactor file server logic + support for file mapping
1 parent 7c53882 commit 5240fb3

File tree

1 file changed

+112
-44
lines changed

1 file changed

+112
-44
lines changed

src/router.rs

Lines changed: 112 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,115 @@
1-
use anyhow::{anyhow, Context, Result};
1+
use anyhow::{anyhow, bail, Context, Result};
22
use log::{trace, warn};
33
use std::{
4-
collections::HashMap,
4+
collections::{HashMap, HashSet},
55
fs,
66
path::{Path, PathBuf},
77
str::FromStr,
88
};
99

10-
use crate::http::{
11-
response_status_codes::HttpStatusCode, HttpMethod, HttpRequest, HttpResponse,
12-
HttpResponseBuilder,
13-
};
10+
use crate::http::{HttpMethod, HttpRequest, HttpResponse, HttpResponseBuilder};
1411

1512
type RoutingCallback = fn(&HttpRequest) -> Result<HttpResponse>;
16-
type StaticFilesMount = (String, String);
13+
14+
#[derive(Debug)]
15+
pub struct FileServer {
16+
pub mount_points: HashSet<MountPoint>,
17+
}
18+
19+
impl FileServer {
20+
pub fn new() -> Self {
21+
Self {
22+
mount_points: HashSet::new(),
23+
}
24+
}
25+
26+
pub fn add_dir_mount(mut self, route: &str, dir_path: &str) -> Result<Self> {
27+
let mount_point = MountPoint {
28+
route: route.to_owned(),
29+
fs_path: PathBuf::from(dir_path),
30+
is_directory: true,
31+
};
32+
33+
if !self.mount_points.insert(mount_point) {
34+
bail!("directory mount already exists");
35+
}
36+
37+
Ok(self)
38+
}
39+
40+
pub fn map_file(mut self, route: &str, file_path: &str) -> Result<Self> {
41+
let mount_point = MountPoint {
42+
route: route.to_owned(),
43+
fs_path: PathBuf::from(file_path),
44+
is_directory: false,
45+
};
46+
47+
if !self.mount_points.insert(mount_point) {
48+
bail!("a similar file map already exists");
49+
}
50+
51+
Ok(self)
52+
}
53+
54+
fn get_file(file_path: PathBuf) -> Result<PathBuf> {
55+
if !file_path.exists() {
56+
bail!("file not found: {}", file_path.display());
57+
}
58+
59+
if !file_path.is_file() {
60+
bail!("not a file: {}", file_path.display());
61+
}
62+
63+
Ok(file_path)
64+
}
65+
66+
pub fn handle_static_file_access(&self, file: &str) -> Result<PathBuf> {
67+
let file_path = self
68+
.mount_points
69+
.iter()
70+
.filter(|mp| !mp.is_directory)
71+
.find(|mp| mp.route == file)
72+
.map(|mp| mp.fs_path.clone());
73+
74+
if let Some(file_path) = file_path {
75+
return FileServer::get_file(file_path);
76+
}
77+
78+
let dir_mount_point = self
79+
.mount_points
80+
.iter()
81+
.filter(|mp| mp.is_directory)
82+
.find(|mp| file.starts_with(&mp.route));
83+
84+
if let Some(dir_mount_point) = dir_mount_point {
85+
let file_name = file
86+
.strip_prefix(&dir_mount_point.route)
87+
.with_context(|| format!("file should have prefix: {}", dir_mount_point.route))?;
88+
89+
let safe_file_name = match Path::new(file_name).file_name() {
90+
Some(filename) => Ok(filename.to_owned()),
91+
None => Err(anyhow!("invalid file name: {file}")),
92+
}?;
93+
94+
let file_path = dir_mount_point.fs_path.join(safe_file_name);
95+
return Self::get_file(file_path);
96+
}
97+
98+
bail!("failed to map file: {file}")
99+
}
100+
}
101+
102+
#[derive(Debug, Hash, PartialEq, Eq)]
103+
pub struct MountPoint {
104+
pub route: String,
105+
pub fs_path: PathBuf,
106+
pub is_directory: bool,
107+
}
17108

18109
#[derive(Debug)]
19110
pub struct Router {
20111
pub routes: HashMap<Route, RoutingCallback>,
21-
pub file_server: Option<StaticFilesMount>,
112+
pub file_server: Option<FileServer>,
22113
}
23114

24115
impl Default for Router {
@@ -35,46 +126,28 @@ impl Router {
35126
}
36127
}
37128

129+
pub fn set_file_server(mut self, file_server: FileServer) -> Self {
130+
self.file_server = Some(file_server);
131+
self
132+
}
133+
38134
pub fn handle_request(&self, request: &HttpRequest) -> Result<HttpResponse> {
39135
let route_def = format!("{} {}", request.method, request.url);
40136
let route = Route::from_str(&route_def)?;
41137
trace!("trying to match route: {route_def}");
42138

43139
if let Some(file_server) = &self.file_server {
44-
let (route_prefix, dir_path) = file_server;
45-
if let Some(filename) = route.path.strip_prefix(route_prefix) {
46-
let safe_filename = match Path::new(filename).file_name() {
47-
Some(filename) => Ok(filename.to_owned()),
48-
None => Err(anyhow!("not a file: {filename}")),
49-
};
50-
51-
if let Err(e) = safe_filename {
52-
warn!("failed to serve file '{filename}': {e}");
140+
match file_server.handle_static_file_access(&route.path) {
141+
Ok(file_path) => {
142+
let mime_type = mime_guess::from_path(&file_path).first_or_octet_stream();
143+
let content = fs::read(file_path)?;
144+
53145
return HttpResponseBuilder::new()
54-
.set_status(HttpStatusCode::BadRequest)
146+
.set_raw_body(content)
147+
.set_content_type(mime_type.as_ref())
55148
.build();
56149
}
57-
58-
let safe_filename = safe_filename.unwrap();
59-
let filepath = PathBuf::from(dir_path).join(safe_filename);
60-
if !filepath.exists() {
61-
let catch_all_route = Route::from_str("GET /*")?;
62-
if let Some(catch_all_callback) = self.routes.get(&catch_all_route) {
63-
return catch_all_callback(request);
64-
} else {
65-
return HttpResponseBuilder::new()
66-
.set_status(HttpStatusCode::NotFound)
67-
.build();
68-
}
69-
}
70-
71-
let mime_type = mime_guess::from_path(&filepath).first_or_octet_stream();
72-
let content = fs::read(filepath)?;
73-
74-
return HttpResponseBuilder::new()
75-
.set_raw_body(content)
76-
.set_content_type(mime_type.as_ref())
77-
.build();
150+
Err(e) => warn!("failed to match file: {e}"),
78151
}
79152
}
80153

@@ -120,11 +193,6 @@ impl Router {
120193
Ok(())
121194
}
122195

123-
pub fn setup_file_serving(mut self, route: &str, directory_path: &str) -> Result<Self> {
124-
self.file_server = Some((route.to_owned(), directory_path.to_owned()));
125-
Ok(self)
126-
}
127-
128196
pub fn get(mut self, path: &str, callback: RoutingCallback) -> Result<Self> {
129197
self.add_route(HttpMethod::GET, path, callback)?;
130198
Ok(self)

0 commit comments

Comments
 (0)