Skip to main content

canic_backup/preflight/
mod.rs

1use crate::{
2    journal::{DownloadOperationMetrics, JournalResumeReport},
3    manifest::{FleetBackupManifest, manifest_validation_summary},
4    persistence::{
5        BackupInspectionReport, BackupIntegrityReport, BackupLayout, BackupProvenanceReport,
6        PersistenceError,
7    },
8    restore::{RestoreMapping, RestorePlan, RestorePlanError, RestorePlanner, RestoreStatus},
9};
10use serde_json::json;
11use std::{
12    fs,
13    path::{Path, PathBuf},
14};
15use thiserror::Error as ThisError;
16
17///
18/// BackupPreflightConfig
19///
20
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct BackupPreflightConfig {
23    pub backup_dir: PathBuf,
24    pub out_dir: PathBuf,
25    pub mapping: Option<PathBuf>,
26}
27
28///
29/// BackupPreflightReport
30///
31
32#[derive(Clone, Debug, Eq, PartialEq)]
33#[expect(
34    clippy::struct_excessive_bools,
35    reason = "preflight reports intentionally mirror machine-readable JSON status flags"
36)]
37pub struct BackupPreflightReport {
38    pub status: String,
39    pub backup_id: String,
40    pub backup_dir: String,
41    pub source_environment: String,
42    pub source_root_canister: String,
43    pub topology_hash: String,
44    pub mapping_path: Option<String>,
45    pub journal_complete: bool,
46    pub journal_operation_metrics: DownloadOperationMetrics,
47    pub inspection_status: String,
48    pub provenance_status: String,
49    pub backup_id_status: String,
50    pub topology_receipts_status: String,
51    pub topology_mismatch_count: usize,
52    pub integrity_verified: bool,
53    pub manifest_design_v1_ready: bool,
54    pub manifest_members: usize,
55    pub backup_unit_count: usize,
56    pub restore_plan_members: usize,
57    pub restore_mapping_supplied: bool,
58    pub restore_all_sources_mapped: bool,
59    pub restore_fixed_members: usize,
60    pub restore_relocatable_members: usize,
61    pub restore_in_place_members: usize,
62    pub restore_mapped_members: usize,
63    pub restore_remapped_members: usize,
64    pub restore_ready: bool,
65    pub restore_readiness_reasons: Vec<String>,
66    pub restore_all_members_have_module_hash: bool,
67    pub restore_all_members_have_wasm_hash: bool,
68    pub restore_all_members_have_code_version: bool,
69    pub restore_all_members_have_checksum: bool,
70    pub restore_members_with_module_hash: usize,
71    pub restore_members_with_wasm_hash: usize,
72    pub restore_members_with_code_version: usize,
73    pub restore_members_with_checksum: usize,
74    pub restore_verification_required: bool,
75    pub restore_all_members_have_checks: bool,
76    pub restore_fleet_checks: usize,
77    pub restore_member_check_groups: usize,
78    pub restore_member_checks: usize,
79    pub restore_members_with_checks: usize,
80    pub restore_total_checks: usize,
81    pub restore_planned_snapshot_uploads: usize,
82    pub restore_planned_snapshot_loads: usize,
83    pub restore_planned_code_reinstalls: usize,
84    pub restore_planned_verification_checks: usize,
85    pub restore_planned_operations: usize,
86    pub restore_planned_phases: usize,
87    pub restore_phase_count: usize,
88    pub restore_dependency_free_members: usize,
89    pub restore_in_group_parent_edges: usize,
90    pub restore_cross_group_parent_edges: usize,
91    pub manifest_validation_path: String,
92    pub backup_status_path: String,
93    pub backup_inspection_path: String,
94    pub backup_provenance_path: String,
95    pub backup_integrity_path: String,
96    pub restore_plan_path: String,
97    pub restore_status_path: String,
98    pub preflight_summary_path: String,
99}
100
101///
102/// BackupPreflightError
103///
104
105#[derive(Debug, ThisError)]
106pub enum BackupPreflightError {
107    #[error(
108        "backup journal {backup_id} is incomplete: {pending_artifacts}/{total_artifacts} artifacts still require resume work"
109    )]
110    IncompleteJournal {
111        backup_id: String,
112        total_artifacts: usize,
113        pending_artifacts: usize,
114    },
115
116    #[error(transparent)]
117    Io(#[from] std::io::Error),
118
119    #[error(transparent)]
120    Json(#[from] serde_json::Error),
121
122    #[error(transparent)]
123    Persistence(#[from] PersistenceError),
124
125    #[error(transparent)]
126    RestorePlan(#[from] RestorePlanError),
127}
128
129///
130/// PreflightArtifactPaths
131///
132
133struct PreflightArtifactPaths {
134    manifest_validation: PathBuf,
135    backup_status: PathBuf,
136    backup_inspection: PathBuf,
137    backup_provenance: PathBuf,
138    backup_integrity: PathBuf,
139    restore_plan: PathBuf,
140    restore_status: PathBuf,
141    preflight_summary: PathBuf,
142}
143
144///
145/// PreflightReportInput
146///
147
148struct PreflightReportInput<'a> {
149    config: &'a BackupPreflightConfig,
150    manifest: &'a FleetBackupManifest,
151    status: &'a JournalResumeReport,
152    inspection: &'a BackupInspectionReport,
153    provenance: &'a BackupProvenanceReport,
154    integrity: &'a BackupIntegrityReport,
155    restore_plan: &'a RestorePlan,
156    paths: &'a PreflightArtifactPaths,
157}
158
159///
160/// PreflightArtifactInput
161///
162
163struct PreflightArtifactInput<'a> {
164    paths: &'a PreflightArtifactPaths,
165    manifest: &'a FleetBackupManifest,
166    status: &'a JournalResumeReport,
167    inspection: &'a BackupInspectionReport,
168    provenance: &'a BackupProvenanceReport,
169    integrity: &'a BackupIntegrityReport,
170    restore_plan: &'a RestorePlan,
171    restore_status: &'a RestoreStatus,
172}
173
174/// Run all no-mutation backup checks and write the standard preflight bundle.
175pub fn run_backup_preflight(
176    config: &BackupPreflightConfig,
177) -> Result<BackupPreflightReport, BackupPreflightError> {
178    fs::create_dir_all(&config.out_dir)?;
179
180    let layout = BackupLayout::new(config.backup_dir.clone());
181    let manifest = layout.read_manifest()?;
182    let status = layout.read_journal()?.resume_report();
183    ensure_complete_status(&status)?;
184    let inspection = layout.inspect()?;
185    let provenance = layout.provenance()?;
186    let integrity = layout.verify_integrity()?;
187    let mapping = config.mapping.as_ref().map(read_mapping).transpose()?;
188    let restore_plan = RestorePlanner::plan(&manifest, mapping.as_ref())?;
189    let restore_status = RestoreStatus::from_plan(&restore_plan);
190    let paths = preflight_artifact_paths(&config.out_dir);
191
192    write_preflight_artifacts(PreflightArtifactInput {
193        paths: &paths,
194        manifest: &manifest,
195        status: &status,
196        inspection: &inspection,
197        provenance: &provenance,
198        integrity: &integrity,
199        restore_plan: &restore_plan,
200        restore_status: &restore_status,
201    })?;
202    let report = build_preflight_report(PreflightReportInput {
203        config,
204        manifest: &manifest,
205        status: &status,
206        inspection: &inspection,
207        provenance: &provenance,
208        integrity: &integrity,
209        restore_plan: &restore_plan,
210        paths: &paths,
211    });
212    write_json_value_file(&paths.preflight_summary, &preflight_summary_value(&report))?;
213    Ok(report)
214}
215
216// Ensure a journal status report has no remaining resume work.
217fn ensure_complete_status(report: &JournalResumeReport) -> Result<(), BackupPreflightError> {
218    if report.is_complete {
219        return Ok(());
220    }
221
222    Err(BackupPreflightError::IncompleteJournal {
223        backup_id: report.backup_id.clone(),
224        total_artifacts: report.total_artifacts,
225        pending_artifacts: report.pending_artifacts,
226    })
227}
228
229// Build the standard preflight artifact path set under one output directory.
230fn preflight_artifact_paths(out_dir: &Path) -> PreflightArtifactPaths {
231    PreflightArtifactPaths {
232        manifest_validation: out_dir.join("manifest-validation.json"),
233        backup_status: out_dir.join("backup-status.json"),
234        backup_inspection: out_dir.join("backup-inspection.json"),
235        backup_provenance: out_dir.join("backup-provenance.json"),
236        backup_integrity: out_dir.join("backup-integrity.json"),
237        restore_plan: out_dir.join("restore-plan.json"),
238        restore_status: out_dir.join("restore-status.json"),
239        preflight_summary: out_dir.join("preflight-summary.json"),
240    }
241}
242
243// Write the standard preflight artifacts before emitting the compact summary.
244fn write_preflight_artifacts(
245    input: PreflightArtifactInput<'_>,
246) -> Result<(), BackupPreflightError> {
247    write_json_value_file(
248        &input.paths.manifest_validation,
249        &manifest_validation_summary(input.manifest),
250    )?;
251    fs::write(
252        &input.paths.backup_status,
253        serde_json::to_vec_pretty(&input.status)?,
254    )?;
255    fs::write(
256        &input.paths.backup_inspection,
257        serde_json::to_vec_pretty(&input.inspection)?,
258    )?;
259    fs::write(
260        &input.paths.backup_provenance,
261        serde_json::to_vec_pretty(&input.provenance)?,
262    )?;
263    fs::write(
264        &input.paths.backup_integrity,
265        serde_json::to_vec_pretty(&input.integrity)?,
266    )?;
267    fs::write(
268        &input.paths.restore_plan,
269        serde_json::to_vec_pretty(&input.restore_plan)?,
270    )?;
271    fs::write(
272        &input.paths.restore_status,
273        serde_json::to_vec_pretty(&input.restore_status)?,
274    )?;
275    Ok(())
276}
277
278// Build the in-memory preflight report mirrored by preflight-summary.json.
279fn build_preflight_report(input: PreflightReportInput<'_>) -> BackupPreflightReport {
280    let identity = &input.restore_plan.identity_summary;
281    let snapshot = &input.restore_plan.snapshot_summary;
282    let verification = &input.restore_plan.verification_summary;
283    let operation = &input.restore_plan.operation_summary;
284    let ordering = &input.restore_plan.ordering_summary;
285
286    BackupPreflightReport {
287        status: "ready".to_string(),
288        backup_id: input.manifest.backup_id.clone(),
289        backup_dir: input.config.backup_dir.display().to_string(),
290        source_environment: input.manifest.source.environment.clone(),
291        source_root_canister: input.manifest.source.root_canister.clone(),
292        topology_hash: input.manifest.fleet.topology_hash.clone(),
293        mapping_path: input
294            .config
295            .mapping
296            .as_ref()
297            .map(|path| path.display().to_string()),
298        journal_complete: input.status.is_complete,
299        journal_operation_metrics: input.status.operation_metrics.clone(),
300        inspection_status: readiness_status(input.inspection.ready_for_verify).to_string(),
301        provenance_status: consistency_status(
302            input.provenance.backup_id_matches && input.provenance.topology_receipts_match,
303        )
304        .to_string(),
305        backup_id_status: match_status(input.provenance.backup_id_matches).to_string(),
306        topology_receipts_status: match_status(input.provenance.topology_receipts_match)
307            .to_string(),
308        topology_mismatch_count: input.provenance.topology_receipt_mismatches.len(),
309        integrity_verified: input.integrity.verified,
310        manifest_design_v1_ready: input.manifest.design_conformance_report().design_v1_ready,
311        manifest_members: input.manifest.fleet.members.len(),
312        backup_unit_count: input.provenance.backup_unit_count,
313        restore_plan_members: input.restore_plan.member_count,
314        restore_mapping_supplied: identity.mapping_supplied,
315        restore_all_sources_mapped: identity.all_sources_mapped,
316        restore_fixed_members: identity.fixed_members,
317        restore_relocatable_members: identity.relocatable_members,
318        restore_in_place_members: identity.in_place_members,
319        restore_mapped_members: identity.mapped_members,
320        restore_remapped_members: identity.remapped_members,
321        restore_ready: input.restore_plan.readiness_summary.ready,
322        restore_readiness_reasons: input.restore_plan.readiness_summary.reasons.clone(),
323        restore_all_members_have_module_hash: snapshot.all_members_have_module_hash,
324        restore_all_members_have_wasm_hash: snapshot.all_members_have_wasm_hash,
325        restore_all_members_have_code_version: snapshot.all_members_have_code_version,
326        restore_all_members_have_checksum: snapshot.all_members_have_checksum,
327        restore_members_with_module_hash: snapshot.members_with_module_hash,
328        restore_members_with_wasm_hash: snapshot.members_with_wasm_hash,
329        restore_members_with_code_version: snapshot.members_with_code_version,
330        restore_members_with_checksum: snapshot.members_with_checksum,
331        restore_verification_required: verification.verification_required,
332        restore_all_members_have_checks: verification.all_members_have_checks,
333        restore_fleet_checks: verification.fleet_checks,
334        restore_member_check_groups: verification.member_check_groups,
335        restore_member_checks: verification.member_checks,
336        restore_members_with_checks: verification.members_with_checks,
337        restore_total_checks: verification.total_checks,
338        restore_planned_snapshot_uploads: operation
339            .effective_planned_snapshot_uploads(input.restore_plan.member_count),
340        restore_planned_snapshot_loads: operation.planned_snapshot_loads,
341        restore_planned_code_reinstalls: operation.planned_code_reinstalls,
342        restore_planned_verification_checks: operation.planned_verification_checks,
343        restore_planned_operations: operation
344            .effective_planned_operations(input.restore_plan.member_count),
345        restore_planned_phases: operation.planned_phases,
346        restore_phase_count: ordering.phase_count,
347        restore_dependency_free_members: ordering.dependency_free_members,
348        restore_in_group_parent_edges: ordering.in_group_parent_edges,
349        restore_cross_group_parent_edges: ordering.cross_group_parent_edges,
350        manifest_validation_path: input.paths.manifest_validation.display().to_string(),
351        backup_status_path: input.paths.backup_status.display().to_string(),
352        backup_inspection_path: input.paths.backup_inspection.display().to_string(),
353        backup_provenance_path: input.paths.backup_provenance.display().to_string(),
354        backup_integrity_path: input.paths.backup_integrity.display().to_string(),
355        restore_plan_path: input.paths.restore_plan.display().to_string(),
356        restore_status_path: input.paths.restore_status.display().to_string(),
357        preflight_summary_path: input.paths.preflight_summary.display().to_string(),
358    }
359}
360
361// Build the compact preflight summary emitted after all checks pass.
362fn preflight_summary_value(report: &BackupPreflightReport) -> serde_json::Value {
363    let mut summary = serde_json::Map::new();
364    insert_preflight_source_summary(&mut summary, report);
365    insert_preflight_restore_summary(&mut summary, report);
366    insert_preflight_report_paths(&mut summary, report);
367    serde_json::Value::Object(summary)
368}
369
370// Insert one named JSON value into the compact preflight summary.
371fn insert_summary_value(
372    summary: &mut serde_json::Map<String, serde_json::Value>,
373    key: &'static str,
374    value: serde_json::Value,
375) {
376    summary.insert(key.to_string(), value);
377}
378
379// Insert a fixed group of named JSON values into the compact preflight summary.
380fn insert_summary_values<const N: usize>(
381    summary: &mut serde_json::Map<String, serde_json::Value>,
382    values: [(&'static str, serde_json::Value); N],
383) {
384    for (key, value) in values {
385        insert_summary_value(summary, key, value);
386    }
387}
388
389// Insert backup source and validation status fields into the summary.
390fn insert_preflight_source_summary(
391    summary: &mut serde_json::Map<String, serde_json::Value>,
392    report: &BackupPreflightReport,
393) {
394    insert_summary_values(
395        summary,
396        [
397            ("status", json!(report.status)),
398            ("backup_id", json!(report.backup_id)),
399            ("backup_dir", json!(report.backup_dir)),
400            ("source_environment", json!(report.source_environment)),
401            ("source_root_canister", json!(report.source_root_canister)),
402            ("topology_hash", json!(report.topology_hash)),
403            ("mapping_path", json!(report.mapping_path)),
404            ("journal_complete", json!(report.journal_complete)),
405            (
406                "journal_operation_metrics",
407                json!(report.journal_operation_metrics),
408            ),
409            ("inspection_status", json!(report.inspection_status)),
410            ("provenance_status", json!(report.provenance_status)),
411            ("backup_id_status", json!(report.backup_id_status)),
412            (
413                "topology_receipts_status",
414                json!(report.topology_receipts_status),
415            ),
416            (
417                "topology_mismatch_count",
418                json!(report.topology_mismatch_count),
419            ),
420            ("integrity_verified", json!(report.integrity_verified)),
421            (
422                "manifest_design_v1_ready",
423                json!(report.manifest_design_v1_ready),
424            ),
425            ("manifest_members", json!(report.manifest_members)),
426            ("backup_unit_count", json!(report.backup_unit_count)),
427        ],
428    );
429}
430
431// Insert restore planning summary fields into the compact preflight summary.
432fn insert_preflight_restore_summary(
433    summary: &mut serde_json::Map<String, serde_json::Value>,
434    report: &BackupPreflightReport,
435) {
436    insert_summary_values(
437        summary,
438        [
439            ("restore_plan_members", json!(report.restore_plan_members)),
440            (
441                "restore_mapping_supplied",
442                json!(report.restore_mapping_supplied),
443            ),
444            (
445                "restore_all_sources_mapped",
446                json!(report.restore_all_sources_mapped),
447            ),
448        ],
449    );
450    insert_preflight_restore_identity_summary(summary, report);
451    insert_preflight_restore_readiness_summary(summary, report);
452    insert_preflight_restore_snapshot_summary(summary, report);
453    insert_preflight_restore_verification_summary(summary, report);
454    insert_preflight_restore_operation_summary(summary, report);
455    insert_preflight_restore_ordering_summary(summary, report);
456}
457
458// Insert restore identity summary fields into the compact preflight summary.
459fn insert_preflight_restore_identity_summary(
460    summary: &mut serde_json::Map<String, serde_json::Value>,
461    report: &BackupPreflightReport,
462) {
463    insert_summary_values(
464        summary,
465        [
466            ("restore_fixed_members", json!(report.restore_fixed_members)),
467            (
468                "restore_relocatable_members",
469                json!(report.restore_relocatable_members),
470            ),
471            (
472                "restore_in_place_members",
473                json!(report.restore_in_place_members),
474            ),
475            (
476                "restore_mapped_members",
477                json!(report.restore_mapped_members),
478            ),
479            (
480                "restore_remapped_members",
481                json!(report.restore_remapped_members),
482            ),
483        ],
484    );
485}
486
487// Insert restore readiness summary fields into the compact preflight summary.
488fn insert_preflight_restore_readiness_summary(
489    summary: &mut serde_json::Map<String, serde_json::Value>,
490    report: &BackupPreflightReport,
491) {
492    insert_summary_values(
493        summary,
494        [
495            ("restore_ready", json!(report.restore_ready)),
496            (
497                "restore_readiness_reasons",
498                json!(report.restore_readiness_reasons),
499            ),
500        ],
501    );
502}
503
504// Insert restore snapshot summary fields into the compact preflight summary.
505fn insert_preflight_restore_snapshot_summary(
506    summary: &mut serde_json::Map<String, serde_json::Value>,
507    report: &BackupPreflightReport,
508) {
509    insert_summary_values(
510        summary,
511        [
512            (
513                "restore_all_members_have_module_hash",
514                json!(report.restore_all_members_have_module_hash),
515            ),
516            (
517                "restore_all_members_have_wasm_hash",
518                json!(report.restore_all_members_have_wasm_hash),
519            ),
520            (
521                "restore_all_members_have_code_version",
522                json!(report.restore_all_members_have_code_version),
523            ),
524            (
525                "restore_all_members_have_checksum",
526                json!(report.restore_all_members_have_checksum),
527            ),
528            (
529                "restore_members_with_module_hash",
530                json!(report.restore_members_with_module_hash),
531            ),
532            (
533                "restore_members_with_wasm_hash",
534                json!(report.restore_members_with_wasm_hash),
535            ),
536            (
537                "restore_members_with_code_version",
538                json!(report.restore_members_with_code_version),
539            ),
540            (
541                "restore_members_with_checksum",
542                json!(report.restore_members_with_checksum),
543            ),
544        ],
545    );
546}
547
548// Insert restore verification summary fields into the compact preflight summary.
549fn insert_preflight_restore_verification_summary(
550    summary: &mut serde_json::Map<String, serde_json::Value>,
551    report: &BackupPreflightReport,
552) {
553    insert_summary_values(
554        summary,
555        [
556            (
557                "restore_verification_required",
558                json!(report.restore_verification_required),
559            ),
560            (
561                "restore_all_members_have_checks",
562                json!(report.restore_all_members_have_checks),
563            ),
564            ("restore_fleet_checks", json!(report.restore_fleet_checks)),
565            (
566                "restore_member_check_groups",
567                json!(report.restore_member_check_groups),
568            ),
569            ("restore_member_checks", json!(report.restore_member_checks)),
570            (
571                "restore_members_with_checks",
572                json!(report.restore_members_with_checks),
573            ),
574            ("restore_total_checks", json!(report.restore_total_checks)),
575        ],
576    );
577}
578
579// Insert restore operation summary fields into the compact preflight summary.
580fn insert_preflight_restore_operation_summary(
581    summary: &mut serde_json::Map<String, serde_json::Value>,
582    report: &BackupPreflightReport,
583) {
584    insert_summary_values(
585        summary,
586        [
587            (
588                "restore_planned_snapshot_uploads",
589                json!(report.restore_planned_snapshot_uploads),
590            ),
591            (
592                "restore_planned_snapshot_loads",
593                json!(report.restore_planned_snapshot_loads),
594            ),
595            (
596                "restore_planned_code_reinstalls",
597                json!(report.restore_planned_code_reinstalls),
598            ),
599            (
600                "restore_planned_verification_checks",
601                json!(report.restore_planned_verification_checks),
602            ),
603            (
604                "restore_planned_operations",
605                json!(report.restore_planned_operations),
606            ),
607            (
608                "restore_planned_phases",
609                json!(report.restore_planned_phases),
610            ),
611        ],
612    );
613}
614
615// Insert restore ordering summary fields into the compact preflight summary.
616fn insert_preflight_restore_ordering_summary(
617    summary: &mut serde_json::Map<String, serde_json::Value>,
618    report: &BackupPreflightReport,
619) {
620    insert_summary_values(
621        summary,
622        [
623            ("restore_phase_count", json!(report.restore_phase_count)),
624            (
625                "restore_dependency_free_members",
626                json!(report.restore_dependency_free_members),
627            ),
628            (
629                "restore_in_group_parent_edges",
630                json!(report.restore_in_group_parent_edges),
631            ),
632            (
633                "restore_cross_group_parent_edges",
634                json!(report.restore_cross_group_parent_edges),
635            ),
636        ],
637    );
638}
639
640// Insert generated report paths into the compact preflight summary.
641fn insert_preflight_report_paths(
642    summary: &mut serde_json::Map<String, serde_json::Value>,
643    report: &BackupPreflightReport,
644) {
645    insert_summary_values(
646        summary,
647        [
648            (
649                "manifest_validation_path",
650                json!(report.manifest_validation_path),
651            ),
652            ("backup_status_path", json!(report.backup_status_path)),
653            (
654                "backup_inspection_path",
655                json!(report.backup_inspection_path),
656            ),
657            (
658                "backup_provenance_path",
659                json!(report.backup_provenance_path),
660            ),
661            ("backup_integrity_path", json!(report.backup_integrity_path)),
662            ("restore_plan_path", json!(report.restore_plan_path)),
663            ("restore_status_path", json!(report.restore_status_path)),
664            (
665                "preflight_summary_path",
666                json!(report.preflight_summary_path),
667            ),
668        ],
669    );
670}
671
672// Return the stable summary status for inspection readiness.
673const fn readiness_status(ready: bool) -> &'static str {
674    if ready { "ready" } else { "not-ready" }
675}
676
677// Return the stable summary status for provenance consistency.
678const fn consistency_status(consistent: bool) -> &'static str {
679    if consistent {
680        "consistent"
681    } else {
682        "inconsistent"
683    }
684}
685
686// Return the stable summary status for equality checks.
687const fn match_status(matches: bool) -> &'static str {
688    if matches { "matched" } else { "mismatched" }
689}
690
691// Read and decode an optional source-to-target restore mapping from disk.
692fn read_mapping(path: &PathBuf) -> Result<RestoreMapping, BackupPreflightError> {
693    let data = fs::read_to_string(path)?;
694    serde_json::from_str(&data).map_err(BackupPreflightError::from)
695}
696
697// Write one pretty JSON value artifact.
698fn write_json_value_file(
699    path: &PathBuf,
700    value: &serde_json::Value,
701) -> Result<(), BackupPreflightError> {
702    fs::write(path, serde_json::to_vec_pretty(value)?)?;
703    Ok(())
704}