canic_backup/restore/apply/journal/commands/
mod.rs1use super::{RestoreApplyJournal, RestoreApplyJournalOperation, RestoreApplyOperationKind};
2use crate::persistence::resolve_backup_artifact_path;
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6#[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 #[must_use]
30 pub fn from_journal(journal: &RestoreApplyJournal) -> Self {
31 Self::from_journal_with_config(journal, &RestoreApplyCommandConfig::default())
32 }
33
34 #[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#[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 fn default() -> Self {
72 Self {
73 program: "dfx".to_string(),
74 network: None,
75 }
76 }
77}
78
79#[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 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
162const 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
176fn 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
187fn 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}