backup_suite/core/
incremental.rs

1//! # 増分バックアップエンジン
2//!
3//! 変更検出ベースの増分バックアップ機能を提供します。
4//!
5//! # 機能
6//!
7//! - **変更検出**: SHA-256ハッシュ比較による変更ファイル検出
8//! - **増分管理**: 親バックアップへの参照管理
9//! - **自動フォールバック**: 初回または前回バックアップなしの場合、フルバックアップに自動切り替え
10//!
11//! # 使用例
12//!
13//! ```no_run
14//! use backup_suite::core::incremental::{BackupType, IncrementalBackupEngine};
15//! use std::path::PathBuf;
16//!
17//! // 初回バックアップ(自動的にフルバックアップになる)
18//! let engine = IncrementalBackupEngine::new(PathBuf::from("./backups"));
19//! let backup_type = engine.determine_backup_type().unwrap();
20//! assert!(matches!(backup_type, BackupType::Full));
21//!
22//! // 2回目以降は増分バックアップが可能
23//! ```
24
25use anyhow::{Context, Result};
26use serde::{Deserialize, Serialize};
27use std::path::{Path, PathBuf};
28
29use super::integrity::BackupMetadata;
30
31/// バックアップタイプ
32///
33/// フルバックアップまたは増分バックアップを識別します。
34///
35/// # バリアント
36///
37/// * Full - 全ファイルをバックアップ(初回または前回なし)
38/// * Incremental - 変更ファイルのみバックアップ(前回バックアップからの差分)
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
40pub enum BackupType {
41    #[default]
42    Full,
43    Incremental,
44}
45
46/// 増分バックアップエンジン
47///
48/// 変更検出とバックアップタイプの決定を担当します。
49///
50/// # フィールド
51///
52/// * `backup_base` - バックアップディレクトリのベースパス
53///
54/// # 使用例
55///
56/// ```no_run
57/// use backup_suite::core::incremental::IncrementalBackupEngine;
58/// use std::path::PathBuf;
59///
60/// let engine = IncrementalBackupEngine::new(PathBuf::from("./backups"));
61/// let backup_type = engine.determine_backup_type().unwrap();
62/// ```
63pub struct IncrementalBackupEngine {
64    backup_base: PathBuf,
65}
66
67impl IncrementalBackupEngine {
68    /// 新しい IncrementalBackupEngine を作成
69    ///
70    /// # 引数
71    ///
72    /// * `backup_base` - バックアップディレクトリのベースパス
73    ///
74    /// # 戻り値
75    ///
76    /// IncrementalBackupEngine インスタンス
77    ///
78    /// # 使用例
79    ///
80    /// ```no_run
81    /// use backup_suite::core::incremental::IncrementalBackupEngine;
82    /// use std::path::PathBuf;
83    ///
84    /// let engine = IncrementalBackupEngine::new(PathBuf::from("./backups"));
85    /// ```
86    #[must_use]
87    pub fn new(backup_base: PathBuf) -> Self {
88        Self { backup_base }
89    }
90
91    /// バックアップタイプを決定
92    ///
93    /// 前回のバックアップが存在するかチェックし、存在すればIncremental、
94    /// 存在しなければFullを返します。
95    ///
96    /// # 戻り値
97    ///
98    /// 成功時は BackupType、失敗時はエラー
99    ///
100    /// # Errors
101    ///
102    /// 以下の場合にエラーを返します:
103    /// * 最新バックアップの検索に失敗した場合
104    ///
105    /// # 使用例
106    ///
107    /// ```no_run
108    /// use backup_suite::core::incremental::IncrementalBackupEngine;
109    /// use std::path::PathBuf;
110    ///
111    /// let engine = IncrementalBackupEngine::new(PathBuf::from("./backups"));
112    /// let backup_type = engine.determine_backup_type().unwrap();
113    /// ```
114    pub fn determine_backup_type(&self) -> Result<BackupType> {
115        match self.find_latest_backup()? {
116            Some(_) => Ok(BackupType::Incremental),
117            None => Ok(BackupType::Full),
118        }
119    }
120
121    /// 最新のバックアップディレクトリを検索
122    ///
123    /// # 戻り値
124    ///
125    /// 成功時は最新バックアップのパス(存在しない場合はNone)、失敗時はエラー
126    ///
127    /// # Errors
128    ///
129    /// 以下の場合にエラーを返します:
130    /// * バックアップディレクトリの読み込みに失敗した場合
131    pub fn find_latest_backup(&self) -> Result<Option<PathBuf>> {
132        if !self.backup_base.exists() {
133            return Ok(None);
134        }
135
136        let mut backups: Vec<PathBuf> = std::fs::read_dir(&self.backup_base)
137            .context("バックアップディレクトリの読み込み失敗")?
138            .filter_map(std::result::Result::ok)
139            .filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
140            .filter(|entry| entry.file_name().to_string_lossy().starts_with("backup_"))
141            .map(|entry| entry.path())
142            // dry-runで作成された空のディレクトリ(.integrityなし)を除外
143            .filter(|path| path.join(".integrity").exists())
144            .collect();
145
146        if backups.is_empty() {
147            return Ok(None);
148        }
149
150        // タイムスタンプでソート(降順)
151        backups.sort_by(|a, b| b.cmp(a));
152        Ok(Some(backups[0].clone()))
153    }
154
155    /// 変更ファイルを検出
156    ///
157    /// 前回のバックアップメタデータと現在のファイルハッシュを比較し、
158    /// 変更されたファイルのリストを返します。
159    ///
160    /// # 引数
161    ///
162    /// * `current_files` - 現在のファイルリスト(相対パス、絶対パス)
163    /// * `previous_metadata` - 前回のバックアップメタデータ
164    ///
165    /// # 戻り値
166    ///
167    /// 変更されたファイルのリスト(相対パス、絶対パス)
168    ///
169    /// # Errors
170    ///
171    /// 以下の場合にエラーを返します:
172    /// * ファイルハッシュの計算に失敗した場合
173    ///
174    /// # 使用例
175    ///
176    /// ```no_run
177    /// use backup_suite::core::incremental::IncrementalBackupEngine;
178    /// use backup_suite::core::integrity::BackupMetadata;
179    /// use std::path::PathBuf;
180    ///
181    /// let engine = IncrementalBackupEngine::new(PathBuf::from("./backups"));
182    /// let files = vec![(PathBuf::from("file.txt"), PathBuf::from("/path/to/file.txt"))];
183    /// let metadata = BackupMetadata::new();
184    /// let changed = engine.detect_changed_files(&files, &metadata).unwrap();
185    /// ```
186    pub fn detect_changed_files(
187        &self,
188        current_files: &[(PathBuf, PathBuf)],
189        previous_metadata: &BackupMetadata,
190    ) -> Result<Vec<(PathBuf, PathBuf)>> {
191        let mut changed_files = Vec::new();
192
193        for (relative_path, absolute_path) in current_files {
194            // 前回のハッシュを取得
195            let previous_hash = previous_metadata.file_hashes.get(relative_path);
196
197            // 現在のハッシュを計算
198            let current_hash = BackupMetadata::compute_file_hash(absolute_path)
199                .context("ハッシュ計算失敗: absolute_path.display()".to_string())?;
200
201            // ハッシュが異なる場合、または新規ファイルの場合は変更とみなす
202            if previous_hash != Some(&current_hash) {
203                changed_files.push((relative_path.clone(), absolute_path.clone()));
204            }
205        }
206
207        Ok(changed_files)
208    }
209
210    /// 前回のバックアップメタデータを読み込み
211    ///
212    /// # 戻り値
213    ///
214    /// 成功時はメタデータ、失敗時はエラー
215    ///
216    /// # Errors
217    ///
218    /// 以下の場合にエラーを返します:
219    /// * 前回のバックアップが見つからない場合
220    /// * メタデータの読み込みに失敗した場合
221    pub fn load_previous_metadata(&self) -> Result<BackupMetadata> {
222        let latest_backup = self
223            .find_latest_backup()?
224            .ok_or_else(|| anyhow::anyhow!("前回のバックアップが見つかりません"))?;
225
226        BackupMetadata::load(&latest_backup).context("前回のバックアップメタデータ読み込み失敗")
227    }
228
229    /// 前回のバックアップ名を取得
230    ///
231    /// # 戻り値
232    ///
233    /// 成功時はバックアップ名、失敗時はエラー
234    ///
235    /// # Errors
236    ///
237    /// 以下の場合にエラーを返します:
238    /// * 最新バックアップの検索に失敗した場合
239    /// * バックアップ名の取得に失敗した場合
240    pub fn get_previous_backup_name(&self) -> Result<Option<String>> {
241        match self.find_latest_backup()? {
242            Some(path) => {
243                let name = path
244                    .file_name()
245                    .and_then(|n| n.to_str())
246                    .map(std::string::ToString::to_string)
247                    .ok_or_else(|| anyhow::anyhow!("バックアップ名取得失敗"))?;
248                Ok(Some(name))
249            }
250            None => Ok(None),
251        }
252    }
253}
254
255/// 増分バックアップチェーンの解決
256///
257/// 増分バックアップの親チェーンを遡り、完全な復元に必要な
258/// 全バックアップディレクトリのリストを返します。
259///
260/// # 引数
261///
262/// * `backup_dir` - 増分バックアップディレクトリ
263///
264/// # 戻り値
265///
266/// バックアップディレクトリのリスト(フルバックアップ→増分1→増分2...の順)
267///
268/// # Errors
269///
270/// 以下の場合にエラーを返します:
271/// * メタデータの読み込みに失敗した場合
272/// * 親ディレクトリの取得に失敗した場合
273/// * 親バックアップディレクトリが見つからない場合
274///
275/// # 使用例
276///
277/// ```no_run
278/// use backup_suite::core::incremental::resolve_backup_chain;
279/// use std::path::PathBuf;
280///
281/// let chain = resolve_backup_chain(&PathBuf::from("./backups/backup_20250107_120000")).unwrap();
282/// for backup in &chain {
283///     println!("復元順: {:?}", backup);
284/// }
285/// ```
286pub fn resolve_backup_chain(backup_dir: &Path) -> Result<Vec<PathBuf>> {
287    let mut chain = Vec::new();
288    let mut current_dir = backup_dir.to_path_buf();
289
290    loop {
291        // 現在のバックアップメタデータを読み込み(存在しない場合は単一バックアップとして扱う)
292        let metadata = match BackupMetadata::load(&current_dir) {
293            Ok(m) => m,
294            Err(_) => {
295                // メタデータが存在しない場合は単一のバックアップとして扱う
296                chain.push(current_dir.clone());
297                break;
298            }
299        };
300
301        // チェーンに追加(逆順で追加、後で反転)
302        chain.push(current_dir.clone());
303
304        // 親バックアップがある場合、そちらへ移動
305        match metadata.parent_backup {
306            Some(parent_name) => {
307                let parent_dir = current_dir
308                    .parent()
309                    .ok_or_else(|| anyhow::anyhow!("親ディレクトリ取得失敗"))?
310                    .join(&parent_name);
311
312                if !parent_dir.exists() {
313                    return Err(anyhow::anyhow!(
314                        "親バックアップが見つかりません: parent_dir.display()"
315                    ));
316                }
317
318                current_dir = parent_dir;
319            }
320            None => {
321                // フルバックアップに到達(ルート)
322                break;
323            }
324        }
325    }
326
327    // 正しい順序に反転(フルバックアップ→増分1→増分2...)
328    chain.reverse();
329    Ok(chain)
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use std::fs;
336    use tempfile::TempDir;
337
338    #[test]
339    fn test_determine_backup_type_no_previous() {
340        let temp = TempDir::new().unwrap();
341        let engine = IncrementalBackupEngine::new(temp.path().to_path_buf());
342
343        let backup_type = engine.determine_backup_type().unwrap();
344        assert_eq!(backup_type, BackupType::Full);
345    }
346
347    #[test]
348    fn test_determine_backup_type_with_previous() {
349        let temp = TempDir::new().unwrap();
350        let backup_dir = temp.path().join("backup_20250107_120000");
351        fs::create_dir(&backup_dir).unwrap();
352
353        // ダミーメタデータ作成
354        let metadata = BackupMetadata::new();
355        metadata.save(&backup_dir).unwrap();
356
357        let engine = IncrementalBackupEngine::new(temp.path().to_path_buf());
358        let backup_type = engine.determine_backup_type().unwrap();
359        assert_eq!(backup_type, BackupType::Incremental);
360    }
361
362    #[test]
363    fn test_detect_changed_files() {
364        use std::io::Write;
365
366        let temp = TempDir::new().unwrap();
367
368        // ファイル1を作成
369        let file1 = temp.path().join("file1.txt");
370        let mut f1 = fs::File::create(&file1).unwrap();
371        f1.write_all(b"original content").unwrap();
372        drop(f1);
373
374        // 前回のメタデータを作成
375        let mut previous_metadata = BackupMetadata::new();
376        let hash1 = BackupMetadata::compute_file_hash(&file1).unwrap();
377        previous_metadata
378            .file_hashes
379            .insert(PathBuf::from("file1.txt"), hash1);
380
381        // ファイル1を変更
382        fs::write(&file1, b"modified content").unwrap();
383
384        // ファイル2を新規追加
385        let file2 = temp.path().join("file2.txt");
386        fs::write(&file2, b"new file").unwrap();
387
388        let current_files = vec![
389            (PathBuf::from("file1.txt"), file1.clone()),
390            (PathBuf::from("file2.txt"), file2.clone()),
391        ];
392
393        let engine = IncrementalBackupEngine::new(temp.path().to_path_buf());
394        let changed = engine
395            .detect_changed_files(&current_files, &previous_metadata)
396            .unwrap();
397
398        // file1(変更)とfile2(新規)の2ファイルが検出されるはず
399        assert_eq!(changed.len(), 2);
400    }
401
402    #[test]
403    fn test_resolve_backup_chain() {
404        let temp = TempDir::new().unwrap();
405
406        // フルバックアップ
407        let full_backup = temp.path().join("backup_20250107_100000");
408        fs::create_dir(&full_backup).unwrap();
409        let mut full_metadata = BackupMetadata::new();
410        full_metadata.backup_type = BackupType::Full;
411        full_metadata.parent_backup = None;
412        full_metadata.save(&full_backup).unwrap();
413
414        // 増分バックアップ1
415        let inc1_backup = temp.path().join("backup_20250107_110000");
416        fs::create_dir(&inc1_backup).unwrap();
417        let mut inc1_metadata = BackupMetadata::new();
418        inc1_metadata.backup_type = BackupType::Incremental;
419        inc1_metadata.parent_backup = Some("backup_20250107_100000".to_string());
420        inc1_metadata.save(&inc1_backup).unwrap();
421
422        // 増分バックアップ2
423        let inc2_backup = temp.path().join("backup_20250107_120000");
424        fs::create_dir(&inc2_backup).unwrap();
425        let mut inc2_metadata = BackupMetadata::new();
426        inc2_metadata.backup_type = BackupType::Incremental;
427        inc2_metadata.parent_backup = Some("backup_20250107_110000".to_string());
428        inc2_metadata.save(&inc2_backup).unwrap();
429
430        // チェーン解決
431        let chain = resolve_backup_chain(&inc2_backup).unwrap();
432
433        // 順序確認(フル→増分1→増分2)
434        assert_eq!(chain.len(), 3);
435        assert!(chain[0].ends_with("backup_20250107_100000"));
436        assert!(chain[1].ends_with("backup_20250107_110000"));
437        assert!(chain[2].ends_with("backup_20250107_120000"));
438    }
439}