@@ -103,6 +103,70 @@ function githubLatestAssetUrl(fileName) {
103103 return `https://github.com/${ UPDATE_GITHUB_OWNER } /${ UPDATE_GITHUB_REPO } /releases/latest/download/${ enc } ` ;
104104}
105105
106+ const GITHUB_API_HEADERS = {
107+ Accept : "application/vnd.github+json" ,
108+ "User-Agent" : "HyperlinksSpaceApp/electron-updater" ,
109+ } ;
110+
111+ /**
112+ * When /releases/latest/download/<name>.zip returns 404, find the portable zip on the latest release
113+ * via the GitHub API (asset names may differ slightly from artifactName).
114+ * @returns {Promise<string|null> } browser_download_url or null
115+ */
116+ async function fetchPortableZipBrowserUrlFromGitHubApi ( netFetch , version , preferredFileName ) {
117+ const apiUrl = `https://api.github.com/repos/${ UPDATE_GITHUB_OWNER } /${ UPDATE_GITHUB_REPO } /releases/latest` ;
118+ const res = await netFetch ( apiUrl , { headers : GITHUB_API_HEADERS } ) ;
119+ if ( ! res . ok ) {
120+ log ( `[updater] GitHub API GET releases/latest: HTTP ${ res . status } ` ) ;
121+ return null ;
122+ }
123+ let data ;
124+ try {
125+ data = await res . json ( ) ;
126+ } catch ( _ ) {
127+ return null ;
128+ }
129+ const assets = Array . isArray ( data . assets ) ? data . assets : [ ] ;
130+ const zips = assets . filter ( ( a ) => a && typeof a . name === "string" && / \. z i p $ / i. test ( a . name ) ) ;
131+ const skipName = ( n ) =>
132+ / b l o c k m a p | \. 7 z \. | \. d e l t a / i. test ( n ) || / - i a 3 2 - | a r m 6 4 | \. m s i $ / i. test ( n ) ;
133+ const candidates = zips . filter ( ( a ) => ! skipName ( a . name ) ) ;
134+
135+ const exact = candidates . find ( ( a ) => a . name === preferredFileName ) ;
136+ if ( exact ?. browser_download_url ) {
137+ log ( `[updater] GitHub API: exact zip match ${ exact . name } ` ) ;
138+ return exact . browser_download_url ;
139+ }
140+
141+ const verLoose = String ( version ) . trim ( ) ;
142+ const withVersion = candidates . filter ( ( a ) => a . name . includes ( verLoose ) ) ;
143+ if ( withVersion . length === 1 && withVersion [ 0 ] . browser_download_url ) {
144+ log ( `[updater] GitHub API: single zip matching version ${ verLoose } : ${ withVersion [ 0 ] . name } ` ) ;
145+ return withVersion [ 0 ] . browser_download_url ;
146+ }
147+
148+ const prefixed = candidates . find (
149+ ( a ) =>
150+ a . name . startsWith ( WIN_PORTABLE_ZIP_PREFIX ) ||
151+ / ^ H y p e r l i n k s S p a c e A p p [ _ - ] / i. test ( a . name ) ||
152+ / H y p e r l i n k s \s * S p a c e / i. test ( a . name ) ,
153+ ) ;
154+ if ( prefixed ?. browser_download_url ) {
155+ log ( `[updater] GitHub API: portable-like zip ${ prefixed . name } ` ) ;
156+ return prefixed . browser_download_url ;
157+ }
158+
159+ if ( candidates . length === 1 && candidates [ 0 ] . browser_download_url ) {
160+ log ( `[updater] GitHub API: only zip on release: ${ candidates [ 0 ] . name } ` ) ;
161+ return candidates [ 0 ] . browser_download_url ;
162+ }
163+
164+ log (
165+ `[updater] GitHub API: could not pick zip (candidates: ${ candidates . map ( ( c ) => c . name ) . join ( ", " ) || "none" } )` ,
166+ ) ;
167+ return null ;
168+ }
169+
106170async function downloadToFile ( netFetch , url , destPath , onProgress ) {
107171 const res = await netFetch ( url ) ;
108172 if ( ! res . ok ) {
@@ -506,21 +570,30 @@ function setupAutoUpdater() {
506570 fs . mkdirSync ( extractDir , { recursive : true } ) ;
507571
508572 const zipPath = path . join ( versionDir , meta . fileName ) ;
509- const zipUrl = githubLatestAssetUrl ( meta . fileName ) ;
510- await downloadToFile (
511- ( u ) => net . fetch ( u ) ,
512- zipUrl ,
513- zipPath ,
514- ( received , total ) => {
515- const dl = total > 0 ? received / total : 0 ;
516- const dlPct = total > 0 ? Math . round ( 100 * dl ) : 0 ;
517- const overall = Math . min ( PROGRESS_DOWNLOAD_CAP , Math . round ( PROGRESS_DOWNLOAD_CAP * dl ) ) ;
518- pushUi ( {
519- text : `Downloading and preparing update… ${ dlPct } %` ,
520- percent : overall ,
521- } ) ;
522- } ,
523- ) ;
573+ const primaryZipUrl = githubLatestAssetUrl ( meta . fileName ) ;
574+ const onZipProgress = ( received , total ) => {
575+ const dl = total > 0 ? received / total : 0 ;
576+ const dlPct = total > 0 ? Math . round ( 100 * dl ) : 0 ;
577+ const overall = Math . min ( PROGRESS_DOWNLOAD_CAP , Math . round ( PROGRESS_DOWNLOAD_CAP * dl ) ) ;
578+ pushUi ( {
579+ text : `Downloading and preparing update… ${ dlPct } %` ,
580+ percent : overall ,
581+ } ) ;
582+ } ;
583+ try {
584+ await downloadToFile ( ( u ) => net . fetch ( u ) , primaryZipUrl , zipPath , onZipProgress ) ;
585+ } catch ( e ) {
586+ const msg = String ( e ?. message || e ) ;
587+ if ( ! / 4 0 4 / . test ( msg ) ) throw e ;
588+ const altUrl = await fetchPortableZipBrowserUrlFromGitHubApi (
589+ ( u , init ) => net . fetch ( u , init ) ,
590+ meta . version ,
591+ meta . fileName ,
592+ ) ;
593+ if ( ! altUrl ) throw e ;
594+ log ( `[updater] primary zip 404; downloading from GitHub API URL` ) ;
595+ await downloadToFile ( ( u ) => net . fetch ( u ) , altUrl , zipPath , onZipProgress ) ;
596+ }
524597
525598 pushUi ( { text : "Verifying update…" , percent : PROGRESS_DOWNLOAD_CAP + 2 } ) ;
526599
0 commit comments