use bytes::Bytes; use tracing::instrument; use crate::{ crypto, error::Result, types::*, }; use super::SyncKitClient; use super::helpers::check_response; impl SyncKitClient { /// Request a presigned upload URL for a blob. /// Returns (upload_url, already_exists). If `already_exists` is true, /// the blob is already on the server and no upload is needed. #[instrument(skip(self))] pub async fn blob_upload_url( &self, hash: &str, size_bytes: i64, ) -> Result { if size_bytes < 0 { return Err(crate::error::SyncKitError::Internal( "size_bytes must be non-negative".into(), )); } let token = self.require_token()?; let body = Bytes::from(serde_json::to_vec(&BlobUploadUrlRequest { hash: hash.to_string(), size_bytes, })?); self.retry_request_json(|| { let req = self .http .post(&self.endpoints.blobs_upload) .bearer_auth(&token) .header("content-type", "application/json") .body(body.clone()); async move { check_response(req.send().await?).await } }) .await } /// Upload blob data directly to S3 via a presigned PUT URL. /// /// Encrypts the data with the master key before uploading. The plaintext /// is never sent to the server, preserving the E2E encryption guarantee. #[instrument(skip(self, presigned_url, data))] pub async fn blob_upload(&self, presigned_url: &str, data: Vec) -> Result<()> { let master_key = self.require_master_key()?; let encrypted = Bytes::from(crypto::encrypt_bytes(&data, &master_key)?); self.retry_request(|| { let req = self .http .put(presigned_url) .header("content-type", "application/octet-stream") .body(encrypted.clone()); async move { check_response(req.send().await?).await } }) .await?; Ok(()) } /// Confirm that a blob upload completed successfully. /// The server verifies the object exists in S3 and records it. #[instrument(skip(self))] pub async fn blob_confirm(&self, hash: &str, size_bytes: i64) -> Result<()> { if size_bytes < 0 { return Err(crate::error::SyncKitError::Internal( "size_bytes must be non-negative".into(), )); } let token = self.require_token()?; let body = Bytes::from(serde_json::to_vec(&BlobConfirmRequest { hash: hash.to_string(), size_bytes, })?); self.retry_request(|| { let req = self .http .post(&self.endpoints.blobs_confirm) .bearer_auth(&token) .header("content-type", "application/json") .body(body.clone()); async move { check_response(req.send().await?).await } }) .await?; Ok(()) } /// Get a presigned download URL for a blob by hash. #[instrument(skip(self))] pub async fn blob_download_url(&self, hash: &str) -> Result { let token = self.require_token()?; let body = Bytes::from(serde_json::to_vec(&BlobDownloadUrlRequest { hash: hash.to_string(), })?); let download: BlobDownloadUrlResponse = self .retry_request_json(|| { let req = self .http .post(&self.endpoints.blobs_download) .bearer_auth(&token) .header("content-type", "application/json") .body(body.clone()); async move { check_response(req.send().await?).await } }) .await?; Ok(download.download_url) } /// Download blob data from S3 via a presigned GET URL. /// /// Decrypts the data with the master key after downloading. The server /// only ever stores encrypted blobs, preserving the E2E encryption guarantee. #[instrument(skip(self, presigned_url))] pub async fn blob_download(&self, presigned_url: &str) -> Result> { let resp = self .retry_request(|| { let req = self.http.get(presigned_url); async move { check_response(req.send().await?).await } }) .await?; let encrypted = resp.bytes().await?.to_vec(); let master_key = self.require_master_key()?; crypto::decrypt_bytes(&encrypted, &master_key) } } #[cfg(test)] mod tests { use crate::types::*; #[test] fn blob_upload_url_response_deserialization() { let json = r#"{"upload_url": "https://s3.example.com/upload", "already_exists": false}"#; let resp: BlobUploadUrlResponse = serde_json::from_str(json).unwrap(); assert_eq!(resp.upload_url, "https://s3.example.com/upload"); assert!(!resp.already_exists); let json = r#"{"upload_url": "", "already_exists": true}"#; let resp: BlobUploadUrlResponse = serde_json::from_str(json).unwrap(); assert!(resp.already_exists); } #[test] fn blob_upload_url_request_serialization() { let req = BlobUploadUrlRequest { hash: "sha256-abc123".to_string(), size_bytes: 1024, }; let json = serde_json::to_string(&req).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(parsed["hash"], "sha256-abc123"); assert_eq!(parsed["size_bytes"], 1024); } #[test] fn blob_confirm_request_serialization() { let req = BlobConfirmRequest { hash: "sha256-def456".to_string(), size_bytes: 2048, }; let json = serde_json::to_string(&req).unwrap(); let parsed: serde_json::Value = serde_json::from_str(&json).unwrap(); assert_eq!(parsed["hash"], "sha256-def456"); assert_eq!(parsed["size_bytes"], 2048); } }