Skip to main content

max / makenotwork

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