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