rush_sync_server/commands/cleanup/
command.rs

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