Skip to main content

canic_backup/restore/apply/journal/commands/
mod.rs

1use super::{RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind};
2use crate::persistence::resolve_backup_artifact_path;
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6///
7/// RestoreApplyCommandPreview
8///
9
10#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
11#[expect(
12    clippy::struct_excessive_bools,
13    reason = "runner preview exposes machine-readable availability and safety flags"
14)]
15pub struct RestoreApplyCommandPreview {
16    pub response_version: u16,
17    pub backup_id: String,
18    pub ready: bool,
19    pub complete: bool,
20    pub operation_available: bool,
21    pub command_available: bool,
22    pub blocked_reasons: Vec<String>,
23    pub operation: Option<RestoreApplyJournalOperation>,
24    pub command: Option<RestoreApplyRunnerCommand>,
25}
26
27impl RestoreApplyCommandPreview {
28    /// Build a no-execute runner command preview from a restore apply journal.
29    #[must_use]
30    pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
31        Self::from_journal_with_config(journal, &RestoreApplyCommandConfig::default())
32    }
33
34    /// Build a configured no-execute runner command preview from a journal.
35    #[must_use]
36    pub fn from_journal_with_config(
37        journal: &RestoreApplyJournal,
38        config: &RestoreApplyCommandConfig,
39    ) -> Self {
40        let operation = journal.next_transition_operation().cloned();
41        let command = operation.as_ref().and_then(|operation| {
42            RestoreApplyRunnerCommand::from_operation(operation, journal, config)
43        });
44
45        Self {
46            response_version: 1,
47            backup_id: journal.backup_id.clone(),
48            ready: journal.ready,
49            complete: journal.is_complete(),
50            operation_available: operation.is_some(),
51            command_available: command.is_some(),
52            blocked_reasons: journal.blocked_reasons.clone(),
53            operation,
54            command,
55        }
56    }
57}
58
59///
60/// RestoreApplyCommandConfig
61///
62
63#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
64pub struct RestoreApplyCommandConfig {
65    pub program: String,
66    pub network: Option<String>,
67}
68
69impl Default for RestoreApplyCommandConfig {
70    /// Build the default restore apply command preview configuration.
71    fn default() -> Self {
72        Self {
73            program: "dfx".to_string(),
74            network: None,
75        }
76    }
77}
78
79///
80/// RestoreApplyRunnerCommand
81///
82
83#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
84pub struct RestoreApplyRunnerCommand {
85    pub program: String,
86    pub args: Vec<String>,
87    pub mutates: bool,
88    pub requires_stopped_canister: bool,
89    pub note: String,
90}
91
92impl RestoreApplyRunnerCommand {
93    // Build a no-execute dfx command preview for one ready operation.
94    fn from_operation(
95        operation: &RestoreApplyJournalOperation,
96        journal: &RestoreApplyJournal,
97        config: &RestoreApplyCommandConfig,
98    ) -> Option<Self> {
99        match operation.operation {
100            RestoreApplyOperationKind::UploadSnapshot => {
101                let artifact_path = upload_artifact_command_path(operation, journal)?;
102                Some(Self {
103                    program: config.program.clone(),
104                    args: dfx_canister_args(
105                        config,
106                        vec![
107                            "snapshot".to_string(),
108                            "upload".to_string(),
109                            "--dir".to_string(),
110                            artifact_path,
111                            operation.target_canister.clone(),
112                        ],
113                    ),
114                    mutates: true,
115                    requires_stopped_canister: false,
116                    note: "uploads the downloaded snapshot artifact to the target canister"
117                        .to_string(),
118                })
119            }
120            RestoreApplyOperationKind::LoadSnapshot => {
121                let snapshot_id = journal.uploaded_snapshot_id_for_load(operation)?;
122                Some(Self {
123                    program: config.program.clone(),
124                    args: dfx_canister_args(
125                        config,
126                        vec![
127                            "snapshot".to_string(),
128                            "load".to_string(),
129                            operation.target_canister.clone(),
130                            snapshot_id.to_string(),
131                        ],
132                    ),
133                    mutates: true,
134                    requires_stopped_canister: true,
135                    note: "loads the uploaded snapshot into the target canister".to_string(),
136                })
137            }
138            RestoreApplyOperationKind::VerifyMember | RestoreApplyOperationKind::VerifyFleet => {
139                match operation.verification_kind.as_deref() {
140                    Some("status") => Some(Self {
141                        program: config.program.clone(),
142                        args: dfx_canister_args(
143                            config,
144                            vec!["status".to_string(), operation.target_canister.clone()],
145                        ),
146                        mutates: false,
147                        requires_stopped_canister: false,
148                        note: verification_command_note(
149                            &operation.operation,
150                            "checks target canister status",
151                            "checks target fleet root canister status",
152                        )
153                        .to_string(),
154                    }),
155                    Some(_) | None => None,
156                }
157            }
158        }
159    }
160}
161
162// Return an operator note for member-level or fleet-level verification commands.
163const fn verification_command_note(
164    operation: &RestoreApplyOperationKind,
165    member_note: &'static str,
166    fleet_note: &'static str,
167) -> &'static str {
168    match operation {
169        RestoreApplyOperationKind::VerifyFleet => fleet_note,
170        RestoreApplyOperationKind::UploadSnapshot
171        | RestoreApplyOperationKind::LoadSnapshot
172        | RestoreApplyOperationKind::VerifyMember => member_note,
173    }
174}
175
176// Build `dfx canister` arguments with the optional network selector.
177fn dfx_canister_args(config: &RestoreApplyCommandConfig, mut tail: Vec<String>) -> Vec<String> {
178    let mut args = vec!["canister".to_string()];
179    if let Some(network) = &config.network {
180        args.push("--network".to_string());
181        args.push(network.clone());
182    }
183    args.append(&mut tail);
184    args
185}
186
187// Resolve upload artifact paths the same way validation resolved them.
188fn upload_artifact_command_path(
189    operation: &RestoreApplyJournalOperation,
190    journal: &RestoreApplyJournal,
191) -> Option<String> {
192    let artifact_path = operation.artifact_path.as_ref()?;
193    let backup_root = journal.backup_root.as_ref()?;
194    resolve_backup_artifact_path(Path::new(backup_root), artifact_path)
195        .map(|path| path.to_string_lossy().to_string())
196}