Skip to content
File

Blob: src/workerd/api/r2-bucket.c++

59.3 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "r2-bucket.h"
6 
7#include "r2-multipart.h"
8#include "r2-rpc.h"
9#include "util.h"
10 
11#include <workerd/api/http.h>
12#include <workerd/api/r2-api.capnp.h>
13#include <workerd/api/streams/readable.h>
14#include <workerd/util/http-util.h>
15 
16#include <capnp/compat/json.h>
17#include <capnp/message.h>
18#include <kj/compat/http.h>
19#include <kj/encoding.h>
20 
21#include <array>
22#include <cmath>
23#include <regex>
24 
25namespace workerd::api::public_beta {
26kj::Own<kj::HttpClient> r2GetClient(
27 IoContext& context, uint subrequestChannel, R2UserTracing user) {
28 TraceContext traceContext = context.makeUserTraceSpan(user.op);
29 traceContext.setTag("rpc.service"_kjc, "r2"_kjc);
30 traceContext.setTag(user.method.key, user.method.value);
31 KJ_IF_SOME(b, user.bucket) {
32 traceContext.setTag("cloudflare.r2.bucket"_kjc, b);
33 }
34 KJ_IF_SOME(tag, user.extraTag) {
35 traceContext.setTag(tag.key, tag.value);
36 }
37 
38 // TODO(o11y): Attach trace context to awaitIo call to match operation lifetime better?
39 return context.getHttpClient(subrequestChannel, true, kj::none, traceContext)
40 .attach(kj::mv(traceContext));
41}
42 
43static bool isWholeNumber(double x) {
44 double intpart;
45 return modf(x, &intpart) == 0;
46}
47 
48// TODO(perf): Would be nice to expose the v8 internals for parsing a date/stringifying it as
49// something an embedder can call directly rather than doing this rigamarole. It would also avoid
50// concerns about the user overriding the methods we're invoking.
51static kj::Date parseDate(jsg::Lock& js, kj::StringPtr value) {
52 return js.date(value);
53}
54 
55static kj::String toUTCString(jsg::Lock& js, kj::Date date) {
56 return js.date(date).toUTCString(js);
57}
58 
59static kj::String toISOString(jsg::Lock& js, kj::Date date) {
60 return js.date(date).toISOString(js);
61}
62 
63enum class OptionalMetadata : uint16_t {
64 Http = static_cast<uint8_t>(R2ListRequest::IncludeField::HTTP),
65 Custom = static_cast<uint8_t>(R2ListRequest::IncludeField::CUSTOM),
66};
67 
68template <typename T>
69concept HeadResultT = std::is_base_of_v<R2Bucket::HeadResult, T>;
70 
71template <HeadResultT T, typename... Args>
72static jsg::Ref<T> parseObjectMetadata(jsg::Lock& js,
73 R2HeadResponse::Reader responseReader,
74 kj::ArrayPtr<const OptionalMetadata> expectedOptionalFields,
75 Args&&... args) {
76 // optionalFieldsExpected is initialized by default to HTTP + CUSTOM if the user doesn't specify
77 // anything. If they specify the empty array, then nothing is returned.
78 kj::Date uploaded =
79 kj::UNIX_EPOCH + responseReader.getUploadedMillisecondsSinceEpoch() * kj::MILLISECONDS;
80 
81 jsg::Optional<R2Bucket::HttpMetadata> httpMetadata;
82 if (responseReader.hasHttpFields()) {
83 R2Bucket::HttpMetadata m;
84 
85 auto httpFields = responseReader.getHttpFields();
86 if (httpFields.hasContentType()) {
87 m.contentType = kj::str(httpFields.getContentType());
88 }
89 if (httpFields.hasContentDisposition()) {
90 m.contentDisposition = kj::str(httpFields.getContentDisposition());
91 }
92 if (httpFields.hasContentEncoding()) {
93 m.contentEncoding = kj::str(httpFields.getContentEncoding());
94 }
95 if (httpFields.hasContentLanguage()) {
96 m.contentLanguage = kj::str(httpFields.getContentLanguage());
97 }
98 if (httpFields.hasCacheControl()) {
99 m.cacheControl = kj::str(httpFields.getCacheControl());
100 }
101 if (httpFields.getCacheExpiry() != 0xffffffffffffffff) {
102 m.cacheExpiry = kj::UNIX_EPOCH + httpFields.getCacheExpiry() * kj::MILLISECONDS;
103 }
104 
105 httpMetadata = kj::mv(m);
106 } else if (std::find(expectedOptionalFields.begin(), expectedOptionalFields.end(),
107 OptionalMetadata::Http) != expectedOptionalFields.end()) {
108 // HTTP metadata was asked for but the object didn't have anything.
109 httpMetadata = R2Bucket::HttpMetadata{};
110 }
111 
112 jsg::Optional<jsg::Dict<kj::String>> customMetadata;
113 if (responseReader.hasCustomFields()) {
114 customMetadata = jsg::Dict<kj::String>{.fields =
115 KJ_MAP(field, responseReader.getCustomFields()) {
116 jsg::Dict<kj::String>::Field item;
117 item.name = kj::str(field.getK());
118 item.value = kj::str(field.getV());
119 return item;
120 }};
121 } else if (std::find(expectedOptionalFields.begin(), expectedOptionalFields.end(),
122 OptionalMetadata::Custom) != expectedOptionalFields.end()) {
123 // Custom metadata was asked for but the object didn't have anything.
124 customMetadata = jsg::Dict<kj::String>{};
125 }
126 
127 jsg::Optional<R2Bucket::Range> range;
128 
129 if (responseReader.hasRange()) {
130 auto rangeBuilder = responseReader.getRange();
131 range = R2Bucket::Range{
132 .offset = static_cast<double>(rangeBuilder.getOffset()),
133 .length = static_cast<double>(rangeBuilder.getLength()),
134 };
135 }
136 
137 jsg::Ref<R2Bucket::Checksums> checksums =
138 js.alloc<R2Bucket::Checksums>(kj::none, kj::none, kj::none, kj::none, kj::none);
139 
140 if (responseReader.hasChecksums()) {
141 R2Checksums::Reader checksumsBuilder = responseReader.getChecksums();
142 if (checksumsBuilder.hasMd5()) {
143 // Note that we don't check the length of checksums in here. We know
144 // that some artifact were stored with truncated checksums (e.g. 8 bytes
145 // instead of 16 for some). We're not validating the checksum lengths
146 // here and instead we're just passing them through.
147 checksums->md5 = kj::heapArray(checksumsBuilder.getMd5());
148 }
149 if (checksumsBuilder.hasSha1()) {
150 checksums->sha1 = kj::heapArray(checksumsBuilder.getSha1());
151 }
152 if (checksumsBuilder.hasSha256()) {
153 checksums->sha256 = kj::heapArray(checksumsBuilder.getSha256());
154 }
155 if (checksumsBuilder.hasSha384()) {
156 checksums->sha384 = kj::heapArray(checksumsBuilder.getSha384());
157 }
158 if (checksumsBuilder.hasSha512()) {
159 checksums->sha512 = kj::heapArray(checksumsBuilder.getSha512());
160 }
161 }
162 
163 jsg::Optional<kj::String> ssecKeyMd5;
164 
165 if (responseReader.hasSsec()) {
166 auto ssecBuilder = responseReader.getSsec();
167 ssecKeyMd5 = kj::str(ssecBuilder.getKeyMd5());
168 }
169 
170 return js.alloc<T>(kj::str(responseReader.getName()), kj::str(responseReader.getVersion()),
171 responseReader.getSize(), kj::str(responseReader.getEtag()), kj::mv(checksums), uploaded,
172 kj::mv(httpMetadata), kj::mv(customMetadata), range,
173 kj::str(responseReader.getStorageClass()), kj::mv(ssecKeyMd5), kj::fwd<Args>(args)...);
174}
175 
176template <HeadResultT T, typename... Args>
177static kj::Maybe<jsg::Ref<T>> parseObjectMetadata(jsg::Lock& js,
178 kj::StringPtr action,
179 R2Result& r2Result,
180 const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType,
181 Args&&... args) {
182 if (r2Result.objectNotFound()) {
183 return kj::none;
184 }
185 if (!r2Result.preconditionFailed()) {
186 r2Result.throwIfError(action, errorType);
187 }
188 
189 // Non-list operations always return these.
190 std::array expectedFieldsOwned = {OptionalMetadata::Http, OptionalMetadata::Custom};
191 kj::ArrayPtr<OptionalMetadata> expectedFields = {
192 expectedFieldsOwned.data(), expectedFieldsOwned.size()};
193 
194 capnp::MallocMessageBuilder responseMessage;
195 capnp::JsonCodec json;
196 // Annoyingly our R2GetResponse alias isn't emitted.
197 json.handleByAnnotation<R2HeadResponse>();
198 auto responseBuilder = responseMessage.initRoot<R2HeadResponse>();
199 json.decode(KJ_ASSERT_NONNULL(r2Result.metadataPayload), responseBuilder);
200 
201 return parseObjectMetadata<T>(js, responseBuilder, expectedFields, kj::fwd<Args>(args)...);
202}
203 
204namespace {
205 
206void addEtagsToBuilder(
207 capnp::List<R2Etag>::Builder etagListBuilder, kj::ArrayPtr<R2Bucket::Etag> etagArray) {
208 R2Bucket::Etag* currentEtag = etagArray.begin();
209 for (unsigned int i = 0; i < etagArray.size(); i++) {
210 KJ_SWITCH_ONEOF(*currentEtag) {
211 KJ_CASE_ONEOF(e, R2Bucket::WildcardEtag) {
212 etagListBuilder[i].initType().setWildcard();
213 }
214 KJ_CASE_ONEOF(e, R2Bucket::StrongEtag) {
215 etagListBuilder[i].initType().setStrong();
216 etagListBuilder[i].setValue(e.value);
217 }
218 KJ_CASE_ONEOF(e, R2Bucket::WeakEtag) {
219 etagListBuilder[i].initType().setWeak();
220 etagListBuilder[i].setValue(e.value);
221 }
222 }
223 currentEtag = std::next(currentEtag);
224 }
225}
226 
227} // namespace
228 
229kj::String buildEtagsString(kj::ArrayPtr<R2Bucket::Etag> etagArray) {
230 return kj::strArray(KJ_MAP(etag, etagArray) {
231 KJ_SWITCH_ONEOF(etag) {
232 KJ_CASE_ONEOF(e, R2Bucket::WildcardEtag) {
233 return kj::str("*");
234 }
235 KJ_CASE_ONEOF(e, R2Bucket::StrongEtag) {
236 return kj::str(e.value);
237 }
238 KJ_CASE_ONEOF(e, R2Bucket::WeakEtag) {
239 return kj::str(e.value);
240 }
241 }
242 KJ_UNREACHABLE;
243 }, ", ");
244}
245 
246template <typename Builder, typename Options>
247void initOnlyIf(TraceContext& traceContext, jsg::Lock& js, Builder& builder, Options& o) {
248 KJ_IF_SOME(i, o.onlyIf) {
249 R2Bucket::UnwrappedConditional c = [&] {
250 KJ_SWITCH_ONEOF(i) {
251 KJ_CASE_ONEOF(conditional, R2Bucket::Conditional) {
252 return R2Bucket::UnwrappedConditional(conditional);
253 }
254 KJ_CASE_ONEOF(h, jsg::Ref<Headers>) {
255 return R2Bucket::UnwrappedConditional(js, *h);
256 }
257 }
258 KJ_UNREACHABLE;
259 }();
260 
261 R2Conditional::Builder onlyIfBuilder = builder.initOnlyIf();
262 KJ_IF_SOME(etagArray, c.etagMatches) {
263 capnp::List<R2Etag>::Builder etagMatchList = onlyIfBuilder.initEtagMatches(etagArray.size());
264 addEtagsToBuilder(
265 etagMatchList, kj::arrayPtr<R2Bucket::Etag>(etagArray.begin(), etagArray.size()));
266 traceContext.setTag(
267 "cloudflare.r2.request.only_if.etag_matches"_kjc, buildEtagsString(etagArray));
268 }
269 KJ_IF_SOME(etagArray, c.etagDoesNotMatch) {
270 auto etagDoesNotMatchList = onlyIfBuilder.initEtagDoesNotMatch(etagArray.size());
271 addEtagsToBuilder(
272 etagDoesNotMatchList, kj::arrayPtr<R2Bucket::Etag>(etagArray.begin(), etagArray.size()));
273 traceContext.setTag(
274 "cloudflare.r2.request.only_if.etag_does_not_match"_kjc, buildEtagsString(etagArray));
275 }
276 KJ_IF_SOME(d, c.uploadedBefore) {
277 onlyIfBuilder.setUploadedBefore((d - kj::UNIX_EPOCH) / kj::MILLISECONDS);
278 if (c.secondsGranularity) {
279 onlyIfBuilder.setSecondsGranularity(true);
280 }
281 traceContext.setTag("cloudflare.r2.request.only_if.uploaded_before"_kjc, toISOString(js, d));
282 }
283 KJ_IF_SOME(d, c.uploadedAfter) {
284 onlyIfBuilder.setUploadedAfter((d - kj::UNIX_EPOCH) / kj::MILLISECONDS);
285 if (c.secondsGranularity) {
286 onlyIfBuilder.setSecondsGranularity(true);
287 }
288 traceContext.setTag("cloudflare.r2.request.only_if.uploaded_after"_kjc, toISOString(js, d));
289 }
290 }
291}
292 
293kj::Maybe<kj::String> buildSsecKey(
294 kj::Maybe<kj::OneOf<kj::Array<byte>, kj::String>> maybeRawSsecKey) {
295 KJ_IF_SOME(rawSsecKey, maybeRawSsecKey) {
296 KJ_SWITCH_ONEOF(rawSsecKey) {
297 KJ_CASE_ONEOF(keyString, kj::String) {
298 JSG_REQUIRE(std::regex_match(keyString.begin(), keyString.end(), std::regex("^[0-9a-f]+$")),
299 Error, "SSE-C Key has invalid format");
300 JSG_REQUIRE(keyString.size() == 64, Error, "SSE-C Key must be 32 bytes in length");
301 return kj::str(keyString);
302 }
303 KJ_CASE_ONEOF(keyBuff, kj::Array<byte>) {
304 JSG_REQUIRE(keyBuff.size() == 32, Error, "SSE-C Key must be 32 bytes in length");
305 return kj::encodeHex(keyBuff);
306 }
307 }
308 }
309 return kj::none;
310}
311 
312static void addR2ResponseSpanTags(TraceContext& traceContext, R2Result& r2Result) {
313 traceContext.setTag("cloudflare.r2.response.success"_kjc, r2Result.success());
314 KJ_IF_SOME(e, r2Result.getR2ErrorMessage()) {
315 traceContext.setTag("error.type"_kjc, e.asPtr());
316 traceContext.setTag("cloudflare.r2.error.message"_kjc, e.asPtr());
317 }
318 KJ_IF_SOME(v4, r2Result.v4ErrorCode()) {
319 traceContext.setTag("cloudflare.r2.error.code"_kjc, static_cast<int64_t>(v4));
320 }
321}
322 
323void addHeadResultSpanTags(
324 jsg::Lock& js, TraceContext& traceContext, R2Bucket::HeadResult& headResult) {
325 traceContext.setTag("cloudflare.r2.response.etag"_kjc, headResult.getEtag());
326 traceContext.setTag("cloudflare.r2.response.size"_kjc, headResult.getSize());
327 traceContext.setTag(
328 "cloudflare.r2.response.uploaded"_kjc, toISOString(js, headResult.getUploaded()));
329 auto checksums = headResult.getChecksums();
330 KJ_IF_SOME(md5, checksums.get()->md5) {
331 traceContext.setTag("cloudflare.r2.response.checksum.md5"_kjc, kj::encodeHex(md5));
332 }
333 KJ_IF_SOME(sha1, checksums.get()->sha1) {
334 traceContext.setTag("cloudflare.r2.response.checksum.sha1"_kjc, kj::encodeHex(sha1));
335 }
336 KJ_IF_SOME(sha256, checksums.get()->sha256) {
337 traceContext.setTag("cloudflare.r2.response.checksum.sha256"_kjc, kj::encodeHex(sha256));
338 }
339 KJ_IF_SOME(sha384, checksums.get()->sha384) {
340 traceContext.setTag("cloudflare.r2.response.checksum.sha384"_kjc, kj::encodeHex(sha384));
341 }
342 KJ_IF_SOME(sha512, checksums.get()->sha512) {
343 traceContext.setTag("cloudflare.r2.response.checksum.sha512"_kjc, kj::encodeHex(sha512));
344 }
345 
346 traceContext.setTag("cloudflare.r2.response.storage_class"_kjc, headResult.getStorageClass());
347 KJ_IF_SOME(_, headResult.getSSECKeyMd5()) {
348 traceContext.setTag("cloudflare.r2.response.ssec_key"_kjc, true);
349 }
350 KJ_IF_SOME(httpMetadata, headResult.getHttpMetadata()) {
351 KJ_IF_SOME(ct, httpMetadata.contentType) {
352 traceContext.setTag("cloudflare.r2.response.content_type"_kjc, ct.asPtr());
353 }
354 KJ_IF_SOME(ce, httpMetadata.contentEncoding) {
355 traceContext.setTag("cloudflare.r2.response.content_encoding"_kjc, ce.asPtr());
356 }
357 KJ_IF_SOME(cd, httpMetadata.contentDisposition) {
358 traceContext.setTag("cloudflare.r2.response.content_disposition"_kjc, cd.asPtr());
359 }
360 KJ_IF_SOME(cl, httpMetadata.contentLanguage) {
361 traceContext.setTag("cloudflare.r2.response.content_language"_kjc, cl.asPtr());
362 }
363 KJ_IF_SOME(cc, httpMetadata.cacheControl) {
364 traceContext.setTag("cloudflare.r2.response.cache_control"_kjc, cc.asPtr());
365 }
366 KJ_IF_SOME(ce, httpMetadata.cacheExpiry) {
367 traceContext.setTag("cloudflare.r2.response.cache_expiry"_kjc, toISOString(js, ce));
368 }
369 }
370 KJ_IF_SOME(_, headResult.getCustomMetadata()) {
371 traceContext.setTag("cloudflare.r2.response.custom_metadata"_kjc, true);
372 }
373}
374 
375template <typename Builder, typename Options>
376void initGetOptions(TraceContext& traceContext, jsg::Lock& js, Builder& builder, Options& o) {
377 initOnlyIf(traceContext, js, builder, o);
378 KJ_IF_SOME(range, o.range) {
379 KJ_SWITCH_ONEOF(range) {
380 KJ_CASE_ONEOF(r, R2Bucket::Range) {
381 auto rangeBuilder = builder.initRange();
382 KJ_IF_SOME(offset, r.offset) {
383 JSG_REQUIRE(offset >= 0, RangeError, "Invalid range. Starting offset (", offset,
384 ") must be greater than or equal to 0.");
385 JSG_REQUIRE(isWholeNumber(offset), RangeError, "Invalid range. Starting offset (", offset,
386 ") must be an integer, not floating point.");
387 rangeBuilder.setOffset(static_cast<uint64_t>(offset));
388 traceContext.setTag(
389 "cloudflare.r2.request.range.offset"_kjc, static_cast<int64_t>(offset));
390 }
391 
392 KJ_IF_SOME(length, r.length) {
393 JSG_REQUIRE(length >= 0, RangeError, "Invalid range. Length (", length,
394 ") must be greater than or equal to 0.");
395 JSG_REQUIRE(isWholeNumber(length), RangeError, "Invalid range. Length (", length,
396 ") must be an integer, not floating point.");
397 
398 rangeBuilder.setLength(static_cast<uint64_t>(length));
399 traceContext.setTag(
400 "cloudflare.r2.request.range.length"_kjc, static_cast<int64_t>(length));
401 }
402 KJ_IF_SOME(suffix, r.suffix) {
403 JSG_REQUIRE(r.offset == kj::none, TypeError, "Suffix is incompatible with offset.");
404 JSG_REQUIRE(r.length == kj::none, TypeError, "Suffix is incompatible with length.");
405 
406 JSG_REQUIRE(suffix >= 0, RangeError, "Invalid suffix. Suffix (", suffix,
407 ") must be greater than or equal to 0.");
408 JSG_REQUIRE(isWholeNumber(suffix), RangeError, "Invalid range. Suffix (", suffix,
409 ") must be an integer, not floating point.");
410 
411 rangeBuilder.setSuffix(static_cast<uint64_t>(suffix));
412 traceContext.setTag(
413 "cloudflare.r2.request.range.suffix"_kjc, static_cast<int64_t>(suffix));
414 }
415 }
416 
417 KJ_CASE_ONEOF(h, jsg::Ref<Headers>) {
418 KJ_IF_SOME(e, h->getCommon(js, capnp::CommonHeaderName::RANGE)) {
419 builder.setRangeHeader(kj::str(e));
420 traceContext.setTag("cloudflare.r2.request.range"_kjc, e.asPtr());
421 }
422 }
423 }
424 }
425 kj::Maybe<kj::String> maybeSsecKey = buildSsecKey(kj::mv(o.ssecKey));
426 KJ_IF_SOME(ssecKey, maybeSsecKey) {
427 auto ssecBuilder = builder.initSsec();
428 ssecBuilder.setKey(ssecKey);
429 traceContext.setTag("cloudflare.r2.request.ssec_key"_kjc, true);
430 }
431}
432 
433static bool isQuotedEtag(kj::StringPtr etag) {
434 return etag.startsWith("\"") && etag.endsWith("\"");
435}
436 
437jsg::Promise<kj::Maybe<jsg::Ref<R2Bucket::HeadResult>>> R2Bucket::head(jsg::Lock& js,
438 kj::String key,
439 const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType,
440 CompatibilityFlags::Reader flags) {
441 return js.evalNow([&] {
442 auto& context = IoContext::current();
443 TraceContext traceContext = context.makeUserTraceSpan("r2_head"_kjc);
444 
445 traceContext.setTag("cloudflare.binding.type"_kjc, "r2"_kjc);
446 KJ_IF_SOME(b, this->bindingName()) {
447 traceContext.setTag("cloudflare.binding.name"_kjc, b);
448 }
449 traceContext.setTag("cloudflare.r2.operation"_kjc, "HeadObject"_kjc);
450 KJ_IF_SOME(b, this->bucketName()) {
451 traceContext.setTag("cloudflare.r2.bucket"_kjc, b);
452 }
453 traceContext.setTag("cloudflare.r2.request.key"_kjc, key.asPtr());
454 
455 capnp::JsonCodec json;
456 json.handleByAnnotation<R2BindingRequest>();
457 json.setHasMode(capnp::HasMode::NON_DEFAULT);
458 capnp::MallocMessageBuilder requestMessage;
459 
460 auto requestBuilder = requestMessage.initRoot<R2BindingRequest>();
461 requestBuilder.setVersion(VERSION_PUBLIC_BETA);
462 auto payloadBuilder = requestBuilder.initPayload();
463 auto headBuilder = payloadBuilder.initHead();
464 headBuilder.setObject(key);
465 
466 auto requestJson = json.encode(requestBuilder);
467 kj::StringPtr components[1];
468 auto path = fillR2Path(components, adminBucket);
469 auto client = context.getHttpClient(clientIndex, true, kj::none, traceContext);
470 auto promise = doR2HTTPGetRequest(kj::mv(client), kj::mv(requestJson), path, jwt, flags);
471 
472 return context.awaitIo(js, kj::mv(promise),
473 [&errorType, traceContext = kj::mv(traceContext)](
474 jsg::Lock& js, R2Result r2Result) mutable {
475 addR2ResponseSpanTags(traceContext, r2Result);
476 
477 auto result = parseObjectMetadata<HeadResult>(js, "head", r2Result, errorType);
478 KJ_IF_SOME(r, result) {
479 addHeadResultSpanTags(js, traceContext, *r.get());
480 }
481 return result;
482 });
483 });
484}
485 
486R2Bucket::FeatureFlags::FeatureFlags(CompatibilityFlags::Reader featureFlags)
487 : listHonorsIncludes(featureFlags.getR2ListHonorIncludeFields()) {}
488 
489jsg::Promise<kj::OneOf<kj::Maybe<jsg::Ref<R2Bucket::GetResult>>, jsg::Ref<R2Bucket::HeadResult>>>
490R2Bucket::get(jsg::Lock& js,
491 kj::String key,
492 jsg::Optional<GetOptions> options,
493 const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType,
494 CompatibilityFlags::Reader flags) {
495 return js.evalNow([&] {
496 auto& context = IoContext::current();
497 TraceContext traceContext = context.makeUserTraceSpan("r2_get"_kjc);
498 
499 traceContext.setTag("cloudflare.binding.type"_kjc, "r2"_kjc);
500 KJ_IF_SOME(b, this->bindingName()) {
501 traceContext.setTag("cloudflare.binding.name"_kjc, b);
502 }
503 traceContext.setTag("cloudflare.r2.operation"_kjc, "GetObject"_kjc);
504 KJ_IF_SOME(b, this->bucketName()) {
505 traceContext.setTag("cloudflare.r2.bucket"_kjc, b);
506 }
507 traceContext.setTag("cloudflare.r2.request.key"_kjc, key.asPtr());
508 
509 capnp::JsonCodec json;
510 json.handleByAnnotation<R2BindingRequest>();
511 json.setHasMode(capnp::HasMode::NON_DEFAULT);
512 capnp::MallocMessageBuilder requestMessage;
513 
514 auto requestBuilder = requestMessage.initRoot<R2BindingRequest>();
515 requestBuilder.setVersion(VERSION_PUBLIC_BETA);
516 auto payloadBuilder = requestBuilder.initPayload();
517 auto getBuilder = payloadBuilder.initGet();
518 getBuilder.setObject(key);
519 
520 KJ_IF_SOME(o, options) {
521 initGetOptions(traceContext, js, getBuilder, o);
522 }
523 auto requestJson = json.encode(requestBuilder);
524 kj::StringPtr components[1];
525 auto path = fillR2Path(components, adminBucket);
526 auto client = context.getHttpClient(clientIndex, true, kj::none, traceContext);
527 auto promise = doR2HTTPGetRequest(kj::mv(client), kj::mv(requestJson), path, jwt, flags);
528 
529 return context.awaitIo(js, kj::mv(promise),
530 [&context, &errorType, traceContext = kj::mv(traceContext)](
531 jsg::Lock& js, R2Result r2Result) mutable
532 -> kj::OneOf<kj::Maybe<jsg::Ref<GetResult>>, jsg::Ref<HeadResult>> {
533 kj::OneOf<kj::Maybe<jsg::Ref<GetResult>>, jsg::Ref<HeadResult>> result;
534 
535 addR2ResponseSpanTags(traceContext, r2Result);
536 if (r2Result.preconditionFailed()) {
537 result = KJ_ASSERT_NONNULL(parseObjectMetadata<HeadResult>(js, "get", r2Result, errorType));
538 } else {
539 jsg::Ref<ReadableStream> body = nullptr;
540 
541 KJ_IF_SOME(s, r2Result.stream) {
542 body = js.alloc<ReadableStream>(context, kj::mv(s));
543 r2Result.stream = kj::none;
544 }
545 result = parseObjectMetadata<GetResult>(js, "get", r2Result, errorType, kj::mv(body));
546 }
547 
548 KJ_SWITCH_ONEOF(result) {
549 KJ_CASE_ONEOF(o, jsg::Ref<HeadResult>) {
550 addHeadResultSpanTags(js, traceContext, *o.get());
551 }
552 KJ_CASE_ONEOF(maybeHeadResult, kj::Maybe<jsg::Ref<GetResult>>) {
553 KJ_IF_SOME(o, maybeHeadResult) {
554 addHeadResultSpanTags(js, traceContext, *o.get());
555 }
556 }
557 }
558 
559 return result;
560 });
561 });
562}
563 
564jsg::Promise<kj::Maybe<jsg::Ref<R2Bucket::HeadResult>>> R2Bucket::put(jsg::Lock& js,
565 kj::String key,
566 kj::Maybe<R2PutValue> value,
567 jsg::Optional<PutOptions> options,
568 const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType) {
569 return js.evalNow([&] {
570 auto cancelReader = kj::defer([&] {
571 KJ_IF_SOME(v, value) {
572 KJ_SWITCH_ONEOF(v) {
573 KJ_CASE_ONEOF(v, jsg::Ref<ReadableStream>) {
574 (*v).cancel(js,
575 js.v8Error(
576 "Stream cancelled because the associated put operation encountered an error."));
577 }
578 KJ_CASE_ONEOF_DEFAULT {}
579 }
580 }
581 });
582 
583 auto& context = IoContext::current();
584 TraceContext traceContext = context.makeUserTraceSpan("r2_put"_kjc);
585 
586 traceContext.setTag("cloudflare.binding.type"_kjc, "r2"_kjc);
587 KJ_IF_SOME(b, this->bindingName()) {
588 traceContext.setTag("cloudflare.binding.name"_kjc, b);
589 }
590 traceContext.setTag("cloudflare.r2.operation"_kjc, "PutObject"_kjc);
591 KJ_IF_SOME(b, this->bucketName()) {
592 traceContext.setTag("cloudflare.r2.bucket"_kjc, b);
593 }
594 traceContext.setTag("cloudflare.r2.request.key"_kjc, key.asPtr());
595 
596 capnp::JsonCodec json;
597 json.handleByAnnotation<R2BindingRequest>();
598 json.setHasMode(capnp::HasMode::NON_DEFAULT);
599 capnp::MallocMessageBuilder requestMessage;
600 
601 auto requestBuilder = requestMessage.initRoot<R2BindingRequest>();
602 requestBuilder.setVersion(VERSION_PUBLIC_BETA);
603 auto payloadBuilder = requestBuilder.initPayload();
604 auto putBuilder = payloadBuilder.initPut();
605 putBuilder.setObject(key);
606 
607 HttpMetadata sentHttpMetadata;
608 jsg::Dict<kj::String> sentCustomMetadata;
609 
610 bool hashAlreadySpecified = false;
611 const auto verifyHashNotSpecified = [&] {
612 JSG_REQUIRE(
613 !hashAlreadySpecified, TypeError, "You cannot specify multiple hashing algorithms.");
614 hashAlreadySpecified = true;
615 };
616 
617 KJ_IF_SOME(o, options) {
618 initOnlyIf(traceContext, js, putBuilder, o);
619 KJ_IF_SOME(m, o.customMetadata) {
620 auto fields = putBuilder.initCustomFields(m.fields.size());
621 for (size_t i = 0; i < m.fields.size(); i++) {
622 fields[i].setK(m.fields[i].name);
623 fields[i].setV(m.fields[i].value);
624 }
625 sentCustomMetadata = kj::mv(m);
626 traceContext.setTag("cloudflare.r2.request.custom_metadata"_kjc, true);
627 }
628 KJ_IF_SOME(m, o.httpMetadata) {
629 auto fields = putBuilder.initHttpFields();
630 sentHttpMetadata = [&]() {
631 KJ_SWITCH_ONEOF(m) {
632 KJ_CASE_ONEOF(m, HttpMetadata) {
633 return kj::mv(m);
634 }
635 KJ_CASE_ONEOF(h, jsg::Ref<Headers>) {
636 return HttpMetadata::fromRequestHeaders(js, *h);
637 }
638 }
639 KJ_UNREACHABLE;
640 }();
641 
642 KJ_IF_SOME(ct, sentHttpMetadata.contentType) {
643 fields.setContentType(ct);
644 traceContext.setTag("cloudflare.r2.request.http_metadata.content_type"_kjc, ct.asPtr());
645 }
646 KJ_IF_SOME(ce, sentHttpMetadata.contentEncoding) {
647 fields.setContentEncoding(ce);
648 traceContext.setTag(
649 "cloudflare.r2.request.http_metadata.content_encoding"_kjc, ce.asPtr());
650 }
651 KJ_IF_SOME(cd, sentHttpMetadata.contentDisposition) {
652 fields.setContentDisposition(cd);
653 traceContext.setTag(
654 "cloudflare.r2.request.http_metadata.content_disposition"_kjc, cd.asPtr());
655 }
656 KJ_IF_SOME(cl, sentHttpMetadata.contentLanguage) {
657 fields.setContentLanguage(cl);
658 traceContext.setTag(
659 "cloudflare.r2.request.http_metadata.content_language"_kjc, cl.asPtr());
660 }
661 KJ_IF_SOME(cc, sentHttpMetadata.cacheControl) {
662 fields.setCacheControl(cc);
663 traceContext.setTag("cloudflare.r2.request.http_metadata.cache_control"_kjc, cc.asPtr());
664 }
665 KJ_IF_SOME(ce, sentHttpMetadata.cacheExpiry) {
666 fields.setCacheExpiry((ce - kj::UNIX_EPOCH) / kj::MILLISECONDS);
667 traceContext.setTag("cloudflare.r2.request.http_metadata.cache_expiry"_kjc,
668 (ce - kj::UNIX_EPOCH) / kj::MILLISECONDS);
669 }
670 }
671 KJ_IF_SOME(md5, o.md5) {
672 verifyHashNotSpecified();
673 KJ_SWITCH_ONEOF(md5) {
674 KJ_CASE_ONEOF(binRef, jsg::JsRef<jsg::JsBufferSource>) {
675 auto bin = binRef.getHandle(js);
676 JSG_REQUIRE(bin.size() == 16, TypeError, "MD5 is 16 bytes, not ", bin.size());
677 putBuilder.setMd5(bin.asArrayPtr());
678 traceContext.setTag("cloudflare.r2.request.checksum.type"_kjc, "md5"_kjc);
679 traceContext.setTag(
680 "cloudflare.r2.request.checksum.value"_kjc, kj::encodeHex(bin.asArrayPtr()));
681 }
682 KJ_CASE_ONEOF(hex, jsg::NonCoercible<kj::String>) {
683 JSG_REQUIRE(hex.value.size() == 32, TypeError, "MD5 is 32 hex characters, not ",
684 hex.value.size());
685 const auto decoded = kj::decodeHex(hex.value);
686 JSG_REQUIRE(!decoded.hadErrors, TypeError, "Provided MD5 wasn't a valid hex string");
687 putBuilder.setMd5(decoded);
688 traceContext.setTag("cloudflare.r2.request.checksum.type"_kjc, "md5"_kjc);
689 traceContext.setTag("cloudflare.r2.request.checksum.value"_kjc, hex.value.asPtr());
690 }
691 }
692 }
693 KJ_IF_SOME(sha1, o.sha1) {
694 verifyHashNotSpecified();
695 KJ_SWITCH_ONEOF(sha1) {
696 KJ_CASE_ONEOF(binRef, jsg::JsRef<jsg::JsBufferSource>) {
697 auto bin = binRef.getHandle(js);
698 JSG_REQUIRE(bin.size() == 20, TypeError, "SHA-1 is 20 bytes, not ", bin.size());
699 putBuilder.setSha1(bin.asArrayPtr());
700 traceContext.setTag("cloudflare.r2.request.checksum.type"_kjc, "sha1"_kjc);
701 traceContext.setTag(
702 "cloudflare.r2.request.checksum.value"_kjc, kj::encodeHex(bin.asArrayPtr()));
703 }
704 KJ_CASE_ONEOF(hex, jsg::NonCoercible<kj::String>) {
705 JSG_REQUIRE(hex.value.size() == 40, TypeError, "SHA-1 is 40 hex characters, not ",
706 hex.value.size());
707 const auto decoded = kj::decodeHex(hex.value);
708 JSG_REQUIRE(!decoded.hadErrors, TypeError, "Provided SHA-1 wasn't a valid hex string");
709 putBuilder.setSha1(decoded);
710 traceContext.setTag("cloudflare.r2.request.checksum.type"_kjc, "sha1"_kjc);
711 traceContext.setTag("cloudflare.r2.request.checksum.value"_kjc, hex.value.asPtr());
712 }
713 }
714 }
715 KJ_IF_SOME(sha256, o.sha256) {
716 verifyHashNotSpecified();
717 KJ_SWITCH_ONEOF(sha256) {
718 KJ_CASE_ONEOF(binRef, jsg::JsRef<jsg::JsBufferSource>) {
719 auto bin = binRef.getHandle(js);
720 JSG_REQUIRE(bin.size() == 32, TypeError, "SHA-256 is 32 bytes, not ", bin.size());
721 putBuilder.setSha256(bin.asArrayPtr());
722 traceContext.setTag("cloudflare.r2.request.checksum.type"_kjc, "sha256"_kjc);
723 traceContext.setTag(
724 "cloudflare.r2.request.checksum.value"_kjc, kj::encodeHex(bin.asArrayPtr()));
725 }
726 KJ_CASE_ONEOF(hex, jsg::NonCoercible<kj::String>) {
727 JSG_REQUIRE(hex.value.size() == 64, TypeError, "SHA-256 is 64 hex characters, not ",
728 hex.value.size());
729 const auto decoded = kj::decodeHex(hex.value);
730 JSG_REQUIRE(
731 !decoded.hadErrors, TypeError, "Provided SHA-256 wasn't a valid hex string");
732 putBuilder.setSha256(decoded);
733 traceContext.setTag("cloudflare.r2.request.checksum.type"_kjc, "sha256"_kjc);
734 traceContext.setTag("cloudflare.r2.request.checksum.value"_kjc, hex.value.asPtr());
735 }
736 }
737 }
738 KJ_IF_SOME(sha384, o.sha384) {
739 verifyHashNotSpecified();
740 KJ_SWITCH_ONEOF(sha384) {
741 KJ_CASE_ONEOF(binRef, jsg::JsRef<jsg::JsBufferSource>) {
742 auto bin = binRef.getHandle(js);
743 JSG_REQUIRE(bin.size() == 48, TypeError, "SHA-384 is 48 bytes, not ", bin.size());
744 putBuilder.setSha384(bin.asArrayPtr());
745 traceContext.setTag("cloudflare.r2.request.checksum.type"_kjc, "sha384"_kjc);
746 traceContext.setTag(
747 "cloudflare.r2.request.checksum.value"_kjc, kj::encodeHex(bin.asArrayPtr()));
748 }
749 KJ_CASE_ONEOF(hex, jsg::NonCoercible<kj::String>) {
750 JSG_REQUIRE(hex.value.size() == 96, TypeError, "SHA-384 is 96 hex characters, not ",
751 hex.value.size());
752 const auto decoded = kj::decodeHex(hex.value);
753 JSG_REQUIRE(
754 !decoded.hadErrors, TypeError, "Provided SHA-384 wasn't a valid hex string");
755 putBuilder.setSha384(decoded);
756 traceContext.setTag("cloudflare.r2.request.checksum.type"_kjc, "sha384"_kjc);
757 traceContext.setTag("cloudflare.r2.request.checksum.value"_kjc, hex.value.asPtr());
758 }
759 }
760 }
761 KJ_IF_SOME(sha512, o.sha512) {
762 verifyHashNotSpecified();
763 KJ_SWITCH_ONEOF(sha512) {
764 KJ_CASE_ONEOF(binRef, jsg::JsRef<jsg::JsBufferSource>) {
765 auto bin = binRef.getHandle(js);
766 JSG_REQUIRE(bin.size() == 64, TypeError, "SHA-512 is 64 bytes, not ", bin.size());
767 putBuilder.setSha512(bin.asArrayPtr());
768 traceContext.setTag("cloudflare.r2.request.checksum.type"_kjc, "sha512"_kjc);
769 traceContext.setTag(
770 "cloudflare.r2.request.checksum.value"_kjc, kj::encodeHex(bin.asArrayPtr()));
771 }
772 KJ_CASE_ONEOF(hex, jsg::NonCoercible<kj::String>) {
773 JSG_REQUIRE(hex.value.size() == 128, TypeError, "SHA-512 is 128 hex characters, not ",
774 hex.value.size());
775 const auto decoded = kj::decodeHex(hex.value);
776 JSG_REQUIRE(
777 !decoded.hadErrors, TypeError, "Provided SHA-512 wasn't a valid hex string");
778 putBuilder.setSha512(decoded);
779 traceContext.setTag("cloudflare.r2.request.checksum.type"_kjc, "sha512"_kjc);
780 traceContext.setTag("cloudflare.r2.request.checksum.value"_kjc, hex.value.asPtr());
781 }
782 }
783 }
784 KJ_IF_SOME(s, o.storageClass) {
785 putBuilder.setStorageClass(s);
786 traceContext.setTag("cloudflare.r2.request.storage_class"_kjc, s.asPtr());
787 }
788 kj::Maybe<kj::String> maybeSsecKey = buildSsecKey(kj::mv(o.ssecKey));
789 KJ_IF_SOME(ssecKey, maybeSsecKey) {
790 auto ssecBuilder = putBuilder.initSsec();
791 ssecBuilder.setKey(ssecKey);
792 traceContext.setTag("cloudflare.r2.request.ssec_key"_kjc, true);
793 }
794 }
795 
796 KJ_IF_SOME(v, value) {
797 KJ_SWITCH_ONEOF(v) {
798 KJ_CASE_ONEOF(stream, jsg::Ref<ReadableStream>) {
799 KJ_IF_SOME(size, stream->tryGetLength(StreamEncoding::IDENTITY)) {
800 traceContext.setTag("cloudflare.r2.request.size"_kjc, static_cast<int64_t>(size));
801 }
802 }
803 KJ_CASE_ONEOF(text, jsg::NonCoercible<kj::String>) {
804 traceContext.setTag(
805 "cloudflare.r2.request.size"_kjc, static_cast<int64_t>(text.value.size()));
806 }
807 KJ_CASE_ONEOF(data, kj::Array<byte>) {
808 traceContext.setTag("cloudflare.r2.request.size"_kjc, static_cast<int64_t>(data.size()));
809 }
810 KJ_CASE_ONEOF(blob, jsg::Ref<Blob>) {
811 traceContext.setTag(
812 "cloudflare.r2.request.size"_kjc, static_cast<int64_t>(blob->getSize()));
813 }
814 }
815 }
816 
817 auto requestJson = json.encode(requestBuilder);
818 
819 cancelReader.cancel();
820 kj::StringPtr components[1];
821 auto path = fillR2Path(components, adminBucket);
822 auto client = context.getHttpClient(clientIndex, true, kj::none, traceContext);
823 auto promise =
824 doR2HTTPPutRequest(kj::mv(client), kj::mv(value), kj::none, kj::mv(requestJson), path, jwt);
825 
826 return context.awaitIo(js, kj::mv(promise),
827 [sentHttpMetadata = kj::mv(sentHttpMetadata),
828 sentCustomMetadata = kj::mv(sentCustomMetadata), &errorType,
829 traceContext = kj::mv(traceContext)](
830 jsg::Lock& js, R2Result r2Result) mutable -> kj::Maybe<jsg::Ref<HeadResult>> {
831 addR2ResponseSpanTags(traceContext, r2Result);
832 if (r2Result.preconditionFailed()) {
833 return kj::none;
834 } else {
835 auto result = parseObjectMetadata<HeadResult>(js, "put", r2Result, errorType);
836 KJ_IF_SOME(o, result) {
837 o.get()->httpMetadata = kj::mv(sentHttpMetadata);
838 o.get()->customMetadata = kj::mv(sentCustomMetadata);
839 addHeadResultSpanTags(js, traceContext, *o.get());
840 }
841 return result;
842 }
843 });
844 });
845}
846 
847jsg::Promise<jsg::Ref<R2MultipartUpload>> R2Bucket::createMultipartUpload(jsg::Lock& js,
848 kj::String key,
849 jsg::Optional<MultipartOptions> options,
850 const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType) {
851 return js.evalNow([&] {
852 auto& context = IoContext::current();
853 TraceContext traceContext = context.makeUserTraceSpan("r2_createMultipartUpload"_kjc);
854 
855 traceContext.setTag("cloudflare.binding.type"_kjc, "r2"_kjc);
856 KJ_IF_SOME(b, this->bindingName()) {
857 traceContext.setTag("cloudflare.binding.name"_kjc, b);
858 }
859 traceContext.setTag("cloudflare.r2.operation"_kjc, "CreateMultipartUpload"_kjc);
860 KJ_IF_SOME(b, this->bucketName()) {
861 traceContext.setTag("cloudflare.r2.bucket"_kjc, b);
862 }
863 traceContext.setTag("cloudflare.r2.request.key"_kjc, key.asPtr());
864 
865 capnp::JsonCodec json;
866 json.handleByAnnotation<R2BindingRequest>();
867 json.setHasMode(capnp::HasMode::NON_DEFAULT);
868 capnp::MallocMessageBuilder requestMessage;
869 
870 auto requestBuilder = requestMessage.initRoot<R2BindingRequest>();
871 requestBuilder.setVersion(VERSION_PUBLIC_BETA);
872 auto payloadBuilder = requestBuilder.initPayload();
873 auto createMultipartUploadBuilder = payloadBuilder.initCreateMultipartUpload();
874 createMultipartUploadBuilder.setObject(key);
875 
876 KJ_IF_SOME(o, options) {
877 KJ_IF_SOME(m, o.customMetadata) {
878 auto fields = createMultipartUploadBuilder.initCustomFields(m.fields.size());
879 for (size_t i = 0; i < m.fields.size(); i++) {
880 fields[i].setK(m.fields[i].name);
881 fields[i].setV(m.fields[i].value);
882 }
883 traceContext.setTag("cloudflare.r2.request.custom_metadata"_kjc, true);
884 }
885 KJ_IF_SOME(m, o.httpMetadata) {
886 auto fields = createMultipartUploadBuilder.initHttpFields();
887 HttpMetadata httpMetadata = [&]() {
888 KJ_SWITCH_ONEOF(m) {
889 KJ_CASE_ONEOF(m, HttpMetadata) {
890 return kj::mv(m);
891 }
892 KJ_CASE_ONEOF(h, jsg::Ref<Headers>) {
893 return HttpMetadata::fromRequestHeaders(js, *h);
894 }
895 }
896 KJ_UNREACHABLE;
897 }();
898 
899 KJ_IF_SOME(ct, httpMetadata.contentType) {
900 fields.setContentType(ct);
901 traceContext.setTag("cloudflare.r2.request.http_metadata.content_type"_kjc, ct.asPtr());
902 }
903 KJ_IF_SOME(ce, httpMetadata.contentEncoding) {
904 fields.setContentEncoding(ce);
905 traceContext.setTag(
906 "cloudflare.r2.request.http_metadata.content_encoding"_kjc, ce.asPtr());
907 }
908 KJ_IF_SOME(cd, httpMetadata.contentDisposition) {
909 fields.setContentDisposition(cd);
910 traceContext.setTag(
911 "cloudflare.r2.request.http_metadata.content_disposition"_kjc, cd.asPtr());
912 }
913 KJ_IF_SOME(cl, httpMetadata.contentLanguage) {
914 fields.setContentLanguage(cl);
915 traceContext.setTag(
916 "cloudflare.r2.request.http_metadata.content_language"_kjc, cl.asPtr());
917 }
918 KJ_IF_SOME(cc, httpMetadata.cacheControl) {
919 fields.setCacheControl(cc);
920 traceContext.setTag("cloudflare.r2.request.http_metadata.cache_control"_kjc, cc.asPtr());
921 }
922 KJ_IF_SOME(ce, httpMetadata.cacheExpiry) {
923 fields.setCacheExpiry((ce - kj::UNIX_EPOCH) / kj::MILLISECONDS);
924 traceContext.setTag("cloudflare.r2.request.http_metadata.cache_expiry"_kjc,
925 (ce - kj::UNIX_EPOCH) / kj::MILLISECONDS);
926 }
927 }
928 KJ_IF_SOME(s, o.storageClass) {
929 createMultipartUploadBuilder.setStorageClass(s);
930 traceContext.setTag("cloudflare.r2.request.storage_class"_kjc, s.asPtr());
931 }
932 kj::Maybe<kj::String> maybeSsecKey = buildSsecKey(kj::mv(o.ssecKey));
933 KJ_IF_SOME(ssecKey, maybeSsecKey) {
934 auto ssecBuilder = createMultipartUploadBuilder.initSsec();
935 ssecBuilder.setKey(ssecKey);
936 traceContext.setTag("cloudflare.r2.request.ssec_key"_kjc, true);
937 }
938 }
939 
940 auto requestJson = json.encode(requestBuilder);
941 kj::StringPtr components[1];
942 auto path = fillR2Path(components, adminBucket);
943 auto client = context.getHttpClient(clientIndex, true, kj::none, traceContext);
944 auto promise =
945 doR2HTTPPutRequest(kj::mv(client), kj::none, kj::none, kj::mv(requestJson), path, jwt);
946 
947 return context.awaitIo(js, kj::mv(promise),
948 [&errorType, key = kj::mv(key), this, traceContext = kj::mv(traceContext)](
949 jsg::Lock& js, R2Result r2Result) mutable {
950 addR2ResponseSpanTags(traceContext, r2Result);
951 r2Result.throwIfError("createMultipartUpload", errorType);
952 
953 capnp::MallocMessageBuilder responseMessage;
954 capnp::JsonCodec json;
955 json.handleByAnnotation<R2CreateMultipartUploadResponse>();
956 auto responseBuilder = responseMessage.initRoot<R2CreateMultipartUploadResponse>();
957 
958 json.decode(KJ_ASSERT_NONNULL(r2Result.metadataPayload), responseBuilder);
959 kj::StringPtr uploadId = responseBuilder.getUploadId();
960 traceContext.setTag("cloudflare.r2.response.upload_id"_kjc, uploadId);
961 return js.alloc<R2MultipartUpload>(kj::mv(key), kj::str(uploadId), JSG_THIS);
962 });
963 });
964}
965 
966jsg::Ref<R2MultipartUpload> R2Bucket::resumeMultipartUpload(jsg::Lock& js,
967 kj::String key,
968 kj::String uploadId,
969 const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType) {
970 return js.alloc<R2MultipartUpload>(kj::mv(key), kj::mv(uploadId), JSG_THIS);
971}
972 
973jsg::Promise<void> R2Bucket::delete_(jsg::Lock& js,
974 kj::OneOf<kj::String, kj::Array<kj::String>> keys,
975 const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType) {
976 return js.evalNow([&] {
977 auto& context = IoContext::current();
978 
979 TraceContext traceContext = context.makeUserTraceSpan("r2_delete"_kjc);
980 
981 traceContext.setTag("cloudflare.binding.type"_kjc, "r2"_kjc);
982 KJ_IF_SOME(b, this->bindingName()) {
983 traceContext.setTag("cloudflare.binding.name"_kjc, b);
984 }
985 traceContext.setTag("cloudflare.r2.operation"_kjc, "DeleteObject"_kjc);
986 KJ_IF_SOME(b, this->bucketName()) {
987 traceContext.setTag("cloudflare.r2.bucket"_kjc, b);
988 }
989 
990 capnp::JsonCodec json;
991 json.handleByAnnotation<R2BindingRequest>();
992 capnp::MallocMessageBuilder requestMessage;
993 
994 auto requestBuilder = requestMessage.initRoot<R2BindingRequest>();
995 requestBuilder.setVersion(VERSION_PUBLIC_BETA);
996 auto deleteBuilder = requestBuilder.initPayload().initDelete();
997 
998 KJ_SWITCH_ONEOF(keys) {
999 KJ_CASE_ONEOF(ks, kj::Array<kj::String>) {
1000 auto keys = deleteBuilder.initObjects(ks.size());
1001 for (unsigned int i = 0; i < ks.size(); i++) {
1002 keys.set(i, ks[i]);
1003 }
1004 traceContext.setTag("cloudflare.r2.request.keys"_kjc, kj::str(ks));
1005 }
1006 KJ_CASE_ONEOF(k, kj::String) {
1007 deleteBuilder.setObject(k);
1008 traceContext.setTag("cloudflare.r2.request.keys"_kjc, k.asPtr());
1009 }
1010 }
1011 
1012 auto requestJson = json.encode(requestBuilder);
1013 
1014 kj::StringPtr components[1];
1015 auto path = fillR2Path(components, adminBucket);
1016 auto client = context.getHttpClient(clientIndex, true, kj::none, traceContext);
1017 auto promise =
1018 doR2HTTPPutRequest(kj::mv(client), kj::none, kj::none, kj::mv(requestJson), path, jwt);
1019 
1020 return context.awaitIo(js, kj::mv(promise),
1021 [&errorType, traceContext = kj::mv(traceContext)](
1022 jsg::Lock& js, R2Result r2Result) mutable {
1023 addR2ResponseSpanTags(traceContext, r2Result);
1024 if (r2Result.objectNotFound()) {
1025 return;
1026 }
1027 
1028 r2Result.throwIfError("delete", errorType);
1029 });
1030 });
1031}
1032 
1033jsg::Promise<R2Bucket::ListResult> R2Bucket::list(jsg::Lock& js,
1034 jsg::Optional<ListOptions> options,
1035 const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType,
1036 CompatibilityFlags::Reader flags) {
1037 return js.evalNow([&] {
1038 auto& context = IoContext::current();
1039 TraceContext traceContext = context.makeUserTraceSpan("r2_list"_kjc);
1040 
1041 traceContext.setTag("cloudflare.binding.type"_kjc, "r2"_kjc);
1042 KJ_IF_SOME(b, this->bindingName()) {
1043 traceContext.setTag("cloudflare.binding.name"_kjc, b);
1044 }
1045 traceContext.setTag("cloudflare.r2.operation"_kjc, "ListObjects"_kjc);
1046 KJ_IF_SOME(b, this->bucketName()) {
1047 traceContext.setTag("cloudflare.r2.bucket"_kjc, b);
1048 }
1049 
1050 capnp::JsonCodec json;
1051 json.handleByAnnotation<R2BindingRequest>();
1052 json.setHasMode(capnp::HasMode::NON_DEFAULT);
1053 capnp::MallocMessageBuilder requestMessage;
1054 
1055 auto requestBuilder = requestMessage.initRoot<R2BindingRequest>();
1056 requestBuilder.setVersion(VERSION_PUBLIC_BETA);
1057 auto listBuilder = requestBuilder.initPayload().initList();
1058 
1059 kj::Vector<OptionalMetadata> expectedOptionalFields(2);
1060 
1061 KJ_IF_SOME(o, options) {
1062 KJ_IF_SOME(l, o.limit) {
1063 listBuilder.setLimit(l);
1064 traceContext.setTag("cloudflare.r2.request.limit"_kjc, static_cast<int64_t>(l));
1065 }
1066 KJ_IF_SOME(p, o.prefix) {
1067 listBuilder.setPrefix(p.value);
1068 traceContext.setTag("cloudflare.r2.request.prefix"_kjc, p.value.asPtr());
1069 }
1070 KJ_IF_SOME(c, o.cursor) {
1071 listBuilder.setCursor(c.value);
1072 traceContext.setTag("cloudflare.r2.request.cursor"_kjc, c.value.asPtr());
1073 }
1074 KJ_IF_SOME(d, o.delimiter) {
1075 listBuilder.setDelimiter(d.value);
1076 traceContext.setTag("cloudflare.r2.request.delimiter"_kjc, d.value.asPtr());
1077 }
1078 KJ_IF_SOME(d, o.startAfter) {
1079 listBuilder.setStartAfter(d.value);
1080 traceContext.setTag("cloudflare.r2.request.start_after"_kjc, d.value.asPtr());
1081 }
1082 KJ_IF_SOME(i, o.include) {
1083 using Field = jsg::Dict<uint16_t>::Field;
1084 static const std::array<Field, 2> fields = {
1085 Field{
1086 .name = kj::str("httpMetadata"),
1087 .value = static_cast<uint16_t>(R2ListRequest::IncludeField::HTTP),
1088 },
1089 Field{
1090 .name = kj::str("customMetadata"),
1091 .value = static_cast<uint16_t>(R2ListRequest::IncludeField::CUSTOM),
1092 },
1093 };
1094 
1095 expectedOptionalFields.clear();
1096 
1097 for (const auto& reqField: i) {
1098 if (reqField.value == "httpMetadata") {
1099 traceContext.setTag("cloudflare.r2.request.include.http_metadata"_kjc, true);
1100 } else if (reqField.value == "customMetadata") {
1101 traceContext.setTag("cloudflare.r2.request.include.custom_metadata"_kjc, true);
1102 }
1103 }
1104 
1105 listBuilder.setInclude(KJ_MAP(reqField, i) {
1106 for (const auto& field: fields) {
1107 if (field.name == reqField.value) {
1108 expectedOptionalFields.add(static_cast<OptionalMetadata>(field.value));
1109 return field.value;
1110 }
1111 }
1112 
1113 JSG_FAIL_REQUIRE(RangeError, "Unsupported include value ", reqField.value);
1114 });
1115 } else if (featureFlags.listHonorsIncludes) {
1116 listBuilder.initInclude(0);
1117 }
1118 }
1119 
1120 // TODO(soon): Remove this after the release for 2022-07-04 is cut (from here & R2 worker).
1121 // This just tells the R2 worker that it can honor the `includes` field without breaking back
1122 // compat. If we just started spontaneously honoring the `includes` field then existing Workers
1123 // might suddenly lose http metadata because they weren't explicitly asking for it even though
1124 listBuilder.setNewRuntime(true);
1125 
1126 // TODO(later): Add a sentry message (+ console warning) to check if we have users that aren't
1127 // asking for any optional metadata but are asking it in the result anyway just so that we can
1128 // kill all the compat flag logic.
1129 if (!featureFlags.listHonorsIncludes) {
1130 // Unconditionally send this so that when running against an R2 instance that does honor these
1131 // we do the right back-compat behavior.
1132 auto includes = listBuilder.initInclude(2);
1133 includes.set(0, static_cast<uint16_t>(R2ListRequest::IncludeField::HTTP));
1134 includes.set(1, static_cast<uint16_t>(R2ListRequest::IncludeField::CUSTOM));
1135 expectedOptionalFields.clear();
1136 expectedOptionalFields.add(OptionalMetadata::Http);
1137 expectedOptionalFields.add(OptionalMetadata::Custom);
1138 }
1139 
1140 auto requestJson = json.encode(requestBuilder);
1141 
1142 kj::StringPtr components[1];
1143 auto path = fillR2Path(components, adminBucket);
1144 auto client = context.getHttpClient(clientIndex, true, kj::none, traceContext);
1145 auto promise = doR2HTTPGetRequest(kj::mv(client), kj::mv(requestJson), path, jwt, flags);
1146 
1147 return context.awaitIo(js, kj::mv(promise),
1148 [expectedOptionalFields = expectedOptionalFields.releaseAsArray(), &errorType,
1149 traceContext = kj::mv(traceContext)](jsg::Lock& js, R2Result r2Result) mutable {
1150 addR2ResponseSpanTags(traceContext, r2Result);
1151 r2Result.throwIfError("list", errorType);
1152 
1153 R2Bucket::ListResult result;
1154 capnp::MallocMessageBuilder responseMessage;
1155 capnp::JsonCodec json;
1156 json.handleByAnnotation<R2ListResponse>();
1157 auto responseBuilder = responseMessage.initRoot<R2ListResponse>();
1158 
1159 json.decode(KJ_ASSERT_NONNULL(r2Result.metadataPayload), responseBuilder);
1160 
1161 result.objects = KJ_MAP(o, responseBuilder.getObjects()) {
1162 return parseObjectMetadata<HeadResult>(js, o, expectedOptionalFields);
1163 };
1164 result.truncated = responseBuilder.getTruncated();
1165 if (responseBuilder.hasCursor()) {
1166 result.cursor = kj::str(responseBuilder.getCursor());
1167 traceContext.setTag(
1168 "cloudflare.r2.response.cursor"_kjc, KJ_ASSERT_NONNULL(result.cursor).asPtr());
1169 }
1170 if (responseBuilder.hasDelimitedPrefixes()) {
1171 result.delimitedPrefixes =
1172 KJ_MAP(e, responseBuilder.getDelimitedPrefixes()) { return kj::str(e); };
1173 }
1174 
1175 traceContext.setTag("cloudflare.r2.response.returned_objects"_kjc,
1176 static_cast<int64_t>(result.objects.size()));
1177 traceContext.setTag("cloudflare.r2.response.delimited_prefixes"_kjc,
1178 static_cast<int64_t>(result.delimitedPrefixes.size()));
1179 traceContext.setTag("cloudflare.r2.response.truncated"_kjc, result.truncated);
1180 return kj::mv(result);
1181 });
1182 });
1183}
1184 
1185namespace {
1186 
1187kj::Array<R2Bucket::Etag> parseConditionalEtagHeader(kj::StringPtr condHeader) {
1188 // Iterative parser for conditional etag headers (If-Match / If-None-Match).
1189 // Dispatches on the first non-whitespace character each iteration rather than
1190 // scanning the full remaining string for multiple characters.
1191 kj::Vector<R2Bucket::Etag> etagAccumulator;
1192 bool leadingCommaRequired = false;
1193 for (;;) {
1194 // Strip optional whitespace (OWS) per RFC 7230 §7 list rule.
1195 while (condHeader.size() > 0 && (condHeader[0] == ' ' || condHeader[0] == '\t')) {
1196 condHeader = condHeader.slice(1);
1197 }
1198 if (condHeader.size() == 0) {
1199 return etagAccumulator.releaseAsArray();
1200 }
1201 switch (condHeader[0]) {
1202 case ',': {
1203 // Skip separator commas (including empty list elements per RFC 7230 §7).
1204 condHeader = condHeader.slice(1);
1205 leadingCommaRequired = false;
1206 continue;
1207 }
1208 case '*': {
1209 if (leadingCommaRequired) {
1210 JSG_FAIL_REQUIRE(Error,
1211 "Comma was expected to separate etags. "
1212 "Encountered a wildcard character '*' instead.");
1213 }
1214 // Wildcard overrides all other etags.
1215 etagAccumulator.clear();
1216 etagAccumulator.add(R2Bucket::WildcardEtag{});
1217 return etagAccumulator.releaseAsArray();
1218 }
1219 case 'W': {
1220 if (leadingCommaRequired) {
1221 JSG_FAIL_REQUIRE(Error,
1222 "Comma was expected to separate etags. "
1223 "Encountered a weak quotation character 'W' instead. "
1224 "This would otherwise indicate the start of a new weak etag.");
1225 }
1226 JSG_REQUIRE(condHeader.size() > 2 && condHeader[1] == '/' && condHeader[2] == '"', Error,
1227 "Weak etags must start with W/ and their value must be quoted");
1228 condHeader = condHeader.slice(3); // skip W/"
1229 auto closingQuote = condHeader.findFirst('"');
1230 auto& cq = JSG_REQUIRE_NONNULL(closingQuote, Error, "Unclosed double quote for Etag");
1231 etagAccumulator.add(R2Bucket::WeakEtag{kj::str(condHeader.slice(0, cq))});
1232 condHeader = condHeader.slice(cq + 1);
1233 leadingCommaRequired = true;
1234 continue;
1235 }
1236 case '"': {
1237 if (leadingCommaRequired) {
1238 JSG_FAIL_REQUIRE(Error,
1239 "Comma was expected to separate etags. "
1240 "Encountered a double quote character '\"' instead. "
1241 "This would otherwise indicate the start of a new strong etag.");
1242 }
1243 condHeader = condHeader.slice(1); // skip opening "
1244 auto closingQuote = condHeader.findFirst('"');
1245 auto& cq = JSG_REQUIRE_NONNULL(closingQuote, Error, "Unclosed double quote for Etag");
1246 etagAccumulator.add(R2Bucket::StrongEtag{kj::str(condHeader.slice(0, cq))});
1247 condHeader = condHeader.slice(cq + 1);
1248 leadingCommaRequired = true;
1249 continue;
1250 }
1251 default: {
1252 // No valid etag token starts with this character; stop parsing.
1253 return etagAccumulator.releaseAsArray();
1254 }
1255 }
1256 }
1257 KJ_UNREACHABLE;
1258}
1259 
1260kj::Array<R2Bucket::Etag> buildSingleEtagArray(kj::StringPtr etagValue) {
1261 return etagValue == "*"
1262 ? kj::arr<R2Bucket::Etag>(R2Bucket::WildcardEtag{})
1263 : kj::arr<R2Bucket::Etag>(R2Bucket::StrongEtag{.value = kj::str(etagValue)});
1264}
1265 
1266} // namespace
1267 
1268R2Bucket::UnwrappedConditional::UnwrappedConditional(jsg::Lock& js, Headers& h)
1269 : secondsGranularity(true) {
1270 KJ_IF_SOME(e, h.getCommon(js, capnp::CommonHeaderName::IF_MATCH)) {
1271 etagMatches = parseConditionalEtagHeader(e);
1272 KJ_IF_SOME(arr, etagMatches) {
1273 JSG_REQUIRE(arr.size() > 0, Error, "Invalid ETag in if-match header");
1274 }
1275 }
1276 KJ_IF_SOME(e, h.getCommon(js, capnp::CommonHeaderName::IF_NONE_MATCH)) {
1277 etagDoesNotMatch = parseConditionalEtagHeader(e);
1278 KJ_IF_SOME(arr, etagDoesNotMatch) {
1279 JSG_REQUIRE(arr.size() > 0, Error, "Invalid ETag in if-none-match header");
1280 }
1281 }
1282 KJ_IF_SOME(d, h.getCommon(js, capnp::CommonHeaderName::IF_MODIFIED_SINCE)) {
1283 uploadedAfter = parseDate(js, d);
1284 }
1285 KJ_IF_SOME(d, h.getCommon(js, capnp::CommonHeaderName::IF_UNMODIFIED_SINCE)) {
1286 uploadedBefore = parseDate(js, d);
1287 }
1288}
1289 
1290R2Bucket::UnwrappedConditional::UnwrappedConditional(const Conditional& c)
1291 : secondsGranularity(c.secondsGranularity.orDefault(false)) {
1292 KJ_IF_SOME(e, c.etagMatches) {
1293 JSG_REQUIRE(!isQuotedEtag(e.value), TypeError,
1294 "Conditional ETag should not be wrapped in quotes (", e.value, ").");
1295 etagMatches = buildSingleEtagArray(e.value);
1296 }
1297 KJ_IF_SOME(e, c.etagDoesNotMatch) {
1298 JSG_REQUIRE(!isQuotedEtag(e.value), TypeError,
1299 "Conditional ETag should not be wrapped in quotes (", e.value, ").");
1300 etagDoesNotMatch = buildSingleEtagArray(e.value);
1301 }
1302 KJ_IF_SOME(d, c.uploadedAfter) {
1303 uploadedAfter = d;
1304 }
1305 KJ_IF_SOME(d, c.uploadedBefore) {
1306 uploadedBefore = d;
1307 }
1308}
1309 
1310R2Bucket::HttpMetadata R2Bucket::HttpMetadata::fromRequestHeaders(jsg::Lock& js, Headers& h) {
1311 HttpMetadata result;
1312 KJ_IF_SOME(ct, h.getCommon(js, capnp::CommonHeaderName::CONTENT_TYPE)) {
1313 result.contentType = kj::mv(ct);
1314 }
1315 KJ_IF_SOME(ce, h.getCommon(js, capnp::CommonHeaderName::CONTENT_ENCODING)) {
1316 result.contentEncoding = kj::mv(ce);
1317 }
1318 KJ_IF_SOME(cd, h.getCommon(js, capnp::CommonHeaderName::CONTENT_DISPOSITION)) {
1319 result.contentDisposition = kj::mv(cd);
1320 }
1321 KJ_IF_SOME(cl, h.getCommon(js, capnp::CommonHeaderName::CONTENT_LANGUAGE)) {
1322 result.contentLanguage = kj::mv(cl);
1323 }
1324 KJ_IF_SOME(cc, h.getCommon(js, capnp::CommonHeaderName::CACHE_CONTROL)) {
1325 result.cacheControl = kj::mv(cc);
1326 }
1327 KJ_IF_SOME(ceStr, h.getCommon(js, capnp::CommonHeaderName::EXPIRES)) {
1328 result.cacheExpiry = parseDate(js, ceStr);
1329 }
1330 
1331 return result;
1332}
1333 
1334R2Bucket::HttpMetadata R2Bucket::HttpMetadata::clone() const {
1335 return {
1336 .contentType = mapCopyString(contentType),
1337 .contentLanguage = mapCopyString(contentLanguage),
1338 .contentDisposition = mapCopyString(contentDisposition),
1339 .contentEncoding = mapCopyString(contentEncoding),
1340 .cacheControl = mapCopyString(cacheControl),
1341 .cacheExpiry = cacheExpiry,
1342 };
1343}
1344 
1345void R2Bucket::HeadResult::writeHttpMetadata(jsg::Lock& js, Headers& headers) {
1346 JSG_REQUIRE(httpMetadata != kj::none, TypeError, "HTTP metadata unknown for key `", name,
1347 "`. Did you forget to add 'httpMetadata' to `include` when listing?");
1348 const auto& m = KJ_REQUIRE_NONNULL(httpMetadata);
1349 
1350 KJ_IF_SOME(ct, m.contentType) {
1351 headers.setCommon(capnp::CommonHeaderName::CONTENT_TYPE, kj::str(ct));
1352 }
1353 KJ_IF_SOME(cl, m.contentLanguage) {
1354 headers.setCommon(capnp::CommonHeaderName::CONTENT_LANGUAGE, kj::str(cl));
1355 }
1356 KJ_IF_SOME(cd, m.contentDisposition) {
1357 headers.setCommon(capnp::CommonHeaderName::CONTENT_DISPOSITION, kj::str(cd));
1358 }
1359 KJ_IF_SOME(ce, m.contentEncoding) {
1360 headers.setCommon(capnp::CommonHeaderName::CONTENT_ENCODING, kj::str(ce));
1361 }
1362 KJ_IF_SOME(cc, m.cacheControl) {
1363 headers.setCommon(capnp::CommonHeaderName::CACHE_CONTROL, kj::str(cc));
1364 }
1365 KJ_IF_SOME(ce, m.cacheExpiry) {
1366 headers.setCommon(capnp::CommonHeaderName::EXPIRES, toUTCString(js, ce));
1367 }
1368}
1369 
1370jsg::Promise<jsg::BufferSource> R2Bucket::GetResult::arrayBuffer(jsg::Lock& js) {
1371 return js.evalNow([&] {
1372 JSG_REQUIRE(!body->isDisturbed(), TypeError,
1373 "Body has already been used. "
1374 "It can only be used once. Use tee() first if you need to read it twice.");
1375 
1376 auto& context = IoContext::current();
1377 return body->getController().readAllBytes(js, context.getLimitEnforcer().getBufferingLimit());
1378 });
1379}
1380 
1381jsg::Promise<jsg::BufferSource> R2Bucket::GetResult::bytes(jsg::Lock& js) {
1382 return js.evalNow([&] {
1383 JSG_REQUIRE(!body->isDisturbed(), TypeError,
1384 "Body has already been used. "
1385 "It can only be used once. Use tee() first if you need to read it twice.");
1386 
1387 auto& context = IoContext::current();
1388 return body->getController()
1389 .readAllBytes(js, context.getLimitEnforcer().getBufferingLimit())
1390 .then(js, [](jsg::Lock& js, jsg::BufferSource data) {
1391 return data.getTypedView<v8::Uint8Array>(js);
1392 });
1393 });
1394}
1395 
1396jsg::Promise<kj::String> R2Bucket::GetResult::text(jsg::Lock& js) {
1397 // Copy-pasted from http.c++
1398 return js.evalNow([&] {
1399 JSG_REQUIRE(!body->isDisturbed(), TypeError,
1400 "Body has already been used. "
1401 "It can only be used once. Use tee() first if you need to read it twice.");
1402 
1403 auto& context = IoContext::current();
1404 // A common mistake is to call .text() on non-text content, e.g. because you're implementing a
1405 // search-and-replace across your whole site and you forgot that it'll apply to images too.
1406 // When running in the inspector, let's warn the developer if they do this.
1407 if (context.isInspectorEnabled()) {
1408 // httpMetadata can't be null because GetResult always populates it.
1409 KJ_IF_SOME(type, KJ_REQUIRE_NONNULL(httpMetadata).contentType) {
1410 maybeWarnIfNotText(js, type);
1411 }
1412 }
1413 
1414 return body->getController().readAllText(js, context.getLimitEnforcer().getBufferingLimit());
1415 });
1416}
1417 
1418jsg::Promise<jsg::Value> R2Bucket::GetResult::json(jsg::Lock& js) {
1419 // Copy-pasted from http.c++
1420 return text(js).then(js, [](jsg::Lock& js, kj::String text) { return js.parseJson(text); });
1421}
1422 
1423jsg::Promise<jsg::Ref<Blob>> R2Bucket::GetResult::blob(jsg::Lock& js) {
1424 // Copy-pasted from http.c++
1425 return arrayBuffer(js).then(js, [this](jsg::Lock& js, jsg::BufferSource buffer) {
1426 // httpMetadata can't be null because GetResult always populates it.
1427 kj::String contentType =
1428 mapCopyString(KJ_REQUIRE_NONNULL(httpMetadata).contentType).orDefault(nullptr);
1429 return js.alloc<Blob>(js, buffer.getJsHandle(js), kj::mv(contentType));
1430 });
1431}
1432 
1433R2Bucket::StringChecksums R2Bucket::Checksums::toJSON() {
1434 return {
1435 .md5 = this->md5.map(kj::encodeHex),
1436 .sha1 = this->sha1.map(kj::encodeHex),
1437 .sha256 = this->sha256.map(kj::encodeHex),
1438 .sha384 = this->sha384.map(kj::encodeHex),
1439 .sha512 = this->sha512.map(kj::encodeHex),
1440 };
1441}
1442 
1443namespace {
1444jsg::Optional<jsg::JsArrayBuffer> copyHash(
1445 jsg::Lock& js, const jsg::Optional<kj::Array<kj::byte>>& maybeHash) {
1446 return maybeHash.map(
1447 [&](const kj::Array<kj::byte>& hash) { return jsg::JsArrayBuffer::create(js, hash); });
1448}
1449} // namespace
1450 
1451jsg::Optional<jsg::JsArrayBuffer> R2Bucket::Checksums::getMd5(jsg::Lock& js) {
1452 return copyHash(js, md5);
1453}
1454jsg::Optional<jsg::JsArrayBuffer> R2Bucket::Checksums::getSha1(jsg::Lock& js) {
1455 return copyHash(js, sha1);
1456}
1457jsg::Optional<jsg::JsArrayBuffer> R2Bucket::Checksums::getSha256(jsg::Lock& js) {
1458 return copyHash(js, sha256);
1459}
1460jsg::Optional<jsg::JsArrayBuffer> R2Bucket::Checksums::getSha384(jsg::Lock& js) {
1461 return copyHash(js, sha384);
1462}
1463jsg::Optional<jsg::JsArrayBuffer> R2Bucket::Checksums::getSha512(jsg::Lock& js) {
1464 return copyHash(js, sha512);
1465}
1466 
1467kj::Maybe<jsg::Ref<R2Bucket::HeadResult>> parseHeadResultWrapper(jsg::Lock& js,
1468 kj::StringPtr action,
1469 R2Result& r2Result,
1470 const jsg::TypeHandler<jsg::Ref<R2Error>>& errorType) {
1471 return parseObjectMetadata<R2Bucket::HeadResult>(js, action, r2Result, errorType);
1472}
1473 
1474kj::ArrayPtr<kj::StringPtr> fillR2Path(
1475 kj::StringPtr pathStorage[1], const kj::Maybe<kj::String>& bucket) {
1476 int numComponents = 0;
1477 
1478 KJ_IF_SOME(b, bucket) {
1479 pathStorage[numComponents++] = b;
1480 }
1481 
1482 return kj::arrayPtr(pathStorage, numComponents);
1483}
1484 
1485} // namespace workerd::api::public_beta