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