release_hub/
github.rs

1use crate::{Arch, BundleType, Error, OS, Result, SystemInfo};
2// GitHub release querying and asset selection utilities.
3//
4// This module wraps `octocrab` to fetch releases and provides a simplified
5// representation (`GitHubRelease`, `GitHubAsset`) plus helpers to select the
6// proper asset for the current platform.
7use octocrab::{
8    Octocrab,
9    models::repos::{Asset, Release},
10};
11use semver::Version;
12use url::Url;
13
14/// Minimal GitHub API client configured for a single repository.
15#[derive(Debug, Clone)]
16pub struct GitHubClient {
17    pub octocrab: Octocrab,
18    pub owner: String,
19    pub repo: String,
20}
21
22/// A single downloadable artifact from a GitHub release.
23#[derive(Debug, Clone)]
24pub struct GitHubAsset {
25    pub name: String,
26    pub os: OS,
27    pub arch: Arch,
28    pub browser_download_url: Url,
29    pub size: u64,
30    pub bundle_type: BundleType,
31}
32
33/// Simplified GitHub release information used by the updater.
34#[derive(Debug, Clone)]
35pub struct GitHubRelease {
36    /// Version to install.
37    pub version: Version,
38    /// Release name.
39    pub name: Option<String>,
40    /// Release notes.
41    pub note: Option<String>,
42    /// Release date.
43    pub published_at: Option<String>,
44    /// Assets.
45    pub assets: Vec<GitHubAsset>,
46}
47
48impl TryFrom<Release> for GitHubRelease {
49    type Error = Error;
50
51    fn try_from(release: Release) -> Result<Self> {
52        let version =
53            Version::parse(release.tag_name.trim_start_matches('v')).map_err(Error::Semver)?;
54
55        let assets = get_assets(release.assets)?;
56        Ok(GitHubRelease {
57            version,
58            name: release.name,
59            note: release.body,
60            published_at: release.published_at.map(|dt| dt.to_rfc3339()),
61            assets,
62        })
63    }
64}
65
66impl GitHubClient {
67    /// Create a new GitHub client for `owner/repo`.
68    pub fn new(owner: &str, repo: &str) -> Self {
69        let octocrab = Octocrab::default();
70        Self {
71            octocrab,
72            owner: owner.to_owned(),
73            repo: repo.to_owned(),
74        }
75    }
76
77    /// Get the latest GitHub release for the configured repository.
78    pub async fn get_latest_release(&self) -> Result<Release> {
79        Ok(self
80            .octocrab
81            .repos(&self.owner, &self.repo)
82            .releases()
83            .get_latest()
84            .await?)
85    }
86}
87
88pub fn find_proper_asset(release: &GitHubRelease) -> Result<GitHubAsset> {
89    release.find_proper_asset()
90}
91
92impl GitHubRelease {
93    /// Find the appropriate asset for the local OS/arch.
94    pub fn find_proper_asset(&self) -> Result<GitHubAsset> {
95        let system_info = SystemInfo::current()?;
96        let result = {
97            #[cfg(target_os = "windows")]
98            {
99                self.assets
100                    .iter()
101                    .find(|asset| {
102                        asset.os == system_info.os
103                            && asset.arch == system_info.arch
104                            && asset.bundle_type == BundleType::WindowsSetUp
105                    })
106                    .cloned()
107                    .ok_or(Error::AssetNotFound)?
108            }
109            #[cfg(target_os = "macos")]
110            {
111                self.assets
112                    .iter()
113                    .find(|asset| {
114                        asset.os == system_info.os
115                            && asset.arch == system_info.arch
116                            && asset.bundle_type == BundleType::MacOSAppZip
117                    })
118                    .cloned()
119                    .ok_or(Error::AssetNotFound)?
120            }
121        };
122        Ok(result)
123    }
124    /// The release's download URL for the asset matched to this platform.
125    pub fn download_url(&self) -> Result<Url> {
126        let asset = self.find_proper_asset()?;
127        Ok(asset.browser_download_url)
128    }
129}
130
131fn get_assets(assets: Vec<Asset>) -> Result<Vec<GitHubAsset>> {
132    assets
133        .into_iter()
134        .map(|asset| {
135            let name = asset.name.to_lowercase();
136            let os = if name.contains("macos") || name.contains("darwin") || name.contains("osx") {
137                OS::Macos
138            } else if name.contains("windows") || name.contains("win") {
139                OS::Windows
140            } else {
141                return Err(Error::TargetNotFound("macos or windows".into()));
142            };
143            let arch = if name.contains("x86_64") || name.contains("amd64") {
144                Arch::X86_64
145            } else if name.contains("aarch64") || name.contains("arm64") {
146                Arch::Arm64
147            } else {
148                return Err(Error::TargetNotFound("x86_64 or amd64".into()));
149            };
150            let bundle_type = if name.ends_with(".dmg") {
151                BundleType::MacOSDMG
152            } else if name.ends_with(".app.zip") {
153                BundleType::MacOSAppZip
154            } else if name.ends_with(".msi") {
155                BundleType::WindowsMSI
156            } else if name.ends_with(".exe") {
157                BundleType::WindowsSetUp
158            } else {
159                return Err(Error::TargetNotFound("os-arch".into()));
160            };
161            Ok(GitHubAsset {
162                name,
163                browser_download_url: asset.browser_download_url,
164                size: asset.size as u64,
165                os,
166                arch,
167                bundle_type,
168            })
169        })
170        .collect::<Result<Vec<_>>>()
171}
172
173#[cfg(test)]
174mod tests {
175    use super::*;
176
177    #[tokio::test]
178    async fn test_get_assets() {
179        let client = GitHubClient::new("tangxiangong", "bibcitex");
180        let release: GitHubRelease = client
181            .get_latest_release()
182            .await
183            .unwrap()
184            .try_into()
185            .unwrap();
186        println!("{:?}", release.assets);
187        println!("{:?}", release.find_proper_asset());
188    }
189}