live_server/
lib.rs

1//! Launch a local network server with live reload feature for static pages.
2//!
3//! ## Create live server
4//! ```
5//! use live_server::{listen, Options};
6//!
7//! async fn serve() -> Result<(), Box<dyn std::error::Error>> {
8//!     listen("127.0.0.1:8080", "./").await?.start(Options::default()).await
9//! }
10//! ```
11//!
12//! ## Enable logs (Optional)
13//! ```rust
14//! env_logger::init();
15//! ```
16
17mod file_layer;
18mod http_layer;
19mod utils;
20
21pub use http_layer::server::Options;
22
23use file_layer::watcher::{create_poll_watcher, watch};
24use http_layer::{
25    listener::create_listener,
26    server::{AppState, create_server, serve},
27};
28use local_ip_address::local_ip;
29use notify::{PollWatcher, RecommendedWatcher, Watcher};
30use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache};
31use path_absolutize::Absolutize;
32use std::{
33    error::Error,
34    net::IpAddr,
35    path::{Path, PathBuf},
36    sync::Arc,
37};
38use tokio::{
39    net::TcpListener,
40    sync::{broadcast, mpsc::Receiver},
41};
42
43use crate::file_layer::watcher::create_recommended_watcher;
44
45pub struct Listener<W: Watcher> {
46    tcp_listener: TcpListener,
47    root_path: PathBuf,
48    debouncer: Debouncer<W, RecommendedCache>,
49    rx: Receiver<Result<Vec<DebouncedEvent>, Vec<notify::Error>>>,
50}
51
52impl<W: Watcher + Send + 'static> Listener<W> {
53    /// Start live-server.
54    ///
55    /// ```
56    /// use live_server::{listen, Options};
57    ///
58    /// async fn serve() -> Result<(), Box<dyn std::error::Error>> {
59    ///     listen("127.0.0.1:8080", "./").await?.start(Options::default()).await
60    /// }
61    /// ```
62    pub async fn start(self, options: Options) -> Result<(), Box<dyn Error>> {
63        let (tx, _) = broadcast::channel(16);
64
65        let arc_tx = Arc::new(tx);
66        let app_state = AppState {
67            hard_reload: options.hard_reload,
68            index_listing: options.index_listing,
69            auto_ignore: options.auto_ignore,
70            tx: arc_tx.clone(),
71            root: self.root_path.clone(),
72        };
73
74        let watcher_future = tokio::spawn(watch(
75            self.root_path,
76            self.debouncer,
77            self.rx,
78            arc_tx,
79            options.auto_ignore,
80        ));
81        let server_future = tokio::spawn(serve(self.tcp_listener, create_server(app_state)));
82
83        tokio::try_join!(watcher_future, server_future)?;
84
85        Ok(())
86    }
87
88    /// Return the link of the server, like `http://127.0.0.1:8080`.
89    ///
90    /// ```
91    /// use live_server::{listen, Options};
92    ///
93    /// async fn serve() {
94    ///     let listener = listen("127.0.0.1:8080", "./").await.unwrap();
95    ///     let link = listener.link().unwrap();
96    ///     assert_eq!(link, "http://127.0.0.1:8080");
97    /// }
98    /// ```
99    ///
100    /// This is useful when you did not specify the host or port (e.g. `listen("0.0.0.0:0", ".")`),
101    /// because this method will return the specific address.
102    pub fn link(&self) -> Result<String, Box<dyn Error>> {
103        let addr = self.tcp_listener.local_addr()?;
104        let port = addr.port();
105        let host = addr.ip();
106        let host = match host.is_unspecified() {
107            true => local_ip()?,
108            false => host,
109        };
110
111        Ok(match host {
112            IpAddr::V4(host) => format!("http://{host}:{port}"),
113            IpAddr::V6(host) => format!("http://[{host}]:{port}"),
114        })
115    }
116}
117
118/// Create live-server listener using [RecommendedWatcher].
119///
120/// ```
121/// use live_server::{listen, Options};
122///
123/// async fn serve() -> Result<(), Box<dyn std::error::Error>> {
124///     listen("127.0.0.1:8080", "./").await?.start(Options::default()).await
125/// }
126/// ```
127pub async fn listen(
128    addr: impl AsRef<str>,
129    root: impl AsRef<Path>,
130) -> Result<Listener<RecommendedWatcher>, String> {
131    let tcp_listener = create_listener(addr.as_ref()).await?;
132    let (debouncer, rx) = create_recommended_watcher().await?;
133
134    let abs_root = get_absolute_path(root.as_ref())?;
135    print_listening_on_path(&abs_root)?;
136
137    Ok(Listener {
138        tcp_listener,
139        debouncer,
140        root_path: abs_root,
141        rx,
142    })
143}
144
145/// Create live-server listener using [PollWatcher].
146///
147/// [PollWatcher] is a fallback that manually checks file paths for changes at a regular interval.
148/// It is useful for cases where real-time OS notifications fail, such as when a symbolic link is
149/// atomically replaced, or when the monitored directory itself is moved or renamed.
150///
151/// ```
152/// use live_server::{listen_poll, Options};
153///
154/// async fn serve() -> Result<(), Box<dyn std::error::Error>> {
155///     listen_poll("127.0.0.1:8080", "./").await?.start(Options::default()).await
156/// }
157/// ```
158pub async fn listen_poll(
159    addr: impl AsRef<str>,
160    root: impl AsRef<Path>,
161) -> Result<Listener<PollWatcher>, String> {
162    let tcp_listener = create_listener(addr.as_ref()).await?;
163    let (debouncer, rx) = create_poll_watcher().await?;
164
165    let abs_root = get_absolute_path(root.as_ref())?;
166    print_listening_on_path(&abs_root)?;
167
168    Ok(Listener {
169        tcp_listener,
170        debouncer,
171        root_path: abs_root,
172        rx,
173    })
174}
175
176fn get_absolute_path(path: &Path) -> Result<PathBuf, String> {
177    match path.absolutize() {
178        Ok(path) => Ok(path.to_path_buf()),
179        Err(err) => {
180            let err_msg = format!("Failed to get absolute path of {path:?}: {err}");
181            log::error!("{err_msg}");
182            Err(err_msg)
183        }
184    }
185}
186
187fn print_listening_on_path(path: &PathBuf) -> Result<(), String> {
188    match path.as_os_str().to_str() {
189        Some(path_str) => {
190            log::info!("Listening on {path_str}");
191            Ok(())
192        }
193        None => {
194            let err_msg = format!("Failed to parse path to string for `{path:?}`");
195            log::error!("{err_msg}");
196            Err(err_msg)
197        }
198    }
199}