Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions _examples/r2-image-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strings"

"github.com/syumai/workers"
"github.com/syumai/workers/cloudflare"
"github.com/syumai/workers/cloudflare/r2"
)

// bucketName is R2 bucket name defined in wrangler.toml.
Expand All @@ -23,8 +23,8 @@ func handleErr(w http.ResponseWriter, msg string, err error) {

type server struct{}

func (s *server) bucket() (*cloudflare.R2Bucket, error) {
return cloudflare.NewR2Bucket(bucketName)
func (s *server) bucket() (*r2.Bucket, error) {
return r2.NewBucket(bucketName)
}

func (s *server) post(w http.ResponseWriter, req *http.Request, key string) {
Expand All @@ -45,8 +45,8 @@ func (s *server) post(w http.ResponseWriter, req *http.Request, key string) {
return
}
}
_, err = bucket.Put(key, req.Body, &cloudflare.R2PutOptions{
HTTPMetadata: cloudflare.R2HTTPMetadata{
_, err = bucket.Put(key, req.Body, &r2.PutOptions{
HTTPMetadata: r2.HTTPMetadata{
ContentType: req.Header.Get("Content-Type"),
},
CustomMetadata: map[string]string{"custom-key": "custom-value"},
Expand Down
4 changes: 2 additions & 2 deletions _examples/r2-image-viewer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strings"

"github.com/syumai/workers"
"github.com/syumai/workers/cloudflare"
"github.com/syumai/workers/cloudflare/r2"
)

// bucketName is R2 bucket name defined in wrangler.toml.
Expand All @@ -23,7 +23,7 @@ func handleErr(w http.ResponseWriter, msg string, err error) {
// This example is based on implementation in syumai/workers-playground
// - https://github.com/syumai/workers-playground/blob/e32881648ccc055e3690a0d9c750a834261c333e/r2-image-viewer/src/index.ts#L30
func handler(w http.ResponseWriter, req *http.Request) {
bucket, err := cloudflare.NewR2Bucket(bucketName)
bucket, err := r2.NewBucket(bucketName)
if err != nil {
handleErr(w, "failed to get R2Bucket\n", err)
return
Expand Down
31 changes: 31 additions & 0 deletions cloudflare/r2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cloudflare

import (
"github.com/syumai/workers/cloudflare/r2"
)

// R2Bucket represents interface of Cloudflare Worker's R2 Bucket instance.
// Deprecated: use r2.Bucket instead.
type R2Bucket = r2.Bucket

// NewR2Bucket returns R2Bucket for given variable name.
// Deprecated: use r2.NewBucket instead.
func NewR2Bucket(varName string) (*R2Bucket, error) {
return r2.NewBucket(varName)
}

// R2PutOptions represents Cloudflare R2 put options.
// Deprecated: use r2.PutOptions instead.
type R2PutOptions = r2.PutOptions

// R2Object represents Cloudflare R2 object.
// Deprecated: use r2.Object instead.
type R2Object = r2.Object

// R2HTTPMetadata represents metadata of R2Object.
// Deprecated: use r2.HTTPMetadata instead.
type R2HTTPMetadata = r2.HTTPMetadata

// R2Objects represents Cloudflare R2 objects.
// Deprecated: use r2.Objects instead.
type R2Objects = r2.Objects
135 changes: 135 additions & 0 deletions cloudflare/r2/bucket.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package r2

import (
"fmt"
"io"
"syscall/js"

"github.com/syumai/workers/cloudflare/internal/cfruntimecontext"
"github.com/syumai/workers/internal/jsutil"
)

// Bucket represents interface of Cloudflare Worker's R2 Bucket instance.
// - https://developers.cloudflare.com/r2/runtime-apis/#bucket-method-definitions
// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1006
type Bucket struct {
instance js.Value
}

// NewBucket returns Bucket for given variable name.
// - variable name must be defined in wrangler.toml.
// - see example: https://github.com/syumai/workers/tree/main/_examples/r2-image-viewer
// - if the given variable name doesn't exist on runtime context, returns error.
// - This function panics when a runtime context is not found.
func NewBucket(varName string) (*Bucket, error) {
inst := cfruntimecontext.MustGetRuntimeContextEnv().Get(varName)
if inst.IsUndefined() {
return nil, fmt.Errorf("%s is undefined", varName)
}
return &Bucket{instance: inst}, nil
}

// Head returns the result of `head` call to Bucket.
// - Body field of *Object is always nil for Head call.
// - if the object for given key doesn't exist, returns nil.
// - if a network error happens, returns error.
func (r *Bucket) Head(key string) (*Object, error) {
p := r.instance.Call("head", key)
v, err := jsutil.AwaitPromise(p)
if err != nil {
return nil, err
}
if v.IsNull() {
return nil, nil
}
return toObject(v)
}

// Get returns the result of `get` call to Bucket.
// - if the object for given key doesn't exist, returns nil.
// - if a network error happens, returns error.
func (r *Bucket) Get(key string) (*Object, error) {
p := r.instance.Call("get", key)
v, err := jsutil.AwaitPromise(p)
if err != nil {
return nil, err
}
if v.IsNull() {
return nil, nil
}
return toObject(v)
}

// PutOptions represents Cloudflare R2 put options.
// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1128
type PutOptions struct {
HTTPMetadata HTTPMetadata
CustomMetadata map[string]string
MD5 string
}

func (opts *PutOptions) toJS() js.Value {
if opts == nil {
return js.Undefined()
}
obj := jsutil.NewObject()
if opts.HTTPMetadata != (HTTPMetadata{}) {
obj.Set("httpMetadata", opts.HTTPMetadata.toJS())
}
if opts.CustomMetadata != nil {
// convert map[string]string to map[string]any.
// This makes the map convertible to JS.
// see: https://pkg.go.dev/syscall/js#ValueOf
customMeta := make(map[string]any, len(opts.CustomMetadata))
for k, v := range opts.CustomMetadata {
customMeta[k] = v
}
obj.Set("customMetadata", customMeta)
}
if opts.MD5 != "" {
obj.Set("md5", opts.MD5)
}
return obj
}

// Put returns the result of `put` call to Bucket.
// - This method copies all bytes into memory for implementation restriction.
// - Body field of *Object is always nil for Put call.
// - if a network error happens, returns error.
func (r *Bucket) Put(key string, value io.ReadCloser, opts *PutOptions) (*Object, error) {
// fetch body cannot be ReadableStream. see: https://github.com/whatwg/fetch/issues/1438
b, err := io.ReadAll(value)
if err != nil {
return nil, err
}
defer value.Close()
ua := jsutil.NewUint8Array(len(b))
js.CopyBytesToJS(ua, b)
p := r.instance.Call("put", key, ua.Get("buffer"), opts.toJS())
v, err := jsutil.AwaitPromise(p)
if err != nil {
return nil, err
}
return toObject(v)
}

// Delete returns the result of `delete` call to Bucket.
// - if a network error happens, returns error.
func (r *Bucket) Delete(key string) error {
p := r.instance.Call("delete", key)
if _, err := jsutil.AwaitPromise(p); err != nil {
return err
}
return nil
}

// List returns the result of `list` call to Bucket.
// - if a network error happens, returns error.
func (r *Bucket) List() (*Objects, error) {
p := r.instance.Call("list")
v, err := jsutil.AwaitPromise(p)
if err != nil {
return nil, err
}
return toObjects(v)
}
120 changes: 120 additions & 0 deletions cloudflare/r2/object.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package r2

import (
"errors"
"fmt"
"io"
"syscall/js"
"time"

"github.com/syumai/workers/internal/jsutil"
)

// Object represents Cloudflare R2 object.
// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1094
type Object struct {
instance js.Value
Key string
Version string
Size int
ETag string
HTTPETag string
Uploaded time.Time
HTTPMetadata HTTPMetadata
CustomMetadata map[string]string
// Body is a body of Object.
// This value is nil for the result of the `Head` or `Put` method.
Body io.Reader
}

// TODO: implement
// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1106
// func (o *Object) WriteHTTPMetadata(headers http.Header) {
// }

func (o *Object) BodyUsed() (bool, error) {
v := o.instance.Get("bodyUsed")
if v.IsUndefined() {
return false, errors.New("bodyUsed doesn't exist for this Object")
}
return v.Bool(), nil
}

// toObject converts JavaScript side's Object to *Object.
// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1094
func toObject(v js.Value) (*Object, error) {
uploaded, err := jsutil.DateToTime(v.Get("uploaded"))
if err != nil {
return nil, fmt.Errorf("error converting uploaded: %w", err)
}
r2Meta, err := toHTTPMetadata(v.Get("httpMetadata"))
if err != nil {
return nil, fmt.Errorf("error converting httpMetadata: %w", err)
}
bodyVal := v.Get("body")
var body io.Reader
if !bodyVal.IsUndefined() {
body = jsutil.ConvertReadableStreamToReadCloser(v.Get("body"))
}
return &Object{
instance: v,
Key: v.Get("key").String(),
Version: v.Get("version").String(),
Size: v.Get("size").Int(),
ETag: v.Get("etag").String(),
HTTPETag: v.Get("httpEtag").String(),
Uploaded: uploaded,
HTTPMetadata: r2Meta,
CustomMetadata: jsutil.StrRecordToMap(v.Get("customMetadata")),
Body: body,
}, nil
}

// HTTPMetadata represents metadata of Object.
// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1053
type HTTPMetadata struct {
ContentType string
ContentLanguage string
ContentDisposition string
ContentEncoding string
CacheControl string
CacheExpiry time.Time
}

func toHTTPMetadata(v js.Value) (HTTPMetadata, error) {
if v.IsUndefined() || v.IsNull() {
return HTTPMetadata{}, nil
}
cacheExpiry, err := jsutil.MaybeDate(v.Get("cacheExpiry"))
if err != nil {
return HTTPMetadata{}, fmt.Errorf("error converting cacheExpiry: %w", err)
}
return HTTPMetadata{
ContentType: jsutil.MaybeString(v.Get("contentType")),
ContentLanguage: jsutil.MaybeString(v.Get("contentLanguage")),
ContentDisposition: jsutil.MaybeString(v.Get("contentDisposition")),
ContentEncoding: jsutil.MaybeString(v.Get("contentEncoding")),
CacheControl: jsutil.MaybeString(v.Get("cacheControl")),
CacheExpiry: cacheExpiry,
}, nil
}

func (md *HTTPMetadata) toJS() js.Value {
obj := jsutil.NewObject()
kv := map[string]string{
"contentType": md.ContentType,
"contentLanguage": md.ContentLanguage,
"contentDisposition": md.ContentDisposition,
"contentEncoding": md.ContentEncoding,
"cacheControl": md.CacheControl,
}
for k, v := range kv {
if v != "" {
obj.Set(k, v)
}
}
if !md.CacheExpiry.IsZero() {
obj.Set("cacheExpiry", jsutil.TimeToDate(md.CacheExpiry))
}
return obj
}
44 changes: 44 additions & 0 deletions cloudflare/r2/objects.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package r2

import (
"fmt"
"syscall/js"

"github.com/syumai/workers/internal/jsutil"
)

// Objects represents Cloudflare R2 objects.
// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1121
type Objects struct {
Objects []*Object
Truncated bool
// Cursor indicates next cursor of Objects.
// - This becomes empty string if cursor doesn't exist.
Cursor string
DelimitedPrefixes []string
}

// toObjects converts JavaScript side's Objects to *Objects.
// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1121
func toObjects(v js.Value) (*Objects, error) {
objectsVal := v.Get("objects")
objects := make([]*Object, objectsVal.Length())
for i := 0; i < len(objects); i++ {
obj, err := toObject(objectsVal.Index(i))
if err != nil {
return nil, fmt.Errorf("error converting to Object: %w", err)
}
objects[i] = obj
}
prefixesVal := v.Get("delimitedPrefixes")
prefixes := make([]string, prefixesVal.Length())
for i := 0; i < len(prefixes); i++ {
prefixes[i] = prefixesVal.Index(i).String()
}
return &Objects{
Objects: objects,
Truncated: v.Get("truncated").Bool(),
Cursor: jsutil.MaybeString(v.Get("cursor")),
DelimitedPrefixes: prefixes,
}, nil
}
Loading
Loading