1use crate::client::Client;
3use crate::models::schemas;
4use crate::runtime::Error;
5use base64::Engine as _;
6use std::future::Future;
7use std::pin::Pin;
8use std::task::{Context, Poll};
9
10const MAX_CONCURRENT: usize = 4;
13
14pub struct ChunkedUpload<'a> {
21 client: &'a Client,
22}
23
24impl Client {
25 pub fn chunked_upload(&self) -> ChunkedUpload<'_> {
27 ChunkedUpload { client: self }
28 }
29}
30
31impl ChunkedUpload<'_> {
32 pub async fn upload(
34 &self,
35 content: &[u8],
36 file_name: &str,
37 folder_id: &str,
38 ) -> Result<schemas::Files, Error> {
39 let session = self
40 .client
41 .chunked_uploads
42 .create_file_upload_session(schemas::CreateFileUploadSessionRequest {
43 folder_id: folder_id.to_string(),
44 file_size: content.len() as i64,
45 file_name: file_name.to_string(),
46 })
47 .await?;
48 self.finish(session, content).await
49 }
50
51 pub async fn upload_version(
53 &self,
54 content: &[u8],
55 file_name: &str,
56 file_id: &str,
57 ) -> Result<schemas::Files, Error> {
58 let session = self
59 .client
60 .chunked_uploads
61 .create_file_version_upload_session(
62 file_id.to_string(),
63 schemas::CreateFileVersionUploadSessionRequest {
64 file_size: content.len() as i64,
65 file_name: Some(file_name.to_string()),
66 },
67 )
68 .await?;
69 self.finish(session, content).await
70 }
71
72 async fn finish(
73 &self,
74 session: schemas::UploadSession,
75 content: &[u8],
76 ) -> Result<schemas::Files, Error> {
77 let id = session
78 .id
79 .ok_or_else(|| Error::new("gantry: upload session returned no id"))?;
80 let part_size = match session.part_size {
81 Some(size) if size > 0 => size as usize,
82 _ => {
83 return Err(Error::new(
84 "gantry: upload session returned a non-positive part_size",
85 ));
86 }
87 };
88 let total = content.len();
89
90 let offsets: Vec<usize> = (0..total).step_by(part_size).collect();
94 let mut parts = Vec::with_capacity(offsets.len());
95 for batch in offsets.chunks(MAX_CONCURRENT) {
96 let uploads = batch
97 .iter()
98 .map(|&start| {
99 let end = (start + part_size).min(total);
100 self.upload_part(&id, &content[start..end], start, end, total)
101 })
102 .collect();
103 for result in join_ordered(uploads).await {
104 parts.push(result?);
105 }
106 }
107
108 let digest = format!(
109 "sha={}",
110 base64::engine::general_purpose::STANDARD.encode(sha1(content))
111 );
112 self.client
113 .chunked_uploads
114 .commit_file_upload_session(
115 id,
116 digest,
117 schemas::CommitFileUploadSessionRequest { parts },
118 None,
119 )
120 .await
121 }
122
123 async fn upload_part(
124 &self,
125 id: &str,
126 slice: &[u8],
127 start: usize,
128 end: usize,
129 total: usize,
130 ) -> Result<schemas::UploadPart, Error> {
131 let digest = format!(
132 "sha={}",
133 base64::engine::general_purpose::STANDARD.encode(sha1(slice))
134 );
135 let content_range = format!("bytes {}-{}/{}", start, end - 1, total);
136 let uploaded = self
137 .client
138 .chunked_uploads
139 .update_file_upload_session(id.to_string(), digest, content_range, slice.to_vec())
140 .await?;
141 uploaded
142 .part
143 .ok_or_else(|| Error::new("gantry: upload part returned no part"))
144 }
145}
146
147async fn join_ordered<F>(futures: Vec<F>) -> Vec<F::Output>
153where
154 F: Future,
155 F::Output: Unpin,
156{
157 JoinOrdered {
158 slots: futures
159 .into_iter()
160 .map(|f| (Some(Box::pin(f)), None))
161 .collect(),
162 }
163 .await
164}
165
166struct JoinOrdered<F: Future> {
169 #[allow(clippy::type_complexity)]
170 slots: Vec<(Option<Pin<Box<F>>>, Option<F::Output>)>,
171}
172
173impl<F> Future for JoinOrdered<F>
174where
175 F: Future,
176 F::Output: Unpin,
177{
178 type Output = Vec<F::Output>;
179
180 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
181 let slots = &mut self.get_mut().slots;
182 let mut pending = false;
183 for (future, output) in slots.iter_mut() {
184 if let Some(f) = future {
185 match f.as_mut().poll(cx) {
186 Poll::Ready(value) => {
187 *output = Some(value);
188 *future = None;
189 }
190 Poll::Pending => pending = true,
191 }
192 }
193 }
194 if pending {
195 Poll::Pending
196 } else {
197 Poll::Ready(
198 slots
199 .iter_mut()
200 .map(|(_, out)| out.take().unwrap())
201 .collect(),
202 )
203 }
204 }
205}
206
207fn sha1(data: &[u8]) -> [u8; 20] {
211 let mut h: [u32; 5] = [
212 0x6745_2301,
213 0xEFCD_AB89,
214 0x98BA_DCFE,
215 0x1032_5476,
216 0xC3D2_E1F0,
217 ];
218 let bit_len = (data.len() as u64).wrapping_mul(8);
219
220 let mut msg = data.to_vec();
222 msg.push(0x80);
223 while msg.len() % 64 != 56 {
224 msg.push(0);
225 }
226 msg.extend_from_slice(&bit_len.to_be_bytes());
227
228 for block in msg.chunks_exact(64) {
229 let mut w = [0u32; 80];
230 for (word, bytes) in w.iter_mut().zip(block.chunks_exact(4)) {
231 *word = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
232 }
233 for i in 16..80 {
234 w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
235 }
236
237 let [mut a, mut b, mut c, mut d, mut e] = h;
238 for (i, &word) in w.iter().enumerate() {
239 let (f, k) = match i {
240 0..=19 => ((b & c) | ((!b) & d), 0x5A82_7999_u32),
241 20..=39 => (b ^ c ^ d, 0x6ED9_EBA1),
242 40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1B_BCDC),
243 _ => (b ^ c ^ d, 0xCA62_C1D6),
244 };
245 let temp = a
246 .rotate_left(5)
247 .wrapping_add(f)
248 .wrapping_add(e)
249 .wrapping_add(k)
250 .wrapping_add(word);
251 e = d;
252 d = c;
253 c = b.rotate_left(30);
254 b = a;
255 a = temp;
256 }
257
258 h[0] = h[0].wrapping_add(a);
259 h[1] = h[1].wrapping_add(b);
260 h[2] = h[2].wrapping_add(c);
261 h[3] = h[3].wrapping_add(d);
262 h[4] = h[4].wrapping_add(e);
263 }
264
265 let mut out = [0u8; 20];
266 for (chunk, word) in out.chunks_exact_mut(4).zip(h) {
267 chunk.copy_from_slice(&word.to_be_bytes());
268 }
269 out
270}
271
272#[cfg(test)]
273mod tests {
274 use super::sha1;
275
276 fn hex(bytes: &[u8]) -> String {
277 bytes.iter().map(|b| format!("{b:02x}")).collect()
278 }
279
280 #[test]
281 fn sha1_matches_known_answer_vectors() {
282 assert_eq!(hex(&sha1(b"")), "da39a3ee5e6b4b0d3255bfef95601890afd80709");
286 assert_eq!(
287 hex(&sha1(b"abc")),
288 "a9993e364706816aba3e25717850c26c9cd0d89d"
289 );
290 assert_eq!(
291 hex(&sha1(
292 b"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
293 )),
294 "84983e441c3bd26ebaae4aa1f95129e5e54670f1"
295 );
296 assert_eq!(
297 hex(&sha1(&[b'a'; 55])),
298 "c1c8bbdc22796e28c0e15163d20899b65621d65a"
299 );
300 assert_eq!(
301 hex(&sha1(&[b'a'; 56])),
302 "c2db330f6083854c99d4b5bfb6e8f29f201be699"
303 );
304 assert_eq!(
305 hex(&sha1(&[b'a'; 64])),
306 "0098ba824b5c16427bd7a1122a5a442a25ec644d"
307 );
308 assert_eq!(
309 hex(&sha1(&[b'a'; 1_000_000])),
310 "34aa973cd4c4daa4f61eeb2bdbad27316534016f"
311 );
312 }
313}