From 65a59ce59c1ee630ae76351b47f8dfd1b58a1cd4 Mon Sep 17 00:00:00 2001 From: thek4n Date: Fri, 20 Mar 2026 16:20:54 +0300 Subject: [PATCH] feat: add random file button + image render --- Cargo.lock | 82 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + src/main.rs | 92 +++++++++++++++++++++++++++++++++++++++++++-- templates/dir.html | 1 + templates/file.html | 9 +++++ 5 files changed, 182 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24eb281..0b2eeef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -460,6 +460,17 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -681,8 +692,10 @@ dependencies = [ "axum", "clap", "futures", + "mime_guess", "notify", "pulldown-cmark", + "rand", "serde", "syntect", "tokio", @@ -703,6 +716,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -870,6 +893,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -909,6 +941,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1564,6 +1626,26 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 221e7fc..12d1d93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,3 +24,5 @@ tokio-stream = { version = "0.1", features = ["sync"] } futures = "0.3" clap = { version = "4.5", features = ["derive"] } tower-http = { version = "0.6.8", features = ["trace", "compression-gzip", "cors"] } +mime_guess = "2" +rand = "0.8" diff --git a/src/main.rs b/src/main.rs index 0302c52..ef62ead 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,8 +6,10 @@ use axum::{ routing::get, }; use futures::StreamExt; +use mime_guess::from_path; use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html}; +use rand::seq::SliceRandom; use std::convert::Infallible; use std::fmt::Write; use std::net::{SocketAddr, ToSocketAddrs}; @@ -91,6 +93,7 @@ async fn main() { let app = Router::new() .route("/", get(root)) + .route("/random", get(random_file)) .route("/*path", get(serve_file)) .route("/events/*path", get(sse_handler)) .with_state(state) @@ -170,7 +173,7 @@ async fn sse_handler( async fn serve_file( State(state): State, Path(full_path): Path, -) -> Result, StatusCode> { +) -> Result { if full_path.is_empty() { return Err(StatusCode::NOT_FOUND); } @@ -199,14 +202,44 @@ async fn serve_file( Ok(m) => m, Err(e) => { eprintln!("Ошибка получения метаданных: {e}"); - return Err(StatusCode::INTERNAL_SERVER_ERROR); + return Err(StatusCode::NOT_FOUND); // Лучше NOT_FOUND для отсутствующих файлов } }; + // 1. Если это директория - рендерим индекс if metadata.is_dir() { - return render_directory_index(&safe_path, &full_path).await; + return render_directory_index(&safe_path, &full_path) + .await + .map(|h| h.into_response()); } + // 2. ПРОВЕРКА НА ИЗОБРАЖЕНИЯ (И другую статику) + // Распространенные расширения изображений + let extension = safe_path + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); + let is_image = matches!( + extension.to_lowercase().as_str(), + "png" | "jpg" | "jpeg" | "gif" | "svg" | "webp" | "bmp" | "ico" + ); + + if is_image { + // Читаем файл в байты + let file_content = match fs::read(&safe_path).await { + Ok(content) => content, + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + + // Определяем MIME тип + let mime_type = from_path(&safe_path).first_or_octet_stream(); + + // Возвращаем файл с правильным заголовком Content-Type + return Ok(([(header::CONTENT_TYPE, mime_type.as_ref())], file_content).into_response()); + } + + // 3. Если это не картинка и не папка, считаем что это Markdown (или текст) + // Читаем контент как строку let content = match fs::read_to_string(&safe_path).await { Ok(c) => c, Err(e) => { @@ -244,7 +277,7 @@ async fn serve_file( .replace("{{SSE_URL}}", &format!("/events/{full_path}")) .replace("{{BACK_BUTTON}}", &back_button_html); - Ok(Html(final_html)) + Ok(Html(final_html).into_response()) } async fn render_directory_index( @@ -472,6 +505,57 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: & body_html } +async fn random_file(State(state): State) -> Result { + let mut files: Vec = Vec::new(); + + // Рекурсивно читаем директорию + let mut stack = vec![state.root.clone()]; + + while let Some(current_dir) = stack.pop() { + let mut entries = match fs::read_dir(¤t_dir).await { + Ok(list) => list, + Err(_) => continue, // Пропускаем недоступные папки + }; + + while let Some(entry) = entries + .next_entry() + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + { + let path = entry.path(); + + // Пропускаем скрытые файлы/папки + if entry.file_name().to_string_lossy().starts_with('.') { + continue; + } + + if entry.metadata().await.map(|m| m.is_dir()).unwrap_or(false) { + stack.push(path); + } else { + files.push(path); + } + } + } + + if files.is_empty() { + return Err(StatusCode::NOT_FOUND); + } + + // Выбираем случайный файл + let mut rng = rand::thread_rng(); + let random_path = files.choose(&mut rng).unwrap(); + + // Получаем относительный путь от корня для формирования URL + let relative_path = random_path + .strip_prefix(&state.root) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let url_path = relative_path.to_string_lossy().replace('\\', "/"); // Нормализация для Windows + + // Перенаправление (302 Found) + Ok(axum::response::Redirect::temporary(&format!("/{url_path}"))) +} + fn escape_html(text: &str) -> String { text.replace('&', "&") .replace('<', "<") diff --git a/templates/dir.html b/templates/dir.html index 5baae97..c3492d4 100644 --- a/templates/dir.html +++ b/templates/dir.html @@ -26,6 +26,7 @@ {{FILE_LIST}} diff --git a/templates/file.html b/templates/file.html index ab2815f..950e6d1 100644 --- a/templates/file.html +++ b/templates/file.html @@ -33,6 +33,15 @@ .connected { background-color: #2ecc71; color: #000; } .disconnected { background-color: #e74c3c; color: #fff; } .reconnecting { background-color: #f1c40f; color: #000; } + + img { + max-width: 100%; + height: auto; + display: block; + margin: 1.5em auto; + border-radius: 6px; + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + }