11use std:: collections:: BTreeMap ;
2+ use std:: ffi:: OsString ;
23use std:: fs;
34use std:: path:: { Path , PathBuf } ;
45
56use anyhow:: { Context , Result , anyhow} ;
67use serde:: { Deserialize , Serialize } ;
78
89const DEFAULT_APP_CONFIG : & str = include_str ! ( "../assets/defaults/linuxdo-accelerator.toml" ) ;
10+ const CURRENT_CONFIG_VERSION : & str = env ! ( "CARGO_PKG_VERSION" ) ;
911
1012#[ derive( Debug , Clone , Serialize , Deserialize ) ]
1113pub struct AppConfig {
@@ -129,16 +131,70 @@ fn legacy_network_profile_path(config_path: &Path) -> PathBuf {
129131 . unwrap_or_else ( || PathBuf :: from ( "linuxdo-network.toml" ) )
130132}
131133
134+ fn sibling_path ( config_path : & Path , suffix : & str ) -> PathBuf {
135+ let mut name = config_path
136+ . file_name ( )
137+ . map ( ToOwned :: to_owned)
138+ . unwrap_or_else ( || OsString :: from ( "linuxdo-accelerator.toml" ) ) ;
139+ name. push ( suffix) ;
140+ config_path
141+ . parent ( )
142+ . map ( |parent| parent. join ( & name) )
143+ . unwrap_or_else ( || PathBuf :: from ( & name) )
144+ }
145+
146+ fn version_marker_path ( config_path : & Path ) -> PathBuf {
147+ sibling_path ( config_path, ".version" )
148+ }
149+
150+ fn backup_config_path ( config_path : & Path ) -> PathBuf {
151+ sibling_path ( config_path, ".bak" )
152+ }
153+
154+ fn read_version_marker ( marker_path : & Path ) -> Option < String > {
155+ fs:: read_to_string ( marker_path)
156+ . ok ( )
157+ . map ( |value| value. trim ( ) . to_string ( ) )
158+ }
159+
160+ fn write_version_marker ( marker_path : & Path ) -> Result < ( ) > {
161+ fs:: write ( marker_path, CURRENT_CONFIG_VERSION )
162+ . with_context ( || format ! ( "failed to write {}" , marker_path. display( ) ) )
163+ }
164+
165+ fn marker_matches_current_version ( marker_path : & Path ) -> bool {
166+ matches ! ( read_version_marker( marker_path) , Some ( value) if value == CURRENT_CONFIG_VERSION )
167+ }
168+
132169impl AppConfig {
133170 pub fn load_or_create ( path : & Path ) -> Result < Self > {
134171 if let Some ( parent) = path. parent ( ) {
135172 fs:: create_dir_all ( parent)
136173 . with_context ( || format ! ( "failed to create {}" , parent. display( ) ) ) ?;
137174 }
138175
176+ let marker_path = version_marker_path ( path) ;
177+
178+ if path. exists ( ) && !marker_matches_current_version ( & marker_path) {
179+ let backup_path = backup_config_path ( path) ;
180+ fs:: copy ( path, & backup_path) . with_context ( || {
181+ format ! (
182+ "failed to back up {} to {}" ,
183+ path. display( ) ,
184+ backup_path. display( )
185+ )
186+ } ) ?;
187+ fs:: write ( path, DEFAULT_APP_CONFIG )
188+ . with_context ( || format ! ( "failed to write config {}" , path. display( ) ) ) ?;
189+ write_version_marker ( & marker_path) ?;
190+ cleanup_legacy_network_profile ( path) ?;
191+ return Ok ( default_app_config ( ) ) ;
192+ }
193+
139194 if !path. exists ( ) {
140195 fs:: write ( path, DEFAULT_APP_CONFIG )
141196 . with_context ( || format ! ( "failed to write config {}" , path. display( ) ) ) ?;
197+ write_version_marker ( & marker_path) ?;
142198 cleanup_legacy_network_profile ( path) ?;
143199 return Ok ( default_app_config ( ) ) ;
144200 }
@@ -202,6 +258,7 @@ impl AppConfig {
202258 if !path. exists ( ) {
203259 fs:: write ( path, DEFAULT_APP_CONFIG )
204260 . with_context ( || format ! ( "failed to write config {}" , path. display( ) ) ) ?;
261+ write_version_marker ( & version_marker_path ( path) ) ?;
205262 }
206263 cleanup_legacy_network_profile ( path) ?;
207264 Ok ( ( ) )
@@ -291,7 +348,11 @@ fn cleanup_legacy_network_profile(path: &Path) -> Result<()> {
291348
292349#[ cfg( test) ]
293350mod tests {
294- use super :: { AppConfig , DEFAULT_APP_CONFIG } ;
351+ use super :: {
352+ AppConfig , CURRENT_CONFIG_VERSION , DEFAULT_APP_CONFIG , backup_config_path,
353+ version_marker_path,
354+ } ;
355+ use std:: path:: Path ;
295356 use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
296357
297358 static NEXT_ID : AtomicU64 = AtomicU64 :: new ( 1 ) ;
@@ -302,11 +363,13 @@ mod tests {
302363 let config_path = root. join ( "linuxdo-accelerator.toml" ) ;
303364 let customized = DEFAULT_APP_CONFIG . replace ( "127.211.73.84" , "127.0.0.1" ) ;
304365 std:: fs:: write ( & config_path, customized) . unwrap ( ) ;
366+ write_current_version_marker ( & config_path) ;
305367
306368 let config = AppConfig :: load_or_create ( & config_path) . unwrap ( ) ;
307369
308370 assert_eq ! ( config. listen_host, "127.0.0.1" ) ;
309371 assert_eq ! ( config. hosts_ip, "127.0.0.1" ) ;
372+ assert ! ( !backup_config_path( & config_path) . exists( ) ) ;
310373
311374 cleanup_test_dir ( & root) ;
312375 }
@@ -332,6 +395,7 @@ ca_common_name = "Linux.do Accelerator Root CA"
332395server_common_name = "linux.do"
333396"# ;
334397 std:: fs:: write ( & config_path, customized) . unwrap ( ) ;
398+ write_current_version_marker ( & config_path) ;
335399
336400 let config = AppConfig :: load_or_create ( & config_path) . unwrap ( ) ;
337401 let reloaded = std:: fs:: read_to_string ( & config_path) . unwrap ( ) ;
@@ -341,6 +405,7 @@ server_common_name = "linux.do"
341405 assert_eq ! ( config. certificate_domains, vec![ "linux.do" ] ) ;
342406 assert_eq ! ( config. doh_endpoints, vec![ "https://1.1.1.1/dns-query" ] ) ;
343407 assert_eq ! ( reloaded, customized) ;
408+ assert ! ( !backup_config_path( & config_path) . exists( ) ) ;
344409
345410 cleanup_test_dir ( & root) ;
346411 }
@@ -361,6 +426,7 @@ ca_common_name = "Linux.do Accelerator Root CA"
361426server_common_name = "linux.do"
362427"# ;
363428 std:: fs:: write ( & config_path, customized) . unwrap ( ) ;
429+ write_current_version_marker ( & config_path) ;
364430
365431 let config = AppConfig :: load_or_create ( & config_path) . unwrap ( ) ;
366432 let reloaded = std:: fs:: read_to_string ( & config_path) . unwrap ( ) ;
@@ -375,6 +441,74 @@ server_common_name = "linux.do"
375441 cleanup_test_dir ( & root) ;
376442 }
377443
444+ #[ test]
445+ fn load_or_create_resets_to_defaults_on_version_mismatch_and_keeps_backup ( ) {
446+ let root = create_test_dir ( "upgrade-overwrite" ) ;
447+ let config_path = root. join ( "linuxdo-accelerator.toml" ) ;
448+ let legacy = r#"
449+ listen_host = "127.0.0.1"
450+ hosts_ip = "127.0.0.1"
451+ upstream = "https://linux.do"
452+ doh_endpoints = ["https://example.test/dns-query"]
453+ proxy_domains = ["linux.do"]
454+ hosts_domains = ["linux.do"]
455+ certificate_domains = ["linux.do"]
456+ ca_common_name = "Linux.do Accelerator Root CA"
457+ server_common_name = "linux.do"
458+ "# ;
459+ std:: fs:: write ( & config_path, legacy) . unwrap ( ) ;
460+ std:: fs:: write ( version_marker_path ( & config_path) , "0.0.0-old" ) . unwrap ( ) ;
461+
462+ let config = AppConfig :: load_or_create ( & config_path) . unwrap ( ) ;
463+ let on_disk = std:: fs:: read_to_string ( & config_path) . unwrap ( ) ;
464+ let backup = std:: fs:: read_to_string ( backup_config_path ( & config_path) ) . unwrap ( ) ;
465+ let marker = std:: fs:: read_to_string ( version_marker_path ( & config_path) ) . unwrap ( ) ;
466+
467+ assert_eq ! ( on_disk, DEFAULT_APP_CONFIG ) ;
468+ assert_eq ! ( backup, legacy) ;
469+ assert_eq ! ( marker, CURRENT_CONFIG_VERSION ) ;
470+ assert_eq ! ( config. listen_host, "127.211.73.84" ) ;
471+
472+ cleanup_test_dir ( & root) ;
473+ }
474+
475+ #[ test]
476+ fn load_or_create_resets_to_defaults_when_marker_is_missing ( ) {
477+ let root = create_test_dir ( "upgrade-no-marker" ) ;
478+ let config_path = root. join ( "linuxdo-accelerator.toml" ) ;
479+ let legacy = "listen_host = \" 127.0.0.1\" \n " ;
480+ std:: fs:: write ( & config_path, legacy) . unwrap ( ) ;
481+
482+ let _ = AppConfig :: load_or_create ( & config_path) . unwrap ( ) ;
483+
484+ let on_disk = std:: fs:: read_to_string ( & config_path) . unwrap ( ) ;
485+ let backup = std:: fs:: read_to_string ( backup_config_path ( & config_path) ) . unwrap ( ) ;
486+ let marker = std:: fs:: read_to_string ( version_marker_path ( & config_path) ) . unwrap ( ) ;
487+ assert_eq ! ( on_disk, DEFAULT_APP_CONFIG ) ;
488+ assert_eq ! ( backup, legacy) ;
489+ assert_eq ! ( marker, CURRENT_CONFIG_VERSION ) ;
490+
491+ cleanup_test_dir ( & root) ;
492+ }
493+
494+ #[ test]
495+ fn load_or_create_writes_marker_on_fresh_install ( ) {
496+ let root = create_test_dir ( "fresh-install" ) ;
497+ let config_path = root. join ( "linuxdo-accelerator.toml" ) ;
498+
499+ let _ = AppConfig :: load_or_create ( & config_path) . unwrap ( ) ;
500+
501+ let marker = std:: fs:: read_to_string ( version_marker_path ( & config_path) ) . unwrap ( ) ;
502+ assert_eq ! ( marker, CURRENT_CONFIG_VERSION ) ;
503+ assert ! ( !backup_config_path( & config_path) . exists( ) ) ;
504+
505+ cleanup_test_dir ( & root) ;
506+ }
507+
508+ fn write_current_version_marker ( config_path : & Path ) {
509+ std:: fs:: write ( version_marker_path ( config_path) , CURRENT_CONFIG_VERSION ) . unwrap ( ) ;
510+ }
511+
378512 fn create_test_dir ( name : & str ) -> std:: path:: PathBuf {
379513 let mut path = std:: env:: temp_dir ( ) ;
380514 let id = NEXT_ID . fetch_add ( 1 , Ordering :: Relaxed ) ;
0 commit comments