1use reqwest::{Client, StatusCode};
2use serde::de::DeserializeOwned;
3use serde::{Deserialize, Serialize};
4
5use crate::error::{SyncError, SyncResult};
6
7#[derive(Clone, Debug)]
8pub struct SyncApiClient {
9 client: Client,
10 api_url: String,
11 token: String,
12 hostname: Option<String>,
13 sync_token: Option<String>,
14}
15
16#[derive(Debug, Deserialize)]
17pub struct RegistryToken {
18 pub registry: String,
19 pub username: String,
20 pub token: String,
21}
22
23#[derive(Debug, Deserialize)]
24pub struct DeployResponse {
25 pub status: String,
26 pub app_url: Option<String>,
27}
28
29impl SyncApiClient {
30 pub fn new(api_url: &str, token: &str) -> Self {
31 Self {
32 client: Client::new(),
33 api_url: api_url.to_string(),
34 token: token.to_string(),
35 hostname: None,
36 sync_token: None,
37 }
38 }
39
40 pub fn with_direct_sync(
41 mut self,
42 hostname: Option<String>,
43 sync_token: Option<String>,
44 ) -> Self {
45 self.hostname = hostname;
46 self.sync_token = sync_token;
47 self
48 }
49
50 fn direct_sync_credentials(&self) -> Option<(String, String)> {
51 match (&self.hostname, &self.sync_token) {
52 (Some(hostname), Some(token)) => {
53 let url = format!("https://{}/api/v1/sync/files", hostname);
54 Some((url, token.clone()))
55 },
56 _ => None,
57 }
58 }
59
60 pub async fn upload_files(&self, tenant_id: &str, data: Vec<u8>) -> SyncResult<()> {
61 let (url, token) = self.direct_sync_credentials().unwrap_or_else(|| {
62 (
63 format!("{}/api/v1/cloud/tenants/{}/files", self.api_url, tenant_id),
64 self.token.clone(),
65 )
66 });
67
68 let response = self
69 .client
70 .post(&url)
71 .header("Authorization", format!("Bearer {}", token))
72 .header("Content-Type", "application/octet-stream")
73 .body(data)
74 .send()
75 .await?;
76
77 self.handle_empty_response(response).await
78 }
79
80 pub async fn download_files(&self, tenant_id: &str) -> SyncResult<Vec<u8>> {
81 let (url, token) = self.direct_sync_credentials().unwrap_or_else(|| {
82 (
83 format!("{}/api/v1/cloud/tenants/{}/files", self.api_url, tenant_id),
84 self.token.clone(),
85 )
86 });
87
88 let response = self
89 .client
90 .get(&url)
91 .header("Authorization", format!("Bearer {}", token))
92 .send()
93 .await?;
94
95 self.handle_binary_response(response).await
96 }
97
98 pub async fn get_registry_token(&self, tenant_id: &str) -> SyncResult<RegistryToken> {
99 let url = format!(
100 "{}/api/v1/cloud/tenants/{}/registry-token",
101 self.api_url, tenant_id
102 );
103 self.get(&url).await
104 }
105
106 pub async fn deploy(&self, tenant_id: &str, image: &str) -> SyncResult<DeployResponse> {
107 let url = format!("{}/api/v1/cloud/tenants/{}/deploy", self.api_url, tenant_id);
108 self.post(&url, &serde_json::json!({ "image": image }))
109 .await
110 }
111
112 pub async fn get_tenant_app_id(&self, tenant_id: &str) -> SyncResult<String> {
113 #[derive(Deserialize)]
114 struct TenantInfo {
115 fly_app_name: Option<String>,
116 }
117 let url = format!("{}/api/v1/cloud/tenants/{}", self.api_url, tenant_id);
118 let info: TenantInfo = self.get(&url).await?;
119 info.fly_app_name.ok_or(SyncError::TenantNoApp)
120 }
121
122 pub async fn get_database_url(&self, tenant_id: &str) -> SyncResult<String> {
123 #[derive(Deserialize)]
124 struct DatabaseInfo {
125 database_url: Option<String>,
126 }
127 let url = format!(
128 "{}/api/v1/cloud/tenants/{}/database",
129 self.api_url, tenant_id
130 );
131 let info: DatabaseInfo = self.get(&url).await?;
132 info.database_url.ok_or_else(|| SyncError::ApiError {
133 status: 404,
134 message: "Database URL not available for tenant".to_string(),
135 })
136 }
137
138 async fn get<T: DeserializeOwned>(&self, url: &str) -> SyncResult<T> {
139 let response = self
140 .client
141 .get(url)
142 .header("Authorization", format!("Bearer {}", self.token))
143 .send()
144 .await?;
145
146 self.handle_json_response(response).await
147 }
148
149 async fn post<T: DeserializeOwned, B: Serialize + Sync>(
150 &self,
151 url: &str,
152 body: &B,
153 ) -> SyncResult<T> {
154 let response = self
155 .client
156 .post(url)
157 .header("Authorization", format!("Bearer {}", self.token))
158 .json(body)
159 .send()
160 .await?;
161
162 self.handle_json_response(response).await
163 }
164
165 async fn handle_json_response<T: DeserializeOwned>(
166 &self,
167 response: reqwest::Response,
168 ) -> SyncResult<T> {
169 let status = response.status();
170 if status == StatusCode::UNAUTHORIZED {
171 return Err(SyncError::Unauthorized);
172 }
173 if !status.is_success() {
174 let message = response.text().await?;
175 return Err(SyncError::ApiError {
176 status: status.as_u16(),
177 message,
178 });
179 }
180 Ok(response.json().await?)
181 }
182
183 async fn handle_empty_response(&self, response: reqwest::Response) -> SyncResult<()> {
184 let status = response.status();
185 if !status.is_success() {
186 let message = response.text().await?;
187 return Err(SyncError::ApiError {
188 status: status.as_u16(),
189 message,
190 });
191 }
192 Ok(())
193 }
194
195 async fn handle_binary_response(&self, response: reqwest::Response) -> SyncResult<Vec<u8>> {
196 let status = response.status();
197 if !status.is_success() {
198 return Err(SyncError::ApiError {
199 status: status.as_u16(),
200 message: String::new(),
201 });
202 }
203 Ok(response.bytes().await?.to_vec())
204 }
205}