Skip to main content

systemprompt_sync/
api_client.rs

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}