Skip to main content

mnemo/
maintenance.rs

1//! Nettoyage automatique configurable de l'historique (`mnemo maintenance`).
2//!
3//! La maintenance est **désactivée par défaut** : `mnemo maintenance run` ne
4//! supprime jamais rien tant que `[maintenance] auto_prune_enabled` n'est pas
5//! activé. Même activée, toute suppression réelle exige `--yes` (ou une
6//! confirmation interactive) et, si `auto_backup_before_prune` est vrai, une
7//! sauvegarde complète est créée au préalable.
8
9use anyhow::Result;
10
11use crate::config::{Config, MaintenanceConfig};
12use crate::db::SearchFilter;
13use crate::{backup, config, confirm, db, list, prune};
14
15/// Nombre d'exemples affichés en aperçu.
16const PREVIEW_SAMPLES: usize = 5;
17
18/// Calcule l'horodatage de coupe à partir de la configuration de maintenance.
19/// Renvoie `None` si `auto_prune_after` est invalide (signalé à l'appelant).
20fn cutoff(cfg: &MaintenanceConfig) -> Option<String> {
21    let secs = prune::parse_duration(&cfg.auto_prune_after).ok()?;
22    Some(prune::cutoff_timestamp(secs))
23}
24
25/// `mnemo maintenance status` : affiche l'état et ce qui serait nettoyé.
26pub fn run_status() -> Result<()> {
27    let cfg = Config::load()?;
28    let m = &cfg.maintenance;
29
30    println!("Maintenance :");
31    println!(
32        "  Nettoyage automatique : {}",
33        if m.auto_prune_enabled {
34            "activé"
35        } else {
36            "désactivé"
37        }
38    );
39    println!("  Ancienneté            : {}", m.auto_prune_after);
40    println!(
41        "  Sauvegarde avant purge: {}",
42        if m.auto_backup_before_prune {
43            "oui"
44        } else {
45            "non"
46        }
47    );
48
49    match cutoff(m) {
50        Some(cutoff) => {
51            let conn = db::open(&config::db_path()?)?;
52            let n = db::count_older_than(&conn, &cutoff, &SearchFilter::default())?;
53            println!("  Coupe                 : {cutoff}");
54            println!("  Entrées concernées    : {n}");
55        }
56        None => {
57            println!(
58                "  [ERREUR] auto_prune_after invalide : {:?} (ex : 180d, 6m, 1y)",
59                m.auto_prune_after
60            );
61        }
62    }
63    Ok(())
64}
65
66/// `mnemo maintenance run` : exécute le nettoyage configuré.
67///
68/// - Si la maintenance est désactivée, ne supprime rien (sortie informative).
69/// - `dry_run` montre uniquement ce qui serait supprimé.
70/// - Sans `--yes`, une confirmation interactive est requise (refus en mode non
71///   interactif : compatibilité scripts/CI préservée).
72pub fn run(dry_run: bool, assume_yes: bool) -> Result<()> {
73    let cfg = Config::load()?;
74    let m = &cfg.maintenance;
75
76    if !m.auto_prune_enabled {
77        println!(
78            "Nettoyage automatique désactivé ([maintenance] auto_prune_enabled = false). \
79             Rien à faire."
80        );
81        return Ok(());
82    }
83
84    let cutoff = match cutoff(m) {
85        Some(c) => c,
86        None => {
87            anyhow::bail!(
88                "maintenance.auto_prune_after invalide : {:?} (ex : 180d, 6m, 1y)",
89                m.auto_prune_after
90            );
91        }
92    };
93
94    let conn = db::open(&config::db_path()?)?;
95    let filter = SearchFilter::default();
96    let total = db::count_older_than(&conn, &cutoff, &filter)?;
97
98    if total == 0 {
99        println!("Aucune commande antérieure à {cutoff}. Rien à nettoyer.");
100        return Ok(());
101    }
102
103    println!("{total} commande(s) antérieure(s) à {cutoff} seront supprimées.");
104    let samples = db::fetch_older_than(&conn, &cutoff, &filter, PREVIEW_SAMPLES)?;
105    if !samples.is_empty() {
106        println!("Exemples :");
107        for r in &samples {
108            println!("{}", list::short_line(r));
109        }
110    }
111
112    if dry_run {
113        println!("\n[dry-run] Aucune suppression effectuée.");
114        return Ok(());
115    }
116
117    if !confirm::confirm(&format!("Supprimer ces {total} commande(s) ?"), assume_yes)? {
118        println!("Nettoyage annulé.");
119        return Ok(());
120    }
121
122    if m.auto_backup_before_prune {
123        let safety = backup::create_backup(None)?;
124        println!("Sauvegarde automatique : {}", safety.path.display());
125    }
126
127    let n = db::delete_older_than(&conn, &cutoff, &filter)?;
128    println!("{n} commande(s) supprimée(s).");
129    Ok(())
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135
136    #[test]
137    fn cutoff_none_si_duree_invalide() {
138        let m = MaintenanceConfig {
139            auto_prune_enabled: true,
140            auto_prune_after: "pas-une-duree".to_string(),
141            auto_backup_before_prune: true,
142        };
143        assert!(cutoff(&m).is_none());
144    }
145
146    #[test]
147    fn cutoff_some_si_duree_valide() {
148        let m = MaintenanceConfig {
149            auto_prune_enabled: true,
150            auto_prune_after: "180d".to_string(),
151            auto_backup_before_prune: true,
152        };
153        assert!(cutoff(&m).is_some());
154    }
155}