Files
downloadwf/backend/src/main.rs
T
2026-08-25 12:20:31 +02:00

151 lines
3.9 KiB
Rust

mod config;
mod http;
mod ratelimit;
mod routes;
mod security;
mod storage;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::routing::{delete, get, post};
use axum::Router;
use config::Config;
use ratelimit::RateLimiter;
use storage::Storage;
#[derive(Clone)]
pub struct AppState {
pub config: Config,
pub storage: Arc<Storage>,
pub secret: String,
pub limiter: Arc<RateLimiter>,
pub started_at: Instant,
}
fn build_router(state: AppState) -> Router {
Router::new()
.route("/api/health", get(routes::health))
.route("/api/upload", post(routes::upload::upload))
.route("/api/auth/{key}", post(routes::auth::auth))
.route("/api/files/{id}", delete(routes::delete::delete))
.fallback(routes::download::fallback)
.with_state(state)
}
#[tokio::main]
async fn main() {
dotenvy::dotenv().ok();
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.init();
let config = match config::load() {
Ok(c) => c,
Err(e) => {
tracing::error!("configuration error: {e}");
std::process::exit(1);
}
};
security::set_key_length(config.key_length);
let storage = Arc::new(Storage::new(config.storage_dir.clone()));
if let Err(e) = storage.init().await {
tracing::error!("failed to initialize storage: {e}");
std::process::exit(1);
}
let secret = match storage.load_secret().await {
Ok(s) => s,
Err(e) => {
tracing::error!("failed to load server secret: {e}");
std::process::exit(1);
}
};
let sweep = storage.sweep().await;
if sweep.0 > 0 || sweep.1 > 0 {
tracing::info!(
"startup sweep removed {} orphaned file(s), {} orphaned metadata record(s)",
sweep.0,
sweep.1
);
}
let state = AppState {
limiter: Arc::new(RateLimiter::new(config.rate_limit_max, config.rate_limit_window_ms)),
started_at: Instant::now(),
config: config.clone(),
storage,
secret,
};
// Hourly expiration sweep.
let expiry_state = state.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(3600));
loop {
interval.tick().await;
let removed = expiry_state
.storage
.expire_expired(chrono::Utc::now().timestamp_millis())
.await;
if removed > 0 {
tracing::info!("expiry sweep removed {removed} file(s)");
}
}
});
let app = build_router(state);
let addr = format!("{}:{}", config.host, config.port);
let listener = match tokio::net::TcpListener::bind(&addr).await {
Ok(l) => l,
Err(e) => {
tracing::error!("failed to bind {addr}: {e}");
std::process::exit(1);
}
};
tracing::info!(
"listening on http://{} storage={:?} maxUpload={}",
addr,
config.storage_dir,
config.max_file_size
);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(shutdown_signal())
.await
.expect("server error");
}
async fn shutdown_signal() {
let ctrl_c = async {
let _ = tokio::signal::ctrl_c().await;
};
#[cfg(unix)]
let terminate = async {
let _ = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
tracing::info!("shutdown signal received");
}