1- use reqwest:: StatusCode ;
1+ use reqwest:: { StatusCode , Url } ;
22use std:: fs:: File ;
33use std:: io:: { BufRead , BufReader } ;
44#[ cfg( target_os = "windows" ) ]
@@ -12,21 +12,25 @@ use tauri::{
1212 command,
1313 menu:: { MenuBuilder , MenuItemBuilder } ,
1414 tray:: { MouseButton , MouseButtonState , TrayIconEvent } ,
15- AppHandle , Manager , RunEvent , WindowEvent ,
15+ AppHandle , Manager , ResourceId , RunEvent , Webview , WindowEvent ,
1616} ;
1717use tauri_plugin_autostart:: MacosLauncher ;
18+ use tauri_plugin_updater:: UpdaterExt ;
1819
1920#[ macro_use]
2021extern crate lazy_static;
2122
2223#[ cfg( target_os = "windows" ) ]
2324const CREATE_NO_WINDOW : u32 = 0x08000000 ;
2425const SERVER_JAR_FILE_NAME : & str = "huntly-server.jar" ;
26+ const SERVER_JAR_VERSION_FILE_NAME : & str = "huntly-server.version" ;
2527const SERVER_JAR_RESOURCE_PATH : & str = "server_bin/huntly-server.jar" ;
2628const SERVER_JAR_DATA_DIR : & str = "server_bin" ;
2729const GITHUB_RELEASES_API : & str =
2830 "https://api.github.com/repos/lcomplete/huntly/releases?per_page=100" ;
2931const GITHUB_USER_AGENT : & str = "Huntly-Tauri" ;
32+ const TAURI_RELEASE_TAG_PREFIX : & str = "tauri/v" ;
33+ const TAURI_UPDATE_MANIFEST_FILE_NAME : & str = "latest.json" ;
3034
3135lazy_static ! {
3236 static ref SPRING_BOOT_PROCESS : Mutex <Option <Child >> = Mutex :: new( None ) ;
@@ -89,6 +93,17 @@ struct ServerUpdateInfo {
8993 release_url : Option < String > ,
9094}
9195
96+ #[ derive( serde:: Serialize ) ]
97+ #[ serde( rename_all = "camelCase" ) ]
98+ struct TauriUpdateMetadata {
99+ rid : ResourceId ,
100+ current_version : String ,
101+ version : String ,
102+ date : Option < String > ,
103+ body : Option < String > ,
104+ raw_json : serde_json:: Value ,
105+ }
106+
92107#[ derive( Clone ) ]
93108struct ServerRelease {
94109 version : String ,
@@ -219,7 +234,7 @@ fn collect_server_info(app: &AppHandle) -> ServerInfo {
219234 let jar_path_buf = active_server_jar_path ( app) ;
220235
221236 let java_version = java_path_buf. as_ref ( ) . and_then ( read_java_version) ;
222- let jar_version = jar_path_buf . as_ref ( ) . and_then ( read_server_jar_version ) ;
237+ let jar_version = current_server_jar_version ( app ) ;
223238
224239 ServerInfo {
225240 jar_version,
@@ -271,6 +286,52 @@ fn writable_server_jar_path(app: &AppHandle) -> Result<PathBuf, String> {
271286 Ok ( server_bin_dir. join ( SERVER_JAR_FILE_NAME ) )
272287}
273288
289+ fn writable_server_jar_version_path ( app : & AppHandle ) -> Result < PathBuf , String > {
290+ let app_data_dir = app. path ( ) . app_data_dir ( ) . map_err ( |e| e. to_string ( ) ) ?;
291+ let server_bin_dir = app_data_dir. join ( SERVER_JAR_DATA_DIR ) ;
292+ std:: fs:: create_dir_all ( & server_bin_dir) . map_err ( |e| e. to_string ( ) ) ?;
293+ Ok ( server_bin_dir. join ( SERVER_JAR_VERSION_FILE_NAME ) )
294+ }
295+
296+ fn current_server_jar_version ( app : & AppHandle ) -> Option < String > {
297+ let jar_path = active_server_jar_path ( app) ?;
298+ if is_writable_server_jar_path ( app, & jar_path) {
299+ return read_server_jar_release_version ( app) . or_else ( || read_server_jar_version ( & jar_path) ) ;
300+ }
301+
302+ read_server_jar_version ( & jar_path)
303+ }
304+
305+ fn is_writable_server_jar_path ( app : & AppHandle , jar_path : & Path ) -> bool {
306+ app. path ( )
307+ . app_data_dir ( )
308+ . map ( |app_data_dir| {
309+ jar_path
310+ == app_data_dir
311+ . join ( SERVER_JAR_DATA_DIR )
312+ . join ( SERVER_JAR_FILE_NAME )
313+ } )
314+ . unwrap_or ( false )
315+ }
316+
317+ fn read_server_jar_release_version ( app : & AppHandle ) -> Option < String > {
318+ let version_path = app
319+ . path ( )
320+ . app_data_dir ( )
321+ . ok ( ) ?
322+ . join ( SERVER_JAR_DATA_DIR )
323+ . join ( SERVER_JAR_VERSION_FILE_NAME ) ;
324+ let version = std:: fs:: read_to_string ( version_path)
325+ . ok ( ) ?
326+ . trim ( )
327+ . to_string ( ) ;
328+ if version. is_empty ( ) {
329+ None
330+ } else {
331+ Some ( version)
332+ }
333+ }
334+
274335fn read_java_version ( java_path : & PathBuf ) -> Option < String > {
275336 let output = Command :: new ( java_path) . arg ( "-version" ) . output ( ) . ok ( ) ?;
276337 let stderr = String :: from_utf8_lossy ( & output. stderr ) ;
@@ -315,8 +376,7 @@ fn read_server_jar_version(jar_path: &PathBuf) -> Option<String> {
315376
316377#[ command]
317378async fn check_server_update ( app : AppHandle ) -> Result < ServerUpdateInfo , String > {
318- let current_version =
319- active_server_jar_path ( & app) . and_then ( |path| read_server_jar_version ( & path) ) ;
379+ let current_version = current_server_jar_version ( & app) ;
320380 let release = fetch_latest_server_release ( ) . await ?;
321381 let available = current_version
322382 . as_deref ( )
@@ -342,8 +402,7 @@ async fn install_server_update(app: AppHandle) -> Result<ServerInfo, String> {
342402 return Err ( "Server bundle updates are disabled by HUNTLY_NO_SERVER_JAR." . to_string ( ) ) ;
343403 }
344404
345- let current_version =
346- active_server_jar_path ( & app) . and_then ( |path| read_server_jar_version ( & path) ) ;
405+ let current_version = current_server_jar_version ( & app) ;
347406 let release = fetch_latest_server_release ( ) . await ?;
348407 let available = current_version
349408 . as_deref ( )
@@ -369,12 +428,15 @@ async fn install_server_update(app: AppHandle) -> Result<ServerInfo, String> {
369428 let dest_path = writable_server_jar_path ( & app) ?;
370429 let temp_path = dest_path. with_extension ( "jar.download" ) ;
371430 let backup_path = dest_path. with_extension ( "jar.bak" ) ;
431+ let version_path = writable_server_jar_version_path ( & app) ?;
372432
373433 std:: fs:: write ( & temp_path, bytes. as_ref ( ) ) . map_err ( |e| e. to_string ( ) ) ?;
374434
375435 let downloaded_version = read_server_jar_version ( & temp_path)
376436 . ok_or_else ( || "Downloaded server JAR does not include version metadata." . to_string ( ) ) ?;
377- if normalize_version ( & downloaded_version) != normalize_version ( & release. version ) {
437+ if normalize_version ( & downloaded_version) != normalize_version ( & release. version )
438+ && !is_server_jar_asset_for_version ( & release. asset . name , & release. version )
439+ {
378440 let _ = std:: fs:: remove_file ( & temp_path) ;
379441 return Err ( format ! (
380442 "Downloaded server JAR version {} does not match release version {}." ,
@@ -401,12 +463,102 @@ async fn install_server_update(app: AppHandle) -> Result<ServerInfo, String> {
401463 let _ = std:: fs:: remove_file ( & backup_path) ;
402464 }
403465
466+ std:: fs:: write ( & version_path, format ! ( "{}\n " , release. version) ) . map_err ( |e| e. to_string ( ) ) ?;
467+
404468 Ok ( collect_server_info ( & app) )
405469}
406470
471+ #[ command]
472+ async fn check_tauri_update ( webview : Webview ) -> Result < Option < TauriUpdateMetadata > , String > {
473+ let manifest_url = match fetch_latest_tauri_update_manifest_url ( ) . await ? {
474+ Some ( manifest_url) => manifest_url,
475+ None => return Ok ( None ) ,
476+ } ;
477+
478+ let endpoint = Url :: parse ( & manifest_url) . map_err ( |e| e. to_string ( ) ) ?;
479+ let updater = webview
480+ . updater_builder ( )
481+ . endpoints ( vec ! [ endpoint] )
482+ . map_err ( |e| e. to_string ( ) ) ?
483+ . build ( )
484+ . map_err ( |e| e. to_string ( ) ) ?;
485+
486+ let update = match updater. check ( ) . await {
487+ Ok ( update) => update,
488+ Err ( tauri_plugin_updater:: Error :: ReleaseNotFound ) => return Ok ( None ) ,
489+ Err ( e) => return Err ( e. to_string ( ) ) ,
490+ } ;
491+
492+ if let Some ( update) = update {
493+ let current_version = update. current_version . clone ( ) ;
494+ let version = update. version . clone ( ) ;
495+ let date = update
496+ . raw_json
497+ . get ( "pub_date" )
498+ . and_then ( |value| value. as_str ( ) )
499+ . map ( ToString :: to_string) ;
500+ let body = update. body . clone ( ) ;
501+ let raw_json = update. raw_json . clone ( ) ;
502+ let rid = webview. resources_table ( ) . add ( update) ;
503+
504+ Ok ( Some ( TauriUpdateMetadata {
505+ rid,
506+ current_version,
507+ version,
508+ date,
509+ body,
510+ raw_json,
511+ } ) )
512+ } else {
513+ Ok ( None )
514+ }
515+ }
516+
517+ async fn fetch_latest_tauri_update_manifest_url ( ) -> Result < Option < String > , String > {
518+ let releases = fetch_github_releases ( ) . await ?;
519+ Ok ( releases. into_iter ( ) . find_map ( tauri_update_manifest_url) )
520+ }
521+
522+ fn tauri_update_manifest_url ( release : GithubRelease ) -> Option < String > {
523+ if release. draft || release. prerelease || tauri_release_version ( & release. tag_name ) . is_none ( ) {
524+ return None ;
525+ }
526+
527+ release
528+ . assets
529+ . into_iter ( )
530+ . find ( |asset| asset. name == TAURI_UPDATE_MANIFEST_FILE_NAME )
531+ . map ( |asset| asset. browser_download_url )
532+ }
533+
534+ fn tauri_release_version ( tag_name : & str ) -> Option < String > {
535+ let version = tag_name. strip_prefix ( TAURI_RELEASE_TAG_PREFIX ) ?;
536+ let version_parts: Vec < & str > = version. split ( '.' ) . collect ( ) ;
537+ if version_parts. len ( ) < 3
538+ || !version_parts. iter ( ) . take ( 3 ) . all ( |part| {
539+ part. chars ( )
540+ . next ( )
541+ . map ( |ch| ch. is_ascii_digit ( ) )
542+ . unwrap_or ( false )
543+ } )
544+ {
545+ return None ;
546+ }
547+ Some ( version. to_string ( ) )
548+ }
549+
407550async fn fetch_latest_server_release ( ) -> Result < ServerRelease , String > {
551+ let releases = fetch_github_releases ( ) . await ?;
552+
553+ releases
554+ . into_iter ( )
555+ . find_map ( main_server_release)
556+ . ok_or_else ( || "No main Huntly release with a server JAR asset was found." . to_string ( ) )
557+ }
558+
559+ async fn fetch_github_releases ( ) -> Result < Vec < GithubRelease > , String > {
408560 let client = github_client ( ) ?;
409- let releases : Vec < GithubRelease > = client
561+ client
410562 . get ( GITHUB_RELEASES_API )
411563 . send ( )
412564 . await
@@ -415,12 +567,7 @@ async fn fetch_latest_server_release() -> Result<ServerRelease, String> {
415567 . map_err ( |e| e. to_string ( ) ) ?
416568 . json ( )
417569 . await
418- . map_err ( |e| e. to_string ( ) ) ?;
419-
420- releases
421- . into_iter ( )
422- . find_map ( main_server_release)
423- . ok_or_else ( || "No main Huntly release with a server JAR asset was found." . to_string ( ) )
570+ . map_err ( |e| e. to_string ( ) )
424571}
425572
426573fn github_client ( ) -> Result < reqwest:: Client , String > {
@@ -471,9 +618,11 @@ fn main_release_version(tag_name: &str) -> Option<String> {
471618}
472619
473620fn is_server_jar_asset ( asset_name : & str , version : & str ) -> bool {
474- asset_name == SERVER_JAR_FILE_NAME
475- || asset_name == format ! ( "huntly-server-{}.jar" , version)
476- || ( asset_name. starts_with ( "huntly-server-" ) && asset_name. ends_with ( ".jar" ) )
621+ asset_name == SERVER_JAR_FILE_NAME || is_server_jar_asset_for_version ( asset_name, version)
622+ }
623+
624+ fn is_server_jar_asset_for_version ( asset_name : & str , version : & str ) -> bool {
625+ asset_name == format ! ( "huntly-server-{}.jar" , normalize_version( version) )
477626}
478627
479628fn normalize_version ( version : & str ) -> String {
@@ -906,6 +1055,7 @@ pub fn run() {
9061055 read_settings,
9071056 has_server_jar,
9081057 get_server_info,
1058+ check_tauri_update,
9091059 check_server_update,
9101060 install_server_update,
9111061 set_tray_visible,
0 commit comments