Skip to content

Commit f46637b

Browse files
committed
refactor: move FileServer to seperate mod + use HashMap and fix duplicate route detect + add tests
1 parent ada3e36 commit f46637b

File tree

3 files changed

+153
-106
lines changed

3 files changed

+153
-106
lines changed

src/file_server.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use anyhow::{anyhow, bail, Context, Result};
2+
use std::{
3+
collections::HashMap,
4+
path::{Path, PathBuf},
5+
};
6+
7+
#[derive(Debug, Hash, PartialEq, Eq)]
8+
pub struct MountPoint {
9+
pub route: String,
10+
pub fs_path: PathBuf,
11+
pub is_directory: bool,
12+
}
13+
14+
#[derive(Debug)]
15+
pub struct FileServer {
16+
pub mount_points: HashMap<String, MountPoint>,
17+
}
18+
19+
impl Default for FileServer {
20+
fn default() -> Self {
21+
Self::new()
22+
}
23+
}
24+
25+
impl FileServer {
26+
pub fn new() -> Self {
27+
Self {
28+
mount_points: HashMap::new(),
29+
}
30+
}
31+
32+
fn map(mut self, route: &str, fs_path: &str, is_directory: bool) -> Result<Self> {
33+
let route = route.strip_suffix('/').unwrap_or(route);
34+
let mount_point = MountPoint {
35+
route: route.to_owned(),
36+
fs_path: PathBuf::from(fs_path),
37+
is_directory,
38+
};
39+
40+
if let Some(existing_mp) = self.mount_points.get(route) {
41+
bail!(
42+
"{route} has already been mapped to: {}",
43+
existing_mp.fs_path.display()
44+
);
45+
}
46+
47+
self.mount_points.insert(route.to_owned(), mount_point);
48+
Ok(self)
49+
}
50+
51+
pub fn map_dir(self, route: &str, dir_path: &str) -> Result<Self> {
52+
self.map(route, dir_path, true)
53+
}
54+
55+
pub fn map_file(self, route: &str, file_path: &str) -> Result<Self> {
56+
self.map(route, file_path, false)
57+
}
58+
59+
fn get_file(file_path: PathBuf) -> Result<PathBuf> {
60+
if !file_path.exists() {
61+
bail!("file not found: {}", file_path.display());
62+
}
63+
64+
if !file_path.is_file() {
65+
bail!("not a file: {}", file_path.display());
66+
}
67+
68+
Ok(file_path)
69+
}
70+
71+
pub fn handle_file_access(&self, file: &str) -> Result<PathBuf> {
72+
let file_path = self
73+
.mount_points
74+
.values()
75+
.filter(|mp| !mp.is_directory)
76+
.find(|mp| mp.route == file)
77+
.map(|mp| mp.fs_path.clone());
78+
79+
if let Some(file_path) = file_path {
80+
return FileServer::get_file(file_path);
81+
}
82+
83+
let dir_mount_point = self
84+
.mount_points
85+
.values()
86+
.filter(|mp| mp.is_directory)
87+
.find(|mp| file.starts_with(&mp.route));
88+
89+
if let Some(dir_mount_point) = dir_mount_point {
90+
let file_name = file
91+
.strip_prefix(&dir_mount_point.route)
92+
.with_context(|| format!("file should have prefix: {}", dir_mount_point.route))?;
93+
94+
let safe_file_name = match Path::new(file_name).file_name() {
95+
Some(filename) => Ok(filename.to_owned()),
96+
None => Err(anyhow!("invalid file name: {file}")),
97+
}?;
98+
99+
let file_path = dir_mount_point.fs_path.join(safe_file_name);
100+
return Self::get_file(file_path);
101+
}
102+
103+
bail!("failed to map file: {file}")
104+
}
105+
}
106+
107+
#[cfg(test)]
108+
mod tests {
109+
use super::FileServer;
110+
111+
#[test]
112+
fn test_map_dir_ok() {
113+
let fs = FileServer::new().map_dir("/static", "./relative/static");
114+
115+
assert!(fs.is_ok());
116+
}
117+
118+
#[test]
119+
fn test_map_dir_twice_err() {
120+
let fs = FileServer::new()
121+
.map_dir("/static", "/home/foo/absolute/static")
122+
.unwrap()
123+
.map_dir("/static", "relative/static");
124+
125+
assert!(fs.is_err());
126+
}
127+
128+
#[test]
129+
fn test_map_file_ok() {
130+
let fs =
131+
FileServer::new().map_file("/favicon.ico", "/home/foo/absolute/static/favicon.ico");
132+
133+
assert!(fs.is_ok());
134+
}
135+
136+
#[test]
137+
fn test_map_file_twice_err() {
138+
let fs = FileServer::new()
139+
.map_file("/favicon.ico", "/home/foo/absolute/static/favicon.ico")
140+
.unwrap()
141+
.map_file("/favicon.ico", "relative/static/favicon.ico");
142+
143+
assert!(fs.is_err());
144+
}
145+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
pub mod file_server;
12
pub mod http;
23
pub mod router;
34
pub mod thread_pool;

src/router.rs

Lines changed: 7 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,14 @@
1-
use anyhow::{anyhow, bail, Context, Result};
1+
use anyhow::{anyhow, Context, Result};
22
use log::{trace, warn};
3-
use std::{
4-
collections::{HashMap, HashSet},
5-
fs,
6-
path::{Path, PathBuf},
7-
str::FromStr,
8-
};
3+
use std::{collections::HashMap, fs, str::FromStr};
94

10-
use crate::http::{HttpMethod, HttpRequest, HttpResponse, HttpResponseBuilder};
5+
use crate::{
6+
file_server::FileServer,
7+
http::{HttpMethod, HttpRequest, HttpResponse, HttpResponseBuilder},
8+
};
119

1210
type RoutingCallback = fn(&HttpRequest) -> Result<HttpResponse>;
1311

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 map_dir(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 route = route.strip_suffix('/').unwrap_or(route).to_owned();
42-
43-
let mount_point = MountPoint {
44-
route,
45-
fs_path: PathBuf::from(file_path),
46-
is_directory: false,
47-
};
48-
49-
if !self.mount_points.insert(mount_point) {
50-
bail!("a similar file map already exists");
51-
}
52-
53-
Ok(self)
54-
}
55-
56-
fn get_file(file_path: PathBuf) -> Result<PathBuf> {
57-
if !file_path.exists() {
58-
bail!("file not found: {}", file_path.display());
59-
}
60-
61-
if !file_path.is_file() {
62-
bail!("not a file: {}", file_path.display());
63-
}
64-
65-
Ok(file_path)
66-
}
67-
68-
pub fn handle_static_file_access(&self, file: &str) -> Result<PathBuf> {
69-
let file_path = self
70-
.mount_points
71-
.iter()
72-
.filter(|mp| !mp.is_directory)
73-
.find(|mp| mp.route == file)
74-
.map(|mp| mp.fs_path.clone());
75-
76-
if let Some(file_path) = file_path {
77-
return FileServer::get_file(file_path);
78-
}
79-
80-
let dir_mount_point = self
81-
.mount_points
82-
.iter()
83-
.filter(|mp| mp.is_directory)
84-
.find(|mp| file.starts_with(&mp.route));
85-
86-
if let Some(dir_mount_point) = dir_mount_point {
87-
let file_name = file
88-
.strip_prefix(&dir_mount_point.route)
89-
.with_context(|| format!("file should have prefix: {}", dir_mount_point.route))?;
90-
91-
let safe_file_name = match Path::new(file_name).file_name() {
92-
Some(filename) => Ok(filename.to_owned()),
93-
None => Err(anyhow!("invalid file name: {file}")),
94-
}?;
95-
96-
let file_path = dir_mount_point.fs_path.join(safe_file_name);
97-
return Self::get_file(file_path);
98-
}
99-
100-
bail!("failed to map file: {file}")
101-
}
102-
}
103-
104-
#[derive(Debug, Hash, PartialEq, Eq)]
105-
pub struct MountPoint {
106-
pub route: String,
107-
pub fs_path: PathBuf,
108-
pub is_directory: bool,
109-
}
110-
11112
#[derive(Debug)]
11213
pub struct Router {
11314
pub routes: HashMap<Route, RoutingCallback>,
@@ -142,7 +43,7 @@ impl Router {
14243
route_callback(request)
14344
} else {
14445
if let Some(file_server) = &self.file_server {
145-
match file_server.handle_static_file_access(&route.path) {
46+
match file_server.handle_file_access(&route.path) {
14647
Ok(file_path) => {
14748
let mime_type = mime_guess::from_path(&file_path).first_or_octet_stream();
14849
let content = fs::read(file_path)?;

0 commit comments

Comments
 (0)