Skip to main content

box_open_sdk/
chunked_upload.rs

1// Code generated by box-gantry. DO NOT EDIT.
2use 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
10/// Parts in flight at once (`join_ordered` bounds each batch), capping peak
11/// buffer memory while keeping several requests moving together.
12const MAX_CONCURRENT: usize = 4;
13
14/// An orchestrator for Box chunked (multipart) uploads over a [`Client`]: it
15/// creates an upload session, uploads the content's parts with bounded
16/// concurrency, and commits — for a new file ([`ChunkedUpload::upload`]) or a
17/// new version of an existing file ([`ChunkedUpload::upload_version`]). Box
18/// requires chunked upload for files at or above its minimum session size;
19/// smaller files use the single-shot upload endpoints.
20pub struct ChunkedUpload<'a> {
21    client: &'a Client,
22}
23
24impl Client {
25    /// A chunked-upload orchestrator borrowing this client.
26    pub fn chunked_upload(&self) -> ChunkedUpload<'_> {
27        ChunkedUpload { client: self }
28    }
29}
30
31impl ChunkedUpload<'_> {
32    /// Upload `content` as a new file named `file_name` into `folder_id`.
33    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    /// Upload `content` as a new version of the existing file `file_id`.
52    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        // Upload in batches of MAX_CONCURRENT parts at a time, each batch driven
91        // concurrently (the requests are in flight together) but committed in
92        // order — Box's commit lists parts in offset order.
93        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
147/// Await every future concurrently on the current task, returning results in
148/// input order. No `tokio::spawn`, so it needs neither a runtime feature nor a
149/// `Send`/`'static` bound — enough for I/O-bound part uploads, whose requests
150/// are in flight together and driven by the reactor, without pulling in a
151/// `futures`/`tokio` `rt` dependency the SDK avoids.
152async 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
166/// The [`Future`] backing [`join_ordered`]: it polls each not-yet-ready future
167/// on every wake and finishes once all have produced a value.
168struct 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
207/// SHA-1 (RFC 3174) of `data`. Hand-rolled so the SDK needs no hashing
208/// dependency — its crypto stack (`sha2`, `rsa`) carries no SHA-1, and Box's
209/// chunked-upload digests are `sha=<base64(sha1(bytes))>`.
210fn 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    // Pad: append 0x80, zero-fill to 56 mod 64, then the 64-bit big-endian length.
221    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        // RFC 3174 / NIST vectors, plus the 55/56/64-byte cases that exercise the
283        // padding boundary (56 mod 64) a hand-rolled implementation is most likely
284        // to get wrong, and the classic 1e6-byte vector spanning many blocks.
285        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}