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#[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 pub fn only_if(mut self, only_if: Conditional) -> Self {
28 self.only_if = Some(only_if);
29 self
30 }
31
32 pub fn range(mut self, range: Range) -> Self {
35 self.range = Some(range);
36 self
37 }
38
39 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
73pub struct Conditional {
74 pub etag_matches: Option<String>,
76 pub etag_does_not_match: Option<String>,
78 pub uploaded_before: Option<Date>,
80 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 OffsetWithLength { offset: u64, length: u64 },
99 OffsetToEnd { offset: u64 },
101 Prefix { length: u64 },
103 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#[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 pub fn http_metadata(mut self, metadata: HttpMetadata) -> Self {
182 self.http_metadata = Some(metadata);
183 self
184 }
185
186 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 pub fn md5(self, bytes: impl Into<Vec<u8>>) -> Self {
200 self.checksum_set("md5", bytes)
201 }
202
203 pub fn sha1(self, bytes: impl Into<Vec<u8>>) -> Self {
205 self.checksum_set("sha1", bytes)
206 }
207
208 pub fn sha256(self, bytes: impl Into<Vec<u8>>) -> Self {
210 self.checksum_set("sha256", bytes)
211 }
212
213 pub fn sha384(self, bytes: impl Into<Vec<u8>>) -> Self {
215 self.checksum_set("sha384", bytes)
216 }
217
218 pub fn sha512(self, bytes: impl Into<Vec<u8>>) -> Self {
220 self.checksum_set("sha512", bytes)
221 }
222
223 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#[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 pub fn http_metadata(mut self, metadata: HttpMetadata) -> Self {
274 self.http_metadata = Some(metadata);
275 self
276 }
277
278 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 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#[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#[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 pub fn limit(mut self, limit: u32) -> Self {
372 self.limit = Some(limit);
373 self
374 }
375
376 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
378 self.prefix = Some(prefix.into());
379 self
380 }
381
382 pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
385 self.cursor = Some(cursor.into());
386 self
387 }
388
389 pub fn delimiter(mut self, delimiter: impl Into<String>) -> Self {
391 self.delimiter = Some(delimiter.into());
392 self
393 }
394
395 pub fn include(mut self, include: Vec<Include>) -> Self {
412 self.include = Some(include);
413 self
414 }
415
416 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;