This commit is contained in:
thek4n 2026-02-21 10:40:46 +03:00
parent f2dceef1a3
commit f800d63e02
3 changed files with 145 additions and 64 deletions

50
Cargo.lock generated
View File

@ -67,6 +67,18 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "async-compression"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2"
dependencies = [
"compression-codecs",
"compression-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "async-trait"
version = "0.1.89"
@ -234,6 +246,23 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "compression-codecs"
version = "0.4.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7"
dependencies = [
"compression-core",
"flate2",
"memchr",
]
[[package]]
name = "compression-core"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
[[package]]
name = "crc32fast"
version = "1.5.0"
@ -658,6 +687,7 @@ dependencies = [
"syntect",
"tokio",
"tokio-stream",
"tower-http",
"tracing-subscriber",
]
@ -1233,6 +1263,26 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-http"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"async-compression",
"bitflags 2.11.0",
"bytes",
"futures-core",
"http",
"http-body",
"pin-project-lite",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"

View File

@ -23,3 +23,4 @@ notify = "6.1"
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"] }

View File

@ -1,27 +1,29 @@
use axum::{
extract::{Path, State},
routing::get,
Router,
http::{StatusCode, HeaderMap, header},
response::{Html, Sse, IntoResponse},
extract::{Path, State},
http::{HeaderMap, StatusCode, header},
response::{Html, IntoResponse, Sse},
routing::get,
};
use std::net::{SocketAddr, ToSocketAddrs};
use pulldown_cmark::{Options, html, Event, Tag, CodeBlockKind};
use futures::StreamExt;
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use pulldown_cmark;
use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html};
use std::convert::Infallible;
use std::net::{SocketAddr, ToSocketAddrs};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::fs;
use tokio::sync::broadcast;
use tokio_stream::wrappers::BroadcastStream;
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher, EventKind};
use futures::StreamExt;
use std::convert::Infallible;
use std::time::Duration;
use syntect::easy::HighlightLines;
use syntect::highlighting::ThemeSet;
use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
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};
use clap::Parser;
use std::io;
@ -30,7 +32,6 @@ use std::io;
const TEMPLATE_FILE: &str = include_str!("../templates/file.html");
const TEMPLATE_DIR: &str = include_str!("../templates/dir.html");
#[derive(clap::Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
@ -47,7 +48,6 @@ struct Args {
root: PathBuf,
}
#[derive(Clone)]
struct AppState {
syntax_set: Arc<SyntaxSet>,
@ -65,7 +65,12 @@ async fn main() {
std::process::exit(1);
}
tracing_subscriber::fmt::init();
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info,tower_http=info".into()),
))
.with(tracing_subscriber::fmt::layer())
.init();
let ss = SyntaxSet::load_defaults_newlines();
let ts = ThemeSet::load_defaults();
@ -88,7 +93,8 @@ async fn main() {
.route("/", get(root))
.route("/*path", get(serve_file))
.route("/events/*path", get(sse_handler))
.with_state(state);
.with_state(state)
.layer(TraceLayer::new_for_http());
let addr = resolve_addr(&args.host, args.port).unwrap();
@ -107,9 +113,7 @@ fn resolve_addr(host: &str, port: u16) -> io::Result<SocketAddr> {
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Не удалось разрешить адрес"))
}
async fn root(
State(state): State<AppState>,
) ->Result<Html<String>, StatusCode> {
async fn root(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
render_directory_index(&PathBuf::from(state.root), "").await
}
@ -118,40 +122,47 @@ async fn sse_handler(
Path(full_path): Path<String>,
) -> impl IntoResponse {
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("text/event-stream"));
headers.insert(header::CACHE_CONTROL, header::HeaderValue::from_static("no-cache"));
headers.insert(header::CONNECTION, header::HeaderValue::from_static("keep-alive"));
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/event-stream"),
);
headers.insert(
header::CACHE_CONTROL,
header::HeaderValue::from_static("no-cache"),
);
headers.insert(
header::CONNECTION,
header::HeaderValue::from_static("keep-alive"),
);
let rx = state.tx.subscribe();
let requested_path = full_path.clone();
let stream = BroadcastStream::new(rx)
.filter_map(move |res| {
let req_path = requested_path.clone();
async move {
match res {
Ok(changed_path) => {
if changed_path.contains(&req_path) {
Some(Ok::<axum::response::sse::Event, Infallible>(
axum::response::sse::Event::default()
.event("reload")
.data("")
))
} else {
None
}
let stream = BroadcastStream::new(rx).filter_map(move |res| {
let req_path = requested_path.clone();
async move {
match res {
Ok(changed_path) => {
if changed_path.contains(&req_path) {
Some(Ok::<axum::response::sse::Event, Infallible>(
axum::response::sse::Event::default()
.event("reload")
.data(""),
))
} else {
None
}
Err(_) => None,
}
Err(_) => None,
}
});
}
});
let sse = Sse::new(stream)
.keep_alive(
axum::response::sse::KeepAlive::new()
.interval(Duration::from_secs(15))
.text("ping")
);
let sse = Sse::new(stream).keep_alive(
axum::response::sse::KeepAlive::new()
.interval(Duration::from_secs(15))
.text("ping"),
);
(headers, sse)
}
@ -206,7 +217,11 @@ async fn serve_file(
// Логика кнопки "Назад"
let back_link = if let Some(pos) = full_path.rfind('/') {
let parent = &full_path[..pos];
if parent.is_empty() { "/".to_string() } else { format!("/{}", parent) }
if parent.is_empty() {
"/".to_string()
} else {
format!("/{}", parent)
}
} else {
"/".to_string()
};
@ -246,7 +261,11 @@ async fn render_directory_index(
let mut files: Vec<(String, String, bool)> = Vec::new();
while let Some(entry) = entries.next_entry().await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
while let Some(entry) = entries
.next_entry()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
{
let file_name = entry.file_name().to_string_lossy().to_string();
if file_name.starts_with('.') {
@ -264,19 +283,21 @@ async fn render_directory_index(
files.push((file_name, link_path, is_dir));
}
files.sort_by(|a, b| {
match (a.2, b.2) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.0.cmp(&b.0),
}
files.sort_by(|a, b| match (a.2, b.2) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.0.cmp(&b.0),
});
let mut list_html = String::from("<ul>");
if !request_path.is_empty() && request_path != "files" {
let parent_path = request_path.rsplit_once('/').map(|(p, _)| p).unwrap_or("");
let parent_link = if parent_path.is_empty() { "/".to_string() } else { format!("/{}", parent_path) };
let parent_link = if parent_path.is_empty() {
"/".to_string()
} else {
format!("/{}", parent_path)
};
list_html.push_str(&format!(
r#"<li>
@ -302,7 +323,11 @@ async fn render_directory_index(
}
list_html.push_str("</ul>");
let title_path = if request_path.is_empty() { "" } else { request_path.trim_start_matches('/') };
let title_path = if request_path.is_empty() {
""
} else {
request_path.trim_start_matches('/')
};
let final_html = TEMPLATE_DIR
.replace("{{TITLE_PATH}}", title_path)
@ -326,7 +351,8 @@ async fn run_file_watcher(state: AppState) {
}
},
Config::default(),
).expect("Failed to create watcher");
)
.expect("Failed to create watcher");
let watch_path = PathBuf::from("./notes");
if let Err(e) = watcher.watch(&watch_path, RecursiveMode::Recursive) {
@ -368,7 +394,7 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &
} else {
None
};
},
}
Event::End(Tag::CodeBlock(_)) => {
in_code_block = false;
let is_mermaid = current_lang.as_deref() == Some("mermaid");
@ -398,11 +424,16 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &
let line_with_newline = format!("{}\n", line);
match h.highlight_line(&line_with_newline, ss) {
Ok(regions) => {
let html_line = styled_line_to_highlighted_html(&regions[..], IncludeBackground::No)
.unwrap_or_else(|_| escape_html(&line_with_newline));
let html_line = styled_line_to_highlighted_html(
&regions[..],
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)),
}
Err(_) => {
result_html.push_str(&escape_html(&line_with_newline))
}
}
}
result_html
@ -421,15 +452,14 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &
</div>
<pre style="margin: 0; border-radius: 0 0 6px 6px;"><code>{}</code></pre>
</div>"#,
lang_escaped,
highlighted_html
lang_escaped, highlighted_html
);
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);