Skip to main content

rush_sync_server/commands/cleanup/
command.rs

1// src/commands/cleanup/command.rs
2use crate::commands::command::Command;
3use crate::core::prelude::*;
4use crate::server::types::{ServerContext, ServerStatus};
5
6#[derive(Debug, Default)]
7pub struct CleanupCommand;
8
9impl CleanupCommand {
10    pub fn new() -> Self {
11        Self
12    }
13}
14
15impl Command for CleanupCommand {
16    fn name(&self) -> &'static str {
17        "cleanup"
18    }
19
20    fn description(&self) -> &'static str {
21        "Clean up servers, logs, and www files - supports confirmation and force flags"
22    }
23
24    fn matches(&self, command: &str) -> bool {
25        command.trim().to_lowercase().starts_with("cleanup")
26    }
27
28    fn execute_sync(&self, args: &[&str]) -> Result<String> {
29        let ctx = crate::server::shared::get_shared_context();
30
31        match args.first() {
32            Some(&"stopped") => {
33                let msg = crate::i18n::get_command_translation(
34                    "system.commands.cleanup.confirm_stopped",
35                    &[],
36                );
37                Ok(format!(
38                    "__CONFIRM:__CLEANUP__cleanup --force-stopped__{}",
39                    msg
40                ))
41            }
42            Some(&"failed") => {
43                let msg = crate::i18n::get_command_translation(
44                    "system.commands.cleanup.confirm_failed",
45                    &[],
46                );
47                Ok(format!(
48                    "__CONFIRM:__CLEANUP__cleanup --force-failed__{}",
49                    msg
50                ))
51            }
52            Some(&"logs") => {
53                let msg = crate::i18n::get_command_translation(
54                    "system.commands.cleanup.confirm_logs",
55                    &[],
56                );
57                Ok(format!(
58                    "__CONFIRM:__CLEANUP__cleanup --force-logs__{}",
59                    msg
60                ))
61            }
62            Some(&"all") => {
63                let msg = crate::i18n::get_command_translation(
64                    "system.commands.cleanup.confirm_all",
65                    &[],
66                );
67                Ok(format!("__CONFIRM:__CLEANUP__cleanup --force-all__{}", msg))
68            }
69            Some(&"www") => {
70                if let Some(&server_name) = args.get(1) {
71                    let msg = crate::i18n::get_command_translation(
72                        "system.commands.cleanup.confirm_www_server",
73                        &[server_name],
74                    );
75                    Ok(format!(
76                        "__CONFIRM:__CLEANUP__cleanup --force-www {}__{}",
77                        server_name, msg
78                    ))
79                } else {
80                    let msg = crate::i18n::get_command_translation(
81                        "system.commands.cleanup.confirm_www_all",
82                        &[],
83                    );
84                    Ok(format!("__CONFIRM:__CLEANUP__cleanup --force-www__{}", msg))
85                }
86            }
87            None => {
88                // Default: stopped cleanup with confirmation
89                let msg = crate::i18n::get_command_translation(
90                    "system.commands.cleanup.confirm_stopped",
91                    &[],
92                );
93                Ok(format!(
94                    "__CONFIRM:__CLEANUP__cleanup --force-stopped__{}",
95                    msg
96                ))
97            }
98
99            // Force-Commands (direct execution without confirmation)
100            Some(&"--force-stopped") => Ok(self.cleanup_stopped_servers(ctx)),
101            Some(&"--force-failed") => Ok(self.cleanup_failed_servers(ctx)),
102            Some(&"--force-logs") => {
103                tokio::spawn(async move {
104                    match Self::cleanup_all_server_logs().await {
105                        Ok(msg) => log::info!("Log cleanup result: {}", msg),
106                        Err(e) => log::error!("Log cleanup failed: {}", e),
107                    }
108                });
109                Ok(crate::i18n::get_command_translation(
110                    "system.commands.cleanup.logs_started",
111                    &[],
112                ))
113            }
114            Some(&"--force-www") => {
115                if let Some(&server_name) = args.get(1) {
116                    let name = server_name.to_string();
117                    tokio::spawn(async move {
118                        match Self::cleanup_www_by_name(&name).await {
119                            Ok(msg) => log::info!("WWW cleanup result: {}", msg),
120                            Err(e) => log::error!("WWW cleanup failed: {}", e),
121                        }
122                    });
123                    Ok(crate::i18n::get_command_translation(
124                        "system.commands.cleanup.www_server_started",
125                        &[server_name],
126                    ))
127                } else {
128                    tokio::spawn(async move {
129                        match Self::cleanup_www_directory().await {
130                            Ok(msg) => log::info!("WWW cleanup result: {}", msg),
131                            Err(e) => log::error!("WWW cleanup failed: {}", e),
132                        }
133                    });
134                    Ok(crate::i18n::get_command_translation(
135                        "system.commands.cleanup.www_all_started",
136                        &[],
137                    ))
138                }
139            }
140            Some(&"--force-all") => {
141                // Complete cleanup now includes WWW cleanup
142                let stopped = self.cleanup_stopped_servers(ctx);
143                let failed = self.cleanup_failed_servers(ctx);
144
145                // Start async cleanup tasks for www and logs
146                tokio::spawn(async move {
147                    // WWW cleanup is now included in "all"
148                    let www_cleanup = async {
149                        match Self::cleanup_www_directory().await {
150                            Ok(msg) => log::info!("WWW cleanup result: {}", msg),
151                            Err(e) => log::error!("WWW cleanup failed: {}", e),
152                        }
153                    };
154
155                    let log_cleanup = async {
156                        match Self::cleanup_all_server_logs().await {
157                            Ok(msg) => log::info!("Log cleanup result: {}", msg),
158                            Err(e) => log::error!("Log cleanup failed: {}", e),
159                        }
160                    };
161
162                    // Both tasks run concurrently
163                    tokio::join!(www_cleanup, log_cleanup);
164                });
165
166                let async_cleanup_msg = crate::i18n::get_command_translation(
167                    "system.commands.cleanup.async_started",
168                    &[],
169                );
170                Ok(format!("{}\n{}\n{}", stopped, failed, async_cleanup_msg))
171            }
172
173            _ => Err(AppError::Validation(crate::i18n::get_command_translation(
174                "system.commands.cleanup.usage",
175                &[],
176            ))),
177        }
178    }
179
180    fn priority(&self) -> u8 {
181        50
182    }
183}
184
185impl CleanupCommand {
186    fn cleanup_stopped_servers(&self, ctx: &ServerContext) -> String {
187        let registry = crate::server::shared::get_persistent_registry();
188
189        tokio::spawn(async move {
190            if let Ok(_servers) = registry.load_servers().await {
191                if let Ok((_updated_servers, removed_count)) = registry
192                    .cleanup_servers(crate::server::persistence::CleanupType::Stopped)
193                    .await
194                {
195                    if removed_count > 0 {
196                        log::info!(
197                            "Removed {} stopped servers from persistent registry",
198                            removed_count
199                        );
200                    }
201                }
202            }
203        });
204
205        let mut servers = match ctx.servers.write() {
206            Ok(s) => s,
207            Err(e) => {
208                log::error!("servers lock poisoned: {}", e);
209                return "Error: server lock poisoned".to_string();
210            }
211        };
212        let initial_count = servers.len();
213        servers.retain(|_, server| server.status != ServerStatus::Stopped);
214        let removed_count = initial_count - servers.len();
215
216        if removed_count > 0 {
217            crate::i18n::get_command_translation(
218                "system.commands.cleanup.stopped_success",
219                &[&removed_count.to_string()],
220            )
221        } else {
222            crate::i18n::get_command_translation("system.commands.cleanup.no_stopped", &[])
223        }
224    }
225
226    fn cleanup_failed_servers(&self, ctx: &ServerContext) -> String {
227        let registry = crate::server::shared::get_persistent_registry();
228
229        tokio::spawn(async move {
230            if let Ok(_servers) = registry.load_servers().await {
231                if let Ok((_updated_servers, removed_count)) = registry
232                    .cleanup_servers(crate::server::persistence::CleanupType::Failed)
233                    .await
234                {
235                    if removed_count > 0 {
236                        log::info!(
237                            "Removed {} failed servers from persistent registry",
238                            removed_count
239                        );
240                    }
241                }
242            }
243        });
244
245        let mut servers = match ctx.servers.write() {
246            Ok(s) => s,
247            Err(e) => {
248                log::error!("servers lock poisoned: {}", e);
249                return "Error: server lock poisoned".to_string();
250            }
251        };
252        let initial_count = servers.len();
253        servers.retain(|_, server| server.status != ServerStatus::Failed);
254        let removed_count = initial_count - servers.len();
255
256        if removed_count > 0 {
257            crate::i18n::get_command_translation(
258                "system.commands.cleanup.failed_success",
259                &[&removed_count.to_string()],
260            )
261        } else {
262            crate::i18n::get_command_translation("system.commands.cleanup.no_failed", &[])
263        }
264    }
265
266    pub async fn cleanup_all_server_logs() -> Result<String> {
267        let exe_path = std::env::current_exe().map_err(AppError::Io)?;
268        let base_dir = exe_path.parent().ok_or_else(|| {
269            AppError::Validation("Cannot determine executable directory".to_string())
270        })?;
271
272        let servers_dir = base_dir.join(".rss").join("servers");
273
274        if !servers_dir.exists() {
275            return Ok(crate::i18n::get_command_translation(
276                "system.commands.cleanup.no_logs_dir",
277                &[],
278            ));
279        }
280
281        let mut deleted_files = 0;
282        let mut total_size = 0u64;
283
284        let mut entries = tokio::fs::read_dir(&servers_dir)
285            .await
286            .map_err(AppError::Io)?;
287
288        while let Some(entry) = entries.next_entry().await.map_err(AppError::Io)? {
289            let path = entry.path();
290
291            if path.is_file() {
292                if let Some(extension) = path.extension() {
293                    if extension == "log" || extension == "gz" {
294                        if let Ok(metadata) = tokio::fs::metadata(&path).await {
295                            total_size += metadata.len();
296                        }
297
298                        tokio::fs::remove_file(&path).await.map_err(AppError::Io)?;
299                        deleted_files += 1;
300
301                        log::info!("Deleted log file: {}", path.display());
302                    }
303                }
304            }
305        }
306
307        let size_mb = total_size / (1024 * 1024);
308
309        Ok(crate::i18n::get_command_translation(
310            "system.commands.cleanup.logs_success",
311            &[&deleted_files.to_string(), &size_mb.to_string()],
312        ))
313    }
314
315    pub async fn cleanup_www_directory() -> Result<String> {
316        let exe_path = std::env::current_exe().map_err(AppError::Io)?;
317        let base_dir = exe_path.parent().ok_or_else(|| {
318            AppError::Validation("Cannot determine executable directory".to_string())
319        })?;
320
321        let www_dir = base_dir.join("www");
322
323        if !www_dir.exists() {
324            return Ok(crate::i18n::get_command_translation(
325                "system.commands.cleanup.no_www_dir",
326                &[],
327            ));
328        }
329
330        let mut deleted_dirs = 0;
331        let mut deleted_files = 0;
332        let mut total_size = 0u64;
333
334        let mut entries = tokio::fs::read_dir(&www_dir).await.map_err(AppError::Io)?;
335
336        while let Some(entry) = entries.next_entry().await.map_err(AppError::Io)? {
337            let path = entry.path();
338            let metadata = tokio::fs::metadata(&path).await.map_err(AppError::Io)?;
339
340            // Skip system files (starting with .)
341            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
342                if name.starts_with('.') {
343                    continue;
344                }
345            }
346
347            if metadata.is_dir() {
348                total_size += Self::calculate_directory_size(&path).await.unwrap_or(0);
349                tokio::fs::remove_dir_all(&path)
350                    .await
351                    .map_err(AppError::Io)?;
352                deleted_dirs += 1;
353                log::info!("Deleted directory: {}", path.display());
354            } else if metadata.is_file() {
355                total_size += metadata.len();
356                tokio::fs::remove_file(&path).await.map_err(AppError::Io)?;
357                deleted_files += 1;
358                log::info!("Deleted file: {}", path.display());
359            }
360        }
361
362        let size_mb = total_size / (1024 * 1024);
363
364        Ok(crate::i18n::get_command_translation(
365            "system.commands.cleanup.www_all_success",
366            &[
367                &deleted_dirs.to_string(),
368                &deleted_files.to_string(),
369                &size_mb.to_string(),
370            ],
371        ))
372    }
373
374    pub async fn cleanup_www_by_name(server_name: &str) -> Result<String> {
375        let exe_path = std::env::current_exe().map_err(AppError::Io)?;
376        let base_dir = exe_path.parent().ok_or_else(|| {
377            AppError::Validation("Cannot determine executable directory".to_string())
378        })?;
379
380        let www_dir = base_dir.join("www");
381
382        if !www_dir.exists() {
383            return Ok(crate::i18n::get_command_translation(
384                "system.commands.cleanup.no_www_for_server",
385                &[server_name],
386            ));
387        }
388
389        let mut deleted_dirs = 0;
390        let mut total_size = 0u64;
391
392        let mut entries = tokio::fs::read_dir(&www_dir).await.map_err(AppError::Io)?;
393
394        while let Some(entry) = entries.next_entry().await.map_err(AppError::Io)? {
395            let path = entry.path();
396            let metadata = tokio::fs::metadata(&path).await.map_err(AppError::Io)?;
397
398            if metadata.is_dir() {
399                if let Some(dir_name) = path.file_name().and_then(|n| n.to_str()) {
400                    if Self::matches_server_name(dir_name, server_name) {
401                        total_size += Self::calculate_directory_size(&path).await.unwrap_or(0);
402                        tokio::fs::remove_dir_all(&path)
403                            .await
404                            .map_err(AppError::Io)?;
405                        deleted_dirs += 1;
406                        log::info!("Deleted server directory: {}", path.display());
407                    }
408                }
409            }
410        }
411
412        let size_mb = total_size / (1024 * 1024);
413
414        if deleted_dirs > 0 {
415            Ok(crate::i18n::get_command_translation(
416                "system.commands.cleanup.www_server_success",
417                &[server_name, &deleted_dirs.to_string(), &size_mb.to_string()],
418            ))
419        } else {
420            Ok(crate::i18n::get_command_translation(
421                "system.commands.cleanup.no_www_for_server",
422                &[server_name],
423            ))
424        }
425    }
426
427    async fn calculate_directory_size(dir: &std::path::Path) -> Result<u64> {
428        let mut total_size = 0u64;
429        let mut stack = vec![dir.to_path_buf()];
430
431        while let Some(current_dir) = stack.pop() {
432            let mut entries = tokio::fs::read_dir(&current_dir)
433                .await
434                .map_err(AppError::Io)?;
435
436            while let Some(entry) = entries.next_entry().await.map_err(AppError::Io)? {
437                let metadata = entry.metadata().await.map_err(AppError::Io)?;
438
439                if metadata.is_file() {
440                    total_size += metadata.len();
441                } else if metadata.is_dir() {
442                    stack.push(entry.path());
443                }
444            }
445        }
446
447        Ok(total_size)
448    }
449
450    fn matches_server_name(dir_name: &str, server_name: &str) -> bool {
451        if dir_name == server_name {
452            return true;
453        }
454
455        if dir_name.starts_with(&format!("{}-[", server_name)) {
456            return true;
457        }
458
459        if dir_name.contains(server_name) && dir_name.contains('[') && dir_name.ends_with(']') {
460            if let Some(bracket_start) = dir_name.rfind('[') {
461                if let Some(port_str) = dir_name.get(bracket_start + 1..dir_name.len() - 1) {
462                    return port_str.parse::<u16>().is_ok();
463                }
464            }
465        }
466
467        false
468    }
469}