Skip to main content

brouter_client/
local.rs

1//! This module contains the code to download and run BRouter locally.
2use std::{
3    fs::{self, File},
4    io::{self, Cursor},
5    path::{Path, PathBuf},
6    process::{Command, Stdio},
7    thread,
8    time::Duration,
9};
10
11use reqwest::blocking::get;
12use zip::ZipArchive;
13
14/// BRouter version to use
15const BROUTER_VERSION: &str = "1.7.7";
16
17/// URL to the BRouter server package ZIP
18const BROUTER_URL: &str = "https://github.com/abrensch/brouter/releases/download";
19
20/// A struct representing the BRouter server
21pub struct BRouterServer {
22    /// Base path where BRouter is installed
23    pub base_path: PathBuf,
24    segments_dir: PathBuf,
25    custom_profile_dir: PathBuf,
26    process: Option<std::process::Child>,
27}
28
29impl BRouterServer {
30    /// Create a new BRouterServer instance
31    pub fn new(brouter_dir: &Path) -> Self {
32        let segments_dir = brouter_dir.join("segments4");
33        let custom_profile_dir = brouter_dir.join("custom_profiles");
34
35        BRouterServer {
36            base_path: brouter_dir.to_path_buf(),
37            segments_dir,
38            custom_profile_dir,
39            process: None,
40        }
41    }
42
43    /// Create a new BRouterServer instance in the user's home directory
44    pub fn home() -> Self {
45        let data_dir = xdg::BaseDirectories::new()
46            .get_data_home()
47            .unwrap()
48            .join("brouter");
49
50        std::fs::create_dir_all(&data_dir).unwrap();
51
52        Self::new(&data_dir)
53    }
54
55    fn find_jar_file(&self) -> Option<PathBuf> {
56        for entry in fs::read_dir(&self.base_path).unwrap() {
57            let entry = entry.unwrap();
58
59            if entry.file_name().to_str().unwrap().starts_with("brouter-") {
60                for sub_entry in fs::read_dir(entry.path()).unwrap() {
61                    let sub_entry = sub_entry.unwrap();
62                    if sub_entry.file_name().to_str().unwrap().ends_with(".jar") {
63                        return Some(sub_entry.path());
64                    }
65                }
66            }
67        }
68        None
69    }
70
71    /// Check if the BRouter server has been downloaded
72    pub fn has_downloaded(&self) -> bool {
73        self.find_jar_file().is_some()
74    }
75
76    /// Download and extract the BRouter server
77    pub fn download_brouter(&self) -> Result<(), Box<dyn std::error::Error>> {
78        // Check if the BRouter server is already downloaded
79        if self.find_jar_file().is_some() {
80            return Ok(());
81        }
82
83        let resp = get(format!(
84            "{}/v{}/brouter-{}.zip",
85            BROUTER_URL, BROUTER_VERSION, BROUTER_VERSION
86        ))?;
87
88        if resp.status() != reqwest::StatusCode::OK {
89            return Err(format!("Failed to download BRouter server: {}", resp.status()).into());
90        }
91
92        let bytes = resp.bytes()?;
93
94        let cursor = Cursor::new(bytes);
95
96        let mut archive = ZipArchive::new(cursor)?;
97        archive.extract(&self.base_path)?;
98
99        Ok(())
100    }
101
102    /// Download all segments
103    pub fn download_all_segments(&self) -> Result<(), Box<dyn std::error::Error>> {
104        // Check if the segments directory exists
105        if !self.segments_dir.exists() {
106            fs::create_dir_all(&self.segments_dir)?;
107        }
108
109        for e in (0..=175).step_by(5) {
110            for n in (0..=90).step_by(5) {
111                let segment = format!("E{}_N{}", e, n);
112                self.download_segment(&segment)?;
113            }
114
115            for n in (0..=90).step_by(5) {
116                let segment = format!("E{}_S{}", e, n);
117                self.download_segment(&segment)?;
118            }
119        }
120
121        for w in (0..=175).step_by(5) {
122            for n in (0..=90).step_by(5) {
123                let segment = format!("W{}_N{}", w, n);
124                self.download_segment(&segment)?;
125            }
126
127            for n in (0..=90).step_by(5) {
128                let segment = format!("W{}_S{}", w, n);
129                self.download_segment(&segment)?;
130            }
131        }
132
133        Ok(())
134    }
135
136    /// Download a specific segment
137    pub fn download_segment(&self, segment: &str) -> Result<(), Box<dyn std::error::Error>> {
138        // Check if the segments directory exists
139        if !self.segments_dir.exists() {
140            fs::create_dir_all(&self.segments_dir)?;
141        }
142
143        let segment_path = self.segments_dir.join(format!("{}.rd5", segment));
144
145        // Check if the segment is already downloaded
146        if segment_path.exists() {
147            return Ok(());
148        }
149
150        // Create the segments directory if it doesn't exist
151        fs::create_dir_all(&self.segments_dir)?;
152
153        // Download the segment
154        let mut resp = get(format!(
155            "https://brouter.de/brouter/segments4/{}.rd5",
156            segment
157        ))?;
158        if resp.status() != reqwest::StatusCode::OK {
159            return Err(
160                format!("Failed to download segment {}: {}", segment, resp.status()).into(),
161            );
162        }
163        let mut out = File::create(&segment_path)?;
164        io::copy(&mut resp, &mut out)?;
165
166        Ok(())
167    }
168
169    /// Check if the BRouter server is running
170    pub fn is_running(&mut self) -> bool {
171        if let Some(process) = self.process.as_mut() {
172            match process.try_wait() {
173                Ok(Some(_)) => false, // Process has exited
174                Ok(None) => true,     // Process is still running
175                Err(_) => false,      // Error checking process status
176            }
177        } else {
178            false // No process started
179        }
180    }
181
182    /// Check if the BRouter server is serving requests
183    pub fn is_serving(&self) -> bool {
184        // Check if the server is running and responding on the port
185        match get("http://localhost:17777") {
186            // The root URL should return a 404 Not Found
187            Ok(resp) => resp.status() == reqwest::StatusCode::NOT_FOUND,
188            Err(_) => false,
189        }
190    }
191
192    /// Start the BRouter server
193    pub fn start(&mut self) -> Result<String, Box<dyn std::error::Error>> {
194        // Check if the BRouter server is already running
195        if self.is_running() {
196            return Ok(format!("http://localhost:17777"));
197        }
198
199        let jar_path = self
200            .find_jar_file()
201            .ok_or("BRouter server JAR file not found")?;
202
203        let profile_dir = jar_path.parent().unwrap().join("profiles2");
204
205        // Ensure the custom profile directory exists
206        fs::create_dir_all(&self.custom_profile_dir)?;
207
208        // Start the BRouter server
209        let child = Command::new("java")
210            .current_dir(&self.base_path)
211            .arg("-Xmx128M")
212            .arg("-Xms128M")
213            .arg("-Xmn8M")
214            .arg("-DmaxRunningTime=300") // Request timeout in seconds (0 for no timeout)
215            .arg("-DuseRFCMimeType=false")
216            .arg("-cp")
217            .arg(jar_path)
218            .arg("btools.server.RouteServer")
219            .arg(self.segments_dir.to_str().unwrap())
220            .arg(profile_dir.to_str().unwrap())
221            .arg(self.custom_profile_dir.to_str().unwrap())
222            .arg("17777") // Port
223            .arg("1") // Number of threads
224            .arg("localhost") // Host
225            .stdout(Stdio::inherit())
226            .stderr(Stdio::inherit())
227            .spawn()?;
228
229        self.process = Some(child);
230
231        // Wait until the server is up and responding on the port
232        let mut attempts = 0;
233        while attempts < 10 {
234            if self.is_serving() {
235                break;
236            }
237            attempts += 1;
238            thread::sleep(Duration::from_secs(1));
239        }
240
241        Ok(format!("http://localhost:17777"))
242    }
243
244    /// Stop the BRouter server
245    pub fn stop(&mut self) -> Result<(), Box<dyn std::error::Error>> {
246        if let Some(mut process) = self.process.take() {
247            process.kill()?;
248        }
249        Ok(())
250    }
251}
252
253impl Drop for BRouterServer {
254    fn drop(&mut self) {
255        self.stop().unwrap_or_else(|_| {
256            eprintln!(
257                "Failed to stop BRouter server: {}",
258                self.base_path.display()
259            );
260        });
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_brouter_server() {
270        let mut brouter = BRouterServer::home();
271        brouter.download_brouter().unwrap();
272        assert!(brouter.has_downloaded());
273        brouter.download_segment("E0_N10").unwrap();
274        assert!(brouter.segments_dir.join("E0_N10.rd5").exists());
275        brouter.start().unwrap();
276        assert!(brouter.is_running());
277        brouter.stop().unwrap();
278    }
279}