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#[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
47pub 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
83pub 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
92pub 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
100fn 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
113fn 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
125fn write_status_report(
127 options: &BackupStatusOptions,
128 report: &JournalResumeReport,
129) -> Result<(), BackupCommandError> {
130 output::write_pretty_json(options.out.as_ref(), report)
131}
132
133fn write_report(
135 options: &BackupVerifyOptions,
136 report: &BackupIntegrityReport,
137) -> Result<(), BackupCommandError> {
138 output::write_pretty_json(options.out.as_ref(), report)
139}
140
141const 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;