Skip to main content

rush_sync_server/server/
persistence.rs

1// src/server/persistence.rs
2use crate::core::prelude::*;
3use crate::server::types::{ServerInfo, ServerStatus};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7
8#[derive(Debug, Serialize, Deserialize, Clone)]
9pub struct PersistentServerInfo {
10    pub id: String,
11    pub name: String,
12    pub port: u16,
13    pub status: ServerStatus,
14    pub created_at: String,
15    pub created_timestamp: u64,
16    pub auto_start: bool,
17    pub last_started: Option<String>,
18    pub start_count: u32,
19}
20
21impl From<ServerInfo> for PersistentServerInfo {
22    fn from(info: ServerInfo) -> Self {
23        Self {
24            id: info.id,
25            name: info.name,
26            port: info.port,
27            status: info.status,
28            created_at: info.created_at,
29            created_timestamp: info.created_timestamp,
30            auto_start: false,
31            last_started: None,
32            start_count: 0,
33        }
34    }
35}
36
37impl From<PersistentServerInfo> for ServerInfo {
38    fn from(info: PersistentServerInfo) -> Self {
39        Self {
40            id: info.id,
41            name: info.name,
42            port: info.port,
43            status: info.status,
44            created_at: info.created_at,
45            created_timestamp: info.created_timestamp,
46        }
47    }
48}
49
50pub struct ServerRegistry {
51    file_path: PathBuf,
52}
53
54impl ServerRegistry {
55    pub fn new() -> Result<Self> {
56        let base_dir = crate::core::helpers::get_base_dir()?;
57
58        let file_path = base_dir.join(".rss").join("servers.list");
59
60        if let Some(parent) = file_path.parent() {
61            std::fs::create_dir_all(parent).map_err(AppError::Io)?;
62        }
63
64        Ok(Self { file_path })
65    }
66
67    /// Fallback constructor that never fails — uses temp dir if base_dir is unavailable
68    pub fn with_fallback() -> Self {
69        match Self::new() {
70            Ok(registry) => registry,
71            Err(_) => {
72                let path = std::env::temp_dir().join(".rss").join("servers.list");
73                let _ = std::fs::create_dir_all(path.parent().unwrap_or(&path));
74                Self { file_path: path }
75            }
76        }
77    }
78
79    pub fn get_file_path(&self) -> &PathBuf {
80        &self.file_path
81    }
82
83    pub async fn load_servers(&self) -> Result<HashMap<String, PersistentServerInfo>> {
84        if !self.file_path.exists() {
85            return Ok(HashMap::new());
86        }
87
88        let content = tokio::fs::read_to_string(&self.file_path)
89            .await
90            .map_err(AppError::Io)?;
91        if content.trim().is_empty() {
92            return Ok(HashMap::new());
93        }
94
95        let servers: Vec<PersistentServerInfo> = serde_json::from_str(&content)
96            .map_err(|e| AppError::Validation(format!("Failed to parse server registry: {}", e)))?;
97
98        Ok(servers.into_iter().map(|s| (s.id.clone(), s)).collect())
99    }
100
101    pub async fn save_servers(
102        &self,
103        servers: &HashMap<String, PersistentServerInfo>,
104    ) -> Result<()> {
105        let mut server_list: Vec<PersistentServerInfo> = servers.values().cloned().collect();
106        server_list.sort_by_key(|s| s.created_timestamp);
107
108        let content = serde_json::to_string_pretty(&server_list)
109            .map_err(|e| AppError::Validation(format!("Failed to serialize servers: {}", e)))?;
110
111        let temp_path = self.file_path.with_extension("tmp");
112        tokio::fs::write(&temp_path, content)
113            .await
114            .map_err(AppError::Io)?;
115        tokio::fs::rename(&temp_path, &self.file_path)
116            .await
117            .map_err(AppError::Io)?;
118
119        Ok(())
120    }
121
122    // Generic update helper to reduce boilerplate
123    async fn update_server(
124        &self,
125        server_id: &str,
126        update_fn: impl Fn(&mut PersistentServerInfo),
127    ) -> Result<HashMap<String, PersistentServerInfo>> {
128        let mut servers = self.load_servers().await?;
129        if let Some(server) = servers.get_mut(server_id) {
130            update_fn(server);
131            self.save_servers(&servers).await?;
132        }
133        Ok(servers)
134    }
135
136    pub async fn update_server_status(
137        &self,
138        server_id: &str,
139        status: ServerStatus,
140    ) -> Result<HashMap<String, PersistentServerInfo>> {
141        self.update_server(server_id, |server| {
142            server.status = status;
143            if status == ServerStatus::Running {
144                server.last_started =
145                    Some(chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
146                server.start_count += 1;
147            }
148        })
149        .await
150    }
151
152    pub async fn set_auto_start(
153        &self,
154        server_id: &str,
155        auto_start: bool,
156    ) -> Result<HashMap<String, PersistentServerInfo>> {
157        self.update_server(server_id, |server| {
158            server.auto_start = auto_start;
159        })
160        .await
161    }
162
163    pub async fn add_server(
164        &self,
165        server_info: ServerInfo,
166    ) -> Result<HashMap<String, PersistentServerInfo>> {
167        let mut servers = self.load_servers().await?;
168        let persistent_info = PersistentServerInfo::from(server_info);
169        servers.insert(persistent_info.id.clone(), persistent_info);
170        self.save_servers(&servers).await?;
171        Ok(servers)
172    }
173
174    pub async fn remove_server(
175        &self,
176        server_id: &str,
177    ) -> Result<HashMap<String, PersistentServerInfo>> {
178        let mut servers = self.load_servers().await?;
179        servers.remove(server_id);
180        self.save_servers(&servers).await?;
181        Ok(servers)
182    }
183
184    pub async fn cleanup_servers(
185        &self,
186        cleanup_type: CleanupType,
187    ) -> Result<(HashMap<String, PersistentServerInfo>, usize)> {
188        let mut servers = self.load_servers().await?;
189        let initial_count = servers.len();
190
191        servers.retain(|_, s| match cleanup_type {
192            CleanupType::Stopped => s.status != ServerStatus::Stopped,
193            CleanupType::Failed => s.status != ServerStatus::Failed,
194            CleanupType::All => s.status == ServerStatus::Running,
195        });
196
197        let removed_count = initial_count - servers.len();
198        if removed_count > 0 {
199            self.save_servers(&servers).await?;
200        }
201
202        Ok((servers, removed_count))
203    }
204
205    pub fn get_auto_start_servers(
206        &self,
207        servers: &HashMap<String, PersistentServerInfo>,
208    ) -> Vec<PersistentServerInfo> {
209        servers
210            .values()
211            .filter(|s| s.auto_start && s.status != ServerStatus::Failed)
212            .cloned()
213            .collect()
214    }
215
216    // Directory cleanup utilities
217    pub async fn cleanup_server_directory(&self, server_name: &str, port: u16) -> Result<()> {
218        let base_dir = crate::core::helpers::get_base_dir()?;
219
220        let server_dir = base_dir
221            .join("www")
222            .join(format!("{}-[{}]", server_name, port));
223
224        if server_dir.exists() {
225            std::fs::remove_dir_all(&server_dir).map_err(AppError::Io)?;
226            log::info!("Removed server directory: {:?}", server_dir);
227        }
228        Ok(())
229    }
230
231    pub fn list_www_directories(&self) -> Result<Vec<PathBuf>> {
232        let base_dir = crate::core::helpers::get_base_dir()?;
233
234        let www_dir = base_dir.join("www");
235        if !www_dir.exists() {
236            return Ok(vec![]);
237        }
238
239        let mut directories = Vec::new();
240        for entry in std::fs::read_dir(&www_dir).map_err(AppError::Io)? {
241            let entry = entry.map_err(AppError::Io)?;
242            if entry.file_type().map_err(AppError::Io)?.is_dir() {
243                directories.push(entry.path());
244            }
245        }
246        directories.sort();
247        Ok(directories)
248    }
249}
250
251#[derive(Debug)]
252pub enum CleanupType {
253    Stopped,
254    Failed,
255    All,
256}