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(¤t_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(¤t_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(¤t_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}