ito_core/
backend_client.rs1use std::path::PathBuf;
8use std::time::Duration;
9
10use ito_config::types::BackendApiConfig;
11
12use crate::errors::{CoreError, CoreResult};
13
14#[derive(Debug, Clone)]
20pub struct BackendRuntime {
21 pub base_url: String,
23 pub token: String,
25 pub timeout: Duration,
27 pub max_retries: u32,
29 pub backup_dir: PathBuf,
31 pub org: String,
33 pub repo: String,
35}
36
37impl BackendRuntime {
38 pub fn project_api_prefix(&self) -> String {
40 format!(
41 "{}/api/v1/projects/{}/{}",
42 self.base_url, self.org, self.repo
43 )
44 }
45}
46
47pub fn resolve_backend_runtime(config: &BackendApiConfig) -> CoreResult<Option<BackendRuntime>> {
52 if !config.enabled {
53 return Ok(None);
54 }
55
56 let token = resolve_token(config)?;
57 let backup_dir = resolve_backup_dir(config);
58 let timeout = Duration::from_millis(config.timeout_ms);
59 let (org, repo) = resolve_project_namespace(config)?;
60
61 Ok(Some(BackendRuntime {
62 base_url: config.url.clone(),
63 token,
64 timeout,
65 max_retries: config.max_retries,
66 backup_dir,
67 org,
68 repo,
69 }))
70}
71
72fn resolve_token(config: &BackendApiConfig) -> CoreResult<String> {
74 let env_var = &config.token_env_var;
75 match std::env::var(env_var) {
76 Ok(val) if !val.trim().is_empty() => return Ok(val.trim().to_string()),
77 Ok(_) => {
78 return Err(CoreError::validation(format!(
79 "Backend mode is enabled but environment variable '{env_var}' is empty. \
80 Set the token via '{env_var}' or 'backend.token' in config."
81 )));
82 }
83 Err(_) => {}
84 }
85
86 if let Some(token) = &config.token {
87 let token = token.trim();
88 if !token.is_empty() {
89 return Ok(token.to_string());
90 }
91 }
92
93 Err(CoreError::validation(format!(
94 "Backend mode is enabled but environment variable '{env_var}' is not set. \
95 Set the token via '{env_var}' or 'backend.token' in config."
96 )))
97}
98
99fn resolve_backup_dir(config: &BackendApiConfig) -> PathBuf {
101 if let Some(dir) = &config.backup_dir {
102 return PathBuf::from(dir);
103 }
104
105 let home = std::env::var("HOME")
106 .or_else(|_| std::env::var("USERPROFILE"))
107 .unwrap_or_else(|_| "/tmp".to_string());
108
109 PathBuf::from(home).join(".ito").join("backups")
110}
111
112const ENV_PROJECT_ORG: &str = "ITO_BACKEND_PROJECT_ORG";
114const ENV_PROJECT_REPO: &str = "ITO_BACKEND_PROJECT_REPO";
116
117fn resolve_project_namespace(config: &BackendApiConfig) -> CoreResult<(String, String)> {
125 resolve_project_namespace_with_env(config, ENV_PROJECT_ORG, ENV_PROJECT_REPO)
126}
127
128fn resolve_project_namespace_with_env(
130 config: &BackendApiConfig,
131 org_env_var: &str,
132 repo_env_var: &str,
133) -> CoreResult<(String, String)> {
134 let org = std::env::var(org_env_var)
135 .ok()
136 .filter(|s| !s.trim().is_empty())
137 .map(|s| s.trim().to_string())
138 .or_else(|| {
139 config
140 .project
141 .org
142 .as_deref()
143 .filter(|s| !s.is_empty())
144 .map(String::from)
145 });
146
147 let repo = std::env::var(repo_env_var)
148 .ok()
149 .filter(|s| !s.trim().is_empty())
150 .map(|s| s.trim().to_string())
151 .or_else(|| {
152 config
153 .project
154 .repo
155 .as_deref()
156 .filter(|s| !s.is_empty())
157 .map(String::from)
158 });
159
160 let Some(org) = org else {
161 return Err(CoreError::validation(format!(
162 "Backend mode is enabled but 'backend.project.org' is not set. \
163 Set it in config or via the {org_env_var} environment variable."
164 )));
165 };
166
167 let Some(repo) = repo else {
168 return Err(CoreError::validation(format!(
169 "Backend mode is enabled but 'backend.project.repo' is not set. \
170 Set it in config or via the {repo_env_var} environment variable."
171 )));
172 };
173
174 Ok((org, repo))
175}
176
177pub fn is_retriable_status(status: u16) -> bool {
182 match status {
183 429 => true,
184 s if s >= 500 => true,
185 _ => false,
186 }
187}
188
189pub fn idempotency_key(operation: &str) -> String {
194 format!("{}-{operation}", uuid::Uuid::new_v4())
195}
196
197#[cfg(test)]
198#[path = "backend_client_tests.rs"]
199mod backend_client_tests;