use super::types::*; use reqwest::Client; #[derive(Clone)] pub struct SyncClient { client: Client, base_url: String, token: String, } impl SyncClient { pub fn new(base_url: String, token: String) -> Self { Self { client: Client::new(), base_url: base_url.trim_end_matches('/').to_string(), token, } } fn url(&self, path: &str) -> String { format!("{}/v1/objects/{}", self.base_url, path) } pub async fn stat(&self, key: &str) -> SyncResult { let response = self .client .post(self.url("stat")) .header("Authorization", format!("Bearer {}", self.token)) .json(&StatRequest { key: key.to_string(), }) .send() .await .map_err(|e| SyncError::NetworkError(e.to_string()))?; if response.status().is_client_error() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(SyncError::AuthError(format!("({status}) {body}"))); } response .json() .await .map_err(|e| SyncError::SerializationError(e.to_string())) } pub async fn presign_upload( &self, key: &str, content_type: Option<&str>, ) -> SyncResult { let response = self .client .post(self.url("presign-upload")) .header("Authorization", format!("Bearer {}", self.token)) .json(&PresignUploadRequest { key: key.to_string(), content_type: content_type.map(|s| s.to_string()), expires_in: Some(3600), }) .send() .await .map_err(|e| SyncError::NetworkError(e.to_string()))?; if response.status().is_client_error() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(SyncError::AuthError(format!("({status}) {body}"))); } response .json() .await .map_err(|e| SyncError::SerializationError(e.to_string())) } pub async fn presign_download(&self, key: &str) -> SyncResult { let response = self .client .post(self.url("presign-download")) .header("Authorization", format!("Bearer {}", self.token)) .json(&PresignDownloadRequest { key: key.to_string(), expires_in: Some(3600), }) .send() .await .map_err(|e| SyncError::NetworkError(e.to_string()))?; if response.status().is_client_error() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(SyncError::AuthError(format!("({status}) {body}"))); } response .json() .await .map_err(|e| SyncError::SerializationError(e.to_string())) } pub async fn delete(&self, key: &str, tombstone_key: Option<&str>) -> SyncResult { let response = self .client .post(self.url("delete")) .header("Authorization", format!("Bearer {}", self.token)) .json(&DeleteRequest { key: key.to_string(), tombstone_key: tombstone_key.map(|s| s.to_string()), deleted_at: Some(chrono::Utc::now().to_rfc3339()), }) .send() .await .map_err(|e| SyncError::NetworkError(e.to_string()))?; if response.status().is_client_error() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(SyncError::AuthError(format!("({status}) {body}"))); } response .json() .await .map_err(|e| SyncError::SerializationError(e.to_string())) } pub async fn list(&self, prefix: &str) -> SyncResult { let response = self .client .post(self.url("list")) .header("Authorization", format!("Bearer {}", self.token)) .json(&ListRequest { prefix: prefix.to_string(), max_keys: Some(1000), continuation_token: None, }) .send() .await .map_err(|e| SyncError::NetworkError(e.to_string()))?; if response.status().is_client_error() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(SyncError::AuthError(format!("({status}) {body}"))); } response .json() .await .map_err(|e| SyncError::SerializationError(e.to_string())) } pub async fn upload_bytes( &self, presigned_url: &str, data: &[u8], content_type: Option<&str>, ) -> SyncResult<()> { let mut req = self .client .put(presigned_url) .header("Content-Length", data.len().to_string()) .body(data.to_vec()); if let Some(ct) = content_type { req = req.header("Content-Type", ct); } let response = req .send() .await .map_err(|e| SyncError::NetworkError(e.to_string()))?; if !response.status().is_success() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(SyncError::NetworkError(format!( "Upload failed with status {status}: {body}" ))); } Ok(()) } pub async fn download_bytes(&self, presigned_url: &str) -> SyncResult> { let response = self .client .get(presigned_url) .send() .await .map_err(|e| SyncError::NetworkError(e.to_string()))?; if !response.status().is_success() { return Err(SyncError::NetworkError(format!( "Download failed with status: {}", response.status() ))); } response .bytes() .await .map(|b| b.to_vec()) .map_err(|e| SyncError::NetworkError(e.to_string())) } pub async fn presign_upload_batch( &self, items: Vec<(String, Option)>, ) -> SyncResult { let chunk_size = 500; let mut all_items = Vec::new(); for chunk in items.chunks(chunk_size) { let request = PresignUploadBatchRequest { items: chunk .iter() .map(|(key, content_type)| PresignUploadBatchItem { key: key.clone(), content_type: content_type.clone(), }) .collect(), expires_in: Some(3600), }; let response = self .client .post(self.url("presign-upload-batch")) .header("Authorization", format!("Bearer {}", self.token)) .json(&request) .send() .await .map_err(|e| SyncError::NetworkError(e.to_string()))?; if response.status().is_client_error() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(SyncError::AuthError(format!("({status}) {body}"))); } let batch_response: PresignUploadBatchResponse = response .json() .await .map_err(|e| SyncError::SerializationError(e.to_string()))?; all_items.extend(batch_response.items); } Ok(PresignUploadBatchResponse { items: all_items }) } pub async fn presign_download_batch( &self, keys: Vec, ) -> SyncResult { let chunk_size = 500; let mut all_items = Vec::new(); for chunk in keys.chunks(chunk_size) { let request = PresignDownloadBatchRequest { keys: chunk.to_vec(), expires_in: Some(3600), }; let response = self .client .post(self.url("presign-download-batch")) .header("Authorization", format!("Bearer {}", self.token)) .json(&request) .send() .await .map_err(|e| SyncError::NetworkError(e.to_string()))?; if response.status().is_client_error() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(SyncError::AuthError(format!("({status}) {body}"))); } let batch_response: PresignDownloadBatchResponse = response .json() .await .map_err(|e| SyncError::SerializationError(e.to_string()))?; all_items.extend(batch_response.items); } Ok(PresignDownloadBatchResponse { items: all_items }) } pub async fn delete_prefix( &self, prefix: &str, tombstone_key: Option<&str>, ) -> SyncResult { let response = self .client .post(self.url("delete-prefix")) .header("Authorization", format!("Bearer {}", self.token)) .json(&DeletePrefixRequest { prefix: prefix.to_string(), tombstone_key: tombstone_key.map(|s| s.to_string()), deleted_at: Some(chrono::Utc::now().to_rfc3339()), }) .send() .await .map_err(|e| SyncError::NetworkError(e.to_string()))?; if response.status().is_client_error() { let status = response.status(); let body = response.text().await.unwrap_or_default(); return Err(SyncError::AuthError(format!("({status}) {body}"))); } response .json() .await .map_err(|e| SyncError::SerializationError(e.to_string())) } }