1use crate::{
10 Error, GitHubAsset, GitHubClient, GitHubRelease, Result, extract_path_from_executable,
11};
12use futures_util::StreamExt;
13use http::{HeaderName, header::ACCEPT};
14use reqwest::{
15 ClientBuilder,
16 header::{HeaderMap, HeaderValue},
17};
18use semver::Version;
19use std::{
20 env::current_exe,
21 ffi::OsString,
22 path::{Path, PathBuf},
23 time::Duration,
24};
25use url::Url;
26
27const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
28
29pub struct UpdaterBuilder {
37 app_name: String,
38 github_owner: String,
39 github_repo: String,
40 current_version: Version,
41 executable_path: Option<PathBuf>,
42 headers: HeaderMap,
43 timeout: Option<Duration>,
44 proxy: Option<Url>,
45 installer_args: Vec<OsString>,
46 current_exe_args: Vec<OsString>,
47}
48
49impl UpdaterBuilder {
50 pub fn new(
56 app_name: &str,
57 current_version: Version,
58 github_owner: &str,
59 github_repo: &str,
60 ) -> Self {
61 Self {
62 installer_args: Vec::new(),
63 current_exe_args: Vec::new(),
64 app_name: app_name.to_owned(),
65 current_version,
66 executable_path: None,
67 github_owner: github_owner.to_owned(),
68 github_repo: github_repo.to_owned(),
69 headers: HeaderMap::new(),
70 timeout: None,
71 proxy: None,
72 }
73 }
74
75 pub fn executable_path<P: AsRef<Path>>(mut self, p: P) -> Self {
77 self.executable_path.replace(p.as_ref().into());
78 self
79 }
80
81 pub fn header<K, V>(mut self, key: K, value: V) -> Result<Self>
83 where
84 HeaderName: TryFrom<K>,
85 <HeaderName as TryFrom<K>>::Error: Into<http::Error>,
86 HeaderValue: TryFrom<V>,
87 <HeaderValue as TryFrom<V>>::Error: Into<http::Error>,
88 {
89 let key: std::result::Result<HeaderName, http::Error> = key.try_into().map_err(Into::into);
90 let value: std::result::Result<HeaderValue, http::Error> =
91 value.try_into().map_err(Into::into);
92 self.headers.insert(key?, value?);
93
94 Ok(self)
95 }
96
97 pub fn headers(mut self, headers: HeaderMap) -> Self {
99 self.headers = headers;
100 self
101 }
102
103 pub fn clear_headers(mut self) -> Self {
105 self.headers.clear();
106 self
107 }
108
109 pub fn timeout(mut self, timeout: Duration) -> Self {
111 self.timeout = Some(timeout);
112 self
113 }
114
115 pub fn proxy(mut self, proxy: Url) -> Self {
117 self.proxy.replace(proxy);
118 self
119 }
120
121 pub fn installer_arg<S>(mut self, arg: S) -> Self
123 where
124 S: Into<OsString>,
125 {
126 self.installer_args.push(arg.into());
127 self
128 }
129
130 pub fn installer_args<I, S>(mut self, args: I) -> Self
132 where
133 I: IntoIterator<Item = S>,
134 S: Into<OsString>,
135 {
136 self.installer_args.extend(args.into_iter().map(Into::into));
137 self
138 }
139
140 pub fn clear_installer_args(mut self) -> Self {
142 self.installer_args.clear();
143 self
144 }
145
146 pub fn build(self) -> Result<Updater> {
148 let executable_path = self.executable_path.clone().unwrap_or(current_exe()?);
149
150 let extract_path = if cfg!(target_os = "linux") {
152 executable_path
153 } else {
154 extract_path_from_executable(&executable_path)?
155 };
156
157 let github_client = GitHubClient::new(&self.github_owner, &self.github_repo);
158
159 Ok(Updater {
160 app_name: self.app_name,
161 current_version: self.current_version,
162 proxy: self.proxy,
163 installer_args: self.installer_args,
164 current_exe_args: self.current_exe_args,
165 headers: self.headers,
166 timeout: self.timeout,
167 extract_path,
168 github_client,
169 latest_release: None,
170 proper_asset: None,
171 })
172 }
173}
174
175#[derive(Debug, Clone)]
176pub struct Updater {
178 pub app_name: String,
179 pub current_version: Version,
180 pub proxy: Option<Url>,
181 pub github_client: GitHubClient,
182 pub headers: HeaderMap,
183 pub extract_path: PathBuf,
184 pub timeout: Option<Duration>,
185 pub installer_args: Vec<OsString>,
186 pub current_exe_args: Vec<OsString>,
187 pub latest_release: Option<GitHubRelease>,
188 pub proper_asset: Option<GitHubAsset>,
189}
190
191impl Updater {
192 pub async fn latest_release(&self) -> Result<GitHubRelease> {
194 self.github_client.get_latest_release().await?.try_into()
195 }
196
197 pub fn latest_version(&self) -> Option<Version> {
199 self.latest_release
200 .as_ref()
201 .map(|release| release.version.clone())
202 }
203
204 pub fn asset_size(&self) -> Option<u64> {
206 self.proper_asset.as_ref().map(|asset| asset.size)
207 }
208
209 pub async fn proper_asset(&self) -> Result<GitHubAsset> {
211 let release = self.latest_release().await?;
212 release.find_proper_asset()
213 }
214
215 pub async fn check(&self) -> Result<Option<Updater>> {
218 let latest_release = self.latest_release().await?;
219 if latest_release.version > self.current_version {
220 let asset = latest_release.find_proper_asset()?;
221 Ok(Some(Self {
222 latest_release: Some(latest_release),
223 proper_asset: Some(asset),
224 ..self.clone()
225 }))
226 } else {
227 Ok(None)
228 }
229 }
230
231 pub async fn update<C: FnMut(usize)>(
236 &self,
237 on_chunk: C,
238 ) -> Result<bool> {
240 if let Some(updater) = self.check().await? {
241 updater.download_and_install(on_chunk).await?;
242 Ok(true)
243 } else {
244 Ok(false)
245 }
246 }
247}
248
249impl Updater {
250 pub async fn download<C: FnMut(usize)>(
254 &self,
255 mut on_chunk: C,
256 ) -> Result<Vec<u8>> {
258 let mut headers = self.headers.clone();
260 if !headers.contains_key(ACCEPT) {
261 headers.insert(ACCEPT, HeaderValue::from_static("application/octet-stream"));
262 }
263
264 let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
265 if let Some(timeout) = self.timeout {
266 request = request.timeout(timeout);
267 }
268 if let Some(ref proxy) = self.proxy {
269 let proxy = reqwest::Proxy::all(proxy.as_str())?;
270 request = request.proxy(proxy);
271 }
272
273 let download_url = self
274 .proper_asset
275 .clone()
276 .ok_or(Error::AssetNotFound)?
277 .browser_download_url
278 .clone();
279
280 let response = request
281 .build()?
282 .get(download_url)
283 .headers(headers)
284 .send()
285 .await?;
286
287 if !response.status().is_success() {
288 return Err(Error::Network(format!(
289 "Download request failed with status: {}",
290 response.status()
291 )));
292 }
293
294 let mut buffer = Vec::new();
295
296 let mut stream = response.bytes_stream();
297 while let Some(chunk) = stream.next().await {
298 let chunk = chunk?;
299 on_chunk(chunk.len());
300 buffer.extend(chunk);
301 }
302 Ok(buffer)
303 }
304
305 pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> {
307 self.install_inner(bytes.as_ref())
308 }
309
310 pub fn relaunch(&self) -> Result<()> {
311 self.relaunch_inner()
312 }
313
314 pub async fn download_and_install<C: FnMut(usize)>(
316 &self,
317 on_chunk: C,
318 ) -> Result<()> {
320 let bytes = self.download(on_chunk).await?;
321 self.install(bytes)
322 }
323}