backup_suite/core/config.rs
1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5use super::Target;
6use crate::error::{BackupError, Result as BackupResult};
7use crate::security::{check_read_permission, check_write_permission};
8
9/// スケジュール設定
10///
11/// バックアップの自動実行スケジュールを定義します。
12/// 優先度別に異なる頻度でバックアップを実行できます。
13///
14/// # フィールド
15///
16/// * `enabled` - スケジュール機能の有効/無効
17/// * `high_frequency` - 高優先度のバックアップ頻度("daily", "weekly", "monthly")
18/// * `medium_frequency` - 中優先度のバックアップ頻度
19/// * `low_frequency` - 低優先度のバックアップ頻度
20///
21/// # 使用例
22///
23/// ```no_run
24/// use backup_suite::core::config::ScheduleConfig;
25///
26/// let schedule = ScheduleConfig {
27/// enabled: true,
28/// high_frequency: "daily".to_string(),
29/// medium_frequency: "weekly".to_string(),
30/// low_frequency: "monthly".to_string(),
31/// };
32/// ```
33#[derive(Debug, Serialize, Deserialize)]
34pub struct ScheduleConfig {
35 pub enabled: bool,
36 pub high_frequency: String, // "daily", "weekly", "monthly"
37 pub medium_frequency: String,
38 pub low_frequency: String,
39}
40
41impl Default for ScheduleConfig {
42 fn default() -> Self {
43 Self {
44 enabled: false,
45 high_frequency: "daily".to_string(),
46 medium_frequency: "weekly".to_string(),
47 low_frequency: "monthly".to_string(),
48 }
49 }
50}
51
52/// バックアップ設定
53///
54/// バックアップ先ディレクトリと保存期間を定義します。
55///
56/// # フィールド
57///
58/// * `destination` - バックアップファイルの保存先ディレクトリ
59/// * `auto_cleanup` - 古いバックアップの自動削除を有効にするか
60/// * `keep_days` - バックアップを保持する日数(1-3650日)
61///
62/// # 使用例
63///
64/// ```no_run
65/// use backup_suite::core::config::BackupConfig;
66/// use std::path::PathBuf;
67///
68/// let config = BackupConfig {
69/// destination: PathBuf::from("/backup/storage"),
70/// auto_cleanup: true,
71/// keep_days: 30,
72/// };
73/// ```
74#[derive(Debug, Serialize, Deserialize)]
75pub struct BackupConfig {
76 pub destination: PathBuf,
77 pub auto_cleanup: bool,
78 pub keep_days: u32,
79}
80
81impl Default for BackupConfig {
82 fn default() -> Self {
83 let home = dirs::home_dir().unwrap_or_else(|| PathBuf::from("/tmp"));
84 Self {
85 destination: home.join("backup-suite/backups"),
86 auto_cleanup: false,
87 keep_days: 30,
88 }
89 }
90}
91
92/// メイン設定構造体
93///
94/// `backup-suite` の全体設定を管理します。
95/// TOML形式で永続化され、`~/.config/backup-suite/config.toml` に保存されます。
96///
97/// # フィールド
98///
99/// * `version` - 設定ファイルのバージョン
100/// * `backup` - バックアップ関連の設定
101/// * `schedule` - スケジュール関連の設定
102/// * `targets` - バックアップ対象のリスト
103///
104/// # 使用例
105///
106/// ```no_run
107/// use backup_suite::{Config, Target, Priority};
108/// use std::path::PathBuf;
109///
110/// // デフォルト設定を作成
111/// let mut config = Config::default();
112///
113/// // バックアップ対象を追加
114/// let target = Target::new(
115/// PathBuf::from("/home/user/documents"),
116/// Priority::High,
117/// "重要ドキュメント".to_string()
118/// );
119/// config.add_target(target);
120///
121/// // 設定を保存
122/// config.save().unwrap();
123/// ```
124#[derive(Debug, Serialize, Deserialize)]
125pub struct Config {
126 pub version: String,
127 pub backup: BackupConfig,
128 #[serde(default)]
129 pub schedule: ScheduleConfig,
130 pub targets: Vec<Target>,
131}
132
133impl Default for Config {
134 fn default() -> Self {
135 Self {
136 version: "1.0.0".to_string(),
137 backup: BackupConfig::default(),
138 schedule: ScheduleConfig::default(),
139 targets: vec![],
140 }
141 }
142}
143
144impl Config {
145 /// 設定ファイルのパスを取得
146 ///
147 /// 設定ファイルは `~/.config/backup-suite/config.toml` に配置されます。
148 ///
149 /// # 戻り値
150 ///
151 /// 成功時は設定ファイルのパス、失敗時はエラー
152 ///
153 /// # Errors
154 ///
155 /// 以下の場合にエラーを返します:
156 /// * ホームディレクトリが取得できない場合
157 ///
158 /// # 使用例
159 ///
160 /// ```no_run
161 /// use backup_suite::Config;
162 ///
163 /// let path = Config::config_path().unwrap();
164 /// println!("設定ファイル: {:?}", path);
165 /// ```
166 pub fn config_path() -> Result<PathBuf> {
167 let home = dirs::home_dir().context("ホームディレクトリが見つかりません")?;
168 Ok(home.join(".config/backup-suite/config.toml"))
169 }
170
171 /// 設定ファイルを読み込み
172 ///
173 /// `~/.config/backup-suite/config.toml` から設定を読み込みます。
174 /// ファイルが存在しない場合はデフォルト設定を返します。
175 ///
176 /// # 戻り値
177 ///
178 /// 成功時は Config インスタンス、失敗時はエラー
179 ///
180 /// # Errors
181 ///
182 /// 以下の場合にエラーを返します:
183 /// * 設定ファイルパスの取得に失敗した場合
184 /// * 設定ファイルの読み込みに失敗した場合
185 /// * TOML解析に失敗した場合
186 ///
187 /// # 使用例
188 ///
189 /// ```no_run
190 /// use backup_suite::Config;
191 ///
192 /// let config = Config::load().unwrap_or_default();
193 /// println!("バックアップ先: {:?}", config.backup.destination);
194 /// ```
195 pub fn load() -> Result<Self> {
196 let config_path = Self::config_path()?;
197
198 if !config_path.exists() {
199 // 設定ファイルが存在しない場合はデフォルト設定を返す
200 return Ok(Self::default());
201 }
202
203 let content = std::fs::read_to_string(&config_path)
204 .context("設定ファイル読み込み失敗: config_path.display()".to_string())?;
205
206 let config: Config = toml::from_str(&content).context("TOML解析失敗")?;
207
208 Ok(config)
209 }
210
211 /// 設定ファイルに保存
212 ///
213 /// 現在の設定を `~/.config/backup-suite/config.toml` に保存します。
214 /// 設定ディレクトリが存在しない場合は自動的に作成されます。
215 ///
216 /// # 戻り値
217 ///
218 /// 成功時は `Ok(())`、失敗時はエラー
219 ///
220 /// # Errors
221 ///
222 /// 以下の場合にエラーを返します:
223 /// * 設定ファイルパスの取得に失敗した場合
224 /// * 設定ディレクトリの作成に失敗した場合
225 /// * TOML生成に失敗した場合
226 /// * ファイル書き込みに失敗した場合
227 ///
228 /// # 使用例
229 ///
230 /// ```no_run
231 /// use backup_suite::{Config, Target, Priority};
232 /// use std::path::PathBuf;
233 ///
234 /// let mut config = Config::default();
235 /// let target = Target::new(
236 /// PathBuf::from("/path/to/backup"),
237 /// Priority::High,
238 /// "重要データ".to_string()
239 /// );
240 /// config.add_target(target);
241 /// config.save().unwrap();
242 /// ```
243 pub fn save(&self) -> Result<()> {
244 let config_path = Self::config_path()?;
245
246 // ディレクトリが存在しない場合は作成
247 if let Some(parent) = config_path.parent() {
248 std::fs::create_dir_all(parent).context("設定ディレクトリ作成失敗")?;
249 }
250
251 let content = toml::to_string_pretty(self).context("TOML生成失敗")?;
252
253 std::fs::write(&config_path, content)
254 .context("設定ファイル書き込み失敗: config_path.display()".to_string())?;
255
256 Ok(())
257 }
258
259 /// バックアップ対象を追加
260 ///
261 /// 新しいバックアップ対象を設定に追加します。
262 ///
263 /// # 引数
264 ///
265 /// * `target` - 追加するバックアップ対象
266 ///
267 /// # 使用例
268 ///
269 /// ```no_run
270 /// use backup_suite::{Config, Target, Priority};
271 /// use std::path::PathBuf;
272 ///
273 /// let mut config = Config::default();
274 /// let target = Target::new(
275 /// PathBuf::from("/home/user/documents"),
276 /// Priority::High,
277 /// "ドキュメント".to_string()
278 /// );
279 /// config.add_target(target);
280 /// ```
281 pub fn add_target(&mut self, target: Target) -> bool {
282 // 重複チェック:同じパスがすでに存在する場合は追加しない
283 if self.targets.iter().any(|t| t.path == target.path) {
284 return false;
285 }
286 self.targets.push(target);
287 true
288 }
289
290 /// バックアップ対象を削除
291 ///
292 /// 指定されたパスのバックアップ対象を設定から削除します。
293 ///
294 /// # 引数
295 ///
296 /// * `path` - 削除するバックアップ対象のパス
297 ///
298 /// # 戻り値
299 ///
300 /// 削除された場合は `true`、見つからなかった場合は `false`
301 ///
302 /// # 使用例
303 ///
304 /// ```no_run
305 /// use backup_suite::Config;
306 /// use std::path::PathBuf;
307 ///
308 /// let mut config = Config::load().unwrap();
309 /// let removed = config.remove_target(&PathBuf::from("/old/path"));
310 /// if removed {
311 /// config.save().unwrap();
312 /// }
313 /// ```
314 #[must_use]
315 pub fn remove_target(&mut self, path: &PathBuf) -> bool {
316 let before_len = self.targets.len();
317 self.targets.retain(|t| &t.path != path);
318 self.targets.len() < before_len
319 }
320
321 /// バックアップ対象を更新
322 ///
323 /// 指定されたパスのバックアップ対象の優先度・カテゴリ・除外パターンを更新します。
324 ///
325 /// # 引数
326 ///
327 /// * `path` - 更新するバックアップ対象のパス
328 /// * `priority` - 新しい優先度(Noneの場合は変更しない)
329 /// * `category` - 新しいカテゴリ(Noneの場合は変更しない)
330 /// * `exclude_patterns` - 新しい除外パターン(Noneの場合は変更しない)
331 ///
332 /// # 戻り値
333 ///
334 /// 更新された場合は `true`、見つからなかった場合は `false`
335 ///
336 /// # 使用例
337 ///
338 /// ```no_run
339 /// use backup_suite::{Config, Priority};
340 /// use std::path::PathBuf;
341 ///
342 /// let mut config = Config::load().unwrap();
343 /// let updated = config.update_target(
344 /// &PathBuf::from("/path/to/update"),
345 /// Some(Priority::High),
346 /// Some("新カテゴリ".to_string()),
347 /// None
348 /// );
349 /// if updated {
350 /// config.save().unwrap();
351 /// }
352 /// ```
353 pub fn update_target(
354 &mut self,
355 path: &PathBuf,
356 priority: Option<crate::core::Priority>,
357 category: Option<String>,
358 exclude_patterns: Option<Vec<String>>,
359 ) -> bool {
360 if let Some(target) = self.targets.iter_mut().find(|t| &t.path == path) {
361 if let Some(p) = priority {
362 target.priority = p;
363 }
364 if let Some(c) = category {
365 target.category = c;
366 }
367 if let Some(patterns) = exclude_patterns {
368 target.exclude_patterns = patterns;
369 }
370 true
371 } else {
372 false
373 }
374 }
375
376 /// 優先度でフィルタリング
377 ///
378 /// 指定された優先度のバックアップ対象のみを抽出します。
379 ///
380 /// # 引数
381 ///
382 /// * `priority` - フィルタリングする優先度
383 ///
384 /// # 戻り値
385 ///
386 /// 指定された優先度のバックアップ対象の参照のベクター
387 ///
388 /// # 使用例
389 ///
390 /// ```no_run
391 /// use backup_suite::{Config, Priority};
392 ///
393 /// let config = Config::load().unwrap();
394 /// let high_priority = config.filter_by_priority(&Priority::High);
395 /// println!("高優先度のバックアップ対象: {}件", high_priority.len());
396 /// ```
397 #[must_use]
398 pub fn filter_by_priority(&self, priority: &super::target::Priority) -> Vec<&Target> {
399 self.targets
400 .iter()
401 .filter(|t| &t.priority >= priority)
402 .collect()
403 }
404
405 /// カテゴリでバックアップ対象をフィルタ
406 ///
407 /// 指定されたカテゴリのバックアップ対象のみを取得します。
408 ///
409 /// # 引数
410 ///
411 /// * `category` - フィルタリングするカテゴリ名
412 ///
413 /// # 戻り値
414 ///
415 /// 指定されたカテゴリのバックアップ対象の参照のベクター
416 ///
417 /// # 使用例
418 ///
419 /// ```no_run
420 /// use backup_suite::Config;
421 ///
422 /// let config = Config::load().unwrap();
423 /// let system_targets = config.filter_by_category("system");
424 /// println!("システムカテゴリのバックアップ対象: {}件", system_targets.len());
425 /// ```
426 #[must_use]
427 pub fn filter_by_category(&self, category: &str) -> Vec<&Target> {
428 self.targets
429 .iter()
430 .filter(|t| t.category == category)
431 .collect()
432 }
433
434 /// 設定の妥当性を検証
435 ///
436 /// すべての設定項目が正しく、実行可能であることを確認します。
437 ///
438 /// # 検証項目
439 ///
440 /// - バックアップ先ディレクトリの存在と書き込み権限
441 /// - 保存期間(keep_days)の妥当性(1-3650日)
442 /// - 各ターゲットの存在確認と読み取り権限
443 /// - 除外パターンの正規表現の妥当性
444 ///
445 /// # 戻り値
446 ///
447 /// すべての検証に成功した場合は `Ok(())`、失敗した場合はエラー
448 ///
449 /// # Errors
450 ///
451 /// 以下の場合にエラーを返します:
452 /// * `BackupError::BackupDirectoryCreationError` - バックアップ先ディレクトリの作成に失敗
453 /// * `BackupError::PermissionDenied` - バックアップ先に書き込み権限がない
454 /// * `BackupError::ConfigValidationError` - 保存期間(keep_days)が範囲外(1-3650日)
455 /// * `BackupError::PermissionDenied` - ターゲットに読み取り権限がない
456 /// * `BackupError::RegexError` - 不正な正規表現パターンが含まれている
457 pub fn validate(&self) -> BackupResult<()> {
458 // 1. バックアップ先の妥当性チェック
459 if !self.backup.destination.exists() {
460 std::fs::create_dir_all(&self.backup.destination).map_err(|_| {
461 BackupError::BackupDirectoryCreationError {
462 path: self.backup.destination.clone(),
463 }
464 })?;
465 }
466
467 // 2. バックアップ先の書き込み権限チェック
468 check_write_permission(&self.backup.destination)?;
469
470 // 3. 保存期間の妥当性チェック
471 if self.backup.keep_days == 0 || self.backup.keep_days > 3650 {
472 return Err(BackupError::ConfigValidationError {
473 message: format!(
474 "keep_days は 1-3650 の範囲で指定してください(現在: {})",
475 self.backup.keep_days
476 ),
477 });
478 }
479
480 // 4. 各ターゲットの検証
481 for target in &self.targets {
482 // 4.1 ターゲットの存在確認
483 if !target.path.exists() {
484 eprintln!("警告: バックアップ対象が存在しません: {:?}", target.path);
485 // 警告のみで処理は継続(後で追加される可能性があるため)
486 } else {
487 // 4.2 読み取り権限チェック
488 check_read_permission(&target.path)?;
489 }
490
491 // 4.3 除外パターンの正規表現検証
492 for pattern in &target.exclude_patterns {
493 regex::Regex::new(pattern).map_err(|e| BackupError::RegexError {
494 pattern: pattern.clone(),
495 source: e,
496 })?;
497 }
498 }
499
500 // 5. ターゲットが1つもない場合は警告
501 if self.targets.is_empty() {
502 eprintln!("警告: バックアップ対象が設定されていません");
503 }
504
505 Ok(())
506 }
507}
508
509#[cfg(test)]
510mod tests {
511 use super::*;
512 use crate::core::target::{Priority, TargetType};
513
514 #[test]
515 fn test_default_config() {
516 let config = Config::default();
517 assert_eq!(config.version, "1.0.0");
518 assert_eq!(config.targets.len(), 0);
519 assert_eq!(config.backup.keep_days, 30);
520 }
521
522 #[test]
523 fn test_add_target() {
524 let mut config = Config::default();
525 let target = Target {
526 path: PathBuf::from("/tmp/test.txt"),
527 priority: Priority::High,
528 target_type: TargetType::File,
529 category: "test".to_string(),
530 added_date: chrono::Utc::now(),
531 exclude_patterns: vec![],
532 };
533
534 config.add_target(target);
535 assert_eq!(config.targets.len(), 1);
536 }
537
538 #[test]
539 fn test_remove_target() {
540 let mut config = Config::default();
541 let path = PathBuf::from("/tmp/test.txt");
542 let target = Target {
543 path: path.clone(),
544 priority: Priority::High,
545 target_type: TargetType::File,
546 category: "test".to_string(),
547 added_date: chrono::Utc::now(),
548 exclude_patterns: vec![],
549 };
550
551 config.add_target(target);
552 assert_eq!(config.targets.len(), 1);
553
554 let removed = config.remove_target(&path);
555 assert!(removed);
556 assert_eq!(config.targets.len(), 0);
557 }
558}