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}