Initial blog setup

This commit is contained in:
2025-10-29 20:29:52 +08:00
commit 9229313f9b
17 changed files with 2502 additions and 0 deletions

180
src/main.rs Normal file
View File

@@ -0,0 +1,180 @@
mod git_tracker;
mod post_manager;
mod template_engine;
use axum::{
extract::{Path, State},
http::StatusCode,
response::{Html, IntoResponse, Response},
routing::get,
Router,
};
use clap::Parser;
use std::sync::Arc;
use tokio::process::Command;
use tokio::sync::{Mutex, RwLock};
use tower_http::services::ServeDir;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Disable caching of markdown rendering
#[arg(long)]
no_cache: bool,
}
#[derive(Clone)]
struct AppState {
post_manager: Arc<RwLock<post_manager::PostManager>>,
update_lock: Arc<Mutex<()>>,
}
#[tokio::main]
async fn main() {
let args = Args::parse();
let post_manager = Arc::new(RwLock::new(
post_manager::PostManager::new(".", args.no_cache).expect("Failed to initialize post manager"),
));
let app_state = AppState {
post_manager: post_manager.clone(),
update_lock: Arc::new(Mutex::new(())),
};
if args.no_cache {
println!("Running with caching disabled");
}
let p_router = Router::new()
.route("/:post_name", get(post_handler))
.fallback_service(ServeDir::new("posts"));
let app = Router::new()
.route("/", get(index_handler))
.route("/update", get(update_handler))
.route("/:page", get(all_handler))
.nest("/p", p_router)
.with_state(app_state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.expect("Failed to bind to address");
println!("Server running on http://127.0.0.1:3000");
axum::serve(listener, app)
.await
.expect("Failed to start server");
}
async fn index_handler(State(state): State<AppState>) -> Response {
match render_template("index.html", &state).await {
Ok(html) => Html(html).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
async fn all_handler(State(state): State<AppState>, Path(page): Path<String>) -> Response {
if page.contains("..") {
return (StatusCode::NOT_FOUND, "Invalid path").into_response();
}
match render_template(&format!("page_{}.html", page), &state).await {
Ok(html) => Html(html).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
async fn post_handler(
State(state): State<AppState>,
Path(post_name): Path<String>,
) -> Response {
if post_name.contains("..") {
return (StatusCode::NOT_FOUND, "Invalid path").into_response();
}
let manager = state.post_manager.read().await;
match manager.get_post(&post_name).await {
Some(_post) => {
drop(manager);
match render_post_template(&state, post_name).await {
Ok(html) => Html(html).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e).into_response(),
}
}
None => (StatusCode::NOT_FOUND, "Post not found").into_response(),
}
}
async fn update_handler(State(state): State<AppState>) -> impl IntoResponse {
// Acquire lock to prevent concurrent updates
let _lock = state.update_lock.lock().await;
// Run git pull --autostash
let output = Command::new("git")
.arg("pull")
.arg("--autostash")
.output()
.await;
match output {
Ok(output) => {
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("Git pull failed: {}", stderr);
return (
StatusCode::INTERNAL_SERVER_ERROR,
format!("Git pull failed: {}", stderr),
)
.into_response();
}
let stdout = String::from_utf8_lossy(&output.stdout);
println!("Git pull output: {}", stdout);
// Refresh posts
let mut manager = state.post_manager.write().await;
match manager.refresh_posts() {
Ok(_) => {
println!("Successfully refreshed log pages");
(StatusCode::OK, "Update successful: pulled changes and refreshed log pages")
.into_response()
}
Err(e) => {
eprintln!("Failed to refresh posts: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to refresh posts: {}", e),
)
.into_response()
}
}
}
Err(e) => {
eprintln!("Failed to execute git pull: {}", e);
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Failed to execute git pull: {}", e),
)
.into_response()
}
}
}
async fn render_template(template_name: &str, state: &AppState) -> Result<String, String> {
let template_path = format!("templates/{}", template_name);
let template_content = std::fs::read_to_string(&template_path)
.map_err(|e| format!("Failed to read template: {}", e))?;
let manager = state.post_manager.read().await;
template_engine::render_template(&template_content, &*manager, None).await
}
async fn render_post_template(state: &AppState, post_name: String) -> Result<String, String> {
let template_content = std::fs::read_to_string("templates/post.html")
.map_err(|e| format!("Failed to read post template: {}", e))?;
let manager = state.post_manager.read().await;
template_engine::render_template(&template_content, &*manager, Some(&post_name)).await
}