11use 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 ) ]
68pub 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+
1429impl 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