Skip to content

Commit ab77eeb

Browse files
committed
feat: add default favicon for favicon.png and favicon.ico
fixes #33 Signed-off-by: Thorsten Hans <[email protected]>
1 parent 308d842 commit ab77eeb

File tree

3 files changed

+85
-11
lines changed

3 files changed

+85
-11
lines changed

spin-favicon.ico

3.76 KB
Binary file not shown.

spin-favicon.png

48.8 KB
Loading

src/lib.rs

Lines changed: 85 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ const FALLBACK_PATH_ENV: &str = "FALLBACK_PATH";
3232
const CUSTOM_404_PATH_ENV: &str = "CUSTOM_404_PATH";
3333
/// Directory fallback path (trying to map `/about/` -> `/about/index.html`).
3434
const DIRECTORY_FALLBACK_PATH: &str = "index.html";
35-
35+
// FAVICON_ICO_FILENAME
36+
const FAVICON_ICO_FILENAME: &str = "favicon.ico";
37+
// FAVICON_PNG_FILENAME
38+
const FAVICON_PNG_FILENAME: &str = "favicon.png";
39+
// Fallback favicon.png that is used when user does not a custom one
40+
const FALLBACK_FAVICON_PNG: &[u8] = include_bytes!("../spin-favicon.png");
41+
// Fallback favicon.ico that is used when user does not a custom one
42+
const FALLBACK_FAVICON_ICO: &[u8] = include_bytes!("../spin-favicon.ico");
3643
/// Common Content Encodings
3744
#[derive(Debug, Eq, PartialEq)]
3845
pub enum ContentEncoding {
@@ -82,19 +89,55 @@ fn serve(req: Request) -> Result<Response> {
8289
// resolve the requested path and then try to read the file
8390
// None should indicate that the file does not exist after attempting fallback paths
8491
let body = match FileServer::resolve(path) {
85-
Some(path) => FileServer::read(&path, &enc).ok(),
86-
None => None,
92+
FileServerPath::Physical(path) => FileServer::read(&path, &enc).ok(),
93+
FileServerPath::Embedded(resource) => ResourceServer::read(resource, &enc),
94+
FileServerPath::None => None,
8795
};
88-
8996
let etag = FileServer::get_etag(body.clone());
9097
FileServer::send(body, path, enc, &etag, if_none_match)
9198
}
9299

100+
struct ResourceServer {}
101+
102+
impl ResourceServer {
103+
fn read(resource: &'static [u8], encoding: &ContentEncoding) -> Option<Bytes> {
104+
match encoding {
105+
ContentEncoding::Brotli => {
106+
let mut r = brotli::CompressorReader::new(resource, 4096, BROTLI_LEVEL, 20);
107+
let mut buf = vec![];
108+
r.read_to_end(&mut buf).ok()?;
109+
Some(buf.into())
110+
}
111+
_ => Some(resource.into()),
112+
}
113+
}
114+
}
115+
116+
#[derive(Debug, Eq, PartialEq)]
117+
enum FileServerPath {
118+
Physical(PathBuf),
119+
Embedded(&'static [u8]),
120+
None,
121+
}
122+
123+
trait IsFavicon {
124+
fn is_favicon(&self) -> bool;
125+
}
126+
127+
impl IsFavicon for PathBuf {
128+
fn is_favicon(&self) -> bool {
129+
match self.clone().into_os_string().into_string() {
130+
Ok(s) => s == FAVICON_ICO_FILENAME || s == FAVICON_PNG_FILENAME,
131+
Err(_) => false,
132+
}
133+
}
134+
}
135+
93136
struct FileServer;
94137
impl FileServer {
95138
/// Resolve the request path to a file path.
96-
/// Returns `None` if the path does not exist.
97-
fn resolve(req_path: &str) -> Option<PathBuf> {
139+
/// Returns a `FileServerPath` variant.
140+
fn resolve(req_path: &str) -> FileServerPath {
98141
// fallback to index.html if the path is empty
99142
let mut path = if req_path.is_empty() {
100143
PathBuf::from(DIRECTORY_FALLBACK_PATH)
@@ -107,6 +150,17 @@ impl FileServer {
107150
path.push(DIRECTORY_FALLBACK_PATH);
108151
}
109152

153+
// if path doesn't exist and a favicon is requested, return with corresponding embedded resource
154+
if !path.exists() && path.is_favicon() {
155+
return match path.extension() {
156+
Some(os_string) => match os_string.to_str() {
157+
Some("ico") => FileServerPath::Embedded(FALLBACK_FAVICON_ICO),
158+
Some("png") => FileServerPath::Embedded(FALLBACK_FAVICON_PNG),
159+
_ => FileServerPath::None,
160+
},
161+
None => FileServerPath::None,
162+
};
163+
}
110164
// if still haven't found a file, override with the user-configured fallback path
111165
if !path.exists() {
112166
if let Ok(fallback_path) = std::env::var(FALLBACK_PATH_ENV) {
@@ -115,7 +169,7 @@ impl FileServer {
115169
}
116170

117171
if path.exists() {
118-
return Some(path);
172+
return FileServerPath::Physical(path);
119173
}
120174

121175
// check if user configured a custom 404 path
@@ -125,9 +179,9 @@ impl FileServer {
125179
}
126180

127181
if path.exists() {
128-
Some(path)
182+
FileServerPath::Physical(path)
129183
} else {
130-
None
184+
FileServerPath::None
131185
}
132186
}
133187

@@ -149,8 +203,13 @@ impl FileServer {
149203

150204
/// Return the media type of the file based on the path.
151205
fn mime(uri: &str) -> Option<String> {
152-
let guess = mime_guess::from_path(uri);
153-
guess.first().map(|m| m.to_string())
206+
match uri {
207+
FAVICON_ICO_FILENAME => mime_guess::from_ext("ico"),
208+
FAVICON_PNG_FILENAME => mime_guess::from_ext("png"),
209+
_ => mime_guess::from_path(uri),
210+
}
211+
.first()
212+
.map(|m| m.to_string())
154213
}
155214

156215
fn append_headers(
@@ -404,4 +463,19 @@ mod tests {
404463
let rsp = <super::SpinHttp as spin_http::SpinHttp>::handle_http_request(req);
405464
assert_eq!(rsp.status, 200);
406465
}
466+
467+
#[test]
468+
fn test_serve_fallback_favicon() {
469+
let req = spin_http::Request {
470+
method: spin_http::Method::Get,
471+
uri: "http://thisistest.com/".to_string(),
472+
headers: vec![(PATH_INFO_HEADER.to_string(), "favicon.png".to_string())],
473+
params: vec![],
474+
body: None,
475+
};
476+
let rsp = <super::SpinHttp as spin_http::SpinHttp>::handle_http_request(req);
477+
478+
assert_eq!(rsp.status, StatusCode::OK);
479+
assert_eq!(rsp.body.unwrap(), FALLBACK_FAVICON_PNG);
480+
}
407481
}

0 commit comments

Comments
 (0)