settings.rs 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. use serde::{Deserialize, Serialize};
  2. use std::fs;
  3. use std::path::PathBuf;
  4. #[derive(Debug, Clone, Serialize, Deserialize)]
  5. #[serde(rename_all = "camelCase")]
  6. pub struct AppSettings {
  7. #[serde(default = "default_base_url")]
  8. pub base_url: String,
  9. /// 自定义形象图片 URL(通过 /api/media/upload 上传后本地记录)
  10. #[serde(default)]
  11. pub custom_mascot_url: Option<String>,
  12. }
  13. impl Default for AppSettings {
  14. fn default() -> Self {
  15. Self { base_url: default_base_url(), custom_mascot_url: None }
  16. }
  17. }
  18. fn default_base_url() -> String {
  19. "https://cfc.cf-club.com".to_string()
  20. }
  21. pub struct SettingsStore {
  22. path: PathBuf,
  23. }
  24. impl SettingsStore {
  25. pub fn new(app_config_dir: PathBuf) -> Self {
  26. Self { path: app_config_dir.join("settings.json") }
  27. }
  28. pub fn load(&self) -> AppSettings {
  29. match fs::read_to_string(&self.path) {
  30. Ok(txt) => serde_json::from_str(&txt).unwrap_or_default(),
  31. Err(_) => AppSettings::default(),
  32. }
  33. }
  34. pub fn save(&self, s: &AppSettings) -> Result<(), String> {
  35. if let Some(dir) = self.path.parent() {
  36. fs::create_dir_all(dir).map_err(|e| e.to_string())?;
  37. }
  38. let txt = serde_json::to_string_pretty(s).map_err(|e| e.to_string())?;
  39. fs::write(&self.path, txt).map_err(|e| e.to_string())
  40. }
  41. }
  42. #[cfg(test)]
  43. mod tests {
  44. use super::*;
  45. use std::fs;
  46. use std::sync::atomic::{AtomicUsize, Ordering};
  47. static COUNTER: AtomicUsize = AtomicUsize::new(0);
  48. fn temp_dir() -> std::path::PathBuf {
  49. let n = COUNTER.fetch_add(1, Ordering::SeqCst);
  50. let dir = std::env::temp_dir().join(format!("xafpet-settings-test-{}", n));
  51. let _ = fs::remove_dir_all(&dir);
  52. dir
  53. }
  54. #[test]
  55. fn default_when_file_missing() {
  56. let store = SettingsStore::new(temp_dir());
  57. let cfg = store.load();
  58. assert_eq!(cfg.base_url, "https://cfc.cf-club.com");
  59. }
  60. #[test]
  61. fn default_when_file_malformed() {
  62. let dir = temp_dir();
  63. fs::create_dir_all(&dir).unwrap();
  64. fs::write(dir.join("settings.json"), "not json").unwrap();
  65. let store = SettingsStore::new(dir);
  66. let cfg = store.load();
  67. assert_eq!(cfg.base_url, "https://cfc.cf-club.com");
  68. }
  69. #[test]
  70. fn roundtrip_save_load() {
  71. let dir = temp_dir();
  72. let store = SettingsStore::new(dir.clone());
  73. let mut cfg = store.load();
  74. cfg.base_url = "http://192.168.1.5:9082".into();
  75. store.save(&cfg).unwrap();
  76. let store2 = SettingsStore::new(dir.clone());
  77. assert_eq!(store2.load().base_url, "http://192.168.1.5:9082");
  78. let _ = fs::remove_dir_all(&dir);
  79. }
  80. #[test]
  81. fn custom_mascot_url_roundtrip() {
  82. let dir = temp_dir();
  83. let store = SettingsStore::new(dir.clone());
  84. let mut cfg = store.load();
  85. assert!(cfg.custom_mascot_url.is_none());
  86. cfg.custom_mascot_url = Some("https://cdn.example/x.png".into());
  87. store.save(&cfg).unwrap();
  88. let loaded = SettingsStore::new(dir.clone()).load();
  89. assert_eq!(loaded.custom_mascot_url.as_deref(), Some("https://cdn.example/x.png"));
  90. let _ = fs::remove_dir_all(&dir);
  91. }
  92. }