webdriver_install/
installer.rs

1use crate::{chromedriver::Chromedriver, geckodriver::Geckodriver, DriverFetcher};
2use dirs::home_dir;
3use eyre::{ensure, eyre, Result};
4use flate2::read::GzDecoder;
5use tar::Archive;
6use tracing::debug;
7
8use std::fs::File;
9use std::io::{Cursor, Read};
10use std::path::PathBuf;
11
12static DRIVER_EXECUTABLES: &[&'static str] = &[
13    "geckodriver",
14    "chromedriver",
15    "chromedriver.exe",
16    "geckodriver.exe",
17];
18
19pub enum Driver {
20    Chrome,
21    Gecko,
22}
23
24impl Driver {
25    /// Downloads and unarchives the driver executable to $HOME/.webdrivers
26    ///
27    /// # Example
28    ///
29    /// ```no_run
30    /// # fn main() -> eyre::Result<()> {
31    /// use webdriver_install::Driver;
32    ///
33    /// // Install geckodriver
34    /// Driver::Gecko.install()?;
35    ///
36    /// // Install chromedriver
37    /// Driver::Chrome.install()?;
38    /// # Ok(())
39    /// # }
40    /// ```
41    pub fn install(&self) -> Result<PathBuf> {
42        let target_dir = home_dir().unwrap().join(".webdrivers");
43        std::fs::create_dir_all(&target_dir)?;
44        self.install_into(target_dir)
45    }
46
47    /// Downloads and unarchives the driver executable into the specified `target_dir`
48    ///
49    /// # Example
50    ///
51    /// ```no_run
52    /// # fn main() -> eyre::Result<()> {
53    /// use webdriver_install::Driver;
54    /// use std::path::PathBuf;
55    ///
56    /// // Install geckodriver into /tmp/webdrivers
57    /// Driver::Gecko.install_into(PathBuf::from("/tmp/webdrivers"))?;
58    ///
59    /// // Install chromedriver into /tmp/webdrivers
60    /// Driver::Chrome.install_into(PathBuf::from("/tmp/webdrivers"))?;
61    /// # Ok(())
62    /// # }
63    /// ```
64    pub fn install_into(&self, target_dir: PathBuf) -> Result<PathBuf> {
65        ensure!(target_dir.exists(), "installation directory must exist.");
66        ensure!(
67            target_dir.is_dir(),
68            "installation location must be a directory."
69        );
70
71        let download_url = match self {
72            Self::Gecko => {
73                let version = Geckodriver::new().latest_version()?;
74                Geckodriver::new().direct_download_url(&version)?
75            }
76            Self::Chrome => {
77                let version = Chromedriver::new().latest_version()?;
78                Chromedriver::new().direct_download_url(&version)?
79            }
80        };
81        let resp = reqwest::blocking::get(download_url.clone())?;
82        let archive_content = &resp.bytes()?;
83
84        let archive_filename = download_url
85            .path_segments()
86            .and_then(|s| s.last())
87            .and_then(|name| if name.is_empty() { None } else { Some(name) })
88            .unwrap_or("tmp.bin");
89
90        let executable_path = decompress(archive_filename, archive_content, target_dir.clone())?;
91
92        // Make sure the extracted file will be executable.
93        //
94        // Windows doesn't need that, because all `.exe` files are automatically executable.
95        #[cfg(any(target_os = "linux", target_os = "macos"))]
96        {
97            use std::fs;
98            use std::os::unix::fs::PermissionsExt;
99            fs::set_permissions(&executable_path, fs::Permissions::from_mode(0o775)).unwrap();
100        }
101
102        debug!("stored at {:?}", executable_path);
103        Ok(executable_path)
104    }
105
106    #[doc(hidden)]
107    pub fn as_str<'a>(&self) -> &'a str {
108        match self {
109            Self::Chrome => "chromedriver",
110            Self::Gecko => "geckodriver",
111        }
112    }
113
114    #[doc(hidden)]
115    pub fn from_str(s: &str) -> Option<Self> {
116        match s {
117            "chromedriver" => Some(Self::Chrome),
118            "geckodriver" => Some(Self::Gecko),
119            _ => None,
120        }
121    }
122}
123
124fn decompress(archive_filename: &str, bytes: &[u8], target_dir: PathBuf) -> Result<PathBuf> {
125    match archive_filename {
126        name if name.ends_with("tar.gz") => {
127            let tar = GzDecoder::new(Cursor::new(bytes));
128            let mut archive = Archive::new(tar);
129
130            let driver_executable = archive.entries()?.filter_map(Result::ok).filter(|e| {
131                let filename = e.path().unwrap();
132                debug!("filename: {:?}", filename);
133                DRIVER_EXECUTABLES.contains(&filename.as_os_str().to_str().unwrap())
134            });
135
136            for mut exec in driver_executable {
137                let final_path = target_dir.join(exec.path()?);
138                exec.unpack(&final_path)?;
139
140                return Ok(final_path);
141            }
142        }
143        name if name.ends_with("zip") => {
144            debug!("zip file name: {}", name);
145            let mut zip = zip::ZipArchive::new(Cursor::new(bytes))?;
146
147            let mut zip_bytes: Vec<u8> = vec![];
148            let mut filename: Option<String> = None;
149            for i in 0..zip.len() {
150                let mut file = zip.by_index(i)?;
151                if DRIVER_EXECUTABLES.contains(&file.name()) {
152                    filename = Some(file.name().to_string());
153                    file.read_to_end(&mut zip_bytes)?;
154                    break;
155                }
156            }
157            if let Some(name) = filename {
158                debug!("saving zip file: {}", name);
159                let executable_path = target_dir.join(name);
160                let mut f = File::create(&executable_path)?;
161                std::io::copy(&mut zip_bytes.as_slice(), &mut f)?;
162
163                return Ok(executable_path);
164            }
165        }
166
167        ext => return Err(eyre!("No support for unarchiving {}, yet", ext)),
168    }
169    Err(eyre!(
170        "This installer code should be unreachable! archive_filename: {}",
171        archive_filename
172    ))
173}