1use 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
14const BROUTER_VERSION: &str = "1.7.7";
16
17const BROUTER_URL: &str = "https://github.com/abrensch/brouter/releases/download";
19
20pub struct BRouterServer {
22 pub base_path: PathBuf,
24 segments_dir: PathBuf,
25 custom_profile_dir: PathBuf,
26 process: Option<std::process::Child>,
27}
28
29impl BRouterServer {
30 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 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 pub fn has_downloaded(&self) -> bool {
73 self.find_jar_file().is_some()
74 }
75
76 pub fn download_brouter(&self) -> Result<(), Box<dyn std::error::Error>> {
78 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 pub fn download_all_segments(&self) -> Result<(), Box<dyn std::error::Error>> {
104 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 pub fn download_segment(&self, segment: &str) -> Result<(), Box<dyn std::error::Error>> {
138 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 if segment_path.exists() {
147 return Ok(());
148 }
149
150 fs::create_dir_all(&self.segments_dir)?;
152
153 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 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, Ok(None) => true, Err(_) => false, }
177 } else {
178 false }
180 }
181
182 pub fn is_serving(&self) -> bool {
184 match get("http://localhost:17777") {
186 Ok(resp) => resp.status() == reqwest::StatusCode::NOT_FOUND,
188 Err(_) => false,
189 }
190 }
191
192 pub fn start(&mut self) -> Result<String, Box<dyn std::error::Error>> {
194 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 fs::create_dir_all(&self.custom_profile_dir)?;
207
208 let child = Command::new("java")
210 .current_dir(&self.base_path)
211 .arg("-Xmx128M")
212 .arg("-Xms128M")
213 .arg("-Xmn8M")
214 .arg("-DmaxRunningTime=300") .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") .arg("1") .arg("localhost") .stdout(Stdio::inherit())
226 .stderr(Stdio::inherit())
227 .spawn()?;
228
229 self.process = Some(child);
230
231 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 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}