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",
|
"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]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
@ -681,8 +692,10 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"clap",
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
|
"mime_guess",
|
||||||
"notify",
|
"notify",
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"syntect",
|
"syntect",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -703,6 +716,16 @@ version = "0.3.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
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]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@ -870,6 +893,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@ -909,6 +941,36 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"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]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@ -1564,6 +1626,26 @@ dependencies = [
|
|||||||
"linked-hash-map",
|
"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]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
@ -24,3 +24,5 @@ tokio-stream = { version = "0.1", features = ["sync"] }
|
|||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
clap = { version = "4.5", features = ["derive"] }
|
||||||
tower-http = { version = "0.6.8", features = ["trace", "compression-gzip", "cors"] }
|
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,
|
routing::get,
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use mime_guess::from_path;
|
||||||
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html};
|
use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html};
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::net::{SocketAddr, ToSocketAddrs};
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
@ -91,6 +93,7 @@ async fn main() {
|
|||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(root))
|
.route("/", get(root))
|
||||||
|
.route("/random", get(random_file))
|
||||||
.route("/*path", get(serve_file))
|
.route("/*path", get(serve_file))
|
||||||
.route("/events/*path", get(sse_handler))
|
.route("/events/*path", get(sse_handler))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
@ -170,7 +173,7 @@ async fn sse_handler(
|
|||||||
async fn serve_file(
|
async fn serve_file(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(full_path): Path<String>,
|
Path(full_path): Path<String>,
|
||||||
) -> Result<Html<String>, StatusCode> {
|
) -> Result<impl IntoResponse, StatusCode> {
|
||||||
if full_path.is_empty() {
|
if full_path.is_empty() {
|
||||||
return Err(StatusCode::NOT_FOUND);
|
return Err(StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
@ -199,14 +202,44 @@ async fn serve_file(
|
|||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Ошибка получения метаданных: {e}");
|
eprintln!("Ошибка получения метаданных: {e}");
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(StatusCode::NOT_FOUND); // Лучше NOT_FOUND для отсутствующих файлов
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 1. Если это директория - рендерим индекс
|
||||||
if metadata.is_dir() {
|
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 {
|
let content = match fs::read_to_string(&safe_path).await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@ -244,7 +277,7 @@ async fn serve_file(
|
|||||||
.replace("{{SSE_URL}}", &format!("/events/{full_path}"))
|
.replace("{{SSE_URL}}", &format!("/events/{full_path}"))
|
||||||
.replace("{{BACK_BUTTON}}", &back_button_html);
|
.replace("{{BACK_BUTTON}}", &back_button_html);
|
||||||
|
|
||||||
Ok(Html(final_html))
|
Ok(Html(final_html).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_directory_index(
|
async fn render_directory_index(
|
||||||
@ -472,6 +505,57 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &
|
|||||||
body_html
|
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 {
|
fn escape_html(text: &str) -> String {
|
||||||
text.replace('&', "&")
|
text.replace('&', "&")
|
||||||
.replace('<', "<")
|
.replace('<', "<")
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
{{FILE_LIST}}
|
{{FILE_LIST}}
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<a href="/">← На главную</a>
|
<a href="/">← На главную</a>
|
||||||
|
<a href="/random">🎲 Случайный файл</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -33,6 +33,15 @@
|
|||||||
.connected { background-color: #2ecc71; color: #000; }
|
.connected { background-color: #2ecc71; color: #000; }
|
||||||
.disconnected { background-color: #e74c3c; color: #fff; }
|
.disconnected { background-color: #e74c3c; color: #fff; }
|
||||||
.reconnecting { background-color: #f1c40f; color: #000; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user