Skip to main content

max / synckit-client

5.8 KB · 175 lines History Blame Raw
1 use bytes::Bytes;
2 use tracing::instrument;
3
4 use crate::{
5 crypto,
6 error::Result,
7 types::*,
8 };
9
10 use super::SyncKitClient;
11 use super::helpers::check_response;
12
13 impl SyncKitClient {
14 /// Request a presigned upload URL for a blob.
15 /// Returns (upload_url, already_exists). If `already_exists` is true,
16 /// the blob is already on the server and no upload is needed.
17 #[instrument(skip(self))]
18 pub async fn blob_upload_url(
19 &self,
20 hash: &str,
21 size_bytes: i64,
22 ) -> Result<BlobUploadUrlResponse> {
23 let token = self.require_token()?;
24
25 let body = Bytes::from(serde_json::to_vec(&BlobUploadUrlRequest {
26 hash: hash.to_string(),
27 size_bytes,
28 })?);
29
30 let resp = self
31 .retry_request(|| {
32 let req = self
33 .http
34 .post(&self.endpoints.blobs_upload)
35 .bearer_auth(&token)
36 .header("content-type", "application/json")
37 .body(body.clone());
38 async move { check_response(req.send().await?).await }
39 })
40 .await?;
41 Ok(resp.json().await?)
42 }
43
44 /// Upload blob data directly to S3 via a presigned PUT URL.
45 ///
46 /// Encrypts the data with the master key before uploading. The plaintext
47 /// is never sent to the server, preserving the E2E encryption guarantee.
48 #[instrument(skip(self, presigned_url, data))]
49 pub async fn blob_upload(&self, presigned_url: &str, data: Vec<u8>) -> Result<()> {
50 let master_key = self.require_master_key()?;
51 let encrypted = Bytes::from(crypto::encrypt_bytes(&data, &master_key)?);
52
53 self.retry_request(|| {
54 let req = self
55 .http
56 .put(presigned_url)
57 .header("content-type", "application/octet-stream")
58 .body(encrypted.clone());
59 async move { check_response(req.send().await?).await }
60 })
61 .await?;
62
63 Ok(())
64 }
65
66 /// Confirm that a blob upload completed successfully.
67 /// The server verifies the object exists in S3 and records it.
68 #[instrument(skip(self))]
69 pub async fn blob_confirm(&self, hash: &str, size_bytes: i64) -> Result<()> {
70 let token = self.require_token()?;
71
72 let body = Bytes::from(serde_json::to_vec(&BlobConfirmRequest {
73 hash: hash.to_string(),
74 size_bytes,
75 })?);
76
77 self.retry_request(|| {
78 let req = self
79 .http
80 .post(&self.endpoints.blobs_confirm)
81 .bearer_auth(&token)
82 .header("content-type", "application/json")
83 .body(body.clone());
84 async move { check_response(req.send().await?).await }
85 })
86 .await?;
87 Ok(())
88 }
89
90 /// Get a presigned download URL for a blob by hash.
91 #[instrument(skip(self))]
92 pub async fn blob_download_url(&self, hash: &str) -> Result<String> {
93 let token = self.require_token()?;
94
95 let body = Bytes::from(serde_json::to_vec(&BlobDownloadUrlRequest {
96 hash: hash.to_string(),
97 })?);
98
99 let resp = self
100 .retry_request(|| {
101 let req = self
102 .http
103 .post(&self.endpoints.blobs_download)
104 .bearer_auth(&token)
105 .header("content-type", "application/json")
106 .body(body.clone());
107 async move { check_response(req.send().await?).await }
108 })
109 .await?;
110 let download: BlobDownloadUrlResponse = resp.json().await?;
111 Ok(download.download_url)
112 }
113
114 /// Download blob data from S3 via a presigned GET URL.
115 ///
116 /// Decrypts the data with the master key after downloading. The server
117 /// only ever stores encrypted blobs, preserving the E2E encryption guarantee.
118 #[instrument(skip(self, presigned_url))]
119 pub async fn blob_download(&self, presigned_url: &str) -> Result<Vec<u8>> {
120 let resp = self
121 .retry_request(|| {
122 let req = self.http.get(presigned_url);
123 async move { check_response(req.send().await?).await }
124 })
125 .await?;
126
127 let encrypted = resp.bytes().await?.to_vec();
128 let master_key = self.require_master_key()?;
129 crypto::decrypt_bytes(&encrypted, &master_key)
130 }
131 }
132
133 #[cfg(test)]
134 mod tests {
135 use crate::types::*;
136
137 #[test]
138 fn blob_upload_url_response_deserialization() {
139 let json = r#"{"upload_url": "https://s3.example.com/upload", "already_exists": false}"#;
140 let resp: BlobUploadUrlResponse = serde_json::from_str(json).unwrap();
141 assert_eq!(resp.upload_url, "https://s3.example.com/upload");
142 assert!(!resp.already_exists);
143
144 let json = r#"{"upload_url": "", "already_exists": true}"#;
145 let resp: BlobUploadUrlResponse = serde_json::from_str(json).unwrap();
146 assert!(resp.already_exists);
147 }
148
149 #[test]
150 fn blob_upload_url_request_serialization() {
151 let req = BlobUploadUrlRequest {
152 hash: "sha256-abc123".to_string(),
153 size_bytes: 1024,
154 };
155
156 let json = serde_json::to_string(&req).unwrap();
157 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
158 assert_eq!(parsed["hash"], "sha256-abc123");
159 assert_eq!(parsed["size_bytes"], 1024);
160 }
161
162 #[test]
163 fn blob_confirm_request_serialization() {
164 let req = BlobConfirmRequest {
165 hash: "sha256-def456".to_string(),
166 size_bytes: 2048,
167 };
168
169 let json = serde_json::to_string(&req).unwrap();
170 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
171 assert_eq!(parsed["hash"], "sha256-def456");
172 assert_eq!(parsed["size_bytes"], 2048);
173 }
174 }
175