lib.rs
⎇
Raw
1//! filebrowser-ng server.
2//!
3//! Library target so the HTTP app can be exercised from integration tests
4//! (via `tower::ServiceExt::oneshot`) without binding a real port. The
5//! `filebrowser-ng` binary is a thin wrapper around [`run`].
6
7use std::net::{IpAddr, SocketAddr};
8use std::sync::Arc;
9
10use anyhow::{bail, Context};
11use clap::Parser;
12use tower_http::trace::TraceLayer;
13
14pub mod api;
15pub mod archive;
16mod assets;
17pub mod auth;
18pub mod cli;
19pub mod db;
20pub mod error;
21pub mod fs;
22
23use crate::cli::Cli;
24use crate::db::Db;
25use crate::error::AppState;
26
27/// Validate the CLI config and build the running state + router without
28/// binding the port (so tests can exercise everything up to `serve`).
29pub async fn build_app(cli: &Cli) -> anyhow::Result<(axum::Router, SocketAddr)> {
30 let root = cli
31 .root
32 .canonicalize()
33 .with_context(|| format!("cannot resolve root folder: {}", cli.root.display()))?;
34 if !root.is_dir() {
35 bail!("root folder is not a directory: {}", root.display());
36 }
37
38 let db = Db::open(&cli.db).await?;
39
40 let state = Arc::new(AppState {
41 db,
42 root: root.clone(),
43 https: cli.https,
44 });
45
46 let app = api::router(state).layer(TraceLayer::new_for_http());
47
48 let ip: IpAddr = cli
49 .bind
50 .parse()
51 .context("invalid --bind address")?;
52 let addr = SocketAddr::new(ip, cli.port);
53 Ok((app, addr))
54}
55
56/// Parse the CLI, initialize logging and serve until the process is killed.
57pub async fn run() -> anyhow::Result<()> {
58 tracing_subscriber::fmt()
59 .with_env_filter(
60 tracing_subscriber::EnvFilter::try_from_default_env()
61 .unwrap_or_else(|_| "info,tower_http=warn".into()),
62 )
63 .init();
64
65 let cli = Cli::parse();
66 let (app, addr) = build_app(&cli).await?;
67 let listener = tokio::net::TcpListener::bind(addr)
68 .await
69 .with_context(|| format!("cannot bind to {addr}"))?;
70
71 tracing::info!(root = %cli.root.display(), "filebrowser-ng starting");
72 tracing::info!(addr = %addr, "listening (pass --bind 0.0.0.0 to expose beyond localhost)");
73 axum::serve(listener, app).await?;
74 Ok(())
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use crate::cli::Cli;
81
82 fn cli(root: &std::path::Path, db: &std::path::Path) -> Cli {
83 Cli {
84 root: root.to_path_buf(),
85 db: db.to_path_buf(),
86 port: 8080,
87 bind: "127.0.0.1".into(),
88 https: false,
89 }
90 }
91
92 #[tokio::test]
93 async fn build_app_ok() {
94 let tmp = tempfile::tempdir().unwrap();
95 std::fs::create_dir_all(tmp.path().join("sub")).unwrap();
96 let c = cli(tmp.path(), &tmp.path().join("db.sqlite"));
97 let (app, addr) = build_app(&c).await.unwrap();
98 let _ = app; // Router built fine
99 assert_eq!(addr.to_string(), "127.0.0.1:8080");
100 }
101
102 #[tokio::test]
103 async fn build_app_missing_root_fails() {
104 let tmp = tempfile::tempdir().unwrap();
105 let c = cli(
106 &tmp.path().join("no-such-root"),
107 &tmp.path().join("db.sqlite"),
108 );
109 assert!(build_app(&c).await.is_err());
110 }
111
112 #[tokio::test]
113 async fn build_app_root_must_be_directory() {
114 let tmp = tempfile::tempdir().unwrap();
115 let file = tmp.path().join("a-file");
116 std::fs::write(&file, "x").unwrap();
117 let c = cli(&file, &tmp.path().join("db.sqlite"));
118 assert!(build_app(&c).await.is_err());
119 }
120
121 #[tokio::test]
122 async fn build_app_rejects_bad_bind() {
123 let tmp = tempfile::tempdir().unwrap();
124 let mut c = cli(tmp.path(), &tmp.path().join("db.sqlite"));
125 c.bind = "not-an-ip".into();
126 assert!(build_app(&c).await.is_err());
127 }
128
129 #[tokio::test]
130 async fn build_app_custom_port_bind() {
131 let tmp = tempfile::tempdir().unwrap();
132 let mut c = cli(tmp.path(), &tmp.path().join("db.sqlite"));
133 c.port = 9999;
134 c.bind = "0.0.0.0".into();
135 let (_app, addr) = build_app(&c).await.unwrap();
136 assert_eq!(addr.to_string(), "0.0.0.0:9999");
137 }
138}
139