Skip to content

Commit 1c75557

Browse files
committed
feat: implement GitHub releases URL resolution
- Support 'latest', 'latest-pre', 'latest-all', and specific tags - Match assets using glob patterns - Cache resolved URL with OnceCell
1 parent 5ea196e commit 1c75557

File tree

1 file changed

+116
-2
lines changed

1 file changed

+116
-2
lines changed

src/update_info/github.rs

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::cell::OnceCell;
22

3-
use crate::error::Result;
3+
use serde::Deserialize;
4+
5+
use crate::error::{Error, Result};
46

57
#[derive(Debug, Clone)]
68
pub struct GitHubUpdateInfo {
@@ -11,6 +13,19 @@ pub struct GitHubUpdateInfo {
1113
resolved_url: OnceCell<String>,
1214
}
1315

16+
#[derive(Debug, Clone, Deserialize)]
17+
struct GitHubRelease {
18+
assets: Vec<GitHubAsset>,
19+
#[serde(default)]
20+
prerelease: bool,
21+
}
22+
23+
#[derive(Debug, Clone, Deserialize)]
24+
struct GitHubAsset {
25+
name: String,
26+
browser_download_url: String,
27+
}
28+
1429
impl GitHubUpdateInfo {
1530
pub fn new(username: String, repo: String, tag: String, filename: String) -> Self {
1631
Self {
@@ -31,6 +46,105 @@ impl GitHubUpdateInfo {
3146
}
3247

3348
fn resolve_url(&self) -> Result<String> {
34-
todo!("Implement GitHub API call to resolve zsync URL")
49+
let api_url = match self.tag.as_str() {
50+
"latest" => format!(
51+
"https://api.github.com/repos/{}/{}/releases/latest",
52+
self.username, self.repo
53+
),
54+
"latest-pre" | "latest-all" => format!(
55+
"https://api.github.com/repos/{}/{}/releases",
56+
self.username, self.repo
57+
),
58+
tag => format!(
59+
"https://api.github.com/repos/{}/{}/releases/tags/{}",
60+
self.username, self.repo, tag
61+
),
62+
};
63+
64+
let response = ureq::get(&api_url)
65+
.header("User-Agent", "appimageupdate-rs")
66+
.call()
67+
.map_err(|e| Error::Http(format!("GitHub API request failed: {}", e)))?;
68+
69+
if !response.status().is_success() {
70+
return Err(Error::GitHubApi(format!(
71+
"GitHub API returned status {}",
72+
response.status()
73+
)));
74+
}
75+
76+
let release: GitHubRelease = serde_json::from_reader(response.into_body().into_reader())
77+
.map_err(|e| Error::GitHubApi(format!("Failed to parse GitHub response: {}", e)))?;
78+
79+
let release = if self.tag == "latest-pre" || self.tag == "latest-all" {
80+
let releases: Vec<GitHubRelease> = vec![release];
81+
Self::find_suitable_release(&releases, self.tag == "latest-pre")?
82+
} else {
83+
release
84+
};
85+
86+
let asset_url = Self::find_matching_asset(&release, &self.filename)?;
87+
88+
Ok(format!("{}.zsync", asset_url))
89+
}
90+
91+
fn find_suitable_release(
92+
releases: &[GitHubRelease],
93+
prereleases_only: bool,
94+
) -> Result<GitHubRelease> {
95+
if prereleases_only {
96+
releases
97+
.iter()
98+
.find(|r| r.prerelease)
99+
.cloned()
100+
.ok_or_else(|| Error::GitHubApi("No prerelease found".into()))
101+
} else {
102+
releases
103+
.first()
104+
.cloned()
105+
.ok_or_else(|| Error::GitHubApi("No release found".into()))
106+
}
107+
}
108+
109+
fn find_matching_asset(release: &GitHubRelease, filename_pattern: &str) -> Result<String> {
110+
let pattern = format!("*{}", filename_pattern);
111+
112+
let mut matching_urls: Vec<String> = release
113+
.assets
114+
.iter()
115+
.filter(|asset| Self::glob_match(&pattern, &asset.name))
116+
.map(|asset| asset.browser_download_url.clone())
117+
.collect();
118+
119+
if matching_urls.is_empty() {
120+
return Err(Error::GitHubApi(format!(
121+
"No asset matched pattern: {}",
122+
pattern
123+
)));
124+
}
125+
126+
matching_urls.sort();
127+
matching_urls.reverse();
128+
129+
Ok(matching_urls.remove(0))
130+
}
131+
132+
fn glob_match(pattern: &str, text: &str) -> bool {
133+
let pattern_chars: Vec<char> = pattern.chars().collect();
134+
let text_chars: Vec<char> = text.chars().collect();
135+
Self::glob_match_recursive(&pattern_chars, &text_chars)
136+
}
137+
138+
fn glob_match_recursive(pattern: &[char], text: &[char]) -> bool {
139+
match (pattern.first(), text.first()) {
140+
(None, None) => true,
141+
(Some('*'), _) => {
142+
Self::glob_match_recursive(pattern, &text[1..])
143+
|| Self::glob_match_recursive(&pattern[1..], text)
144+
}
145+
(Some(p), Some(t)) if *p == *t => Self::glob_match_recursive(&pattern[1..], &text[1..]),
146+
(Some('?'), Some(_)) => Self::glob_match_recursive(&pattern[1..], &text[1..]),
147+
_ => false,
148+
}
35149
}
36150
}

0 commit comments

Comments
 (0)