151 lines
3.9 KiB
Rust
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");
|
|
}
|