Skip to main content

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

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