monger_core/
lib.rs

1#[macro_use]
2mod util;
3
4mod client;
5pub mod error;
6mod fs;
7pub mod os;
8pub mod process;
9mod url;
10
11use std::{ffi::OsString, io::ErrorKind::NotFound, process::Child};
12
13use lazy_static::lazy_static;
14use regex::Regex;
15use semver::Version;
16use soup::{NodeExt, QueryBuilderExt, Soup};
17
18use crate::{
19    client::HttpClient,
20    error::{Error, Result},
21    fs::Fs,
22    os::OperatingSystem,
23    process::{exec_command, run_background_command},
24    util::{parse_major_minor_version, select_newer_version},
25};
26
27const MONGODB_VERSION_LIST_URL: &str = "https://dl.mongodb.org/dl/src";
28
29lazy_static! {
30    static ref MONGODB_SEMVER_REGEX: Regex =
31        Regex::new(r"src/mongodb-src-r(\d+\.\d+\.\d+)\.tar\.gz$").unwrap();
32}
33
34#[derive(Debug)]
35pub struct Monger {
36    client: HttpClient,
37    fs: Fs,
38}
39
40#[derive(Debug)]
41pub struct LogFile {
42    pub cluster_id: String,
43    pub port: u16,
44    pub node_type: LogFileType,
45}
46
47#[derive(Clone, Copy, Debug)]
48pub enum LogFileType {
49    DataNode,
50    ShardingRouter,
51    ShardNode { shard_num: usize },
52    ConfigServer,
53}
54
55impl Monger {
56    pub fn new() -> Result<Self> {
57        Ok(Self {
58            client: HttpClient::new()?,
59            fs: Fs::builder().build()?,
60        })
61    }
62
63    pub fn clear_database_files(&self, version_str: &str) -> Result<bool> {
64        self.fs.clear_db_dir(version_str)
65    }
66
67    pub fn clear_cluster_logs(&self, cluster_id: &str) -> Result<bool> {
68        self.fs.clear_cluster_logs(cluster_id)
69    }
70
71    pub fn clear_default_args(&self) -> Result<bool> {
72        self.fs.clear_default_args()
73    }
74
75    pub fn download_mongodb_version_from_url(
76        &self,
77        url: &str,
78        id: &str,
79        force: bool,
80    ) -> Result<()> {
81        if self.fs.version_exists(id) {
82            if force {
83                self.delete_mongodb_version(id)?;
84            } else {
85                return Err(Error::ExistingId { id: id.into() });
86            }
87        }
88
89        let dir = format!("custom-download-{}", id);
90        let file = format!("{}.tgz", dir);
91        let data = self.client.download_url(&url)?;
92
93        self.fs
94            .write_mongodb_download(&file, &dir, &data[..], &id)?;
95
96        Ok(())
97    }
98
99    pub fn download_mongodb_version(
100        &self,
101        version_str: &str,
102        force: bool,
103        os: Option<&str>,
104        id: Option<&str>,
105    ) -> Result<()> {
106        let version = if version_str == "latest" {
107            self.find_latest_mongodb_version()?
108        } else if let Some((major, minor)) = parse_major_minor_version(version_str) {
109            self.find_latest_matching_version(major, minor)?
110        } else {
111            crate::util::parse_version(version_str)?
112        };
113
114        let id = id
115            .map(ToString::to_string)
116            .unwrap_or_else(|| version.to_string());
117
118        if self.fs.version_exists(&id) {
119            if force {
120                self.delete_mongodb_version(&id)?;
121            } else {
122                return Ok(());
123            }
124        }
125
126        let os = if let Some(os_name) = os {
127            OperatingSystem::from_name(os_name).unwrap()
128        } else {
129            OperatingSystem::get(&version)?
130        };
131
132        let url = os.download_url(&version);
133        let file = url.filename();
134        let dir = url.dirname();
135        let url: String = url.into();
136        let data = self.client.download_version(&url, &version_str)?;
137
138        self.fs
139            .write_mongodb_download(&file, &dir, &data[..], &id)?;
140
141        Ok(())
142    }
143
144    fn find_latest_matching_version(&self, major: u64, minor: u64) -> Result<Version> {
145        let response = self.client.get(MONGODB_VERSION_LIST_URL)?;
146        let soup = Soup::from_reader(response)?;
147
148        let matches = soup
149            .tag("a")
150            .attr("href", MONGODB_SEMVER_REGEX.clone())
151            .find_all()
152            .map(|item| {
153                // We know the capture we're looking for will exist (and will be a valid semver
154                // string) due to Soup finding it as a match, so it's safe to unwrap
155                // here.
156                Version::parse(
157                    &*MONGODB_SEMVER_REGEX
158                        .captures(&item.text())
159                        .unwrap()
160                        .get(1)
161                        .unwrap()
162                        .as_str(),
163                )
164                .unwrap()
165            });
166
167        for version in matches {
168            if major == version.major && minor == version.minor {
169                return Ok(version);
170            }
171        }
172
173        Err(Error::VersionNotFound {
174            version: format!("{}.{}", major, minor),
175        })
176    }
177
178    fn find_latest_mongodb_version(&self) -> Result<Version> {
179        let response = self.client.get(MONGODB_VERSION_LIST_URL)?;
180        let soup = Soup::from_reader(response)?;
181
182        let mut newest_stable = None;
183        let mut newest_dev = None;
184
185        for version in soup
186            .tag("a")
187            .attr("href", MONGODB_SEMVER_REGEX.clone())
188            .find_all()
189            .map(|item| {
190                // We know the capture we're looking for will exist (and will be a valid semver
191                // string) due to Soup finding it as a match, so it's safe to unwrap
192                // here.
193                Version::parse(
194                    &*MONGODB_SEMVER_REGEX
195                        .captures(&item.text())
196                        .unwrap()
197                        .get(1)
198                        .unwrap()
199                        .as_str()
200                        .to_string(),
201                )
202                .unwrap()
203            })
204        {
205            if version.minor % 2 == 0 {
206                newest_stable = Some(select_newer_version(newest_stable, version));
207            } else {
208                newest_dev = Some(version);
209            }
210
211            // Since there will only be one dev version in development at a given time, the newest
212            // stable version will never be older than one minor version less than the most recent
213            // dev version.
214            if let Some(ref stable_version) = newest_stable {
215                if let Some(ref dev_version) = newest_dev {
216                    if dev_version.major == stable_version.major
217                        && dev_version.minor == stable_version.minor + 1
218                    {
219                        return Ok(newest_stable.unwrap());
220                    }
221                }
222            }
223        }
224
225        if let Some(version) = newest_stable {
226            Ok(version)
227        } else {
228            Err(Error::InvalidHtml {
229                url: MONGODB_VERSION_LIST_URL.to_string(),
230            })
231        }
232    }
233
234    pub fn delete_mongodb_version(&self, version: &str) -> Result<()> {
235        if self.fs.delete_mongodb_version(version)? {
236            println!("Deleted version {}", version);
237        }
238
239        Ok(())
240    }
241
242    pub fn get_default_args(&self) -> Result<Option<String>> {
243        self.fs.get_default_args()
244    }
245
246    pub fn list_versions(&self) -> Result<Vec<OsString>> {
247        self.fs.list_versions()
248    }
249
250    pub fn prune(&self) -> Result<()> {
251        self.fs.prune()
252    }
253
254    fn process_args(&self, args: Vec<OsString>, version: &str) -> Result<Vec<OsString>> {
255        let mut args = args.into_iter();
256
257        let mut processed_args = Vec::new();
258        let mut found_dbpath = false;
259
260        while let Some(arg) = args.next() {
261            if arg.as_os_str() == "--dbpath" {
262                processed_args.push(arg);
263                found_dbpath = true;
264                break;
265            }
266
267            processed_args.push(arg);
268        }
269
270        if found_dbpath {
271            processed_args.extend(args);
272        } else {
273            processed_args.push("--dbpath".into());
274            processed_args.push(self.fs.create_or_get_db_dir(version)?.into_os_string());
275        }
276
277        Ok(processed_args)
278    }
279
280    pub fn set_default_args(&self, default_args: &str) -> Result<()> {
281        self.fs.set_default_args(default_args)
282    }
283
284    pub fn start_mongod(
285        &self,
286        args: Vec<OsString>,
287        version: &str,
288        exec: bool,
289        save_log: Option<LogFile>,
290    ) -> Result<Child> {
291        let mut processed_args = self.process_args(args, version)?;
292
293        if let Some(default_args) = self.fs.get_default_args()? {
294            processed_args.extend(default_args.split_whitespace().map(Into::into));
295        }
296
297        if let Some(log_file) = save_log {
298            processed_args.push("--logpath".into());
299            processed_args.push(
300                self.fs
301                    .get_and_create_log_file_path(
302                        &log_file.cluster_id,
303                        log_file.port,
304                        log_file.node_type,
305                    )?
306                    .into(),
307            );
308        }
309
310        if exec {
311            Err(self.exec_command("mongod", processed_args, version))
312        } else {
313            self.run_background_command("mongod", processed_args, version)
314        }
315    }
316
317    pub fn start_mongos(
318        &self,
319        mut args: Vec<OsString>,
320        version: &str,
321        exec: bool,
322        save_log: Option<LogFile>,
323    ) -> Result<Child> {
324        if let Some(log_file) = save_log {
325            args.push("--logpath".into());
326            args.push(
327                self.fs
328                    .get_and_create_log_file_path(
329                        &log_file.cluster_id,
330                        log_file.port,
331                        log_file.node_type,
332                    )?
333                    .into(),
334            );
335        }
336
337        if exec {
338            Err(self.exec_command("mongos", args, version))
339        } else {
340            self.run_background_command("mongos", args, version)
341        }
342    }
343
344    pub fn run_background_command(
345        &self,
346        binary_name: &str,
347        args: Vec<OsString>,
348        version: &str,
349    ) -> Result<Child> {
350        if version == "system" {
351            return run_background_command(binary_name, args, std::env::current_dir()?);
352        }
353
354        let result =
355            self.fs
356                .run_background_command(binary_name, args.into_iter().collect(), version);
357
358        match result {
359            Err(Error::Io { ref inner }) if inner.kind() == NotFound => {
360                Err(Error::BinaryNotFound {
361                    binary: binary_name.into(),
362                    version: version.into(),
363                })
364            }
365            other => other,
366        }
367    }
368
369    pub fn exec_command(&self, binary_name: &str, args: Vec<OsString>, version: &str) -> Error {
370        let dir = match std::env::current_dir() {
371            Ok(dir) => dir,
372            Err(e) => return e.into(),
373        };
374
375        let error = if version == "system" {
376            exec_command(binary_name, args, dir)
377        } else {
378            self.fs
379                .exec_command(binary_name, args.into_iter().collect(), version)
380        };
381
382        match error {
383            Error::Io { ref inner } if inner.kind() == NotFound => Error::BinaryNotFound {
384                binary: binary_name.into(),
385                version: version.into(),
386            },
387            other => other,
388        }
389    }
390}