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
15pub 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 pub fn only_if(mut self, only_if: Conditional) -> Self {
27 self.only_if = Some(only_if);
28 self
29 }
30
31 pub fn range(mut self, range: Range) -> Self {
34 self.range = Some(range);
35 self
36 }
37
38 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#[derive(Debug, Clone, Default, PartialEq, Eq)]
72pub struct Conditional {
73 pub etag_matches: Option<String>,
75 pub etag_does_not_match: Option<String>,
77 pub uploaded_before: Option<Date>,
79 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 OffsetWithLength { offset: u64, length: u64 },
98 OffsetToEnd { offset: u64 },
100 Prefix { length: u64 },
102 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
166pub 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 pub fn http_metadata(mut self, metadata: HttpMetadata) -> Self {
180 self.http_metadata = Some(metadata);
181 self
182 }
183
184 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 pub fn md5(self, bytes: impl Into<Vec<u8>>) -> Self {
198 self.checksum_set("md5", bytes)
199 }
200
201 pub fn sha1(self, bytes: impl Into<Vec<u8>>) -> Self {
203 self.checksum_set("sha1", bytes)
204 }
205
206 pub fn sha256(self, bytes: impl Into<Vec<u8>>) -> Self {
208 self.checksum_set("sha256", bytes)
209 }
210
211 pub fn sha384(self, bytes: impl Into<Vec<u8>>) -> Self {
213 self.checksum_set("sha384", bytes)
214 }
215
216 pub fn sha512(self, bytes: impl Into<Vec<u8>>) -> Self {
218 self.checksum_set("sha512", bytes)
219 }
220
221 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
260pub 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 pub fn http_metadata(mut self, metadata: HttpMetadata) -> Self {
271 self.http_metadata = Some(metadata);
272 self
273 }
274
275 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 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#[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
355pub 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 pub fn limit(mut self, limit: u32) -> Self {
368 self.limit = Some(limit);
369 self
370 }
371
372 pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
374 self.prefix = Some(prefix.into());
375 self
376 }
377
378 pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
381 self.cursor = Some(cursor.into());
382 self
383 }
384
385 pub fn delimiter(mut self, delimiter: impl Into<String>) -> Self {
387 self.delimiter = Some(delimiter.into());
388 self
389 }
390
391 pub fn include(mut self, include: Vec<Include>) -> Self {
408 self.include = Some(include);
409 self
410 }
411
412 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;