From a95ca1de256076e6dc185b079b9fb45a5a1a5244 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 29 May 2022 09:17:07 +0900 Subject: [PATCH] implement R2Object / R2Objects --- examples/r2-image-server/go.mod | 9 +---- examples/r2-image-server/go.sum | 4 -- examples/r2-image-server/jsutil.go | 45 ++++++++++++++++++++- examples/r2-image-server/main.go | 40 +++++++++++++++++-- examples/r2-image-server/r2bucket.go | 55 ++++++++++++++++++-------- examples/r2-image-server/r2object.go | 26 ++++++++++-- examples/r2-image-server/r2objects.go | 37 ++++++++++++++++- examples/r2-image-server/wrangler.toml | 7 +++- 8 files changed, 184 insertions(+), 39 deletions(-) diff --git a/examples/r2-image-server/go.mod b/examples/r2-image-server/go.mod index ffea532..9926cb2 100644 --- a/examples/r2-image-server/go.mod +++ b/examples/r2-image-server/go.mod @@ -1,12 +1,7 @@ -module github.com/syumai/workers/examples/simple-json-server +module github.com/syumai/workers/examples/r2-image-server go 1.18 -require ( - github.com/mailru/easyjson v0.7.7 - github.com/syumai/workers v0.0.0-00010101000000-000000000000 -) +require github.com/syumai/workers v0.0.0-00010101000000-000000000000 replace github.com/syumai/workers => ../../ - -require github.com/josharian/intern v1.0.0 // indirect diff --git a/examples/r2-image-server/go.sum b/examples/r2-image-server/go.sum index 7707cb6..e69de29 100644 --- a/examples/r2-image-server/go.sum +++ b/examples/r2-image-server/go.sum @@ -1,4 +0,0 @@ -github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= diff --git a/examples/r2-image-server/jsutil.go b/examples/r2-image-server/jsutil.go index ac55c71..5edeb77 100644 --- a/examples/r2-image-server/jsutil.go +++ b/examples/r2-image-server/jsutil.go @@ -8,10 +8,51 @@ import ( ) var ( - global = js.Global() - objectClass = global.Get("Object") + global = js.Global() + objectClass = global.Get("Object") + promiseClass = global.Get("Promise") + uint8ArrayClass = global.Get("Uint8Array") + errorClass = global.Get("Error") + readableStreamClass = global.Get("ReadableStream") ) +func newObject() js.Value { + return objectClass.New() +} + +func newUint8Array(size int) js.Value { + return uint8ArrayClass.New(size) +} + +func newPromise(fn js.Func) js.Value { + return promiseClass.New(fn) +} + +func awaitPromise(promiseVal js.Value) (js.Value, error) { + resultCh := make(chan js.Value) + errCh := make(chan error) + var then, catch js.Func + then = js.FuncOf(func(_ js.Value, args []js.Value) any { + defer then.Release() + result := args[0] + resultCh <- result + return js.Undefined() + }) + catch = js.FuncOf(func(_ js.Value, args []js.Value) any { + defer catch.Release() + result := args[0] + errCh <- fmt.Errorf("failed on promise: %s", result.Call("toString").String()) + return js.Undefined() + }) + promiseVal.Call("then", then).Call("catch", catch) + select { + case result := <-resultCh: + return result, nil + case err := <-errCh: + return js.Value{}, err + } +} + // dateToTime converts JavaScript side's Data object into time.Time. func dateToTime(v js.Value) (time.Time, error) { milliStr := v.Call("getTime").Call("toString").String() diff --git a/examples/r2-image-server/main.go b/examples/r2-image-server/main.go index a69c015..f03f942 100644 --- a/examples/r2-image-server/main.go +++ b/examples/r2-image-server/main.go @@ -1,13 +1,45 @@ package main import ( + "fmt" + "io" "net/http" "github.com/syumai/workers" - "github.com/syumai/workers/examples/simple-json-server/app" ) -func main() { - http.HandleFunc("/hello", app.HelloHandler) - workers.Serve(nil) // use http.DefaultServeMux +// bucketName is R2 bucket name defined in wrangler.toml. +const bucketName = "BUCKET" + +func handleErr(w http.ResponseWriter, msg string) { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(msg)) +} + +// 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 := NewR2Bucket(bucketName) + if err != nil { + handleErr(w, "failed to get R2Bucket\n") + return + } + imgPath := req.URL.Path + imgObj, err := bucket.Get(imgPath) + if err != nil { + handleErr(w, "failed to get R2Object\n") + return + } + w.Header().Set("Cache-Control", "public, max-age=14400") + w.Header().Set("ETag", fmt.Sprintf("W/%s", imgObj.HTTPETag)) + contentType := "application/octet-stream" + if imgObj.HTTPMetadata.ContentType != nil { + contentType = *imgObj.HTTPMetadata.ContentType + } + w.Header().Set("Content-Type", contentType) + io.Copy(w, imgObj.Body) +} + +func main() { + workers.Serve(http.HandlerFunc(handler)) } diff --git a/examples/r2-image-server/r2bucket.go b/examples/r2-image-server/r2bucket.go index 6f46400..171f522 100644 --- a/examples/r2-image-server/r2bucket.go +++ b/examples/r2-image-server/r2bucket.go @@ -10,11 +10,11 @@ import ( // - https://developers.cloudflare.com/r2/runtime-apis/#bucket-method-definitions // - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1006 type R2Bucket interface { - Head(key string) - Get(key string) *R2Object - Put(key string, value io.Reader) - Delete(key string) - List() []*R2Object + Head(key string) (*R2Object, error) + Get(key string) (*R2Object, error) + Put(key string, value io.Reader) error + Delete(key string) error + List() (*R2Objects, error) } type r2Bucket struct { @@ -23,24 +23,45 @@ type r2Bucket struct { var _ R2Bucket = &r2Bucket{} -func (r *r2Bucket) Head(key string) { +func (r *r2Bucket) Head(key string) (*R2Object, error) { + p := r.instance.Call("head", key) + v, err := awaitPromise(p) + if err != nil { + return nil, err + } + if v.IsNull() { + return nil, nil + } + return toR2Object(v) +} + +func (r *r2Bucket) Get(key string) (*R2Object, error) { + p := r.instance.Call("get", key) + v, err := awaitPromise(p) + if err != nil { + return nil, err + } + if v.IsNull() { + return nil, nil + } + return toR2Object(v) +} + +func (r *r2Bucket) Put(key string, value io.Reader) error { panic("implement me") } -func (r *r2Bucket) Get(key string) *R2Object { - return nil -} - -func (r *r2Bucket) Put(key string, value io.Reader) { +func (r *r2Bucket) Delete(key string) error { panic("implement me") } -func (r *r2Bucket) Delete(key string) { - panic("implement me") -} - -func (r *r2Bucket) List() []*R2Object { - panic("implement me") +func (r *r2Bucket) List() (*R2Objects, error) { + p := r.instance.Call("list") + v, err := awaitPromise(p) + if err != nil { + return nil, err + } + return toR2Objects(v) } func NewR2Bucket(varName string) (R2Bucket, error) { diff --git a/examples/r2-image-server/r2object.go b/examples/r2-image-server/r2object.go index 730c894..26e97b1 100644 --- a/examples/r2-image-server/r2object.go +++ b/examples/r2-image-server/r2object.go @@ -1,13 +1,17 @@ package main import ( + "errors" "fmt" "io" "syscall/js" "time" ) +// R2Object represents JavaScript side's R2Object. +// * https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1094 type R2Object struct { + instance js.Value Key string Version string Size int @@ -16,7 +20,9 @@ type R2Object struct { Uploaded time.Time HTTPMetadata R2HTTPMetadata CustomMetadata map[string]string - Body io.Reader + // Body is a body of R2Object. + // This value becomes nil when `Head` method of R2Bucket is called. + Body io.Reader } // TODO: implement @@ -24,7 +30,15 @@ type R2Object struct { // func (o *R2Object) WriteHTTPMetadata(headers http.Header) { // } -// toR2Object converts JavaScript side's R2Object to R2Object. +func (o *R2Object) BodyUsed() (bool, error) { + v := o.instance.Get("bodyUsed") + if v.IsUndefined() { + return false, errors.New("bodyUsed doesn't exist for this R2Object") + } + return v.Bool(), nil +} + +// toR2Object converts JavaScript side's R2Object to *R2Object. // * https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1094 func toR2Object(v js.Value) (*R2Object, error) { uploaded, err := dateToTime(v.Get("uploaded")) @@ -35,7 +49,13 @@ func toR2Object(v js.Value) (*R2Object, error) { if err != nil { return nil, fmt.Errorf("error converting httpMetadata: %w", err) } + bodyVal := v.Get("body") + var body io.Reader + if !bodyVal.IsUndefined() { + body = convertStreamReaderToReader(v.Get("body").Call("getReader")) + } return &R2Object{ + instance: v, Key: v.Get("key").String(), Version: v.Get("version").String(), Size: v.Get("size").Int(), @@ -44,7 +64,7 @@ func toR2Object(v js.Value) (*R2Object, error) { Uploaded: uploaded, HTTPMetadata: r2Meta, CustomMetadata: strRecordToMap(v.Get("customMetadata")), - Body: convertStreamReaderToReader(v.Get("body").Call("getReader")), + Body: body, }, nil } diff --git a/examples/r2-image-server/r2objects.go b/examples/r2-image-server/r2objects.go index b573837..f6bac6b 100644 --- a/examples/r2-image-server/r2objects.go +++ b/examples/r2-image-server/r2objects.go @@ -1,5 +1,40 @@ package main +import ( + "fmt" + "syscall/js" +) + +// R2Objects represents JavaScript side's R2Objects. +// * https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1121 type R2Objects struct { - Objects []*R2Object + Objects []*R2Object + Truncated bool + Cursor *string + DelimitedPrefixes []string +} + +// toR2Objects converts JavaScript side's R2Objects to *R2Objects. +// * https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1121 +func toR2Objects(v js.Value) (*R2Objects, error) { + objectsVal := v.Get("objects") + objects := make([]*R2Object, objectsVal.Length()) + for i := 0; i < len(objects); i++ { + obj, err := toR2Object(objectsVal.Index(i)) + if err != nil { + return nil, fmt.Errorf("error converting to R2Object: %w", err) + } + objects[i] = obj + } + prefixesVal := objectsVal.Get("delimitedPrefixes") + prefixes := make([]string, prefixesVal.Length()) + for i := 0; i < len(prefixes); i++ { + prefixes[i] = prefixesVal.Index(i).String() + } + return &R2Objects{ + Objects: objects, + Truncated: objectsVal.Get("truncated").Bool(), + Cursor: maybeString(objectsVal.Get("cursor")), + DelimitedPrefixes: prefixes, + }, nil } diff --git a/examples/r2-image-server/wrangler.toml b/examples/r2-image-server/wrangler.toml index 61638e6..b4ab471 100644 --- a/examples/r2-image-server/wrangler.toml +++ b/examples/r2-image-server/wrangler.toml @@ -1,4 +1,4 @@ -name = "simple-json-server" +name = "r2-image-server" main = "./worker.mjs" compatibility_date = "2022-05-13" compatibility_flags = [ @@ -7,3 +7,8 @@ compatibility_flags = [ [build] command = "make build" + +[[r2_buckets]] +binding = 'BUCKET' +bucket_name = 'r2-image-viewer' +preview_bucket_name = 'r2-image-viewer-dev' \ No newline at end of file