backup_suite/core/
cleanup.rs

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/// クリーンアップポリシー
10///
11/// 古いバックアップの削除条件を定義します。
12#[derive(Debug, Clone)]
13pub struct CleanupPolicy {
14    /// 保持期間(日数)
15    pub retention_days: Option<u32>,
16    /// 保持数(最新N個)
17    pub keep_count: Option<usize>,
18    /// 最大合計サイズ(バイト)
19    pub max_total_size: Option<u64>,
20    /// 優先度別保持(高優先度は長く保持)
21    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    /// 保持期間を指定してポリシーを作成
37    #[must_use]
38    pub fn retention_days(days: u32) -> Self {
39        Self {
40            retention_days: Some(days),
41            ..Default::default()
42        }
43    }
44
45    /// 保持数を指定してポリシーを作成
46    #[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    /// 最大サイズを指定してポリシーを作成
56    #[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    /// 優先度別保持を有効化
66    #[must_use]
67    pub fn with_priority_based(mut self) -> Self {
68        self.priority_based = true;
69        self
70    }
71}
72
73/// クリーンアップ結果
74#[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/// バックアップ情報
94#[derive(Debug, Clone)]
95struct BackupInfo {
96    path: PathBuf,
97    modified_time: DateTime<Utc>,
98    size: u64,
99    priority: Option<Priority>,
100}
101
102/// クリーンアップエンジン
103///
104/// 古いバックアップを自動的に削除します。
105pub struct CleanupEngine {
106    policy: CleanupPolicy,
107    dry_run: bool,
108    interactive: bool,
109    audit_log: Option<AuditLog>,
110}
111
112impl CleanupEngine {
113    /// 新しいCleanupEngineを作成
114    #[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    /// 対話的削除を有効化
129    #[must_use]
130    pub fn with_interactive(mut self, interactive: bool) -> Self {
131        self.interactive = interactive;
132        self
133    }
134
135    /// クリーンアップを実行
136    ///
137    /// # Errors
138    ///
139    /// 以下の場合にエラーを返します:
140    /// * 設定ファイルの読み込みに失敗した場合
141    /// * バックアップディレクトリの列挙に失敗した場合
142    /// * ファイルメタデータの取得に失敗した場合
143    /// * 削除対象の決定に失敗した場合
144    /// * 対話的確認の入力処理に失敗した場合
145    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        // 監査ログ: クリーンアップ開始
150        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        // バックアップディレクトリ一覧を取得
164        let mut backups = self.get_backup_list(dest)?;
165
166        // ソート(新しい順)
167        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        // 削除対象を決定
173        let to_delete = self.determine_deletions(&backups)?;
174
175        for backup in to_delete {
176            if self.interactive {
177                // 対話的確認
178                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        // 監査ログ: クリーンアップ完了 or 失敗
204        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    /// バックアップ一覧を取得
230    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            // 優先度を履歴から取得(可能な場合)
248            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    /// ディレクトリサイズを計算
262    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    /// 履歴から優先度を取得
276    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    /// 削除対象を決定
288    fn determine_deletions(&self, backups: &[BackupInfo]) -> Result<Vec<BackupInfo>> {
289        let mut to_delete = Vec::new();
290
291        // 1. 保持期間による削除
292        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                    // 優先度別保持が有効な場合
297                    if self.policy.priority_based {
298                        if let Some(Priority::High) = backup.priority {
299                            // 高優先度は2倍の期間保持
300                            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        // 2. 保持数による削除
316        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        // 3. 最大サイズによる削除
323        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        // 重複を排除
334        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    /// 削除確認(対話的)
341    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
364/// バイト数を人間が読みやすい形式に変換
365fn 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        // テストファイルを作成
412        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); // "hello" + "world" = 10 bytes
419    }
420}