// Copyright (c) 2017-2022 Cloudflare, Inc. // Licensed under the Apache 2.0 license found in the LICENSE file or at: // https://opensource.org/licenses/Apache-2.0 #pragma once #include "r2-rpc.h" #include #include namespace workerd::api { class Headers; } namespace workerd::api::public_beta { struct StringTagParams { kj::LiteralStringConst key; kj::StringPtr value; }; struct R2UserTracing { kj::LiteralStringConst op; StringTagParams method; // Passing Maybe instead of Maybe here – this avoids a branch on // the caller side when bucket is already a Maybe, which is more convenient. kj::Maybe bucket; kj::Maybe extraTag; }; // Helper for creating R2 HTTP Client with the right span tags across operations. This is much // cleaner than setting span tags directly in each function. kj::Own r2GetClient(IoContext& context, uint subrequestChannel, R2UserTracing user); kj::ArrayPtr fillR2Path( kj::StringPtr pathStorage[1], const kj::Maybe& bucket); class R2MultipartUpload; // A capability to an R2 Bucket. class R2Bucket: public jsg::Object { protected: struct friend_tag_t {}; struct FeatureFlags { FeatureFlags(CompatibilityFlags::Reader featureFlags); bool listHonorsIncludes; }; public: // `clientIndex` is what to pass to IoContext::getHttpClient() to get an HttpClient // representing this namespace. explicit R2Bucket(CompatibilityFlags::Reader featureFlags, uint clientIndex) : featureFlags(featureFlags), clientIndex(clientIndex) {} explicit R2Bucket(FeatureFlags featureFlags, uint clientIndex, kj::String bucket, friend_tag_t) : featureFlags(featureFlags), clientIndex(clientIndex), adminBucket(kj::mv(bucket)) {} explicit R2Bucket( FeatureFlags featureFlags, uint clientIndex, kj::String bucket, kj::String binding) : featureFlags(featureFlags), clientIndex(clientIndex), bucket(kj::mv(bucket)), binding(kj::mv(binding)) {} explicit R2Bucket( FeatureFlags featureFlags, uint clientIndex, kj::String bucket, kj::String jwt, friend_tag_t) : featureFlags(featureFlags), clientIndex(clientIndex), adminBucket(kj::mv(bucket)), jwt(kj::mv(jwt)) {} struct Range { jsg::Optional offset; jsg::Optional length; jsg::Optional suffix; JSG_STRUCT(offset, length, suffix); JSG_STRUCT_TS_OVERRIDE(type R2Range = | { offset: number; length?: number } | { offset?: number; length: number } | { suffix: number } ); }; struct Conditional { jsg::Optional> etagMatches; jsg::Optional> etagDoesNotMatch; jsg::Optional uploadedBefore; jsg::Optional uploadedAfter; jsg::Optional secondsGranularity; JSG_STRUCT(etagMatches, etagDoesNotMatch, uploadedBefore, uploadedAfter, secondsGranularity); JSG_STRUCT_TS_OVERRIDE(R2Conditional); }; struct GetOptions { jsg::Optional>> onlyIf; jsg::Optional>> range; jsg::Optional, kj::String>> ssecKey; JSG_STRUCT(onlyIf, range, ssecKey); JSG_STRUCT_TS_OVERRIDE(R2GetOptions); }; struct StringChecksums { jsg::Optional md5; jsg::Optional sha1; jsg::Optional sha256; jsg::Optional sha384; jsg::Optional sha512; JSG_STRUCT(md5, sha1, sha256, sha384, sha512); JSG_STRUCT_TS_OVERRIDE(R2StringChecksums); }; class Checksums: public jsg::Object { public: Checksums(jsg::Optional> md5, jsg::Optional> sha1, jsg::Optional> sha256, jsg::Optional> sha384, jsg::Optional> sha512) : md5(kj::mv(md5)), sha1(kj::mv(sha1)), sha256(kj::mv(sha256)), sha384(kj::mv(sha384)), sha512(kj::mv(sha512)) {} jsg::Optional getMd5(jsg::Lock& js); jsg::Optional getSha1(jsg::Lock& js); jsg::Optional getSha256(jsg::Lock& js); jsg::Optional getSha384(jsg::Lock& js); jsg::Optional getSha512(jsg::Lock& js); StringChecksums toJSON(); JSG_RESOURCE_TYPE(Checksums) { JSG_LAZY_READONLY_INSTANCE_PROPERTY(md5, getMd5); JSG_LAZY_READONLY_INSTANCE_PROPERTY(sha1, getSha1); JSG_LAZY_READONLY_INSTANCE_PROPERTY(sha256, getSha256); JSG_LAZY_READONLY_INSTANCE_PROPERTY(sha384, getSha384); JSG_LAZY_READONLY_INSTANCE_PROPERTY(sha512, getSha512); JSG_METHOD(toJSON); JSG_TS_OVERRIDE(R2Checksums { readonly md5?: ArrayBuffer; readonly sha1?: ArrayBuffer; readonly sha256?: ArrayBuffer; readonly sha384?: ArrayBuffer; readonly sha512?: ArrayBuffer; }); } jsg::Optional> md5; jsg::Optional> sha1; jsg::Optional> sha256; jsg::Optional> sha384; jsg::Optional> sha512; void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { tracker.trackField("md5", md5); tracker.trackField("sha1", sha1); tracker.trackField("sha256", sha256); tracker.trackField("sha384", sha384); tracker.trackField("sha512", sha512); } }; struct HttpMetadata { static HttpMetadata fromRequestHeaders(jsg::Lock& js, Headers& h); jsg::Optional contentType; jsg::Optional contentLanguage; jsg::Optional contentDisposition; jsg::Optional contentEncoding; jsg::Optional cacheControl; jsg::Optional cacheExpiry; JSG_STRUCT(contentType, contentLanguage, contentDisposition, contentEncoding, cacheControl, cacheExpiry); JSG_STRUCT_TS_OVERRIDE(R2HTTPMetadata); HttpMetadata clone() const; JSG_MEMORY_INFO(HttpMetadata) { tracker.trackField("contentType", contentType); tracker.trackField("contentLanguage", contentLanguage); tracker.trackField("contentDisposition", contentDisposition); tracker.trackField("contentEncoding", contentEncoding); tracker.trackField("cacheControl", cacheControl); } }; struct PutOptions { jsg::Optional>> onlyIf; jsg::Optional>> httpMetadata; jsg::Optional> customMetadata; jsg::Optional, jsg::NonCoercible>> md5; jsg::Optional, jsg::NonCoercible>> sha1; jsg::Optional, jsg::NonCoercible>> sha256; jsg::Optional, jsg::NonCoercible>> sha384; jsg::Optional, jsg::NonCoercible>> sha512; jsg::Optional storageClass; jsg::Optional, kj::String>> ssecKey; JSG_STRUCT(onlyIf, httpMetadata, customMetadata, md5, sha1, sha256, sha384, sha512, storageClass, ssecKey); JSG_STRUCT_TS_OVERRIDE(R2PutOptions); }; struct MultipartOptions { jsg::Optional>> httpMetadata; jsg::Optional> customMetadata; jsg::Optional storageClass; jsg::Optional, kj::String>> ssecKey; JSG_STRUCT(httpMetadata, customMetadata, storageClass, ssecKey); JSG_STRUCT_TS_OVERRIDE(R2MultipartOptions); }; class HeadResult: public jsg::Object { public: HeadResult(kj::String name, kj::String version, double size, kj::String etag, jsg::Ref checksums, kj::Date uploaded, jsg::Optional httpMetadata, jsg::Optional> customMetadata, jsg::Optional range, kj::String storageClass, jsg::Optional ssecKeyMd5) : name(kj::mv(name)), version(kj::mv(version)), size(size), etag(kj::mv(etag)), checksums(kj::mv(checksums)), uploaded(uploaded), httpMetadata(kj::mv(httpMetadata)), customMetadata(kj::mv(customMetadata)), range(kj::mv(range)), storageClass(kj::mv(storageClass)), ssecKeyMd5(kj::mv(ssecKeyMd5)) {} kj::StringPtr getName() const { return name; } kj::StringPtr getVersion() const { return version; } double getSize() const { return size; } kj::StringPtr getEtag() const { return etag; } kj::String getHttpEtag() const { return kj::str('"', etag, '"'); } jsg::Ref getChecksums() { return checksums.addRef(); } kj::Date getUploaded() const { return uploaded; } kj::StringPtr getStorageClass() const { return storageClass; } jsg::Optional getSSECKeyMd5() const { return ssecKeyMd5; } jsg::Optional getHttpMetadata() const { return httpMetadata.map([](const HttpMetadata& m) { return m.clone(); }); } const jsg::Optional> getCustomMetadata() const { return customMetadata.map([](const jsg::Dict& m) { return jsg::Dict{ .fields = KJ_MAP(f, m.fields) { return jsg::Dict::Field{.name = kj::str(f.name), .value = kj::str(f.value)}; }, }; }); } jsg::Optional getRange() { return range; } void writeHttpMetadata(jsg::Lock& js, Headers& headers); JSG_RESOURCE_TYPE(HeadResult) { JSG_LAZY_READONLY_INSTANCE_PROPERTY(key, getName); JSG_LAZY_READONLY_INSTANCE_PROPERTY(version, getVersion); JSG_LAZY_READONLY_INSTANCE_PROPERTY(size, getSize); JSG_LAZY_READONLY_INSTANCE_PROPERTY(etag, getEtag); JSG_LAZY_READONLY_INSTANCE_PROPERTY(httpEtag, getHttpEtag); JSG_LAZY_READONLY_INSTANCE_PROPERTY(checksums, getChecksums); JSG_LAZY_READONLY_INSTANCE_PROPERTY(uploaded, getUploaded); JSG_LAZY_READONLY_INSTANCE_PROPERTY(httpMetadata, getHttpMetadata); JSG_LAZY_READONLY_INSTANCE_PROPERTY(customMetadata, getCustomMetadata); JSG_LAZY_READONLY_INSTANCE_PROPERTY(range, getRange); JSG_LAZY_READONLY_INSTANCE_PROPERTY(storageClass, getStorageClass); JSG_LAZY_READONLY_INSTANCE_PROPERTY(ssecKeyMd5, getSSECKeyMd5); JSG_METHOD(writeHttpMetadata); JSG_TS_OVERRIDE(R2Object); } void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { tracker.trackField("name", name); tracker.trackField("version", version); tracker.trackField("etag", etag); tracker.trackField("checksums", checksums); tracker.trackField("httpMetadata", httpMetadata); tracker.trackField("customMetadata", customMetadata); tracker.trackField("ssecKeyMd5", ssecKeyMd5); } protected: kj::String name; kj::String version; double size; kj::String etag; jsg::Ref checksums; kj::Date uploaded; jsg::Optional httpMetadata; jsg::Optional> customMetadata; jsg::Optional range; kj::String storageClass; jsg::Optional ssecKeyMd5; friend class R2Bucket; }; class GetResult: public HeadResult { public: GetResult(kj::String name, kj::String version, double size, kj::String etag, jsg::Ref checksums, kj::Date uploaded, jsg::Optional httpMetadata, jsg::Optional> customMetadata, jsg::Optional range, kj::String storageClass, jsg::Optional ssecKeyMd5, jsg::Ref body) : HeadResult(kj::mv(name), kj::mv(version), size, kj::mv(etag), kj::mv(checksums), uploaded, kj::mv(KJ_ASSERT_NONNULL(httpMetadata)), kj::mv(KJ_ASSERT_NONNULL(customMetadata)), range, kj::mv(storageClass), kj::mv(ssecKeyMd5)), body(kj::mv(body)) {} jsg::Ref getBody() { return body.addRef(); } bool getBodyUsed() { return body->isDisturbed(); } jsg::Promise arrayBuffer(jsg::Lock& js); jsg::Promise bytes(jsg::Lock& js); jsg::Promise text(jsg::Lock& js); jsg::Promise json(jsg::Lock& js); jsg::Promise> blob(jsg::Lock& js); JSG_RESOURCE_TYPE(GetResult) { JSG_INHERIT(HeadResult); JSG_READONLY_PROTOTYPE_PROPERTY(body, getBody); JSG_READONLY_PROTOTYPE_PROPERTY(bodyUsed, getBodyUsed); JSG_METHOD(arrayBuffer); JSG_METHOD(bytes); JSG_METHOD(text); JSG_METHOD(json); JSG_METHOD(blob); JSG_TS_OVERRIDE(R2ObjectBody { json(): Promise; bytes(): Promise; arrayBuffer(): Promise; }); } void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { tracker.trackField("body", body); } private: jsg::Ref body; }; struct ListResult { kj::Array> objects; bool truncated; jsg::Optional cursor; kj::Array delimitedPrefixes; JSG_STRUCT(objects, truncated, cursor, delimitedPrefixes); JSG_STRUCT_TS_OVERRIDE(type R2Objects = { objects: R2Object[]; delimitedPrefixes: string[]; } & ( | { truncated: true; cursor: string } | { truncated: false } )); }; struct ListOptions { jsg::Optional limit; jsg::Optional> prefix; jsg::Optional> cursor; jsg::Optional> delimiter; jsg::Optional> startAfter; jsg::Optional>> include; JSG_STRUCT(limit, prefix, cursor, delimiter, startAfter, include); JSG_STRUCT_TS_OVERRIDE(type R2ListOptions = never); // Delete the auto-generated ListOptions definition, we instead define it // with R2Bucket so we can access compatibility flags. Note, even though // we're deleting the definition, all definitions will still be renamed // from `R2BucketListOptions` to `R2ListOptions`. }; jsg::Promise>> head(jsg::Lock& js, kj::String key, const jsg::TypeHandler>& errorType, CompatibilityFlags::Reader flags); jsg::Promise>, jsg::Ref>> get(jsg::Lock& js, kj::String key, jsg::Optional options, const jsg::TypeHandler>& errorType, CompatibilityFlags::Reader flags); jsg::Promise>> put(jsg::Lock& js, kj::String key, kj::Maybe value, jsg::Optional options, const jsg::TypeHandler>& errorType); jsg::Promise> createMultipartUpload(jsg::Lock& js, kj::String key, jsg::Optional options, const jsg::TypeHandler>& errorType); jsg::Ref resumeMultipartUpload(jsg::Lock& js, kj::String key, kj::String uploadId, const jsg::TypeHandler>& errorType); jsg::Promise delete_(jsg::Lock& js, kj::OneOf> keys, const jsg::TypeHandler>& errorType); jsg::Promise list(jsg::Lock& js, jsg::Optional options, const jsg::TypeHandler>& errorType, CompatibilityFlags::Reader flags); JSG_RESOURCE_TYPE(R2Bucket, CompatibilityFlags::Reader flags) { JSG_METHOD(head); JSG_METHOD(get); JSG_METHOD(put); JSG_METHOD(createMultipartUpload); JSG_METHOD(resumeMultipartUpload); JSG_METHOD_NAMED(delete, delete_); JSG_METHOD(list); JSG_TS_ROOT(); JSG_TS_OVERRIDE({ // The order of these matters, since typescript tries to match function signatures in order get(key: string, options: R2GetOptions & { onlyIf: R2BucketConditional | Headers }): Promise; get(key: string, options?: R2GetOptions): Promise; put( key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions & { onlyIf: R2BucketConditional | Headers } ): Promise; put(key: string, value: ReadableStream | ArrayBuffer | ArrayBufferView | string | null | Blob, options?: R2PutOptions): Promise; }); // Exclude `R2Object` from `get` return type if `onlyIf` not specified, and exclude `null` from `put` return type // Rather than using the auto-generated R2ListOptions definition, we define // it here so we can access compatibility flags from JSG_RESOURCE_TYPE. if (flags.getR2ListHonorIncludeFields()) { JSG_TS_DEFINE(interface R2ListOptions { limit?: number; prefix?: string; cursor?: string; delimiter?: string; startAfter?: string; include?: ("httpMetadata" | "customMetadata")[]; }); } else { JSG_TS_DEFINE(interface R2ListOptions { limit?: number; prefix?: string; cursor?: string; delimiter?: string; startAfter?: string; }); // Omit `include` field if compatibility flag disabled as ignored } } struct WildcardEtag {}; struct WeakEtag { kj::String value; }; struct StrongEtag { kj::String value; }; using Etag = kj::OneOf; struct UnwrappedConditional { UnwrappedConditional(jsg::Lock& js, Headers& h); UnwrappedConditional(const Conditional& c); kj::Maybe> etagMatches; kj::Maybe> etagDoesNotMatch; kj::Maybe uploadedBefore; kj::Maybe uploadedAfter; bool secondsGranularity = false; }; void visitForMemoryInfo(jsg::MemoryTracker& tracker) const { tracker.trackField("adminBucket", adminBucket); tracker.trackField("jwt", jwt); } protected: kj::Maybe adminBucketName() const { return adminBucket; } kj::Maybe bucketName() const { return bucket; } kj::Maybe bindingName() const { return binding; } private: FeatureFlags featureFlags; uint clientIndex; kj::Maybe adminBucket; kj::Maybe bucket; kj::Maybe binding; kj::Maybe jwt; friend class R2MultipartUpload; }; // Non-generic wrapper avoid moving the parseObjectMetadata implementation into this header file // by making use of dynamic dispatch. kj::Maybe> parseHeadResultWrapper(jsg::Lock& js, kj::StringPtr action, R2Result& r2Result, const jsg::TypeHandler>& errorType); void addHeadResultSpanTags(jsg::Lock& js, TraceContext& traceContext, R2Bucket::HeadResult& result); } // namespace workerd::api::public_beta