Files
IronPad-Docker/backend/src/main.rs
2026-02-06 00:22:14 +01:00

135 lines
4.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use axum::{routing::get, Router};
use tokio::net::TcpListener;
use tower_http::cors::CorsLayer;
use tower_http::services::ServeDir;
use tracing::{info, warn};
pub mod config;
mod models;
mod routes;
mod services;
mod watcher;
mod websocket;
/// Find an available port and return the bound listener.
/// Avoids TOCTOU race by keeping the listener alive.
async fn find_available_port() -> (TcpListener, u16) {
for port in 3000..=3010 {
let addr = SocketAddr::from(([127, 0, 0, 1], port));
if let Ok(listener) = TcpListener::bind(addr).await {
return (listener, port);
}
}
panic!("No available ports in range 30003010");
}
#[tokio::main]
async fn main() {
// Logging
tracing_subscriber::fmt().init();
// Resolve data directory (production vs development mode)
config::init_data_dir();
// Find port and bind (listener kept alive to avoid race condition)
let (listener, port) = find_available_port().await;
// WebSocket state (shared across handlers)
let ws_state = Arc::new(websocket::WsState::new());
// Start file watcher
let ws_state_clone = ws_state.clone();
if let Err(e) = watcher::start_watcher(ws_state_clone).await {
warn!("File watcher failed to start: {}", e);
}
// Initialize git repo if needed
if let Err(e) = services::git::init_repo() {
warn!("Git init skipped: {}", e);
}
// Start auto-commit background task (tries to commit every 60s)
services::git::start_auto_commit();
// CORS layer (permissive for local-only app)
let cors = CorsLayer::permissive();
// API router
let api_router = Router::new()
// Notes CRUD
.route(
"/notes",
get(routes::notes::list_notes).post(routes::notes::create_note),
)
.nest("/notes", routes::notes::router())
// Tasks
.nest("/tasks", routes::tasks::router())
// Search
.nest("/search", routes::search::router())
// Git
.nest("/git", routes::git::router())
// Projects
.nest("/projects", routes::projects::router())
// Daily notes
.nest("/daily", routes::daily::router())
// Assets
.nest("/assets", routes::assets::router());
// App router with WebSocket state
let mut app = Router::new()
.route("/health", get(|| async { "ok" }))
.route(
"/ws",
get({
let ws = ws_state.clone();
move |upgrade: axum::extract::WebSocketUpgrade| {
websocket::ws_handler(upgrade, axum::extract::State(ws))
}
}),
)
.nest("/api", api_router)
.layer(cors);
// Check for embedded frontend (production mode)
let static_dir = Path::new("static");
let has_frontend = static_dir.join("index.html").exists();
if has_frontend {
// Production mode: serve frontend from static/ and use SPA fallback
info!("Production mode: serving frontend from static/");
let serve_dir = ServeDir::new("static")
.fallback(tower_http::services::ServeFile::new("static/index.html"));
app = app.fallback_service(serve_dir);
} else {
// Development mode: API-only
app = app.fallback(|| async {
"Ironpad API server running. Use 'npm run dev' in frontend/ for the GUI."
});
}
// Start server
info!("🚀 Ironpad running on http://localhost:{port}");
// Auto-open browser in production mode
if has_frontend {
let url = format!("http://localhost:{}", port);
tokio::spawn(async move {
// Small delay to ensure server is ready
tokio::time::sleep(std::time::Duration::from_millis(300)).await;
if let Err(e) = webbrowser::open(&url) {
tracing::warn!(
"Failed to open browser: {}. Open http://localhost:{} manually.",
e,
port
);
}
});
}
axum::serve(listener, app).await.expect("Server failed");
}