Implement logo.png and color.txt on backend#8
Conversation
There was a problem hiding this comment.
Pull request overview
Adds dedicated backend routes for logo.png and color.txt so they can be served (preferably) from the filesystem next to the executable, with a fallback to the embedded frontend asset map.
Changes:
- Introduced
/logo.pngand/color.txtActix handlers with filesystem + embedded fallback logic. - Registered the new services in the Actix
Appstartup. - Added
actix-filesdependency to support serving files from disk.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/routes/mod.rs | Exposes new color and logo route modules. |
| src/routes/logo.rs | Implements /logo.png handler with filesystem/embedded fallback. |
| src/routes/color.rs | Implements /color.txt handler with filesystem/embedded fallback. |
| src/main.rs | Registers the new route services in the server. |
| Cargo.toml | Adds actix-files dependency and reformats some dependency entries. |
| Cargo.lock | Locks new transitive dependencies from actix-files. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| async fn color_txt(req: HttpRequest) -> HttpResponse<EitherBody<BoxBody>> { | ||
| let file_path = current_exe() | ||
| .ok() | ||
| .and_then(|p| p.parent().map(|p| p.join("color.txt"))); | ||
|
|
||
| if let Some(path) = file_path | ||
| && path.exists() | ||
| { | ||
| tracing::debug!("Serving color.txt from file system: {path:?}"); | ||
| NamedFile::open_async(path).await.respond_to(&req) | ||
| } else { |
There was a problem hiding this comment.
The two branches of this handler build different HttpResponse body types: the embedded/not-found branch maps into EitherBody<BoxBody>, but the filesystem branch returns the NamedFile response directly. This will not type-check unless you map the filesystem response into the same body type, or (simpler) drop EitherBody entirely and return a plain HttpResponse from all branches.
| if let Some(path) = file_path | ||
| && path.exists() | ||
| { | ||
| tracing::debug!("Serving color.txt from file system: {path:?}"); | ||
| NamedFile::open_async(path).await.respond_to(&req) |
There was a problem hiding this comment.
path.exists() performs a synchronous filesystem metadata check on the async request path and introduces a TOCTOU window (file may disappear/change between exists() and open_async). Prefer attempting NamedFile::open_async directly and falling back to the embedded resource only on Err, which also avoids blocking work per request.
| if let Some(path) = file_path | |
| && path.exists() | |
| { | |
| tracing::debug!("Serving color.txt from file system: {path:?}"); | |
| NamedFile::open_async(path).await.respond_to(&req) | |
| if let Some(path) = file_path { | |
| match NamedFile::open_async(&path).await { | |
| Ok(file) => { | |
| tracing::debug!("Serving color.txt from file system: {path:?}"); | |
| file.respond_to(&req).map_into_left_body() | |
| } | |
| Err(_) => frontend::get("color.txt") | |
| .map(|content| { | |
| tracing::debug!("Serving color.txt from embedded resource"); | |
| HttpResponse::Ok() | |
| .content_type(mime::TEXT_PLAIN) | |
| .body(content) | |
| .map_into_left_body() | |
| }) | |
| .unwrap_or_else(|| { | |
| tracing::debug!("File not found"); | |
| HttpResponse::NotFound() | |
| .body("Not found") | |
| .map_into_right_body() | |
| }), | |
| } |
| #[get("/color.txt")] | ||
| #[tracing::instrument(skip_all, name = "color.txt")] | ||
| async fn color_txt(req: HttpRequest) -> HttpResponse<EitherBody<BoxBody>> { | ||
| let file_path = current_exe() | ||
| .ok() | ||
| .and_then(|p| p.parent().map(|p| p.join("color.txt"))); | ||
|
|
||
| if let Some(path) = file_path | ||
| && path.exists() | ||
| { | ||
| tracing::debug!("Serving color.txt from file system: {path:?}"); | ||
| NamedFile::open_async(path).await.respond_to(&req) | ||
| } else { | ||
| frontend::get("color.txt") | ||
| .map(|content| { | ||
| tracing::debug!("Serving color.txt from embedded resource"); | ||
| HttpResponse::Ok() | ||
| .content_type(mime::TEXT_PLAIN) | ||
| .body(content) | ||
| .map_into_left_body() | ||
| }) | ||
| .unwrap_or_else(|| { | ||
| tracing::debug!("File not found"); | ||
| HttpResponse::NotFound() | ||
| .body("Not found") | ||
| .map_into_right_body() | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
This file duplicates the same “check next to executable → otherwise embedded resource → otherwise 404” logic as logo.rs. Consider extracting a small helper (e.g., serve_embedded_or_fs(req, filename, content_type)) to reduce duplication and keep behavior consistent if you later tweak caching/logging/error handling.
| #[get("/color.txt")] | |
| #[tracing::instrument(skip_all, name = "color.txt")] | |
| async fn color_txt(req: HttpRequest) -> HttpResponse<EitherBody<BoxBody>> { | |
| let file_path = current_exe() | |
| .ok() | |
| .and_then(|p| p.parent().map(|p| p.join("color.txt"))); | |
| if let Some(path) = file_path | |
| && path.exists() | |
| { | |
| tracing::debug!("Serving color.txt from file system: {path:?}"); | |
| NamedFile::open_async(path).await.respond_to(&req) | |
| } else { | |
| frontend::get("color.txt") | |
| .map(|content| { | |
| tracing::debug!("Serving color.txt from embedded resource"); | |
| HttpResponse::Ok() | |
| .content_type(mime::TEXT_PLAIN) | |
| .body(content) | |
| .map_into_left_body() | |
| }) | |
| .unwrap_or_else(|| { | |
| tracing::debug!("File not found"); | |
| HttpResponse::NotFound() | |
| .body("Not found") | |
| .map_into_right_body() | |
| }) | |
| } | |
| } | |
| async fn serve_embedded_or_fs( | |
| req: &HttpRequest, | |
| filename: &str, | |
| content_type: mime::Mime, | |
| ) -> HttpResponse<EitherBody<BoxBody>> { | |
| let file_path = current_exe() | |
| .ok() | |
| .and_then(|p| p.parent().map(|p| p.join(filename))); | |
| if let Some(path) = file_path | |
| && path.exists() | |
| { | |
| tracing::debug!("Serving {filename} from file system: {path:?}"); | |
| NamedFile::open_async(path).await.respond_to(req) | |
| } else { | |
| frontend::get(filename) | |
| .map(|content| { | |
| tracing::debug!("Serving {filename} from embedded resource"); | |
| HttpResponse::Ok() | |
| .content_type(content_type) | |
| .body(content) | |
| .map_into_left_body() | |
| }) | |
| .unwrap_or_else(|| { | |
| tracing::debug!("File not found: {filename}"); | |
| HttpResponse::NotFound() | |
| .body("Not found") | |
| .map_into_right_body() | |
| }) | |
| } | |
| } | |
| #[get("/color.txt")] | |
| #[tracing::instrument(skip_all, name = "color.txt")] | |
| async fn color_txt(req: HttpRequest) -> HttpResponse<EitherBody<BoxBody>> { | |
| serve_embedded_or_fs(&req, "color.txt", mime::TEXT_PLAIN).await | |
| } |
| async fn logo_png(req: HttpRequest) -> HttpResponse<EitherBody<BoxBody>> { | ||
| let file_path = current_exe() | ||
| .ok() | ||
| .and_then(|p| p.parent().map(|p| p.join("logo.png"))); | ||
|
|
||
| if let Some(path) = file_path | ||
| && path.exists() | ||
| { | ||
| tracing::debug!("Serving logo from file system: {path:?}"); | ||
| NamedFile::open_async(path).await.respond_to(&req) | ||
| } else { |
There was a problem hiding this comment.
The two branches of this handler build different HttpResponse body types: the embedded/not-found branch maps into EitherBody<BoxBody>, but the filesystem branch returns the NamedFile response directly. This will not type-check unless you map the filesystem response into the same body type, or (simpler) drop EitherBody entirely and return a plain HttpResponse from all branches.
| if let Some(path) = file_path | ||
| && path.exists() | ||
| { | ||
| tracing::debug!("Serving logo from file system: {path:?}"); | ||
| NamedFile::open_async(path).await.respond_to(&req) | ||
| } else { | ||
| frontend::get("logo.png") | ||
| .map(|content| { | ||
| tracing::debug!("Serving logo from embedded resource"); | ||
| HttpResponse::Ok() | ||
| .content_type(mime::IMAGE_PNG) | ||
| .body(content) | ||
| .map_into_left_body() | ||
| }) | ||
| .unwrap_or_else(|| { | ||
| tracing::debug!("File not found"); | ||
| HttpResponse::NotFound() | ||
| .body("Not found") | ||
| .map_into_right_body() | ||
| }) | ||
| } |
There was a problem hiding this comment.
path.exists() performs a synchronous filesystem metadata check on the async request path and introduces a TOCTOU window (file may disappear/change between exists() and open_async). Prefer attempting NamedFile::open_async directly and falling back to the embedded resource only on Err, which also avoids blocking work per request.
| if let Some(path) = file_path | |
| && path.exists() | |
| { | |
| tracing::debug!("Serving logo from file system: {path:?}"); | |
| NamedFile::open_async(path).await.respond_to(&req) | |
| } else { | |
| frontend::get("logo.png") | |
| .map(|content| { | |
| tracing::debug!("Serving logo from embedded resource"); | |
| HttpResponse::Ok() | |
| .content_type(mime::IMAGE_PNG) | |
| .body(content) | |
| .map_into_left_body() | |
| }) | |
| .unwrap_or_else(|| { | |
| tracing::debug!("File not found"); | |
| HttpResponse::NotFound() | |
| .body("Not found") | |
| .map_into_right_body() | |
| }) | |
| } | |
| if let Some(path) = file_path { | |
| match NamedFile::open_async(path.clone()).await { | |
| Ok(file) => { | |
| tracing::debug!("Serving logo from file system: {path:?}"); | |
| return file.respond_to(&req); | |
| } | |
| Err(err) => { | |
| tracing::debug!("Failed to serve logo from file system ({path:?}): {err}"); | |
| } | |
| } | |
| } | |
| frontend::get("logo.png") | |
| .map(|content| { | |
| tracing::debug!("Serving logo from embedded resource"); | |
| HttpResponse::Ok() | |
| .content_type(mime::IMAGE_PNG) | |
| .body(content) | |
| .map_into_left_body() | |
| }) | |
| .unwrap_or_else(|| { | |
| tracing::debug!("File not found"); | |
| HttpResponse::NotFound() | |
| .body("Not found") | |
| .map_into_right_body() | |
| }) |
| #[get("/logo.png")] | ||
| #[tracing::instrument(skip_all, name = "logo.png")] | ||
| async fn logo_png(req: HttpRequest) -> HttpResponse<EitherBody<BoxBody>> { | ||
| let file_path = current_exe() | ||
| .ok() | ||
| .and_then(|p| p.parent().map(|p| p.join("logo.png"))); | ||
|
|
||
| if let Some(path) = file_path | ||
| && path.exists() | ||
| { | ||
| tracing::debug!("Serving logo from file system: {path:?}"); | ||
| NamedFile::open_async(path).await.respond_to(&req) | ||
| } else { | ||
| frontend::get("logo.png") | ||
| .map(|content| { | ||
| tracing::debug!("Serving logo from embedded resource"); | ||
| HttpResponse::Ok() | ||
| .content_type(mime::IMAGE_PNG) | ||
| .body(content) | ||
| .map_into_left_body() | ||
| }) | ||
| .unwrap_or_else(|| { | ||
| tracing::debug!("File not found"); | ||
| HttpResponse::NotFound() | ||
| .body("Not found") | ||
| .map_into_right_body() | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
This file duplicates the same “check next to executable → otherwise embedded resource → otherwise 404” logic as logo.rs. Consider extracting a small helper (e.g., serve_embedded_or_fs(req, filename, content_type)) to reduce duplication and keep behavior consistent if you later tweak caching/logging/error handling.
| #[get("/logo.png")] | |
| #[tracing::instrument(skip_all, name = "logo.png")] | |
| async fn logo_png(req: HttpRequest) -> HttpResponse<EitherBody<BoxBody>> { | |
| let file_path = current_exe() | |
| .ok() | |
| .and_then(|p| p.parent().map(|p| p.join("logo.png"))); | |
| if let Some(path) = file_path | |
| && path.exists() | |
| { | |
| tracing::debug!("Serving logo from file system: {path:?}"); | |
| NamedFile::open_async(path).await.respond_to(&req) | |
| } else { | |
| frontend::get("logo.png") | |
| .map(|content| { | |
| tracing::debug!("Serving logo from embedded resource"); | |
| HttpResponse::Ok() | |
| .content_type(mime::IMAGE_PNG) | |
| .body(content) | |
| .map_into_left_body() | |
| }) | |
| .unwrap_or_else(|| { | |
| tracing::debug!("File not found"); | |
| HttpResponse::NotFound() | |
| .body("Not found") | |
| .map_into_right_body() | |
| }) | |
| } | |
| } | |
| async fn serve_embedded_or_fs( | |
| req: &HttpRequest, | |
| filename: &str, | |
| content_type: mime::Mime, | |
| ) -> HttpResponse<EitherBody<BoxBody>> { | |
| let file_path = current_exe() | |
| .ok() | |
| .and_then(|p| p.parent().map(|p| p.join(filename))); | |
| if let Some(path) = file_path | |
| && path.exists() | |
| { | |
| tracing::debug!("Serving {filename} from file system: {path:?}"); | |
| NamedFile::open_async(path).await.respond_to(req) | |
| } else { | |
| frontend::get(filename) | |
| .map(|content| { | |
| tracing::debug!("Serving {filename} from embedded resource"); | |
| HttpResponse::Ok() | |
| .content_type(content_type) | |
| .body(content) | |
| .map_into_left_body() | |
| }) | |
| .unwrap_or_else(|| { | |
| tracing::debug!("File not found: {filename}"); | |
| HttpResponse::NotFound() | |
| .body("Not found") | |
| .map_into_right_body() | |
| }) | |
| } | |
| } | |
| #[get("/logo.png")] | |
| #[tracing::instrument(skip_all, name = "logo.png")] | |
| async fn logo_png(req: HttpRequest) -> HttpResponse<EitherBody<BoxBody>> { | |
| serve_embedded_or_fs(&req, "logo.png", mime::IMAGE_PNG).await | |
| } |
No description provided.