1use std::env::{self, consts::EXE_SUFFIX};
5use std::path::{Path, PathBuf};
6
7use crate::http_client::{self, header, HeaderMap, HttpResponse};
8
9use crate::backends::find_rel_next_link;
10use crate::version::bump_is_greater;
11use crate::{
12 errors::*,
13 get_target,
14 update::{Release, ReleaseAsset, ReleaseUpdate},
15 DEFAULT_PROGRESS_CHARS, DEFAULT_PROGRESS_TEMPLATE,
16};
17
18impl ReleaseAsset {
19 fn from_asset(asset: &serde_json::Value) -> Result<ReleaseAsset> {
24 let download_url = asset["url"]
25 .as_str()
26 .ok_or_else(|| format_err!(Error::Release, "Asset missing `url`"))?;
27 let name = asset["name"]
28 .as_str()
29 .ok_or_else(|| format_err!(Error::Release, "Asset missing `name`"))?;
30 Ok(ReleaseAsset {
31 download_url: download_url.to_owned(),
32 name: name.to_owned(),
33 })
34 }
35}
36
37impl Release {
38 fn from_release(release: &serde_json::Value) -> Result<Release> {
39 let tag = release["tag_name"]
40 .as_str()
41 .ok_or_else(|| format_err!(Error::Release, "Release missing `tag_name`"))?;
42 let date = release["created_at"]
43 .as_str()
44 .ok_or_else(|| format_err!(Error::Release, "Release missing `created_at`"))?;
45 let name = release["name"].as_str().unwrap_or(tag);
46 let assets = release["assets"]
47 .as_array()
48 .ok_or_else(|| format_err!(Error::Release, "No assets found"))?;
49 let body = release["body"].as_str().map(String::from);
50 let assets = assets
51 .iter()
52 .map(ReleaseAsset::from_asset)
53 .collect::<Result<Vec<ReleaseAsset>>>()?;
54 Ok(Release {
55 name: name.to_owned(),
56 version: tag.trim_start_matches('v').to_owned(),
57 date: date.to_owned(),
58 body,
59 assets,
60 })
61 }
62}
63
64#[derive(Clone, Debug)]
66pub struct ReleaseListBuilder {
67 repo_owner: Option<String>,
68 repo_name: Option<String>,
69 target: Option<String>,
70 auth_token: Option<String>,
71 custom_url: Option<String>,
72}
73impl ReleaseListBuilder {
74 pub fn repo_owner(&mut self, owner: &str) -> &mut Self {
76 self.repo_owner = Some(owner.to_owned());
77 self
78 }
79
80 pub fn repo_name(&mut self, name: &str) -> &mut Self {
82 self.repo_name = Some(name.to_owned());
83 self
84 }
85
86 pub fn with_target(&mut self, target: &str) -> &mut Self {
88 self.target = Some(target.to_owned());
89 self
90 }
91
92 pub fn with_url(&mut self, url: &str) -> &mut Self {
96 self.custom_url = Some(url.to_owned());
97 self
98 }
99
100 pub fn auth_token(&mut self, auth_token: &str) -> &mut Self {
107 self.auth_token = Some(auth_token.to_owned());
108 self
109 }
110
111 pub fn build(&self) -> Result<ReleaseList> {
113 Ok(ReleaseList {
114 repo_owner: if let Some(ref owner) = self.repo_owner {
115 owner.to_owned()
116 } else {
117 bail!(Error::Config, "`repo_owner` required")
118 },
119 repo_name: if let Some(ref name) = self.repo_name {
120 name.to_owned()
121 } else {
122 bail!(Error::Config, "`repo_name` required")
123 },
124 target: self.target.clone(),
125 auth_token: self.auth_token.clone(),
126 custom_url: self.custom_url.clone(),
127 })
128 }
129}
130
131#[derive(Clone, Debug)]
134pub struct ReleaseList {
135 repo_owner: String,
136 repo_name: String,
137 target: Option<String>,
138 auth_token: Option<String>,
139 custom_url: Option<String>,
140}
141impl ReleaseList {
142 pub fn configure() -> ReleaseListBuilder {
144 ReleaseListBuilder {
145 repo_owner: None,
146 repo_name: None,
147 target: None,
148 auth_token: None,
149 custom_url: None,
150 }
151 }
152
153 pub fn fetch(self) -> Result<Vec<Release>> {
156 set_ssl_vars!();
157 let api_url = format!(
158 "{}/repos/{}/{}/releases",
159 self.custom_url
160 .as_ref()
161 .unwrap_or(&"https://api.github.com".to_string()),
162 self.repo_owner,
163 self.repo_name
164 );
165 let releases = self.fetch_releases(&api_url)?;
166 let releases = match self.target {
167 None => releases,
168 Some(ref target) => releases
169 .into_iter()
170 .filter(|r| r.has_target_asset(target))
171 .collect::<Vec<_>>(),
172 };
173 Ok(releases)
174 }
175
176 fn fetch_releases(&self, url: &str) -> Result<Vec<Release>> {
177 let resp = http_client::get(
178 &format!("{url}?per_page=100"),
179 api_headers(&self.auth_token)?,
180 )?;
181 if !resp.status().is_success() {
182 bail!(
183 Error::Network,
184 "api request failed with status: {:?} - for: {:?}",
185 resp.status(),
186 url
187 )
188 }
189 let headers = resp.headers().clone();
190
191 let releases = resp.json::<serde_json::Value>()?;
192 let releases = releases
193 .as_array()
194 .ok_or_else(|| format_err!(Error::Release, "No releases found"))?;
195 let mut releases = releases
196 .iter()
197 .map(Release::from_release)
198 .collect::<Result<Vec<Release>>>()?;
199
200 let links = headers.get_all(header::LINK);
203
204 let next_link = links
205 .iter()
206 .filter_map(|link| {
207 if let Ok(link) = link.to_str() {
208 find_rel_next_link(link)
209 } else {
210 None
211 }
212 })
213 .next();
214
215 Ok(match next_link {
216 None => releases,
217 Some(link) => {
218 releases.extend(self.fetch_releases(link)?);
219 releases
220 }
221 })
222 }
223}
224
225#[derive(Debug)]
230pub struct UpdateBuilder {
231 repo_owner: Option<String>,
232 repo_name: Option<String>,
233 target: Option<String>,
234 identifier: Option<String>,
235 bin_name: Option<String>,
236 bin_install_path: Option<PathBuf>,
237 bin_path_in_archive: Option<String>,
238 show_download_progress: bool,
239 show_output: bool,
240 no_confirm: bool,
241 current_version: Option<String>,
242 target_version: Option<String>,
243 progress_template: String,
244 progress_chars: String,
245 auth_token: Option<String>,
246 custom_url: Option<String>,
247 #[cfg(feature = "signatures")]
248 verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>,
249}
250
251impl UpdateBuilder {
252 pub fn new() -> Self {
254 Default::default()
255 }
256
257 pub fn repo_owner(&mut self, owner: &str) -> &mut Self {
259 self.repo_owner = Some(owner.to_owned());
260 self
261 }
262
263 pub fn repo_name(&mut self, name: &str) -> &mut Self {
265 self.repo_name = Some(name.to_owned());
266 self
267 }
268
269 pub fn with_url(&mut self, url: &str) -> &mut Self {
273 self.custom_url = Some(url.to_owned());
274 self
275 }
276
277 pub fn current_version(&mut self, ver: &str) -> &mut Self {
280 self.current_version = Some(ver.to_owned());
281 self
282 }
283
284 pub fn target_version_tag(&mut self, ver: &str) -> &mut Self {
290 self.target_version = Some(ver.to_owned());
291 self
292 }
293
294 pub fn target(&mut self, target: &str) -> &mut Self {
298 self.target = Some(target.to_owned());
299 self
300 }
301
302 pub fn identifier(&mut self, identifier: &str) -> &mut Self {
306 self.identifier = Some(identifier.to_owned());
307 self
308 }
309
310 pub fn bin_name(&mut self, name: &str) -> &mut Self {
315 let raw_bin_name = format!("{}{}", name.trim_end_matches(EXE_SUFFIX), EXE_SUFFIX);
316 if self.bin_path_in_archive.is_none() {
317 self.bin_path_in_archive = Some(raw_bin_name.clone());
318 }
319 self.bin_name = Some(raw_bin_name);
320 self
321 }
322
323 pub fn bin_install_path<A: AsRef<Path>>(&mut self, bin_install_path: A) -> &mut Self {
326 self.bin_install_path = Some(PathBuf::from(bin_install_path.as_ref()));
327 self
328 }
329
330 pub fn bin_path_in_archive(&mut self, bin_path: &str) -> &mut Self {
366 self.bin_path_in_archive = Some(bin_path.to_owned());
367 self
368 }
369
370 pub fn show_download_progress(&mut self, show: bool) -> &mut Self {
372 self.show_download_progress = show;
373 self
374 }
375
376 pub fn set_progress_style(
378 &mut self,
379 progress_template: String,
380 progress_chars: String,
381 ) -> &mut Self {
382 self.progress_template = progress_template;
383 self.progress_chars = progress_chars;
384 self
385 }
386
387 pub fn show_output(&mut self, show: bool) -> &mut Self {
389 self.show_output = show;
390 self
391 }
392
393 pub fn no_confirm(&mut self, no_confirm: bool) -> &mut Self {
395 self.no_confirm = no_confirm;
396 self
397 }
398
399 pub fn auth_token(&mut self, auth_token: &str) -> &mut Self {
406 self.auth_token = Some(auth_token.to_owned());
407 self
408 }
409
410 #[cfg(feature = "signatures")]
415 pub fn verifying_keys(
416 &mut self,
417 keys: impl Into<Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>>,
418 ) -> &mut Self {
419 self.verifying_keys = keys.into();
420 self
421 }
422
423 pub fn build(&self) -> Result<Box<dyn ReleaseUpdate>> {
428 let bin_install_path = if let Some(v) = &self.bin_install_path {
429 v.clone()
430 } else {
431 env::current_exe()?
432 };
433
434 Ok(Box::new(Update {
435 repo_owner: if let Some(ref owner) = self.repo_owner {
436 owner.to_owned()
437 } else {
438 bail!(Error::Config, "`repo_owner` required")
439 },
440 repo_name: if let Some(ref name) = self.repo_name {
441 name.to_owned()
442 } else {
443 bail!(Error::Config, "`repo_name` required")
444 },
445 target: self
446 .target
447 .as_ref()
448 .map(|t| t.to_owned())
449 .unwrap_or_else(|| get_target().to_owned()),
450 identifier: self.identifier.clone(),
451 bin_name: if let Some(ref name) = self.bin_name {
452 name.to_owned()
453 } else {
454 bail!(Error::Config, "`bin_name` required")
455 },
456 bin_install_path,
457 bin_path_in_archive: if let Some(ref bin_path) = self.bin_path_in_archive {
458 bin_path.to_owned()
459 } else {
460 bail!(Error::Config, "`bin_path_in_archive` required")
461 },
462 current_version: if let Some(ref ver) = self.current_version {
463 ver.to_owned()
464 } else {
465 bail!(Error::Config, "`current_version` required")
466 },
467 target_version: self.target_version.as_ref().map(|v| v.to_owned()),
468 show_download_progress: self.show_download_progress,
469 progress_template: self.progress_template.clone(),
470 progress_chars: self.progress_chars.clone(),
471 show_output: self.show_output,
472 no_confirm: self.no_confirm,
473 auth_token: self.auth_token.clone(),
474 custom_url: self.custom_url.clone(),
475 #[cfg(feature = "signatures")]
476 verifying_keys: self.verifying_keys.clone(),
477 }))
478 }
479}
480
481#[derive(Debug)]
483pub struct Update {
484 repo_owner: String,
485 repo_name: String,
486 target: String,
487 identifier: Option<String>,
488 current_version: String,
489 target_version: Option<String>,
490 bin_name: String,
491 bin_install_path: PathBuf,
492 bin_path_in_archive: String,
493 show_download_progress: bool,
494 show_output: bool,
495 no_confirm: bool,
496 progress_template: String,
497 progress_chars: String,
498 auth_token: Option<String>,
499 custom_url: Option<String>,
500 #[cfg(feature = "signatures")]
501 verifying_keys: Vec<[u8; zipsign_api::PUBLIC_KEY_LENGTH]>,
502}
503impl Update {
504 pub fn configure() -> UpdateBuilder {
506 UpdateBuilder::new()
507 }
508}
509
510impl ReleaseUpdate for Update {
511 fn get_latest_release(&self) -> Result<Release> {
512 set_ssl_vars!();
513 let api_url = format!(
514 "{}/repos/{}/{}/releases/latest",
515 self.custom_url
516 .as_ref()
517 .unwrap_or(&"https://api.github.com".to_string()),
518 self.repo_owner,
519 self.repo_name
520 );
521 let resp = http_client::get(&api_url, api_headers(&self.auth_token)?)?;
522 if !resp.status().is_success() {
523 bail!(
524 Error::Network,
525 "api request failed with status: {:?} - for: {:?}",
526 resp.status(),
527 api_url
528 )
529 }
530 let json = resp.json::<serde_json::Value>()?;
531 Release::from_release(&json)
532 }
533
534 fn get_latest_releases(&self, current_version: &str) -> Result<Vec<Release>> {
535 set_ssl_vars!();
536 let api_url = format!(
537 "{}/repos/{}/{}/releases",
538 self.custom_url
539 .as_ref()
540 .unwrap_or(&"https://api.github.com".to_string()),
541 self.repo_owner,
542 self.repo_name
543 );
544 let resp = http_client::get(&api_url, api_headers(&self.auth_token)?)?;
545 if !resp.status().is_success() {
546 bail!(
547 Error::Network,
548 "api request failed with status: {:?} - for: {:?}",
549 resp.status(),
550 api_url
551 )
552 }
553
554 let json = resp.json::<serde_json::Value>()?;
555 json.as_array()
556 .ok_or_else(|| format_err!(Error::Release, "No releases found"))
557 .and_then(|releases| {
558 releases
559 .iter()
560 .map(Release::from_release)
561 .filter(|r| {
562 r.as_ref().map_or(false, |r| {
563 bump_is_greater(current_version, &r.version).unwrap_or(false)
564 })
565 })
566 .collect::<Result<Vec<Release>>>()
567 })
568 }
569
570 fn get_release_version(&self, ver: &str) -> Result<Release> {
571 set_ssl_vars!();
572 let api_url = format!(
573 "{}/repos/{}/{}/releases/tags/{}",
574 self.custom_url
575 .as_ref()
576 .unwrap_or(&"https://api.github.com".to_string()),
577 self.repo_owner,
578 self.repo_name,
579 ver
580 );
581 let resp = http_client::get(&api_url, api_headers(&self.auth_token)?)?;
582 if !resp.status().is_success() {
583 bail!(
584 Error::Network,
585 "api request failed with status: {:?} - for: {:?}",
586 resp.status(),
587 api_url
588 )
589 }
590 let json = resp.json::<serde_json::Value>()?;
591 Release::from_release(&json)
592 }
593
594 fn current_version(&self) -> String {
595 self.current_version.to_owned()
596 }
597
598 fn target(&self) -> String {
599 self.target.clone()
600 }
601
602 fn target_version(&self) -> Option<String> {
603 self.target_version.clone()
604 }
605
606 fn identifier(&self) -> Option<String> {
607 self.identifier.clone()
608 }
609
610 fn bin_name(&self) -> String {
611 self.bin_name.clone()
612 }
613
614 fn bin_install_path(&self) -> PathBuf {
615 self.bin_install_path.clone()
616 }
617
618 fn bin_path_in_archive(&self) -> String {
619 self.bin_path_in_archive.clone()
620 }
621
622 fn show_download_progress(&self) -> bool {
623 self.show_download_progress
624 }
625
626 fn show_output(&self) -> bool {
627 self.show_output
628 }
629
630 fn no_confirm(&self) -> bool {
631 self.no_confirm
632 }
633
634 fn progress_template(&self) -> String {
635 self.progress_template.to_owned()
636 }
637
638 fn progress_chars(&self) -> String {
639 self.progress_chars.to_owned()
640 }
641
642 fn auth_token(&self) -> Option<String> {
643 self.auth_token.clone()
644 }
645
646 fn api_headers(&self, auth_token: &Option<String>) -> Result<HeaderMap> {
647 api_headers(auth_token)
648 }
649
650 #[cfg(feature = "signatures")]
651 fn verifying_keys(&self) -> &[[u8; zipsign_api::PUBLIC_KEY_LENGTH]] {
652 &self.verifying_keys
653 }
654}
655
656impl Default for UpdateBuilder {
657 fn default() -> Self {
658 Self {
659 repo_owner: None,
660 repo_name: None,
661 target: None,
662 identifier: None,
663 bin_name: None,
664 bin_install_path: None,
665 bin_path_in_archive: None,
666 show_download_progress: false,
667 show_output: true,
668 no_confirm: false,
669 current_version: None,
670 target_version: None,
671 progress_template: DEFAULT_PROGRESS_TEMPLATE.to_string(),
672 progress_chars: DEFAULT_PROGRESS_CHARS.to_string(),
673 auth_token: None,
674 custom_url: None,
675 #[cfg(feature = "signatures")]
676 verifying_keys: vec![],
677 }
678 }
679}
680
681fn api_headers(auth_token: &Option<String>) -> Result<header::HeaderMap> {
682 let mut headers = header::HeaderMap::new();
683 headers.insert(
684 header::USER_AGENT,
685 "rust/self-update"
686 .parse()
687 .expect("github invalid user-agent"),
688 );
689
690 if let Some(token) = auth_token {
691 headers.insert(
692 header::AUTHORIZATION,
693 format!("token {}", token)
694 .parse()
695 .map_err(|err| Error::Config(format!("Failed to parse auth token: {}", err)))?,
696 );
697 };
698
699 Ok(headers)
700}