fix bugs, harden server, strip-based dithering, update docs
Firmware:
- fix 90° rotation (was transpose)
- fix Adafruit_Thermal constructor (spurious DTR pin arg)
- wire up uploadImage; heatInterval now applied
- PSRAM-free strip dithering: 2-row buffers (~1KB) replace 153KB PSRAM alloc
- consolidate all pins into config.h; BUTTON_PIN → Config::PIN_BUTTON
- constrain contrast/brightness/heat in settings save handler
- uploadImage size param int → size_t
Server:
- canonicalize upload_dir at startup (fixes path traversal guard)
- path traversal guard in serve_image
- replace unwrap in gallery_data with error handling
- IMAGE_WIDTH/IMAGE_HEIGHT named constants
Gallery:
- innerHTML → createElement (XSS-safe)
- encodeURIComponent on image URLs
- replace("_"," ") → regex /_/g
Docs: rewrite README, clarify GUEST_MANUAL placeholders
This commit is contained in:
@@ -79,19 +79,32 @@
|
||||
}
|
||||
|
||||
function addCard(filename) {
|
||||
const div = document.createElement("div");
|
||||
div.className = "card";
|
||||
div.innerHTML = `
|
||||
<a href="/gallery/img/${filename}" target="_blank">
|
||||
<img src="/gallery/img/${filename}" loading="lazy" alt="Party Photo">
|
||||
</a>
|
||||
<div class="meta">${filename
|
||||
.replace("capture_", "")
|
||||
.replace(".png", "")
|
||||
.replace("_", " ")}</div>
|
||||
`;
|
||||
// Prepend to show new ones at top
|
||||
gallery.prepend(div);
|
||||
const url = `/gallery/img/${encodeURIComponent(filename)}`;
|
||||
const label = filename
|
||||
.replace("capture_", "")
|
||||
.replace(".png", "")
|
||||
.replace(/_/g, " ");
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.target = "_blank";
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = url;
|
||||
img.loading = "lazy";
|
||||
img.alt = "Party Photo";
|
||||
a.appendChild(img);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "meta";
|
||||
meta.textContent = label;
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "card";
|
||||
card.appendChild(a);
|
||||
card.appendChild(meta);
|
||||
|
||||
gallery.prepend(card);
|
||||
}
|
||||
|
||||
// Initial load and poll every 5 seconds
|
||||
|
||||
@@ -34,6 +34,7 @@ async fn main() -> anyhow::Result<()> {
|
||||
PathBuf::from(env::var("UPLOAD_DIR").unwrap_or_else(|_| "./uploads".to_string()));
|
||||
|
||||
tokio::fs::create_dir_all(&upload_dir).await?;
|
||||
let upload_dir = upload_dir.canonicalize()?;
|
||||
let state = Arc::new(AppState {
|
||||
upload_dir,
|
||||
password,
|
||||
@@ -89,7 +90,13 @@ async fn gallery_view() -> Html<&'static str> {
|
||||
}
|
||||
|
||||
async fn gallery_data(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
let mut entries = tokio::fs::read_dir(&state.upload_dir).await.unwrap();
|
||||
let mut entries = match tokio::fs::read_dir(&state.upload_dir).await {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to read upload dir: {}", e);
|
||||
return axum::Json(Vec::<String>::new()).into_response();
|
||||
}
|
||||
};
|
||||
let mut images = Vec::new();
|
||||
|
||||
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||
@@ -104,7 +111,7 @@ async fn gallery_data(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||
}
|
||||
|
||||
images.sort_by(|a, b| b.cmp(a));
|
||||
axum::Json(images)
|
||||
axum::Json(images).into_response()
|
||||
}
|
||||
|
||||
async fn serve_image(
|
||||
@@ -112,7 +119,13 @@ async fn serve_image(
|
||||
Path(filename): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let path = state.upload_dir.join(&filename);
|
||||
match tokio::fs::read(&path).await {
|
||||
let Ok(canonical) = path.canonicalize() else {
|
||||
return StatusCode::NOT_FOUND.into_response();
|
||||
};
|
||||
if !canonical.starts_with(&state.upload_dir) {
|
||||
return StatusCode::BAD_REQUEST.into_response();
|
||||
}
|
||||
match tokio::fs::read(&canonical).await {
|
||||
Ok(bytes) => ([(header::CONTENT_TYPE, "image/png")], bytes).into_response(),
|
||||
Err(_) => StatusCode::NOT_FOUND.into_response(),
|
||||
}
|
||||
@@ -150,10 +163,13 @@ async fn basic_auth(
|
||||
.into_response()
|
||||
}
|
||||
|
||||
// Must match FRAMESIZE_QVGA rotated 90°: camera captures 320×240, firmware rotates to 240×320.
|
||||
const IMAGE_WIDTH: u32 = 240;
|
||||
const IMAGE_HEIGHT: u32 = 320;
|
||||
|
||||
fn convert_and_save(data: &[u8], path: &PathBuf) -> anyhow::Result<()> {
|
||||
// Dimensions from ESP32 code (240x320)
|
||||
let width = 240;
|
||||
let height = 320;
|
||||
let width = IMAGE_WIDTH;
|
||||
let height = IMAGE_HEIGHT;
|
||||
|
||||
let mut img = GrayImage::new(width, height);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user