[go: up one dir, main page]

worker/r2/
builder.rs

1use std::{collections::HashMap, convert::TryFrom};
2
3use js_sys::{Array, Date as JsDate, JsString, Object as JsObject, Uint8Array};
4use wasm_bindgen::{JsCast, JsValue};
5use wasm_bindgen_futures::JsFuture;
6use worker_sys::{
7    R2Bucket as EdgeR2Bucket, R2HttpMetadata as R2HttpMetadataSys,
8    R2MultipartUpload as EdgeR2MultipartUpload, R2Object as EdgeR2Object, R2Range as R2RangeSys,
9};
10
11use crate::{Date, Error, MultipartUpload, ObjectInner, Objects, Result};
12
13use super::{Data, Object};
14
15/// Options for configuring the [get](crate::r2::Bucket::get) operation.
16#[derive(Debug)]
17pub struct GetOptionsBuilder<'bucket> {
18    pub(crate) edge_bucket: &'bucket EdgeR2Bucket,
19    pub(crate) key: String,
20    pub(crate) only_if: Option<Conditional>,
21    pub(crate) range: Option<Range>,
22}
23
24impl GetOptionsBuilder<'_> {
25    /// Specifies that the object should only be returned given satisfaction of certain conditions
26    /// in the [R2Conditional]. Refer to [Conditional operations](https://developers.cloudflare.com/r2/runtime-apis/#conditional-operations).
27    pub fn only_if(mut self, only_if: Conditional) -> Self {
28        self.only_if = Some(only_if);
29        self
30    }
31
32    /// Specifies that only a specific length (from an optional offset) or suffix of bytes from the
33    /// object should be returned. Refer to [Ranged reads](https://developers.cloudflare.com/r2/runtime-apis/#ranged-reads).
34    pub fn range(mut self, range: Range) -> Self {
35        self.range = Some(range);
36        self
37    }
38
39    /// Executes the GET operation on the R2 bucket.
40    pub async fn execute(self) -> Result<Option<Object>> {
41        let name: String = self.key;
42        let get_promise = self.edge_bucket.get(
43            name,
44            js_object! {
45                "onlyIf" => self.only_if.map(JsObject::from),
46                "range" => self.range.map(JsObject::from),
47            }
48            .into(),
49        )?;
50
51        let value = JsFuture::from(get_promise).await?;
52
53        if value.is_null() {
54            return Ok(None);
55        }
56
57        let res: EdgeR2Object = value.into();
58        let inner = if JsString::from("bodyUsed").js_in(&res) {
59            ObjectInner::Body(res.unchecked_into())
60        } else {
61            ObjectInner::NoBody(res)
62        };
63
64        Ok(Some(Object { inner }))
65    }
66}
67
68/// You can pass an [Conditional] object to [GetOptionsBuilder]. If the condition check fails,
69/// the body will not be returned. This will make [get](crate::r2::Bucket::get) have lower latency.
70///
71/// For more information about conditional requests, refer to [RFC 7232](https://datatracker.ietf.org/doc/html/rfc7232).
72#[derive(Debug, Clone, Default, PartialEq, Eq)]
73pub struct Conditional {
74    /// Performs the operation if the object’s etag matches the given string.
75    pub etag_matches: Option<String>,
76    /// Performs the operation if the object’s etag does not match the given string.
77    pub etag_does_not_match: Option<String>,
78    /// Performs the operation if the object was uploaded before the given date.
79    pub uploaded_before: Option<Date>,
80    /// Performs the operation if the object was uploaded after the given date.
81    pub uploaded_after: Option<Date>,
82}
83
84impl From<Conditional> for JsObject {
85    fn from(val: Conditional) -> Self {
86        js_object! {
87            "etagMatches" => JsValue::from(val.etag_matches),
88            "etagDoesNotMatch" => JsValue::from(val.etag_does_not_match),
89            "uploadedBefore" => JsValue::from(val.uploaded_before.map(JsDate::from)),
90            "uploadedAfter" => JsValue::from(val.uploaded_after.map(JsDate::from)),
91        }
92    }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum Range {
97    /// Read `length` bytes starting at `offset`.
98    OffsetWithLength { offset: u64, length: u64 },
99    /// Read from `offset` to the end of the object.
100    OffsetToEnd { offset: u64 },
101    /// Read `length` bytes starting at the beginning of the object.
102    Prefix { length: u64 },
103    /// Read `suffix` bytes from the end of the object.
104    Suffix { suffix: u64 },
105}
106
107const MAX_SAFE_INTEGER: u64 = js_sys::Number::MAX_SAFE_INTEGER as u64;
108
109fn check_range_precision(value: u64) -> f64 {
110    assert!(
111        value <= MAX_SAFE_INTEGER,
112        "Integer precision loss when converting to JavaScript number"
113    );
114    value as f64
115}
116
117impl From<Range> for JsObject {
118    fn from(val: Range) -> Self {
119        match val {
120            Range::OffsetWithLength { offset, length } => js_object! {
121                "offset" => Some(check_range_precision(offset)),
122                "length" => Some(check_range_precision(length)),
123                "suffix" => JsValue::UNDEFINED,
124            },
125            Range::OffsetToEnd { offset } => js_object! {
126                "offset" => Some(check_range_precision(offset)),
127                "length" => JsValue::UNDEFINED,
128                "suffix" => JsValue::UNDEFINED,
129            },
130            Range::Prefix { length } => js_object! {
131                "offset" => JsValue::UNDEFINED,
132                "length" => Some(check_range_precision(length)),
133                "suffix" => JsValue::UNDEFINED,
134            },
135            Range::Suffix { suffix } => js_object! {
136                "offset" => JsValue::UNDEFINED,
137                "length" => JsValue::UNDEFINED,
138                "suffix" => Some(check_range_precision(suffix)),
139            },
140        }
141    }
142}
143
144impl TryFrom<R2RangeSys> for Range {
145    type Error = Error;
146
147    fn try_from(val: R2RangeSys) -> Result<Self> {
148        Ok(match (val.offset, val.length, val.suffix) {
149            (Some(offset), Some(length), None) => Self::OffsetWithLength {
150                offset: offset.round() as u64,
151                length: length.round() as u64,
152            },
153            (Some(offset), None, None) => Self::OffsetToEnd {
154                offset: offset.round() as u64,
155            },
156            (None, Some(length), None) => Self::Prefix {
157                length: length.round() as u64,
158            },
159            (None, None, Some(suffix)) => Self::Suffix {
160                suffix: suffix.round() as u64,
161            },
162            _ => return Err(Error::JsError("invalid range".into())),
163        })
164    }
165}
166
167/// Options for configuring the [put](crate::r2::Bucket::put) operation.
168#[derive(Debug)]
169pub struct PutOptionsBuilder<'bucket> {
170    pub(crate) edge_bucket: &'bucket EdgeR2Bucket,
171    pub(crate) key: String,
172    pub(crate) value: Data,
173    pub(crate) http_metadata: Option<HttpMetadata>,
174    pub(crate) custom_metadata: Option<HashMap<String, String>>,
175    pub(crate) checksum: Option<Vec<u8>>,
176    pub(crate) checksum_algorithm: String,
177}
178
179impl PutOptionsBuilder<'_> {
180    /// Various HTTP headers associated with the object. Refer to [HttpMetadata].
181    pub fn http_metadata(mut self, metadata: HttpMetadata) -> Self {
182        self.http_metadata = Some(metadata);
183        self
184    }
185
186    /// A map of custom, user-defined metadata that will be stored with the object.
187    pub fn custom_metadata(mut self, metadata: impl Into<HashMap<String, String>>) -> Self {
188        self.custom_metadata = Some(metadata.into());
189        self
190    }
191
192    fn checksum_set(mut self, algorithm: &str, checksum: impl Into<Vec<u8>>) -> Self {
193        self.checksum_algorithm = algorithm.into();
194        self.checksum = Some(checksum.into());
195        self
196    }
197
198    /// A md5 hash to use to check the received object’s integrity.
199    pub fn md5(self, bytes: impl Into<Vec<u8>>) -> Self {
200        self.checksum_set("md5", bytes)
201    }
202
203    /// A sha1 hash to use to check the received object’s integrity.
204    pub fn sha1(self, bytes: impl Into<Vec<u8>>) -> Self {
205        self.checksum_set("sha1", bytes)
206    }
207
208    /// A sha256 hash to use to check the received object’s integrity.
209    pub fn sha256(self, bytes: impl Into<Vec<u8>>) -> Self {
210        self.checksum_set("sha256", bytes)
211    }
212
213    /// A sha384 hash to use to check the received object’s integrity.
214    pub fn sha384(self, bytes: impl Into<Vec<u8>>) -> Self {
215        self.checksum_set("sha384", bytes)
216    }
217
218    /// A sha512 hash to use to check the received object’s integrity.
219    pub fn sha512(self, bytes: impl Into<Vec<u8>>) -> Self {
220        self.checksum_set("sha512", bytes)
221    }
222
223    /// Executes the PUT operation on the R2 bucket.
224    pub async fn execute(self) -> Result<Object> {
225        let value: JsValue = self.value.into();
226        let name: String = self.key;
227
228        let put_promise = self.edge_bucket.put(
229            name,
230            value,
231            js_object! {
232                "httpMetadata" => self.http_metadata.map(JsObject::from),
233                "customMetadata" => match self.custom_metadata {
234                    Some(metadata) => {
235                        let obj = JsObject::new();
236                        for (k, v) in metadata.into_iter() {
237                            js_sys::Reflect::set(&obj, &JsString::from(k), &JsString::from(v))?;
238                        }
239                        obj.into()
240                    }
241                    None => JsValue::UNDEFINED,
242                },
243                self.checksum_algorithm => self.checksum.map(|bytes| {
244                    let arr = Uint8Array::new_with_length(bytes.len() as _);
245                    arr.copy_from(&bytes);
246                    arr.buffer()
247                }),
248            }
249            .into(),
250        )?;
251        let res: EdgeR2Object = JsFuture::from(put_promise).await?.into();
252        let inner = if JsString::from("bodyUsed").js_in(&res) {
253            ObjectInner::Body(res.unchecked_into())
254        } else {
255            ObjectInner::NoBody(res)
256        };
257
258        Ok(Object { inner })
259    }
260}
261
262/// Options for configuring the [create_multipart_upload](crate::r2::Bucket::create_multipart_upload) operation.
263#[derive(Debug)]
264pub struct CreateMultipartUploadOptionsBuilder<'bucket> {
265    pub(crate) edge_bucket: &'bucket EdgeR2Bucket,
266    pub(crate) key: String,
267    pub(crate) http_metadata: Option<HttpMetadata>,
268    pub(crate) custom_metadata: Option<HashMap<String, String>>,
269}
270
271impl CreateMultipartUploadOptionsBuilder<'_> {
272    /// Various HTTP headers associated with the object. Refer to [HttpMetadata].
273    pub fn http_metadata(mut self, metadata: HttpMetadata) -> Self {
274        self.http_metadata = Some(metadata);
275        self
276    }
277
278    /// A map of custom, user-defined metadata that will be stored with the object.
279    pub fn custom_metadata(mut self, metadata: impl Into<HashMap<String, String>>) -> Self {
280        self.custom_metadata = Some(metadata.into());
281        self
282    }
283
284    /// Executes the multipart upload creation operation on the R2 bucket.
285    pub async fn execute(self) -> Result<MultipartUpload> {
286        let key: String = self.key;
287
288        let create_multipart_upload_promise = self.edge_bucket.create_multipart_upload(
289            key,
290            js_object! {
291                "httpMetadata" => self.http_metadata.map(JsObject::from),
292                "customMetadata" => match self.custom_metadata {
293                    Some(metadata) => {
294                        let obj = JsObject::new();
295                        for (k, v) in metadata.into_iter() {
296                            js_sys::Reflect::set(&obj, &JsString::from(k), &JsString::from(v))?;
297                        }
298                        obj.into()
299                    }
300                    None => JsValue::UNDEFINED,
301                },
302            }
303            .into(),
304        )?;
305        let inner: EdgeR2MultipartUpload = JsFuture::from(create_multipart_upload_promise)
306            .await?
307            .into();
308
309        Ok(MultipartUpload { inner })
310    }
311}
312
313/// Metadata that's automatically rendered into R2 HTTP API endpoints.
314/// ```
315/// * contentType -> content-type
316/// * contentLanguage -> content-language
317/// etc...
318/// ```
319/// This data is echoed back on GET responses based on what was originally
320/// assigned to the object (and can typically also be overriden when issuing
321/// the GET request).
322#[derive(Debug, Clone, Default, PartialEq, Eq)]
323pub struct HttpMetadata {
324    pub content_type: Option<String>,
325    pub content_language: Option<String>,
326    pub content_disposition: Option<String>,
327    pub content_encoding: Option<String>,
328    pub cache_control: Option<String>,
329    pub cache_expiry: Option<Date>,
330}
331
332impl From<HttpMetadata> for JsObject {
333    fn from(val: HttpMetadata) -> Self {
334        js_object! {
335            "contentType" => val.content_type,
336            "contentLanguage" => val.content_language,
337            "contentDisposition" => val.content_disposition,
338            "contentEncoding" => val.content_encoding,
339            "cacheControl" => val.cache_control,
340            "cacheExpiry" => val.cache_expiry.map(JsDate::from),
341        }
342    }
343}
344
345impl From<R2HttpMetadataSys> for HttpMetadata {
346    fn from(val: R2HttpMetadataSys) -> Self {
347        Self {
348            content_type: val.content_type().unwrap(),
349            content_language: val.content_language().unwrap(),
350            content_disposition: val.content_disposition().unwrap(),
351            content_encoding: val.content_encoding().unwrap(),
352            cache_control: val.cache_control().unwrap(),
353            cache_expiry: val.cache_expiry().unwrap().map(Into::into),
354        }
355    }
356}
357
358/// Options for configuring the [list](crate::r2::Bucket::list) operation.
359#[derive(Debug)]
360pub struct ListOptionsBuilder<'bucket> {
361    pub(crate) edge_bucket: &'bucket EdgeR2Bucket,
362    pub(crate) limit: Option<u32>,
363    pub(crate) prefix: Option<String>,
364    pub(crate) cursor: Option<String>,
365    pub(crate) delimiter: Option<String>,
366    pub(crate) include: Option<Vec<Include>>,
367}
368
369impl ListOptionsBuilder<'_> {
370    /// The number of results to return. Defaults to 1000, with a maximum of 1000.
371    pub fn limit(mut self, limit: u32) -> Self {
372        self.limit = Some(limit);
373        self
374    }
375
376    /// The prefix to match keys against. Keys will only be returned if they start with given prefix.
377    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
378        self.prefix = Some(prefix.into());
379        self
380    }
381
382    /// An opaque token that indicates where to continue listing objects from. A cursor can be
383    /// retrieved from a previous list operation.
384    pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
385        self.cursor = Some(cursor.into());
386        self
387    }
388
389    /// The character to use when grouping keys.
390    pub fn delimiter(mut self, delimiter: impl Into<String>) -> Self {
391        self.delimiter = Some(delimiter.into());
392        self
393    }
394
395    /// If you populate this array, then items returned will include this metadata.
396    /// A tradeoff is that fewer results may be returned depending on how big this
397    /// data is. For now the caps are TBD but expect the total memory usage for a list
398    /// operation may need to be <1MB or even <128kb depending on how many list operations
399    /// you are sending into one bucket. Make sure to look at `truncated` for the result
400    /// rather than having logic like
401    ///
402    /// ```no_run
403    /// while listed.len() < limit {
404    ///     listed = bucket.list()
405    ///         .limit(limit),
406    ///         .include(vec![Include::CustomMetadata])
407    ///         .execute()
408    ///         .await?;
409    /// }
410    /// ```
411    pub fn include(mut self, include: Vec<Include>) -> Self {
412        self.include = Some(include);
413        self
414    }
415
416    /// Executes the LIST operation on the R2 bucket.
417    pub async fn execute(self) -> Result<Objects> {
418        let list_promise = self.edge_bucket.list(
419            js_object! {
420                "limit" => self.limit,
421                "prefix" => self.prefix,
422                "cursor" => self.cursor,
423                "delimiter" => self.delimiter,
424                "include" => self
425                    .include
426                    .map(|include| {
427                        let arr = Array::new();
428                        for include in include {
429                            arr.push(&JsString::from(match include {
430                                Include::HttpMetadata => "httpMetadata",
431                                Include::CustomMetadata => "customMetadata",
432                            }));
433                        }
434                        arr.into()
435                    })
436                    .unwrap_or(JsValue::UNDEFINED),
437            }
438            .into(),
439        )?;
440        let inner = JsFuture::from(list_promise).await?.into();
441        Ok(Objects { inner })
442    }
443}
444
445#[derive(Debug, Clone, PartialEq, Eq)]
446pub enum Include {
447    HttpMetadata,
448    CustomMetadata,
449}
450
451macro_rules! js_object {
452    {$($key: expr => $value: expr),* $(,)?} => {{
453        let obj = JsObject::new();
454        $(
455            {
456                let res = ::js_sys::Reflect::set(&obj, &JsString::from($key), &JsValue::from($value));
457                debug_assert!(res.is_ok(), "setting properties should never fail on our dictionary objects");
458            }
459        )*
460        obj
461    }};
462}
463pub(crate) use js_object;