feat: add random file button + image render
This commit is contained in:
parent
de74585eee
commit
65a59ce59c
82
Cargo.lock
generated
82
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"
|
||||
|
||||
92
src/main.rs
92
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<AppState>,
|
||||
Path(full_path): Path<String>,
|
||||
) -> Result<Html<String>, StatusCode> {
|
||||
) -> Result<impl IntoResponse, StatusCode> {
|
||||
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<AppState>) -> Result<impl IntoResponse, StatusCode> {
|
||||
let mut files: Vec<PathBuf> = 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('<', "<")
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
{{FILE_LIST}}
|
||||
<div class="footer">
|
||||
<a href="/">← На главную</a>
|
||||
<a href="/random">🎲 Случайный файл</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user