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: String,
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: &str,
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: current_version.to_owned(),
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 let current_version = Version::parse(&self.current_version)?;
160
161 Ok(Updater {
162 app_name: self.app_name,
163 current_version,
164 proxy: self.proxy,
165 installer_args: self.installer_args,
166 current_exe_args: self.current_exe_args,
167 headers: self.headers,
168 timeout: self.timeout,
169 extract_path,
170 github_client,
171 latest_release: None,
172 proper_asset: None,
173 })
174 }
175}
176
177#[derive(Debug, Clone)]
178pub struct Updater {
180 pub app_name: String,
181 pub current_version: Version,
182 pub proxy: Option<Url>,
183 pub github_client: GitHubClient,
184 pub headers: HeaderMap,
185 pub extract_path: PathBuf,
186 pub timeout: Option<Duration>,
187 pub installer_args: Vec<OsString>,
188 pub current_exe_args: Vec<OsString>,
189 pub latest_release: Option<GitHubRelease>,
190 pub proper_asset: Option<GitHubAsset>,
191}
192
193impl Updater {
194 pub async fn latest_release(&self) -> Result<GitHubRelease> {
196 self.github_client.get_latest_release().await?.try_into()
197 }
198
199 pub fn latest_version(&self) -> Option<Version> {
201 self.latest_release
202 .as_ref()
203 .map(|release| release.version.clone())
204 }
205
206 pub fn asset_size(&self) -> Option<u64> {
208 self.proper_asset.as_ref().map(|asset| asset.size)
209 }
210
211 pub async fn proper_asset(&self) -> Result<GitHubAsset> {
213 let release = self.latest_release().await?;
214 release.find_proper_asset()
215 }
216
217 pub async fn check(&self) -> Result<Option<Updater>> {
220 let latest_release = self.latest_release().await?;
221 if latest_release.version > self.current_version {
222 let asset = latest_release.find_proper_asset()?;
223 Ok(Some(Self {
224 latest_release: Some(latest_release),
225 proper_asset: Some(asset),
226 ..self.clone()
227 }))
228 } else {
229 Ok(None)
230 }
231 }
232
233 pub async fn update<C: FnMut(usize)>(
238 &self,
239 on_chunk: C,
240 ) -> Result<bool> {
242 if let Some(updater) = self.check().await? {
243 updater.download_and_install(on_chunk).await?;
244 Ok(true)
245 } else {
246 Ok(false)
247 }
248 }
249}
250
251impl Updater {
252 pub async fn download<C: FnMut(usize)>(
256 &self,
257 mut on_chunk: C,
258 ) -> Result<Vec<u8>> {
260 let mut headers = self.headers.clone();
262 if !headers.contains_key(ACCEPT) {
263 headers.insert(ACCEPT, HeaderValue::from_static("application/octet-stream"));
264 }
265
266 let mut request = ClientBuilder::new().user_agent(UPDATER_USER_AGENT);
267 if let Some(timeout) = self.timeout {
268 request = request.timeout(timeout);
269 }
270 if let Some(ref proxy) = self.proxy {
271 let proxy = reqwest::Proxy::all(proxy.as_str())?;
272 request = request.proxy(proxy);
273 }
274
275 let download_url = self
276 .proper_asset
277 .clone()
278 .ok_or(Error::AssetNotFound)?
279 .browser_download_url
280 .clone();
281
282 let response = request
283 .build()?
284 .get(download_url)
285 .headers(headers)
286 .send()
287 .await?;
288
289 if !response.status().is_success() {
290 return Err(Error::Network(format!(
291 "Download request failed with status: {}",
292 response.status()
293 )));
294 }
295
296 let mut buffer = Vec::new();
297
298 let mut stream = response.bytes_stream();
299 while let Some(chunk) = stream.next().await {
300 let chunk = chunk?;
301 on_chunk(chunk.len());
302 buffer.extend(chunk);
303 }
304 Ok(buffer)
305 }
306
307 pub fn install(&self, bytes: impl AsRef<[u8]>) -> Result<()> {
309 self.install_inner(bytes.as_ref())
310 }
311
312 pub fn relaunch(&self) -> Result<()> {
313 self.relaunch_inner()
314 }
315
316 pub async fn download_and_install<C: FnMut(usize)>(
318 &self,
319 on_chunk: C,
320 ) -> Result<()> {
322 let bytes = self.download(on_chunk).await?;
323 self.install(bytes)
324 }
325}