innate_core/backup/
command.rs1#[derive(Subcommand)]
2pub enum BackupCommands {
3 Run {
5 #[arg(long)]
7 force: bool,
8 },
9 Status,
11 List,
13 Prune,
15}
16pub(crate) fn run_command(action: &BackupCommands, db_path: &Path) -> anyhow::Result<()> {
17 use super::R2BackupService;
18
19 let settings = crate::settings::load();
20 let cfg = settings.backup.as_ref().ok_or_else(|| {
21 anyhow::anyhow!(
22 "No backup config found in ~/.innate/settings.json.\n\
23 Add a \"backup\" section with \"enable\": true and \"r2\" credentials to enable R2 backup."
24 )
25 })?;
26
27 if let BackupCommands::Status = action {
29 let state = R2BackupService::last_backup_state();
30 println!("R2 backup enabled : {}", cfg.enable);
31 println!(
32 "R2 bucket : {}",
33 cfg.r2.as_ref().map(|r| r.bucket.as_str()).unwrap_or("-")
34 );
35 println!(
36 "Last backup : {}",
37 state.last_backup_at.as_deref().unwrap_or("never")
38 );
39 println!(
40 "Last backup key : {}",
41 state.last_backup_key.as_deref().unwrap_or("-")
42 );
43 let due = R2BackupService::needs_backup(cfg.auto_backup_interval_hours);
44 println!(
45 "Backup due : {}",
46 if cfg.enable && due {
47 "yes"
48 } else if !cfg.enable {
49 "disabled"
50 } else {
51 "no"
52 }
53 );
54 println!("Interval (h) : {}", cfg.auto_backup_interval_hours);
55 println!("Retention (days) : {}", cfg.retention_days);
56 println!("Min backups : {}", cfg.min_backups);
57 return Ok(());
58 }
59
60 if !cfg.enable {
61 anyhow::bail!(
62 "R2 backup is disabled (backup.enable = false).\n\
63 Set \"enable\": true in the backup section of ~/.innate/settings.json to activate."
64 );
65 }
66 let r2_cfg = cfg
67 .r2
68 .as_ref()
69 .ok_or_else(|| anyhow::anyhow!("backup.r2 not configured in ~/.innate/settings.json"))?;
70
71 match action {
72 BackupCommands::Run { force } => {
73 if !force && !R2BackupService::needs_backup(cfg.auto_backup_interval_hours) {
74 let state = R2BackupService::last_backup_state();
75 println!(
76 "backup not due yet (last: {}; interval: {}h). Use --force to override.",
77 state.last_backup_at.as_deref().unwrap_or("never"),
78 cfg.auto_backup_interval_hours
79 );
80 return Ok(());
81 }
82 println!("Starting backup to R2 bucket '{}'…", r2_cfg.bucket);
83 let svc = R2BackupService::from_config(r2_cfg)?;
84 let result = svc.backup_now(db_path, cfg.retention_days, cfg.min_backups)?;
85 println!("Backed up: {} ({} bytes)", result.key, result.size_bytes);
86 if !result.prune.deleted.is_empty() {
87 println!("Pruned {} old backup(s):", result.prune.deleted.len());
88 for k in &result.prune.deleted {
89 println!(" - {k}");
90 }
91 }
92 if result.prune.protected_by_min > 0 {
93 println!(
94 " ({} old backup(s) kept to satisfy min_backups={})",
95 result.prune.protected_by_min, cfg.min_backups
96 );
97 }
98 println!("Done. {} backup(s) remain in R2.", result.prune.kept);
99 }
100 BackupCommands::Status => unreachable!(), BackupCommands::List => {
102 let svc = R2BackupService::from_config(r2_cfg)?;
103 let backups = svc.list_backups()?;
104 if backups.is_empty() {
105 println!("No backups found in R2.");
106 } else {
107 println!("{} backup(s):", backups.len());
108 for b in &backups {
109 println!(" {} | {} | {} bytes", b.last_modified, b.key, b.size_bytes);
110 }
111 }
112 }
113 BackupCommands::Prune => {
114 let svc = R2BackupService::from_config(r2_cfg)?;
115 let result = svc.prune_old_backups(cfg.retention_days, cfg.min_backups)?;
116 if result.deleted.is_empty() {
117 println!("Nothing to prune ({} backup(s) kept).", result.kept);
118 } else {
119 println!("Deleted {} backup(s):", result.deleted.len());
120 for k in &result.deleted {
121 println!(" - {k}");
122 }
123 if result.protected_by_min > 0 {
124 println!(
125 " ({} old backup(s) kept to satisfy min_backups={})",
126 result.protected_by_min, cfg.min_backups
127 );
128 }
129 println!("{} backup(s) remain.", result.kept);
130 }
131 }
132 }
133 Ok(())
134}
135use std::path::Path;
136
137use clap::Subcommand;