diff --git a/src/review.rs b/src/review.rs index ec9bce2..097ba11 100644 --- a/src/review.rs +++ b/src/review.rs @@ -33,6 +33,9 @@ pub fn launch_review(groups: &[DuplicateGroup], entries: &[ImageEntry]) { let header = Header::from_bytes("Content-Type", "text/html; charset=utf-8").unwrap(); let _ = req.respond(Response::from_string(&html).with_header(header)); } + (Method::Get, u) if u.starts_with("/image?") => { + handle_serve_image(req, u); + } (Method::Post, "/delete") => { handle_delete(req); } @@ -86,6 +89,71 @@ fn handle_delete(mut req: tiny_http::Request) -> usize { deleted } +fn handle_serve_image(req: tiny_http::Request, url: &str) { + let path = url.strip_prefix("/image?path=").unwrap_or(""); + let path = urldecode(path); + let file_path = std::path::Path::new(&path); + + if !file_path.exists() { + let _ = req.respond(Response::from_string("not found").with_status_code(404)); + return; + } + + let data = match fs::read(file_path) { + Ok(d) => d, + Err(_) => { + let _ = req.respond(Response::from_string("read error").with_status_code(500)); + return; + } + }; + + let content_type = match file_path.extension().and_then(|e| e.to_str()).map(|e| e.to_lowercase()).as_deref() { + Some("png") => "image/png", + Some("gif") => "image/gif", + Some("webp") => "image/webp", + Some("bmp") => "image/bmp", + _ => "image/jpeg", + }; + + let header = Header::from_bytes("Content-Type", content_type).unwrap(); + let _ = req.respond(Response::from_data(data).with_header(header)); +} + +fn urldecode(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + let mut chars = s.bytes(); + while let Some(b) = chars.next() { + if b == b'%' { + let h = chars.next().unwrap_or(b'0'); + let l = chars.next().unwrap_or(b'0'); + let hex = [h, l]; + if let Ok(val) = u8::from_str_radix(&String::from_utf8_lossy(&hex), 16) { + result.push(val as char); + } + } else if b == b'+' { + result.push(' '); + } else { + result.push(b as char); + } + } + result +} + +fn urlencode(s: &str) -> String { + let mut result = String::with_capacity(s.len() * 3); + for b in s.bytes() { + match b { + b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => { + result.push(b as char); + } + _ => { + result.push_str(&format!("%{:02X}", b)); + } + } + } + result +} + fn handle_ignore( mut req: tiny_http::Request, groups: &[DuplicateGroup], @@ -153,26 +221,6 @@ fn make_thumbnail_data_uri(path: &std::path::Path) -> String { format!("data:image/jpeg;base64,{b64}") } -fn make_fullres_data_uri(path: &std::path::Path) -> String { - let data = match fs::read(path) { - Ok(d) => d, - Err(_) => return String::from("data:image/png;base64,"), - }; - - let img = match image::ImageReader::new(std::io::Cursor::new(&data)) - .with_guessed_format() - .ok() - .and_then(|r| r.decode().ok()) - { - Some(i) => i, - None => return String::from("data:image/png;base64,"), - }; - - let mut buf = std::io::Cursor::new(Vec::new()); - img.write_to(&mut buf, image::ImageFormat::Jpeg).unwrap_or(()); - let b64 = base64::engine::general_purpose::STANDARD.encode(buf.into_inner()); - format!("data:image/jpeg;base64,{b64}") -} fn build_review_html(groups: &[DuplicateGroup]) -> String { let mut html = String::from(r#" @@ -254,15 +302,15 @@ h1 { text-align: center; margin-bottom: 20px; color: #e94560; } for (pidx, path) in group.paths.iter().enumerate() { let data_uri = make_thumbnail_data_uri(path); - let full_uri = make_fullres_data_uri(path); let display_path = path.display(); + let encoded_path = urlencode(&display_path.to_string()); let size = fs::metadata(path) .map(|m| format_size(m.len())) .unwrap_or_else(|_| "?".to_string()); html.push_str(&format!( r#"
-{display_path} +{display_path}
{display_path}
{size}