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") => {
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 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 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 let stopped = self.cleanup_stopped_servers(ctx);
144 let failed = self.cleanup_failed_servers(ctx);
145
146 tokio::spawn(async move {
148 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 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 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 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 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(¤t_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}