1use anyhow::Result;
2use chrono::{DateTime, Utc};
3use std::path::{Path, PathBuf};
4use walkdir::WalkDir;
5
6use super::{BackupHistory, Config, Priority};
7use crate::security::{AuditEvent, AuditLog};
8
9#[derive(Debug, Clone)]
13pub struct CleanupPolicy {
14 pub retention_days: Option<u32>,
16 pub keep_count: Option<usize>,
18 pub max_total_size: Option<u64>,
20 pub priority_based: bool,
22}
23
24impl Default for CleanupPolicy {
25 fn default() -> Self {
26 Self {
27 retention_days: Some(30),
28 keep_count: None,
29 max_total_size: None,
30 priority_based: false,
31 }
32 }
33}
34
35impl CleanupPolicy {
36 #[must_use]
38 pub fn retention_days(days: u32) -> Self {
39 Self {
40 retention_days: Some(days),
41 ..Default::default()
42 }
43 }
44
45 #[must_use]
47 pub fn keep_count(count: usize) -> Self {
48 Self {
49 keep_count: Some(count),
50 retention_days: None,
51 ..Default::default()
52 }
53 }
54
55 #[must_use]
57 pub fn max_size(size_bytes: u64) -> Self {
58 Self {
59 max_total_size: Some(size_bytes),
60 retention_days: None,
61 ..Default::default()
62 }
63 }
64
65 #[must_use]
67 pub fn with_priority_based(mut self) -> Self {
68 self.priority_based = true;
69 self
70 }
71}
72
73#[derive(Debug)]
75pub struct CleanupResult {
76 pub total_checked: usize,
77 pub deleted: usize,
78 pub freed_bytes: u64,
79 pub errors: Vec<String>,
80}
81
82impl CleanupResult {
83 fn new() -> Self {
84 Self {
85 total_checked: 0,
86 deleted: 0,
87 freed_bytes: 0,
88 errors: Vec::new(),
89 }
90 }
91}
92
93#[derive(Debug, Clone)]
95struct BackupInfo {
96 path: PathBuf,
97 modified_time: DateTime<Utc>,
98 size: u64,
99 priority: Option<Priority>,
100}
101
102pub struct CleanupEngine {
106 policy: CleanupPolicy,
107 dry_run: bool,
108 interactive: bool,
109 audit_log: Option<AuditLog>,
110}
111
112impl CleanupEngine {
113 #[must_use]
115 pub fn new(policy: CleanupPolicy, dry_run: bool) -> Self {
116 let audit_log = AuditLog::new()
117 .map_err(|e| eprintln!("警告: 監査ログの初期化に失敗しました: {e}"))
118 .ok();
119
120 Self {
121 policy,
122 dry_run,
123 interactive: false,
124 audit_log,
125 }
126 }
127
128 #[must_use]
130 pub fn with_interactive(mut self, interactive: bool) -> Self {
131 self.interactive = interactive;
132 self
133 }
134
135 pub fn cleanup(&mut self) -> Result<CleanupResult> {
146 let user = AuditLog::current_user();
147 let days = self.policy.retention_days.unwrap_or(0);
148
149 if let Some(ref mut audit_log) = self.audit_log {
151 let _ = audit_log
152 .log(AuditEvent::cleanup_started(&user, days))
153 .map_err(|e| eprintln!("警告: 監査ログの記録に失敗しました: {e}"));
154 }
155
156 let config = Config::load()?;
157 let dest = &config.backup.destination;
158
159 if !dest.exists() {
160 return Ok(CleanupResult::new());
161 }
162
163 let mut backups = self.get_backup_list(dest)?;
165
166 backups.sort_by(|a, b| b.modified_time.cmp(&a.modified_time));
168
169 let mut result = CleanupResult::new();
170 result.total_checked = backups.len();
171
172 let to_delete = self.determine_deletions(&backups)?;
174
175 for backup in to_delete {
176 if self.interactive {
177 if !self.confirm_deletion(&backup)? {
179 continue;
180 }
181 }
182
183 if self.dry_run {
184 println!("🗑️ [ドライラン] 削除予定: {:?}", backup.path);
185 result.deleted += 1;
186 result.freed_bytes += backup.size;
187 } else {
188 match std::fs::remove_dir_all(&backup.path) {
189 Ok(_) => {
190 println!("🗑️ 削除完了: {:?}", backup.path);
191 result.deleted += 1;
192 result.freed_bytes += backup.size;
193 }
194 Err(e) => {
195 result
196 .errors
197 .push(format!("削除失敗 {:?}: {}", backup.path, e));
198 }
199 }
200 }
201 }
202
203 if let Some(ref mut audit_log) = self.audit_log {
205 let metadata = serde_json::json!({
206 "total_checked": result.total_checked,
207 "deleted": result.deleted,
208 "freed_bytes": result.freed_bytes,
209 "policy": format!("{:?}", self.policy),
210 });
211
212 let event = if result.errors.is_empty() {
213 AuditEvent::cleanup_completed(&user, metadata)
214 } else {
215 AuditEvent::cleanup_failed(
216 &user,
217 format!("{}件のエラーが発生しました", result.errors.len()),
218 )
219 };
220
221 let _ = audit_log
222 .log(event)
223 .map_err(|e| eprintln!("警告: 監査ログの記録に失敗しました: {e}"));
224 }
225
226 Ok(result)
227 }
228
229 fn get_backup_list(&self, dest: &Path) -> Result<Vec<BackupInfo>> {
231 let mut backups = Vec::new();
232
233 for entry in WalkDir::new(dest)
234 .max_depth(1)
235 .into_iter()
236 .filter_map(std::result::Result::ok)
237 {
238 if !entry.file_type().is_dir() || entry.path() == dest {
239 continue;
240 }
241
242 let path = entry.path().to_path_buf();
243 let metadata = std::fs::metadata(&path)?;
244 let modified_time: DateTime<Utc> = metadata.modified()?.into();
245 let size = self.calculate_size(&path)?;
246
247 let priority = self.get_priority_from_history(&path);
249
250 backups.push(BackupInfo {
251 path,
252 modified_time,
253 size,
254 priority,
255 });
256 }
257
258 Ok(backups)
259 }
260
261 fn calculate_size(&self, dir: &Path) -> Result<u64> {
263 let mut total = 0;
264 for entry in WalkDir::new(dir)
265 .into_iter()
266 .filter_map(std::result::Result::ok)
267 {
268 if entry.file_type().is_file() {
269 total += entry.metadata()?.len();
270 }
271 }
272 Ok(total)
273 }
274
275 fn get_priority_from_history(&self, backup_dir: &Path) -> Option<Priority> {
277 if let Ok(history) = BackupHistory::load_all() {
278 history
279 .iter()
280 .find(|h| h.backup_dir == backup_dir)
281 .and_then(|h| h.priority)
282 } else {
283 None
284 }
285 }
286
287 fn determine_deletions(&self, backups: &[BackupInfo]) -> Result<Vec<BackupInfo>> {
289 let mut to_delete = Vec::new();
290
291 if let Some(days) = self.policy.retention_days {
293 let cutoff = Utc::now() - chrono::Duration::days(days as i64);
294 for backup in backups {
295 if backup.modified_time < cutoff {
296 if self.policy.priority_based {
298 if let Some(Priority::High) = backup.priority {
299 let high_priority_cutoff =
301 Utc::now() - chrono::Duration::days((days * 2) as i64);
302 if backup.modified_time < high_priority_cutoff {
303 to_delete.push(backup.clone());
304 }
305 } else {
306 to_delete.push(backup.clone());
307 }
308 } else {
309 to_delete.push(backup.clone());
310 }
311 }
312 }
313 }
314
315 if let Some(keep) = self.policy.keep_count {
317 if backups.len() > keep {
318 to_delete.extend_from_slice(&backups[keep..]);
319 }
320 }
321
322 if let Some(max_size) = self.policy.max_total_size {
324 let mut current_size = 0u64;
325 for backup in backups {
326 current_size += backup.size;
327 if current_size > max_size {
328 to_delete.push(backup.clone());
329 }
330 }
331 }
332
333 to_delete.sort_by(|a, b| a.path.cmp(&b.path));
335 to_delete.dedup_by(|a, b| a.path == b.path);
336
337 Ok(to_delete)
338 }
339
340 fn confirm_deletion(&self, backup: &BackupInfo) -> Result<bool> {
342 use dialoguer::Confirm;
343
344 println!("\n削除候補:");
345 println!(" パス: {:?}", backup.path);
346 println!(
347 " 作成日時: {}",
348 backup.modified_time.format("%Y-%m-%d %H:%M:%S")
349 );
350 println!(" サイズ: {}", format_bytes(backup.size));
351 if let Some(ref priority) = backup.priority {
352 println!(" 優先度: {priority:?}");
353 }
354
355 let confirm = Confirm::new()
356 .with_prompt("このバックアップを削除しますか?")
357 .default(false)
358 .interact()?;
359
360 Ok(confirm)
361 }
362}
363
364fn format_bytes(bytes: u64) -> String {
366 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
367 let mut size = bytes as f64;
368 let mut unit_index = 0;
369
370 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
371 size /= 1024.0;
372 unit_index += 1;
373 }
374
375 format!("{:.2} {}", size, UNITS[unit_index])
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381 use std::fs;
382 use tempfile::TempDir;
383
384 #[test]
385 fn test_format_bytes() {
386 assert_eq!(format_bytes(1024), "1.00 KB");
387 assert_eq!(format_bytes(1_048_576), "1.00 MB");
388 assert_eq!(format_bytes(1_073_741_824), "1.00 GB");
389 }
390
391 #[test]
392 fn test_cleanup_policy_retention_days() {
393 let policy = CleanupPolicy::retention_days(30);
394 assert_eq!(policy.retention_days, Some(30));
395 assert_eq!(policy.keep_count, None);
396 }
397
398 #[test]
399 fn test_cleanup_policy_keep_count() {
400 let policy = CleanupPolicy::keep_count(10);
401 assert_eq!(policy.keep_count, Some(10));
402 assert_eq!(policy.retention_days, None);
403 }
404
405 #[test]
406 fn test_calculate_size() {
407 let temp = TempDir::new().unwrap();
408 let dir = temp.path().join("test_dir");
409 fs::create_dir_all(&dir).unwrap();
410
411 fs::write(dir.join("file1.txt"), b"hello").unwrap();
413 fs::write(dir.join("file2.txt"), b"world").unwrap();
414
415 let engine = CleanupEngine::new(CleanupPolicy::default(), false);
416 let size = engine.calculate_size(&dir).unwrap();
417
418 assert_eq!(size, 10); }
420}