rush_sync_server/commands/cleanup/
command.rs1use 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 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 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 let stopped = self.cleanup_stopped_servers(ctx);
143 let failed = self.cleanup_failed_servers(ctx);
144
145 tokio::spawn(async move {
147 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 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 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(¤t_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}