bamboo_server/server/
web_service.rs1use std::path::PathBuf;
2
3use actix_files as fs;
4use actix_web::{web, App, HttpServer};
5use tokio::sync::oneshot;
6use tracing::{error, info};
7
8use super::listeners::DEFAULT_WORKER_COUNT;
9use crate::app_state::AppState;
10use crate::config::{build_cors, build_security_headers};
11use crate::routes::{configure_routes, configure_routes_with_rate_limiting};
12
13pub struct WebService {
18 shutdown_tx: Option<oneshot::Sender<()>>,
19 server_handle: Option<tokio::task::JoinHandle<()>>,
20 bamboo_home_dir: PathBuf,
22 port: u16,
23}
24
25impl WebService {
26 pub fn new(bamboo_home_dir: PathBuf) -> Self {
31 Self {
32 shutdown_tx: None,
33 server_handle: None,
34 bamboo_home_dir,
35 port: 3456, }
37 }
38
39 pub async fn start(&mut self, port: u16) -> Result<(), String> {
41 self.start_with_bind(port, "127.0.0.1").await
42 }
43
44 pub async fn start_with_bind(&mut self, port: u16, bind: &str) -> Result<(), String> {
46 info!("Starting web service...");
47 if self.server_handle.is_some() {
48 return Err("Web service is already running".to_string());
49 }
50
51 let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
52 self.port = port;
53
54 let app_state = web::Data::new(
55 AppState::new(self.bamboo_home_dir.clone())
56 .await
57 .map_err(|e| format!("Failed to initialize app state: {e}"))?,
58 );
59 let bind_addr = bind.to_string();
60 let listen_addr = format!("{bind}:{port}");
61 let bind_for_log = bind_addr.clone();
62
63 let server = HttpServer::new(move || {
64 App::new()
65 .app_data(app_state.clone())
66 .wrap(build_cors(&bind_addr, port))
67 .configure(configure_routes) })
69 .workers(DEFAULT_WORKER_COUNT)
70 .bind(&listen_addr)
71 .map_err(|e| format!("Failed to bind server: {e}"))?
72 .run();
73
74 let server_handle = tokio::spawn(async move {
75 tokio::select! {
76 result = server => {
77 if let Err(e) = result {
78 error!("Server error: {}", e);
79 }
80 }
81 _ = &mut shutdown_rx => {
82 info!("Web service shutdown signal received");
83 }
84 }
85 });
86
87 self.shutdown_tx = Some(shutdown_tx);
88 self.server_handle = Some(server_handle);
89
90 info!(
91 "Web service started successfully on http://{}:{}",
92 bind_for_log, port
93 );
94 Ok(())
95 }
96
97 pub async fn start_with_bind_and_static(
100 &mut self,
101 port: u16,
102 bind: &str,
103 static_dir: PathBuf,
104 ) -> Result<(), String> {
105 info!("Starting web service with static frontend...");
106 if self.server_handle.is_some() {
107 return Err("Web service is already running".to_string());
108 }
109
110 let (shutdown_tx, mut shutdown_rx) = oneshot::channel::<()>();
111 self.port = port;
112
113 let static_dir = static_dir
114 .canonicalize()
115 .map_err(|e| format!("Static directory not found: {:?}: {}", static_dir, e))?;
116 if !static_dir.is_dir() {
117 return Err(format!(
118 "Static path is not a directory: {}",
119 static_dir.display()
120 ));
121 }
122
123 let app_state = web::Data::new(
124 AppState::new(self.bamboo_home_dir.clone())
125 .await
126 .map_err(|e| format!("Failed to initialize app state: {e}"))?,
127 );
128 let bind_addr = bind.to_string();
129 let listen_addr = format!("{bind}:{port}");
130 let bind_for_log = bind_addr.clone();
131
132 let server = HttpServer::new(move || {
133 App::new()
134 .app_data(web::JsonConfig::default().limit(25 * 1024 * 1024))
135 .app_data(web::PayloadConfig::new(30 * 1024 * 1024))
136 .app_data(app_state.clone())
137 .wrap(build_cors(&bind_addr, port))
138 .wrap(build_security_headers())
139 .configure(configure_routes_with_rate_limiting)
140 .service(
141 fs::Files::new("/", static_dir.clone())
142 .index_file("index.html")
143 .prefer_utf8(true)
144 .disable_content_disposition()
145 .disable_content_disposition(),
146 )
147 })
148 .workers(DEFAULT_WORKER_COUNT)
149 .bind(&listen_addr)
150 .map_err(|e| format!("Failed to bind server: {e}"))?
151 .run();
152
153 let server_handle = tokio::spawn(async move {
154 tokio::select! {
155 result = server => {
156 if let Err(e) = result {
157 error!("Server error: {}", e);
158 }
159 }
160 _ = &mut shutdown_rx => {
161 info!("Web service shutdown signal received");
162 }
163 }
164 });
165
166 self.shutdown_tx = Some(shutdown_tx);
167 self.server_handle = Some(server_handle);
168
169 info!(
170 "Web service with static frontend started successfully on http://{}:{}",
171 bind_for_log, port
172 );
173 Ok(())
174 }
175
176 pub async fn stop(&mut self) -> Result<(), String> {
178 if let Some(shutdown_tx) = self.shutdown_tx.take() {
179 if shutdown_tx.send(()).is_err() {
180 error!("Failed to send shutdown signal");
181 return Err("Error sending shutdown signal".to_string());
182 }
183
184 if let Some(handle) = self.server_handle.take() {
185 if let Err(e) = handle.await {
186 error!("Error waiting for server shutdown: {}", e);
187 return Err(format!("Error waiting for server shutdown: {}", e));
188 }
189 }
190
191 info!("Web service stopped successfully");
192 }
193
194 Ok(())
195 }
196
197 pub fn is_running(&self) -> bool {
199 self.server_handle.is_some()
200 }
201
202 pub fn port(&self) -> u16 {
204 self.port
205 }
206}
207
208impl Drop for WebService {
209 fn drop(&mut self) {
210 if let Some(shutdown_tx) = self.shutdown_tx.take() {
211 let _ = shutdown_tx.send(());
212 }
213 }
214}