//! filebrowser-ng server. //! //! Library target so the HTTP app can be exercised from integration tests //! (via `tower::ServiceExt::oneshot`) without binding a real port. The //! `filebrowser-ng` binary is a thin wrapper around [`run`]. use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use anyhow::{bail, Context}; use clap::Parser; use tower_http::trace::TraceLayer; pub mod api; pub mod archive; mod assets; pub mod auth; pub mod cli; pub mod db; pub mod error; pub mod fs; use crate::cli::Cli; use crate::db::Db; use crate::error::AppState; /// Validate the CLI config and build the running state + router without /// binding the port (so tests can exercise everything up to `serve`). pub async fn build_app(cli: &Cli) -> anyhow::Result<(axum::Router, SocketAddr)> { let root = cli .root .canonicalize() .with_context(|| format!("cannot resolve root folder: {}", cli.root.display()))?; if !root.is_dir() { bail!("root folder is not a directory: {}", root.display()); } let db = Db::open(&cli.db).await?; let state = Arc::new(AppState { db, root: root.clone(), https: cli.https, }); let app = api::router(state).layer(TraceLayer::new_for_http()); let ip: IpAddr = cli.bind.parse().context("invalid --bind address")?; let addr = SocketAddr::new(ip, cli.port); Ok((app, addr)) } /// Parse the CLI, initialize logging and serve until the process is killed. pub async fn run() -> anyhow::Result<()> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "info,tower_http=warn".into()), ) .init(); let cli = Cli::parse(); let (app, addr) = build_app(&cli).await?; let listener = tokio::net::TcpListener::bind(addr) .await .with_context(|| format!("cannot bind to {addr}"))?; tracing::info!(root = %cli.root.display(), "filebrowser-ng starting"); tracing::info!(addr = %addr, "listening (pass --bind 0.0.0.0 to expose beyond localhost)"); axum::serve(listener, app).await?; Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::cli::Cli; fn cli(root: &std::path::Path, db: &std::path::Path) -> Cli { Cli { root: root.to_path_buf(), db: db.to_path_buf(), port: 8080, bind: "127.0.0.1".into(), https: false, } } #[tokio::test] async fn build_app_ok() { let tmp = tempfile::tempdir().unwrap(); std::fs::create_dir_all(tmp.path().join("sub")).unwrap(); let c = cli(tmp.path(), &tmp.path().join("db.sqlite")); let (app, addr) = build_app(&c).await.unwrap(); let _ = app; // Router built fine assert_eq!(addr.to_string(), "127.0.0.1:8080"); } #[tokio::test] async fn build_app_missing_root_fails() { let tmp = tempfile::tempdir().unwrap(); let c = cli( &tmp.path().join("no-such-root"), &tmp.path().join("db.sqlite"), ); assert!(build_app(&c).await.is_err()); } #[tokio::test] async fn build_app_root_must_be_directory() { let tmp = tempfile::tempdir().unwrap(); let file = tmp.path().join("a-file"); std::fs::write(&file, "x").unwrap(); let c = cli(&file, &tmp.path().join("db.sqlite")); assert!(build_app(&c).await.is_err()); } #[tokio::test] async fn build_app_rejects_bad_bind() { let tmp = tempfile::tempdir().unwrap(); let mut c = cli(tmp.path(), &tmp.path().join("db.sqlite")); c.bind = "not-an-ip".into(); assert!(build_app(&c).await.is_err()); } #[tokio::test] async fn build_app_custom_port_bind() { let tmp = tempfile::tempdir().unwrap(); let mut c = cli(tmp.path(), &tmp.path().join("db.sqlite")); c.port = 9999; c.bind = "0.0.0.0".into(); let (_app, addr) = build_app(&c).await.unwrap(); assert_eq!(addr.to_string(), "0.0.0.0:9999"); } }