Skip to main content

canic_cli/backup/
mod.rs

1use crate::{output, version_text};
2use canic_backup::{
3    journal::JournalResumeReport,
4    persistence::{BackupIntegrityReport, BackupLayout, PersistenceError},
5};
6use std::ffi::OsString;
7use thiserror::Error as ThisError;
8
9mod options;
10
11pub use options::{BackupStatusOptions, BackupVerifyOptions};
12
13///
14/// BackupCommandError
15///
16
17#[derive(Debug, ThisError)]
18pub enum BackupCommandError {
19    #[error("{0}")]
20    Usage(&'static str),
21
22    #[error("missing required option {0}")]
23    MissingOption(&'static str),
24
25    #[error("unknown option {0}")]
26    UnknownOption(String),
27
28    #[error(
29        "backup journal {backup_id} is incomplete: {pending_artifacts}/{total_artifacts} artifacts still require resume work"
30    )]
31    IncompleteJournal {
32        backup_id: String,
33        total_artifacts: usize,
34        pending_artifacts: usize,
35    },
36
37    #[error(transparent)]
38    Io(#[from] std::io::Error),
39
40    #[error(transparent)]
41    Json(#[from] serde_json::Error),
42
43    #[error(transparent)]
44    Persistence(#[from] PersistenceError),
45}
46
47/// Run a backup subcommand.
48pub fn run<I>(args: I) -> Result<(), BackupCommandError>
49where
50    I: IntoIterator<Item = OsString>,
51{
52    let mut args = args.into_iter();
53    let Some(command) = args.next().and_then(|arg| arg.into_string().ok()) else {
54        return Err(BackupCommandError::Usage(usage()));
55    };
56
57    match command.as_str() {
58        "status" => {
59            let options = BackupStatusOptions::parse(args)?;
60            let report = backup_status(&options)?;
61            write_status_report(&options, &report)?;
62            enforce_status_requirements(&options, &report)?;
63            Ok(())
64        }
65        "verify" => {
66            let options = BackupVerifyOptions::parse(args)?;
67            let report = verify_backup(&options)?;
68            write_report(&options, &report)?;
69            Ok(())
70        }
71        "help" | "--help" | "-h" => {
72            println!("{}", usage());
73            Ok(())
74        }
75        "version" | "--version" | "-V" => {
76            println!("{}", version_text());
77            Ok(())
78        }
79        _ => Err(BackupCommandError::UnknownOption(command)),
80    }
81}
82
83/// Summarize a backup journal's resumable state.
84pub fn backup_status(
85    options: &BackupStatusOptions,
86) -> Result<JournalResumeReport, BackupCommandError> {
87    let layout = BackupLayout::new(options.dir.clone());
88    let journal = layout.read_journal()?;
89    Ok(journal.resume_report())
90}
91
92/// Verify a backup directory's manifest, journal, and durable artifacts.
93pub fn verify_backup(
94    options: &BackupVerifyOptions,
95) -> Result<BackupIntegrityReport, BackupCommandError> {
96    let layout = BackupLayout::new(options.dir.clone());
97    layout.verify_integrity().map_err(BackupCommandError::from)
98}
99
100// Ensure a journal status report has no remaining resume work.
101fn ensure_complete_status(report: &JournalResumeReport) -> Result<(), BackupCommandError> {
102    if report.is_complete {
103        return Ok(());
104    }
105
106    Err(BackupCommandError::IncompleteJournal {
107        backup_id: report.backup_id.clone(),
108        total_artifacts: report.total_artifacts,
109        pending_artifacts: report.pending_artifacts,
110    })
111}
112
113// Enforce caller-requested status requirements after the JSON report is written.
114fn enforce_status_requirements(
115    options: &BackupStatusOptions,
116    report: &JournalResumeReport,
117) -> Result<(), BackupCommandError> {
118    if !options.require_complete {
119        return Ok(());
120    }
121
122    ensure_complete_status(report)
123}
124
125// Write the journal status report to stdout or a requested output file.
126fn write_status_report(
127    options: &BackupStatusOptions,
128    report: &JournalResumeReport,
129) -> Result<(), BackupCommandError> {
130    output::write_pretty_json(options.out.as_ref(), report)
131}
132
133// Write the integrity report to stdout or a requested output file.
134fn write_report(
135    options: &BackupVerifyOptions,
136    report: &BackupIntegrityReport,
137) -> Result<(), BackupCommandError> {
138    output::write_pretty_json(options.out.as_ref(), report)
139}
140
141// Return backup command usage text.
142const fn usage() -> &'static str {
143    "usage: canic backup <command> [<args>]\n\ncommands:\n  verify      Verify layout, journal agreement, and durable artifact checksums.\n  status      Summarize resumable download journal state."
144}
145
146#[cfg(test)]
147mod tests;