autoupdater/apis/
github.rs

1use std::{cmp::Ordering, fmt::Display};
2
3use serde::Deserialize;
4
5use crate::{error::Error, ReleaseAsset};
6
7use super::{DownloadApiTrait, SimpleTag};
8
9#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Clone)]
10pub struct GithubAsset {
11    pub name: String,
12    pub url: String,
13}
14
15impl Display for GithubAsset {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        writeln!(f, "Name: {}", self.name)?;
18        writeln!(f, "Name: {}", self.url)
19    }
20}
21
22impl ReleaseAsset for GithubAsset {
23    fn get_name(&self) -> &str {
24        &self.name
25    }
26
27    fn get_download_url(&self) -> &str {
28        &self.url
29    }
30
31    fn download(
32        &self,
33        additional_headers: Vec<(&str, &str)>,
34        download_callback: Option<impl Fn(f32)>,
35    ) -> Result<(), Error> {
36        crate::download(self, additional_headers, download_callback)
37    }
38}
39
40#[derive(Debug, PartialEq, Eq, Hash, Deserialize, Clone)]
41pub struct GithubRelease {
42    pub tag_name: String,
43    pub target_commitish: String,
44    pub name: String,
45    pub prerelease: bool,
46    pub assets: Vec<GithubAsset>,
47    pub body: String,
48}
49
50impl Display for GithubRelease {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        writeln!(f, "Tag: {}", self.tag_name)?;
53        writeln!(f, "Branch: {}", self.target_commitish)?;
54        writeln!(f, "Name: {}", self.name)?;
55        writeln!(f, "Prerelease: {}", self.prerelease)?;
56        writeln!(f, "Assets:")?;
57        for asset in &self.assets {
58            writeln!(f, "{}", asset)?;
59        }
60
61        Ok(())
62    }
63}
64
65#[derive(Debug, PartialEq, Eq, Hash)]
66pub struct GithubApi {
67    api_url: Option<String>,
68    owner: String,
69    repo: String,
70    auth_token: Option<String>,
71    branch: Option<String>,
72    prerelease: bool,
73    specific_tag: Option<String>,
74    current_version: Option<String>,
75    asset_name: Option<String>,
76}
77
78impl GithubApi {
79    pub fn new(owner: &str, repo: &str) -> Self {
80        GithubApi {
81            api_url: None,
82            owner: owner.to_string(),
83            repo: repo.to_string(),
84            auth_token: None,
85            branch: None,
86            prerelease: false,
87            specific_tag: None,
88            current_version: None,
89            asset_name: None,
90        }
91    }
92
93    /// Sets custom github api url
94    pub fn api_url(mut self, api_url: &str) -> Self {
95        self.api_url = Some(api_url.to_string());
96        self
97    }
98
99    /// Sets auth token.
100    pub fn auth_token(mut self, auth_token: &str) -> Self {
101        self.auth_token = Some(format!("auth {auth_token}"));
102        self
103    }
104
105    /// Sets branch from which to get releases.
106    pub fn branch(mut self, branch: &str) -> Self {
107        self.branch = Some(branch.to_string());
108        self
109    }
110
111    /// Sets if prerelease should be included in the list of releases.
112    pub fn prerelease(mut self, prerelease: bool) -> Self {
113        self.prerelease = prerelease;
114        self
115    }
116
117    /// Sets specific version tag to get.
118    pub fn specific_tag(mut self, specific_tag: &str) -> Self {
119        self.specific_tag = Some(specific_tag.to_string());
120        self
121    }
122
123    /// Sets current version of the application, this is used to determine if the latest release is newer than the current version.
124    pub fn current_version(mut self, current_version: &str) -> Self {
125        self.current_version = Some(current_version.to_string());
126        self
127    }
128
129    /// Sets asset name to download.
130    pub fn asset_name(mut self, asset_name: &str) -> Self {
131        self.asset_name = Some(asset_name.to_string());
132        self
133    }
134
135    fn get_releases(&self, per_page: i32, page: i32) -> Result<Vec<GithubRelease>, Error> {
136        let api_url = format!(
137            "https://{}/repos/{}/{}/releases?per_page={}&page={}",
138            self.api_url.as_deref().unwrap_or("api.github.com"),
139            self.owner,
140            self.repo,
141            per_page,
142            page
143        );
144
145        let mut request = ureq::get(&api_url).set("user-agent", "rust-ureq/updater");
146        if let Some(token) = &self.auth_token {
147            request = request.set("authorization", token);
148        }
149
150        let release_list: Vec<GithubRelease> = request.call()?.into_json()?;
151        Ok(release_list)
152    }
153
154    fn filter_release(&self, e: &GithubRelease) -> bool {
155        if !self.prerelease && e.prerelease {
156            return false;
157        }
158        let specific_tag = match self.specific_tag {
159            Some(ref tag) => *tag == e.tag_name,
160            None => true,
161        };
162
163        let branch = match self.branch {
164            Some(ref branch) => *branch == e.target_commitish,
165            None => true,
166        };
167
168        let asset_name = match self.asset_name {
169            Some(ref asset_name) => e.assets.iter().any(|e| e.name == *asset_name),
170            None => true,
171        };
172
173        specific_tag && branch && asset_name
174    }
175
176    fn match_releases<'releases>(
177        &self,
178        releases: &'releases [GithubRelease],
179    ) -> Vec<&'releases GithubRelease> {
180        releases.iter().filter(|e| self.filter_release(e)).collect()
181    }
182
183    /// Gets the latest release
184    pub fn send(
185        &self,
186        sort_func: Option<&impl Fn(&str, &str) -> Ordering>,
187    ) -> Result<GithubRelease, Error> {
188        let mut releases = self.get_releases(100, 1)?;
189
190        let mut page = 3;
191        let mut new_releases = self.get_releases(100, 2)?;
192        while !new_releases.is_empty() {
193            releases.extend(new_releases);
194            new_releases = self.get_releases(100, page)?;
195            page += 1;
196        }
197
198        let mut matching = self.match_releases(&releases);
199        if matching.is_empty() {
200            return Err(Error::NoRelease);
201        }
202
203        match sort_func {
204            Some(sort_func) => {
205                matching.sort_by(|a, b| sort_func(&a.tag_name, &b.tag_name));
206            }
207            None => matching.sort_by(|a, b| SimpleTag::simple_compare(&a.tag_name, &b.tag_name)),
208        };
209
210        let latest_release = matching.last().ok_or(Error::NoRelease)?;
211        Ok((*latest_release).clone())
212    }
213
214    /// Gets the latest release
215    pub fn get_latest(
216        &self,
217        sort_func: Option<&impl Fn(&str, &str) -> Ordering>,
218    ) -> Result<GithubRelease, Error> {
219        self.send(sort_func)
220    }
221
222    /// Gets all releases
223    /// NOTE: this is kinda slow so use it only if you need it
224    pub fn get_all(&self) -> Result<Vec<GithubRelease>, Error> {
225        let mut releases = Vec::new();
226        let mut page = 1;
227        loop {
228            let fetched_releases = self.get_releases(100, page)?;
229            if fetched_releases.is_empty() {
230                break;
231            }
232
233            releases.extend(fetched_releases);
234            page += 1;
235        }
236
237        Ok(releases
238            .into_iter()
239            .filter(|e| self.filter_release(e))
240            .collect::<Vec<_>>())
241    }
242
243    /// Gets the newest release if the newest release is newer than the current one.
244    ///
245    /// sort_func is used to compare two release versions if the format is not x.y.z
246    pub fn get_newer(
247        &self,
248        sort_func: Option<&impl Fn(&str, &str) -> Ordering>,
249    ) -> Result<Option<GithubRelease>, Error> {
250        let latest_release = self.send(sort_func)?;
251        let is_newer = match self.current_version {
252            Some(ref current_version) => {
253                let newer = match sort_func {
254                    Some(sort_func) => sort_func(&latest_release.tag_name, current_version),
255                    None => SimpleTag::simple_compare(&latest_release.tag_name, current_version),
256                };
257
258                newer == Ordering::Greater
259            }
260            None => true,
261        };
262
263        if is_newer {
264            Ok(Some(latest_release))
265        } else {
266            Ok(None)
267        }
268    }
269}
270
271impl DownloadApiTrait for GithubApi {
272    fn download<Asset: ReleaseAsset>(
273        &self,
274        asset: &Asset,
275        download_callback: Option<impl Fn(f32)>,
276    ) -> Result<(), Error> {
277        let mut headers = Vec::new();
278
279        if let Some(token) = self.auth_token.as_deref() {
280            headers.push(("authorization", token));
281        }
282
283        asset.download(headers, download_callback)
284    }
285}