Compare commits
3 Commits
f2dceef1a3
...
b277c95ba8
| Author | SHA1 | Date | |
|---|---|---|---|
| b277c95ba8 | |||
| 91711c9e07 | |||
| f800d63e02 |
46
.pre-commit-config.yaml
Normal file
46
.pre-commit-config.yaml
Normal file
@ -0,0 +1,46 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
# - id: no-commit-to-branch
|
||||
# args: [--branch, master, --branch, main, --pattern, release/.*]
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: mixed-line-ending
|
||||
- id: check-merge-conflict
|
||||
- id: check-shebang-scripts-are-executable
|
||||
- id: check-executables-have-shebangs
|
||||
|
||||
- repo: https://github.com/jorisroovers/gitlint
|
||||
rev: v0.19.1
|
||||
hooks:
|
||||
- id: gitlint
|
||||
args: [
|
||||
"--ignore", "body-is-missing", "--contrib=CT1", "--msg-filename"
|
||||
]
|
||||
stages: [commit-msg]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: clippy
|
||||
name: clippy
|
||||
language: system
|
||||
entry: cargo clippy
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
|
||||
- id: cargo-fmt
|
||||
name: cargo fmt
|
||||
language: system
|
||||
entry: cargo fmt
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
|
||||
- id: check-todos # [NOGREP]
|
||||
name: check-todos # [NOGREP]
|
||||
language: system
|
||||
entry: sh -c "! grep --color=always --binary-files=without-match --dereference-recursive --exclude-dir='notes' --exclude-dir='node_modules' --exclude-dir='.git' --exclude='.pre-commit-config.yaml' --exclude-dir='venv' 'TODO'" # [NOGREP]
|
||||
pass_filenames: false
|
||||
always_run: true
|
||||
50
Cargo.lock
generated
50
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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"] }
|
||||
|
||||
169
src/main.rs
169
src/main.rs
@ -1,27 +1,28 @@
|
||||
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 futures::StreamExt;
|
||||
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
use pulldown_cmark::{CodeBlockKind, Event, Options, Tag, html};
|
||||
use std::convert::Infallible;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use pulldown_cmark::{Options, html, Event, Tag, CodeBlockKind};
|
||||
use pulldown_cmark;
|
||||
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 +31,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 +47,6 @@ struct Args {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
syntax_set: Arc<SyntaxSet>,
|
||||
@ -65,7 +64,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 +92,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,10 +112,8 @@ 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> {
|
||||
render_directory_index(&PathBuf::from(state.root), "").await
|
||||
async fn root(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
||||
render_directory_index(&state.root, "").await
|
||||
}
|
||||
|
||||
async fn sse_handler(
|
||||
@ -118,40 +121,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 +216,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 +260,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 +282,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 +322,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)
|
||||
@ -317,16 +341,17 @@ async fn run_file_watcher(state: AppState) {
|
||||
let tx_fs_clone = tx_fs.clone();
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |res: Result<notify::Event, notify::Error>| {
|
||||
if let Ok(event) = res {
|
||||
if matches!(event.kind, EventKind::Modify(_)) {
|
||||
for path in event.paths {
|
||||
let _ = tx_fs_clone.blocking_send(path);
|
||||
}
|
||||
if let Ok(event) = res
|
||||
&& matches!(event.kind, EventKind::Modify(_))
|
||||
{
|
||||
for path in event.paths {
|
||||
let _ = tx_fs_clone.blocking_send(path);
|
||||
}
|
||||
}
|
||||
},
|
||||
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 +393,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 +423,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(®ions[..], IncludeBackground::No)
|
||||
.unwrap_or_else(|_| escape_html(&line_with_newline));
|
||||
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)),
|
||||
}
|
||||
Err(_) => {
|
||||
result_html.push_str(&escape_html(&line_with_newline))
|
||||
}
|
||||
}
|
||||
}
|
||||
result_html
|
||||
@ -421,15 +451,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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user