webdriver_install/
installer.rs1use 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 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 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 #[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}