1use crate::{Arch, BundleType, Error, OS, Result, SystemInfo};
2use octocrab::{
8 Octocrab,
9 models::repos::{Asset, Release},
10};
11use semver::Version;
12use url::Url;
13
14#[derive(Debug, Clone)]
16pub struct GitHubClient {
17 pub octocrab: Octocrab,
18 pub owner: String,
19 pub repo: String,
20}
21
22#[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#[derive(Debug, Clone)]
35pub struct GitHubRelease {
36 pub version: Version,
38 pub name: Option<String>,
40 pub note: Option<String>,
42 pub published_at: Option<String>,
44 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 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 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 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 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}