diff --git a/src/main.rs b/src/main.rs
index ef62ead..34fc9ef 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -8,7 +8,6 @@ use axum::{
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;
@@ -20,9 +19,7 @@ use tokio::fs;
use tokio::sync::broadcast;
use tokio_stream::wrappers::BroadcastStream;
-use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
-use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
use syntect::parsing::SyntaxSet;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
@@ -30,7 +27,9 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use clap::Parser;
use std::io;
-// Загрузка шаблонов при компиляции
+mod markdown;
+use markdown::markdown_to_html;
+
const TEMPLATE_FILE: &str = include_str!("../templates/file.html");
const TEMPLATE_DIR: &str = include_str!("../templates/dir.html");
@@ -101,7 +100,7 @@ async fn main() {
let addr = resolve_addr(&args.host, args.port).unwrap();
- println!("Сервер запущен на http://{addr}");
+ println!("Server started on {addr}");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
@@ -181,7 +180,6 @@ async fn serve_file(
let mut requested_path = PathBuf::from(&state.root);
requested_path.push(&full_path);
- // Безопасность путей
let Ok(safe_path) = fs::canonicalize(&requested_path).await else {
return Err(StatusCode::NOT_FOUND);
};
@@ -191,30 +189,24 @@ async fn serve_file(
};
if !safe_path.starts_with(&base_dir) {
- eprintln!(
- "Попытка выхода за пределы директории: {}",
- safe_path.display()
- );
+ eprintln!("Path traversal: {}", safe_path.display());
return Err(StatusCode::FORBIDDEN);
}
let metadata = match fs::metadata(&safe_path).await {
Ok(m) => m,
Err(e) => {
- eprintln!("Ошибка получения метаданных: {e}");
- return Err(StatusCode::NOT_FOUND); // Лучше NOT_FOUND для отсутствующих файлов
+ eprintln!("Error getting metadata: {e}");
+ return Err(StatusCode::NOT_FOUND);
}
};
- // 1. Если это директория - рендерим индекс
if metadata.is_dir() {
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())
@@ -225,30 +217,24 @@ async fn serve_file(
);
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) => {
- eprintln!("Ошибка чтения: {e}");
+ eprintln!("Error reading: {e}");
return Err(StatusCode::INTERNAL_SERVER_ERROR);
}
};
- // Логика кнопки "Назад"
let back_link = if let Some(pos) = full_path.rfind('/') {
let parent = &full_path[..pos];
if parent.is_empty() {
@@ -260,22 +246,13 @@ async fn serve_file(
"/".to_string()
};
- let back_button_html = format!(
- r#"
"#
- );
-
let html_content = markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path);
// Заполнение шаблона
let final_html = TEMPLATE_FILE
.replace("{{CONTENT}}", &html_content)
.replace("{{SSE_URL}}", &format!("/events/{full_path}"))
- .replace("{{BACK_BUTTON}}", &back_button_html);
+ .replace("{{BACK_LINK}}", &back_link);
Ok(Html(final_html).into_response())
}
@@ -287,7 +264,7 @@ async fn render_directory_index(
let mut entries = match fs::read_dir(dir_path).await {
Ok(list) => list,
Err(e) => {
- eprintln!("Ошибка чтения директории: {e}");
+ eprintln!("Error directory reading: {e}");
return Err(StatusCode::FORBIDDEN);
}
};
@@ -388,7 +365,7 @@ async fn run_file_watcher(state: AppState) {
)
.expect("Failed to create watcher");
- let watch_path = PathBuf::from(state.root);
+ let watch_path = state.root;
if let Err(e) = watcher.watch(&watch_path, RecursiveMode::Recursive) {
eprintln!("Ошибка настройки watcher: {e}");
return;
@@ -401,120 +378,15 @@ async fn run_file_watcher(state: AppState) {
}
}
-fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &str) -> String {
- let theme = &ts.themes["base16-ocean.dark"];
-
- let mut options = Options::empty();
- options.insert(Options::ENABLE_TABLES);
- options.insert(Options::ENABLE_FOOTNOTES);
- options.insert(Options::ENABLE_STRIKETHROUGH);
- options.insert(Options::ENABLE_TASKLISTS);
- options.insert(Options::ENABLE_SMART_PUNCTUATION);
-
- let parser = pulldown_cmark::Parser::new_ext(markdown, options);
-
- let mut processed_events: Vec = Vec::new();
- let mut in_code_block = false;
- let mut current_lang: Option = None;
- let mut current_code = String::new();
-
- for event in parser {
- match event {
- Event::Start(Tag::CodeBlock(kind)) => {
- in_code_block = true;
- current_code.clear();
- current_lang = if let CodeBlockKind::Fenced(l) = kind {
- Some(l.to_string())
- } else {
- None
- };
- }
- Event::End(Tag::CodeBlock(_)) => {
- in_code_block = false;
- let is_mermaid = current_lang.as_deref() == Some("mermaid");
-
- if is_mermaid {
- let escaped_code = escape_html(¤t_code);
- let mermaid_html = format!(
- r#""#
- );
- processed_events.push(Event::Html(mermaid_html.into()));
- } else {
- let lang_display = current_lang.as_deref().unwrap_or("text");
- let lang_escaped = escape_html(lang_display);
-
- let highlighted_html = if let Some(lang) = ¤t_lang {
- if let Some(syntax) = ss.find_syntax_by_token(lang) {
- let mut h = HighlightLines::new(syntax, theme);
- let mut result_html = String::new();
- for line in current_code.lines() {
- let line_with_newline = format!("{line}\n");
- match h.highlight_line(&line_with_newline, ss) {
- Ok(regions) => {
- let html_line = styled_line_to_highlighted_html(
- ®ions[..],
- IncludeBackground::No,
- )
- .unwrap_or_else(|_| escape_html(&line_with_newline));
- result_html.push_str(&html_line);
- }
- Err(_) => {
- result_html.push_str(&escape_html(&line_with_newline));
- }
- }
- }
- result_html
- } else {
- escape_html(¤t_code)
- }
- } else {
- escape_html(¤t_code)
- };
-
- let code_container = format!(
- r#""#
- );
- processed_events.push(Event::Html(code_container.into()));
- }
- }
- Event::Text(text) if in_code_block => {
- current_code.push_str(&text);
- }
- _ => {
- if !in_code_block {
- processed_events.push(event);
- }
- }
- }
- }
-
- let mut body_html = String::new();
- html::push_html(&mut body_html, processed_events.into_iter());
- 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, // Пропускаем недоступные папки
+ Err(_) => continue,
};
while let Some(entry) = entries
@@ -524,7 +396,6 @@ async fn random_file(State(state): State) -> Result) -> Result String {
- text.replace('&', "&")
- .replace('<', "<")
- .replace('>', ">")
- .replace('"', """)
- .replace('\'', "'")
-}
diff --git a/src/markdown.rs b/src/markdown.rs
new file mode 100644
index 0000000..2a5abf6
--- /dev/null
+++ b/src/markdown.rs
@@ -0,0 +1,117 @@
+use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html};
+use syntect::easy::HighlightLines;
+use syntect::highlighting::ThemeSet;
+use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
+use syntect::parsing::SyntaxSet;
+
+pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &str) -> String {
+ let theme = &ts.themes["base16-ocean.dark"];
+
+ let mut options = Options::empty();
+ options.insert(Options::ENABLE_TABLES);
+ options.insert(Options::ENABLE_FOOTNOTES);
+ options.insert(Options::ENABLE_STRIKETHROUGH);
+ options.insert(Options::ENABLE_TASKLISTS);
+ options.insert(Options::ENABLE_SMART_PUNCTUATION);
+
+ let parser = pulldown_cmark::Parser::new_ext(markdown, options);
+
+ let mut processed_events: Vec = Vec::new();
+ let mut in_code_block = false;
+ let mut current_lang: Option = None;
+ let mut current_code = String::new();
+
+ for event in parser {
+ match event {
+ Event::Start(Tag::CodeBlock(kind)) => {
+ in_code_block = true;
+ current_code.clear();
+ current_lang = if let CodeBlockKind::Fenced(l) = kind {
+ Some(l.to_string())
+ } else {
+ None
+ };
+ }
+ Event::End(Tag::CodeBlock(_)) => {
+ in_code_block = false;
+ let is_mermaid = current_lang.as_deref() == Some("mermaid");
+
+ if is_mermaid {
+ let escaped_code = escape_html(¤t_code);
+ let mermaid_html = format!(
+ r#""#
+ );
+ processed_events.push(Event::Html(mermaid_html.into()));
+ } else {
+ let lang_display = current_lang.as_deref().unwrap_or("text");
+ let lang_escaped = escape_html(lang_display);
+
+ let highlighted_html = if let Some(lang) = ¤t_lang {
+ if let Some(syntax) = ss.find_syntax_by_token(lang) {
+ let mut h = HighlightLines::new(syntax, theme);
+ let mut result_html = String::new();
+ for line in current_code.lines() {
+ let line_with_newline = format!("{line}\n");
+ match h.highlight_line(&line_with_newline, ss) {
+ Ok(regions) => {
+ let html_line = styled_line_to_highlighted_html(
+ ®ions[..],
+ IncludeBackground::No,
+ )
+ .unwrap_or_else(|_| escape_html(&line_with_newline));
+ result_html.push_str(&html_line);
+ }
+ Err(_) => {
+ result_html.push_str(&escape_html(&line_with_newline));
+ }
+ }
+ }
+ result_html
+ } else {
+ escape_html(¤t_code)
+ }
+ } else {
+ escape_html(¤t_code)
+ };
+
+ let code_container = format!(
+ r#""#
+ );
+ processed_events.push(Event::Html(code_container.into()));
+ }
+ }
+ Event::Text(text) if in_code_block => {
+ current_code.push_str(&text);
+ }
+ _ => {
+ if !in_code_block {
+ processed_events.push(event);
+ }
+ }
+ }
+ }
+
+ let mut body_html = String::new();
+ html::push_html(&mut body_html, processed_events.into_iter());
+ body_html
+}
+
+fn escape_html(text: &str) -> String {
+ text.replace('&', "&")
+ .replace('<', "<")
+ .replace('>', ">")
+ .replace('"', """)
+ .replace('\'', "'")
+}
diff --git a/templates/file.html b/templates/file.html
index 950e6d1..87ec8e5 100644
--- a/templates/file.html
+++ b/templates/file.html
@@ -46,7 +46,12 @@
- {{BACK_BUTTON}}
+
{{CONTENT}}