ignition_core/client/backup.rs
1//! Backup capability constants + query builders (04-04, RIG-04) — the
2//! gwbk wire: **stream down, octet up** (04-RESEARCH §Backup
3//! endpoints, 83-api postman primary).
4//!
5//! `GET /data/api/v1/backup?type=roaming` answers the portable `.gwbk`
6//! byte stream (`Accept: application/octet-stream`); the download
7//! rides the 03-02 `download_to_file` pipeline helper VERBATIM (the
8//! ONE streaming body-consumption site — no `Vec<u8>` anywhere on the
9//! down path; gwbks are tens of MB, Pitfall 2).
10//!
11//! `POST /data/api/v1/backup` is the RESTORE: the gwbk bytes as a RAW
12//! `application/octet-stream` body — **NOT multipart** (the postman
13//! collection's exact shape) — with the four scope params sent
14//! EXPLICITLY (`restoreDisabled`, `disableTempProjectBackup`,
15//! `renameEnabled`, `restoreLocal`): the server is the authority on
16//! defaults, agents see what was sent. Restore is synchronous AND
17//! blocks a gateway restart afterward (Pitfall 6), so BOTH directions
18//! ride the 300 s per-request class — a short timeout kills mid-
19//! restore into unknown state.
20//!
21//! Auth: 401 HTML unauthenticated (live-verified shape) — requires a
22//! token like every `/data` route.
23
24use std::time::Duration;
25
26/// GET/POST path of the backup capability.
27pub(crate) const BACKUP_PATH: &str = "/data/api/v1/backup";
28
29/// The `type` query param of the download — `roaming` = the PORTABLE
30/// backup (cross-gateway; the rig snapshot + standalone default), `all`
31/// includes gateway-specific state (07-02's `--type` param, research
32/// Focus 7: the ONE honest signature change — the baked query const
33/// became a builder over this enum).
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum BackupType {
36 /// `?type=roaming` — portable across gateways (the default).
37 Roaming,
38 /// `?type=all` — includes gateway-specific state.
39 All,
40}
41
42impl BackupType {
43 /// The wire value of the `type` query param.
44 pub fn wire(self) -> &'static str {
45 match self {
46 Self::Roaming => "roaming",
47 Self::All => "all",
48 }
49 }
50}
51
52impl Default for BackupType {
53 /// Roaming is the default — the portable backup (pinned by the
54 /// path-builder unit test so the default query cannot drift).
55 fn default() -> Self {
56 Self::Roaming
57 }
58}
59
60/// The download path with its query — the type param rides the path
61/// string into `download_to_file`'s single `path` parameter (the url
62/// join preserves it — no helper signature churn for one param; the
63/// 04-04 const became this builder when 07-02 param-ized the type).
64pub(crate) fn backup_download_path(backup_type: BackupType) -> String {
65 format!("{BACKUP_PATH}?type={}", backup_type.wire())
66}
67
68/// The `Accept` header the download sends — the postman collection's
69/// exact value (the server answers the gwbk bytes).
70pub(crate) const BACKUP_ACCEPT: &str = "application/octet-stream";
71
72/// Per-request class for BOTH backup directions (Pitfall 6): gwbk
73/// generation is not instant, and a restore POST blocks while the
74/// gateway restores — a short timeout kills mid-operation into
75/// unknown state.
76pub const BACKUP_TIMEOUT: Duration = Duration::from_secs(300);
77
78/// The restore POST's query pairs — every scope param EXPLICIT
79/// (all `false`: disabled restores and renames are the honest
80/// round-trip defaults; agents see exactly what was sent, the server
81/// stays the authority on what an omitted param would mean).
82/// `newName` rides ONLY when `renameEnabled=true`, so it is absent
83/// here by construction.
84pub(crate) fn restore_query() -> [(String, String); 4] {
85 [
86 ("restoreDisabled".to_string(), "false".to_string()),
87 ("disableTempProjectBackup".to_string(), "false".to_string()),
88 ("renameEnabled".to_string(), "false".to_string()),
89 ("restoreLocal".to_string(), "false".to_string()),
90 ]
91}
92
93#[cfg(test)]
94mod tests {
95 use std::time::Duration;
96
97 use super::{BACKUP_TIMEOUT, BackupType, backup_download_path, restore_query};
98
99 /// Pitfall 6 pin: BOTH directions ride the 300 s per-request class
100 /// (the same constant serves download and restore — one budget,
101 /// one truth).
102 #[test]
103 fn backup_timeout_is_the_300s_class() {
104 assert_eq!(BACKUP_TIMEOUT, Duration::from_secs(300));
105 }
106
107 /// The download query rides the path builder — pinned so the
108 /// default (roaming) and the `all` variant cannot silently drift
109 /// out of the URL (the 04-04 const-pin, builder edition).
110 #[test]
111 fn download_path_carries_the_type_query() {
112 assert_eq!(
113 backup_download_path(BackupType::default()),
114 "/data/api/v1/backup?type=roaming",
115 "roaming remains the DEFAULT query"
116 );
117 assert_eq!(
118 backup_download_path(BackupType::Roaming),
119 "/data/api/v1/backup?type=roaming"
120 );
121 assert_eq!(
122 backup_download_path(BackupType::All),
123 "/data/api/v1/backup?type=all"
124 );
125 }
126
127 /// All four restore params, all explicit false, exactly once —
128 /// the request-shape truth the wiremock pin asserts end-to-end.
129 #[test]
130 fn restore_query_is_four_explicit_falses() {
131 let pairs = restore_query();
132 assert_eq!(pairs.len(), 4);
133 for (_, value) in &pairs {
134 assert_eq!(value, "false", "every scope param is explicit false");
135 }
136 let names: Vec<&str> = pairs.iter().map(|(name, _)| name.as_str()).collect();
137 assert_eq!(
138 names,
139 [
140 "restoreDisabled",
141 "disableTempProjectBackup",
142 "renameEnabled",
143 "restoreLocal"
144 ],
145 "the postman param set, no newName (renameEnabled=false)"
146 );
147 }
148}