Skip to main content

self_update/backends/
github.rs

1/*!
2GitHub releases
3*/
4use 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    /// Parse a release-asset json object
20    ///
21    /// Errors:
22    ///     * Missing required name & download-url keys
23    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/// `ReleaseList` Builder
65#[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    /// Set the repo owner, used to build a github api url
75    pub fn repo_owner(&mut self, owner: &str) -> &mut Self {
76        self.repo_owner = Some(owner.to_owned());
77        self
78    }
79
80    /// Set the repo name, used to build a github api url
81    pub fn repo_name(&mut self, name: &str) -> &mut Self {
82        self.repo_name = Some(name.to_owned());
83        self
84    }
85
86    /// Set the optional arch `target` name, used to filter available releases
87    pub fn with_target(&mut self, target: &str) -> &mut Self {
88        self.target = Some(target.to_owned());
89        self
90    }
91
92    /// Set the optional github url, e.g. for a github enterprise installation.
93    /// The url should provide the path to your API endpoint and end without a trailing slash,
94    /// for example `https://api.github.com` or `https://github.mycorp.com/api/v3`
95    pub fn with_url(&mut self, url: &str) -> &mut Self {
96        self.custom_url = Some(url.to_owned());
97        self
98    }
99
100    /// Set the authorization token, used in requests to the github api url
101    ///
102    /// This is to support private repos where you need a GitHub auth token.
103    /// **Make sure not to bake the token into your app**; it is recommended
104    /// you obtain it via another mechanism, such as environment variables
105    /// or prompting the user for input
106    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    /// Verify builder args, returning a `ReleaseList`
112    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/// `ReleaseList` provides a builder api for querying a GitHub repo,
132/// returning a `Vec` of available `Release`s
133#[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    /// Initialize a ReleaseListBuilder
143    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    /// Retrieve a list of `Release`s.
154    /// If specified, filter for those containing a specified `target`
155    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        // handle paged responses containing `Link` header:
201        // `Link: <https://api.github.com/resource?page=2>; rel="next"`
202        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/// `github::Update` builder
226///
227/// Configure download and installation from
228/// `https://api.github.com/repos/<repo_owner>/<repo_name>/releases/latest`
229#[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    /// Initialize a new builder
253    pub fn new() -> Self {
254        Default::default()
255    }
256
257    /// Set the repo owner, used to build a github api url
258    pub fn repo_owner(&mut self, owner: &str) -> &mut Self {
259        self.repo_owner = Some(owner.to_owned());
260        self
261    }
262
263    /// Set the repo name, used to build a github api url
264    pub fn repo_name(&mut self, name: &str) -> &mut Self {
265        self.repo_name = Some(name.to_owned());
266        self
267    }
268
269    /// Set the optional github url, e.g. for a github enterprise installation.
270    /// The url should provide the path to your API endpoint and end without a trailing slash,
271    /// for example `https://api.github.com` or `https://github.mycorp.com/api/v3`
272    pub fn with_url(&mut self, url: &str) -> &mut Self {
273        self.custom_url = Some(url.to_owned());
274        self
275    }
276
277    /// Set the current app version, used to compare against the latest available version.
278    /// The `cargo_crate_version!` macro can be used to pull the version from your `Cargo.toml`
279    pub fn current_version(&mut self, ver: &str) -> &mut Self {
280        self.current_version = Some(ver.to_owned());
281        self
282    }
283
284    /// Set the target version tag to update to. This will be used to search for a release
285    /// by tag name:
286    /// `/repos/:owner/:repo/releases/tags/:tag`
287    ///
288    /// If not specified, the latest available release is used.
289    pub fn target_version_tag(&mut self, ver: &str) -> &mut Self {
290        self.target_version = Some(ver.to_owned());
291        self
292    }
293
294    /// Set the target triple that will be downloaded, e.g. `x86_64-unknown-linux-gnu`.
295    ///
296    /// If unspecified, the build target of the crate will be used
297    pub fn target(&mut self, target: &str) -> &mut Self {
298        self.target = Some(target.to_owned());
299        self
300    }
301
302    /// Set the identifiable token for the asset in case of multiple compatible assets
303    ///
304    /// If unspecified, the first asset matching the target will be chosen
305    pub fn identifier(&mut self, identifier: &str) -> &mut Self {
306        self.identifier = Some(identifier.to_owned());
307        self
308    }
309
310    /// Set the exe's name. Also sets `bin_path_in_archive` if it hasn't already been set.
311    ///
312    /// This method will append the platform specific executable file suffix
313    /// (see `std::env::consts::EXE_SUFFIX`) to the name if it's missing.
314    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    /// Set the installation path for the new exe, defaults to the current
324    /// executable's path
325    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    /// Set the path of the exe inside the release tarball. This is the location
331    /// of the executable relative to the base of the tar'd directory and is the
332    /// path that will be copied to the `bin_install_path`. If not specified, this
333    /// will default to the value of `bin_name`. This only needs to be specified if
334    /// the path to the binary (from the root of the tarball) is not equal to just
335    /// the `bin_name`.
336    ///
337    /// This also supports variable paths:
338    /// - `{{ bin }}` is replaced with the value of `bin_name`
339    /// - `{{ target }}` is replaced with the value of `target`
340    /// - `{{ version }}` is replaced with the value of `target_version` if set,
341    /// otherwise the value of the latest available release version is used.
342    ///
343    /// # Example
344    ///
345    /// For a `myapp` binary with `windows` target and latest release version `1.2.3`,
346    /// the tarball `myapp.tar.gz` has the contents:
347    ///
348    /// ```shell
349    /// myapp.tar/
350    ///  |------- windows-1.2.3-bin/
351    ///  |         |--- myapp  # <-- executable
352    /// ```
353    ///
354    /// The path provided should be:
355    ///
356    /// ```
357    /// # use self_update::backends::github::Update;
358    /// # fn run() -> Result<(), Box<::std::error::Error>> {
359    /// Update::configure()
360    ///     .bin_path_in_archive("{{ target }}-{{ version }}-bin/{{ bin }}")
361    /// #   .build()?;
362    /// # Ok(())
363    /// # }
364    /// ```
365    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    /// Toggle download progress bar, defaults to `off`.
371    pub fn show_download_progress(&mut self, show: bool) -> &mut Self {
372        self.show_download_progress = show;
373        self
374    }
375
376    /// Set download progress style.
377    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    /// Toggle update output information, defaults to `true`.
388    pub fn show_output(&mut self, show: bool) -> &mut Self {
389        self.show_output = show;
390        self
391    }
392
393    /// Toggle download confirmation. Defaults to `false`.
394    pub fn no_confirm(&mut self, no_confirm: bool) -> &mut Self {
395        self.no_confirm = no_confirm;
396        self
397    }
398
399    /// Set the authorization token, used in requests to the github api url
400    ///
401    /// This is to support private repos where you need a GitHub auth token.
402    /// **Make sure not to bake the token into your app**; it is recommended
403    /// you obtain it via another mechanism, such as environment variables
404    /// or prompting the user for input
405    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    /// Specify a slice of ed25519ph verifying keys to validate a download's authenticity
411    ///
412    /// If the feature is activated AND at least one key was provided, a download is verifying.
413    /// At least one key has to match.
414    #[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    /// Confirm config and create a ready-to-use `Update`
424    ///
425    /// * Errors:
426    ///     * Config - Invalid `Update` configuration
427    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/// Updates to a specified or latest release distributed via GitHub
482#[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    /// Initialize a new `Update` builder
505    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}