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    /// Mutex to serialize all file operations (prevents race conditions on concurrent writes)
53    write_lock: tokio::sync::Mutex<()>,
54}
55
56impl ServerRegistry {
57    pub fn new() -> Result<Self> {
58        let base_dir = crate::core::helpers::get_base_dir()?;
59
60        let file_path = base_dir.join(".rss").join("servers.list");
61
62        if let Some(parent) = file_path.parent() {
63            std::fs::create_dir_all(parent).map_err(AppError::Io)?;
64        }
65
66        Ok(Self {
67            file_path,
68            write_lock: tokio::sync::Mutex::new(()),
69        })
70    }
71
72    /// Fallback constructor that never fails — uses temp dir if base_dir is unavailable
73    pub fn with_fallback() -> Self {
74        match Self::new() {
75            Ok(registry) => registry,
76            Err(_) => {
77                let path = std::env::temp_dir().join(".rss").join("servers.list");
78                let _ = std::fs::create_dir_all(path.parent().unwrap_or(&path));
79                Self {
80                    file_path: path,
81                    write_lock: tokio::sync::Mutex::new(()),
82                }
83            }
84        }
85    }
86
87    pub fn get_file_path(&self) -> &PathBuf {
88        &self.file_path
89    }
90
91    pub async fn load_servers(&self) -> Result<HashMap<String, PersistentServerInfo>> {
92        if !self.file_path.exists() {
93            return Ok(HashMap::new());
94        }
95
96        let content = tokio::fs::read_to_string(&self.file_path)
97            .await
98            .map_err(AppError::Io)?;
99        if content.trim().is_empty() {
100            return Ok(HashMap::new());
101        }
102
103        let servers: Vec<PersistentServerInfo> = serde_json::from_str(&content)
104            .map_err(|e| AppError::Validation(format!("Failed to parse server registry: {}", e)))?;
105
106        Ok(servers.into_iter().map(|s| (s.id.clone(), s)).collect())
107    }
108
109    pub async fn save_servers(
110        &self,
111        servers: &HashMap<String, PersistentServerInfo>,
112    ) -> Result<()> {
113        let mut server_list: Vec<PersistentServerInfo> = servers.values().cloned().collect();
114        server_list.sort_by_key(|s| s.created_timestamp);
115
116        let content = serde_json::to_string_pretty(&server_list)
117            .map_err(|e| AppError::Validation(format!("Failed to serialize servers: {}", e)))?;
118
119        let temp_path = self.file_path.with_extension("tmp");
120        tokio::fs::write(&temp_path, content)
121            .await
122            .map_err(AppError::Io)?;
123        tokio::fs::rename(&temp_path, &self.file_path)
124            .await
125            .map_err(AppError::Io)?;
126
127        Ok(())
128    }
129
130    // Generic update helper — serialized by write_lock to prevent race conditions
131    async fn update_server(
132        &self,
133        server_id: &str,
134        update_fn: impl Fn(&mut PersistentServerInfo),
135    ) -> Result<HashMap<String, PersistentServerInfo>> {
136        let _lock = self.write_lock.lock().await;
137        let mut servers = self.load_servers().await?;
138        if let Some(server) = servers.get_mut(server_id) {
139            update_fn(server);
140            self.save_servers(&servers).await?;
141        }
142        Ok(servers)
143    }
144
145    pub async fn update_server_status(
146        &self,
147        server_id: &str,
148        status: ServerStatus,
149    ) -> Result<HashMap<String, PersistentServerInfo>> {
150        self.update_server(server_id, |server| {
151            server.status = status;
152            if status == ServerStatus::Running {
153                server.last_started =
154                    Some(chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
155                server.start_count += 1;
156            }
157        })
158        .await
159    }
160
161    pub async fn set_auto_start(
162        &self,
163        server_id: &str,
164        auto_start: bool,
165    ) -> Result<HashMap<String, PersistentServerInfo>> {
166        self.update_server(server_id, |server| {
167            server.auto_start = auto_start;
168        })
169        .await
170    }
171
172    pub async fn add_server(
173        &self,
174        server_info: ServerInfo,
175    ) -> Result<HashMap<String, PersistentServerInfo>> {
176        let _lock = self.write_lock.lock().await;
177        let mut servers = self.load_servers().await?;
178        let persistent_info = PersistentServerInfo::from(server_info);
179        servers.insert(persistent_info.id.clone(), persistent_info);
180        self.save_servers(&servers).await?;
181        Ok(servers)
182    }
183
184    pub async fn remove_server(
185        &self,
186        server_id: &str,
187    ) -> Result<HashMap<String, PersistentServerInfo>> {
188        let _lock = self.write_lock.lock().await;
189        let mut servers = self.load_servers().await?;
190        servers.remove(server_id);
191        self.save_servers(&servers).await?;
192        Ok(servers)
193    }
194
195    pub async fn cleanup_servers(
196        &self,
197        cleanup_type: CleanupType,
198    ) -> Result<(HashMap<String, PersistentServerInfo>, usize)> {
199        let _lock = self.write_lock.lock().await;
200        let mut servers = self.load_servers().await?;
201        let initial_count = servers.len();
202
203        servers.retain(|_, s| match cleanup_type {
204            CleanupType::Stopped => s.status != ServerStatus::Stopped,
205            CleanupType::Failed => s.status != ServerStatus::Failed,
206            CleanupType::All => s.status == ServerStatus::Running,
207        });
208
209        let removed_count = initial_count - servers.len();
210        if removed_count > 0 {
211            self.save_servers(&servers).await?;
212        }
213
214        Ok((servers, removed_count))
215    }
216
217    pub fn get_auto_start_servers(
218        &self,
219        servers: &HashMap<String, PersistentServerInfo>,
220    ) -> Vec<PersistentServerInfo> {
221        servers
222            .values()
223            .filter(|s| s.auto_start && s.status != ServerStatus::Failed)
224            .cloned()
225            .collect()
226    }
227
228    // Directory cleanup utilities
229    pub async fn cleanup_server_directory(&self, server_name: &str, port: u16) -> Result<()> {
230        let base_dir = crate::core::helpers::get_base_dir()?;
231
232        let server_dir = base_dir
233            .join("www")
234            .join(format!("{}-[{}]", server_name, port));
235
236        if server_dir.exists() {
237            std::fs::remove_dir_all(&server_dir).map_err(AppError::Io)?;
238            log::info!("Removed server directory: {:?}", server_dir);
239        }
240        Ok(())
241    }
242
243    pub fn list_www_directories(&self) -> Result<Vec<PathBuf>> {
244        let base_dir = crate::core::helpers::get_base_dir()?;
245
246        let www_dir = base_dir.join("www");
247        if !www_dir.exists() {
248            return Ok(vec![]);
249        }
250
251        let mut directories = Vec::new();
252        for entry in std::fs::read_dir(&www_dir).map_err(AppError::Io)? {
253            let entry = entry.map_err(AppError::Io)?;
254            if entry.file_type().map_err(AppError::Io)?.is_dir() {
255                directories.push(entry.path());
256            }
257        }
258        directories.sort();
259        Ok(directories)
260    }
261}
262
263#[derive(Debug)]
264pub enum CleanupType {
265    Stopped,
266    Failed,
267    All,
268}