From 3981556bb3b95e416fef0fe7d14dd4d7232f41c6 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 22 May 2022 22:46:08 +0900 Subject: [PATCH 1/4] WIP - add R2 image server example --- examples/r2-image-server/Makefile | 12 +++ examples/r2-image-server/README.md | 52 +++++++++ examples/r2-image-server/go.mod | 12 +++ examples/r2-image-server/go.sum | 4 + examples/r2-image-server/jsutil.go | 58 ++++++++++ examples/r2-image-server/main.go | 13 +++ examples/r2-image-server/r2bucket.go | 52 +++++++++ examples/r2-image-server/r2object.go | 75 +++++++++++++ examples/r2-image-server/r2objects.go | 5 + examples/r2-image-server/stream.go | 140 +++++++++++++++++++++++++ examples/r2-image-server/worker.mjs | 17 +++ examples/r2-image-server/wrangler.toml | 9 ++ 12 files changed, 449 insertions(+) create mode 100644 examples/r2-image-server/Makefile create mode 100644 examples/r2-image-server/README.md create mode 100644 examples/r2-image-server/go.mod create mode 100644 examples/r2-image-server/go.sum create mode 100644 examples/r2-image-server/jsutil.go create mode 100644 examples/r2-image-server/main.go create mode 100644 examples/r2-image-server/r2bucket.go create mode 100644 examples/r2-image-server/r2object.go create mode 100644 examples/r2-image-server/r2objects.go create mode 100644 examples/r2-image-server/stream.go create mode 100644 examples/r2-image-server/worker.mjs create mode 100644 examples/r2-image-server/wrangler.toml diff --git a/examples/r2-image-server/Makefile b/examples/r2-image-server/Makefile new file mode 100644 index 0000000..c9d7430 --- /dev/null +++ b/examples/r2-image-server/Makefile @@ -0,0 +1,12 @@ +.PHONY: dev +dev: + wrangler dev + +.PHONY: build +build: + mkdir -p dist + tinygo build -o ./dist/app.wasm -target wasm ./main.go + +.PHONY: publish +publish: + wrangler publish diff --git a/examples/r2-image-server/README.md b/examples/r2-image-server/README.md new file mode 100644 index 0000000..9bd46df --- /dev/null +++ b/examples/r2-image-server/README.md @@ -0,0 +1,52 @@ +# simple-json-server + +* A simple HTTP JSON server implemented in Go and compiled with tinygo. + +## Example + +* https://simple-json-server.syumai.workers.dev + +### Request + +``` +curl --location --request POST 'https://simple-json-server.syumai.workers.dev/hello' \ +--header 'Content-Type: application/json' \ +--data-raw '{ + "name": "syumai" +}' +``` + +### Response + +```json +{ + "message": "Hello, syumai!" +} +``` + +## Development + +### Requirements + +This project requires these tools to be installed globally. + +* wrangler +* tinygo +* [easyjson](https://github.com/mailru/easyjson) + - `go install github.com/mailru/easyjson/...@latest` + +### Commands + +``` +make dev # run dev server +make build # build Go Wasm binary +make publish # publish worker +``` + +## Author + +syumai + +## License + +MIT diff --git a/examples/r2-image-server/go.mod b/examples/r2-image-server/go.mod new file mode 100644 index 0000000..ffea532 --- /dev/null +++ b/examples/r2-image-server/go.mod @@ -0,0 +1,12 @@ +module github.com/syumai/workers/examples/simple-json-server + +go 1.18 + +require ( + github.com/mailru/easyjson v0.7.7 + 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 new file mode 100644 index 0000000..7707cb6 --- /dev/null +++ b/examples/r2-image-server/go.sum @@ -0,0 +1,4 @@ +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 new file mode 100644 index 0000000..ac55c71 --- /dev/null +++ b/examples/r2-image-server/jsutil.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "strconv" + "syscall/js" + "time" +) + +var ( + global = js.Global() + objectClass = global.Get("Object") +) + +// 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() + milli, err := strconv.ParseInt(milliStr, 10, 64) + if err != nil { + return time.Time{}, fmt.Errorf("failed to convert Date to time.Time: %w", err) + } + return time.UnixMilli(milli), nil +} + +// strRecordToMap converts JavaScript side's Record into map[string]string. +func strRecordToMap(v js.Value) map[string]string { + entries := objectClass.Call("entries", v) + entriesLen := entries.Get("length").Int() + result := make(map[string]string, entriesLen) + for i := 0; i < entriesLen; i++ { + entry := entries.Index(i) + key := entry.Index(0).String() + value := entry.Index(1).String() + result[key] = value + } + return result +} + +// maybeString returns string value of given JavaScript value or returns nil if the value is undefined. +func maybeString(v js.Value) *string { + if v.IsUndefined() { + return nil + } + s := v.String() + return &s +} + +// maybeDate returns time.Time value of given JavaScript Date value or returns nil if the value is undefined. +func maybeDate(v js.Value) (*time.Time, error) { + if v.IsUndefined() { + return nil, nil + } + d, err := dateToTime(v) + if err != nil { + return nil, err + } + return &d, nil +} diff --git a/examples/r2-image-server/main.go b/examples/r2-image-server/main.go new file mode 100644 index 0000000..a69c015 --- /dev/null +++ b/examples/r2-image-server/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "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 +} diff --git a/examples/r2-image-server/r2bucket.go b/examples/r2-image-server/r2bucket.go new file mode 100644 index 0000000..6f46400 --- /dev/null +++ b/examples/r2-image-server/r2bucket.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "io" + "syscall/js" +) + +// R2Bucket 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 R2Bucket interface { + Head(key string) + Get(key string) *R2Object + Put(key string, value io.Reader) + Delete(key string) + List() []*R2Object +} + +type r2Bucket struct { + instance js.Value +} + +var _ R2Bucket = &r2Bucket{} + +func (r *r2Bucket) Head(key string) { + panic("implement me") +} + +func (r *r2Bucket) Get(key string) *R2Object { + return nil +} + +func (r *r2Bucket) Put(key string, value io.Reader) { + panic("implement me") +} + +func (r *r2Bucket) Delete(key string) { + panic("implement me") +} + +func (r *r2Bucket) List() []*R2Object { + panic("implement me") +} + +func NewR2Bucket(varName string) (R2Bucket, error) { + inst := js.Global().Get(varName) + if inst.IsUndefined() { + return nil, fmt.Errorf("%s is undefined", varName) + } + return &r2Bucket{instance: inst}, nil +} diff --git a/examples/r2-image-server/r2object.go b/examples/r2-image-server/r2object.go new file mode 100644 index 0000000..730c894 --- /dev/null +++ b/examples/r2-image-server/r2object.go @@ -0,0 +1,75 @@ +package main + +import ( + "fmt" + "io" + "syscall/js" + "time" +) + +type R2Object struct { + Key string + Version string + Size int + ETag string + HTTPETag string + Uploaded time.Time + HTTPMetadata R2HTTPMetadata + CustomMetadata map[string]string + Body io.Reader +} + +// TODO: implement +// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1106 +// func (o *R2Object) WriteHTTPMetadata(headers http.Header) { +// } + +// 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")) + if err != nil { + return nil, fmt.Errorf("error converting uploaded: %w", err) + } + r2Meta, err := toR2HTTPMetadata(v.Get("httpMetadata")) + if err != nil { + return nil, fmt.Errorf("error converting httpMetadata: %w", err) + } + return &R2Object{ + 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: strRecordToMap(v.Get("customMetadata")), + Body: convertStreamReaderToReader(v.Get("body").Call("getReader")), + }, nil +} + +// R2HTTPMetadata represents metadata of R2 Object. +// * https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1053 +type R2HTTPMetadata struct { + ContentType *string + ContentLanguage *string + ContentDisposition *string + ContentEncoding *string + CacheControl *string + CacheExpiry *time.Time +} + +func toR2HTTPMetadata(v js.Value) (R2HTTPMetadata, error) { + cacheExpiry, err := maybeDate(v.Get("cacheExpiry")) + if err != nil { + return R2HTTPMetadata{}, fmt.Errorf("error converting cacheExpiry: %w", err) + } + return R2HTTPMetadata{ + ContentType: maybeString(v.Get("contentType")), + ContentLanguage: maybeString(v.Get("contentLanguage")), + ContentDisposition: maybeString(v.Get("contentDisposition")), + ContentEncoding: maybeString(v.Get("contentEncoding")), + CacheControl: maybeString(v.Get("cacheControl")), + CacheExpiry: cacheExpiry, + }, nil +} diff --git a/examples/r2-image-server/r2objects.go b/examples/r2-image-server/r2objects.go new file mode 100644 index 0000000..b573837 --- /dev/null +++ b/examples/r2-image-server/r2objects.go @@ -0,0 +1,5 @@ +package main + +type R2Objects struct { + Objects []*R2Object +} diff --git a/examples/r2-image-server/stream.go b/examples/r2-image-server/stream.go new file mode 100644 index 0000000..fa0a525 --- /dev/null +++ b/examples/r2-image-server/stream.go @@ -0,0 +1,140 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "syscall/js" +) + +// streamReaderToReader implements io.Reader sourced from ReadableStreamDefaultReader. +// * ReadableStreamDefaultReader: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader +// * This implementation is based on: https://deno.land/std@0.139.0/streams/conversion.ts#L76 +type streamReaderToReader struct { + buf bytes.Buffer + streamReader js.Value +} + +// Read reads bytes from ReadableStreamDefaultReader. +func (sr *streamReaderToReader) Read(p []byte) (n int, err error) { + if sr.buf.Len() == 0 { + promise := sr.streamReader.Call("read") + 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] + if result.Get("done").Bool() { + errCh <- io.EOF + return js.Undefined() + } + resultCh <- result.Get("value") + return js.Undefined() + }) + catch = js.FuncOf(func(_ js.Value, args []js.Value) any { + defer catch.Release() + result := args[0] + errCh <- fmt.Errorf("JavaScript error on read: %s", result.Call("toString").String()) + return js.Undefined() + }) + promise.Call("then", then).Call("catch", catch) + select { + case result := <-resultCh: + chunk := make([]byte, result.Get("byteLength").Int()) + _ = js.CopyBytesToGo(chunk, result) + // The length written is always the same as the length of chunk, so it can be discarded. + // - https://pkg.go.dev/bytes#Buffer.Write + _, err := sr.buf.Write(chunk) + if err != nil { + return 0, err + } + case err := <-errCh: + return 0, err + } + } + return sr.buf.Read(p) +} + +// convertStreamReaderToReader converts ReadableStreamDefaultReader to io.Reader. +func convertStreamReaderToReader(sr js.Value) io.Reader { + return &streamReaderToReader{ + streamReader: sr, + } +} + +// readerToReadableStream implements ReadableStream sourced from io.ReadCloser. +// * ReadableStream: https://developer.mozilla.org/docs/Web/API/ReadableStream +// * This implementation is based on: https://deno.land/std@0.139.0/streams/conversion.ts#L230 +type readerToReadableStream struct { + reader io.ReadCloser + chunkBuf []byte +} + +// Pull implements ReadableStream's pull method. +// * https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream#pull +func (rs *readerToReadableStream) Pull(controller js.Value) error { + n, err := rs.reader.Read(rs.chunkBuf) + if err == io.EOF { + if err := rs.reader.Close(); err != nil { + return err + } + controller.Call("close") + return nil + } + if err != nil { + jsErr := errorClass.New(err.Error()) + controller.Call("error", jsErr) + if err := rs.reader.Close(); err != nil { + return err + } + return err + } + ua := newUint8Array(n) + _ = js.CopyBytesToJS(ua, rs.chunkBuf[:n]) + controller.Call("enqueue", ua) + return nil +} + +// Cancel implements ReadableStream's cancel method. +// * https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream#cancel +func (rs *readerToReadableStream) Cancel() error { + return rs.reader.Close() +} + +// https://deno.land/std@0.139.0/streams/conversion.ts#L5 +const defaultChunkSize = 16_640 + +// convertReaderToReadableStream converts io.ReadCloser to ReadableStream. +func convertReaderToReadableStream(reader io.ReadCloser) js.Value { + stream := &readerToReadableStream{ + reader: reader, + chunkBuf: make([]byte, defaultChunkSize), + } + rsInit := newObject() + rsInit.Set("pull", js.FuncOf(func(_ js.Value, args []js.Value) any { + var cb js.Func + cb = js.FuncOf(func(this js.Value, pArgs []js.Value) any { + defer cb.Release() + resolve := pArgs[0] + reject := pArgs[1] + controller := args[0] + err := stream.Pull(controller) + if err != nil { + reject.Invoke(errorClass.New(err.Error())) + return js.Undefined() + } + resolve.Invoke() + return js.Undefined() + }) + return newPromise(cb) + })) + rsInit.Set("cancel", js.FuncOf(func(js.Value, []js.Value) any { + err := stream.Cancel() + if err != nil { + panic(err) + } + return js.Undefined() + })) + return readableStreamClass.New(rsInit) +} diff --git a/examples/r2-image-server/worker.mjs b/examples/r2-image-server/worker.mjs new file mode 100644 index 0000000..a0bb726 --- /dev/null +++ b/examples/r2-image-server/worker.mjs @@ -0,0 +1,17 @@ +import "../assets/polyfill_performance.js"; +import "../assets/wasm_exec.js"; +import mod from "./dist/app.wasm"; + +const go = new Go(); + +const load = WebAssembly.instantiate(mod, go.importObject).then((instance) => { + go.run(instance); + return instance; +}); + +export default { + async fetch(req) { + await load; + return handleRequest(req); + }, +}; diff --git a/examples/r2-image-server/wrangler.toml b/examples/r2-image-server/wrangler.toml new file mode 100644 index 0000000..61638e6 --- /dev/null +++ b/examples/r2-image-server/wrangler.toml @@ -0,0 +1,9 @@ +name = "simple-json-server" +main = "./worker.mjs" +compatibility_date = "2022-05-13" +compatibility_flags = [ + "streams_enable_constructors" +] + +[build] +command = "make build" From a95ca1de256076e6dc185b079b9fb45a5a1a5244 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 29 May 2022 09:17:07 +0900 Subject: [PATCH 2/4] 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 From 78dc0e3ad8d1253a7c96c2bccd55e8ba12aec5af Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 29 May 2022 09:30:15 +0900 Subject: [PATCH 3/4] update implementation of R2 example --- examples/r2-image-server/Makefile | 2 +- examples/r2-image-server/jsutil.go | 4 ++++ examples/r2-image-server/main.go | 11 ++++++++--- examples/r2-image-server/r2bucket.go | 1 + examples/r2-image-server/r2object.go | 1 + examples/r2-image-server/worker.mjs | 14 ++++++++------ 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/examples/r2-image-server/Makefile b/examples/r2-image-server/Makefile index c9d7430..320ddc3 100644 --- a/examples/r2-image-server/Makefile +++ b/examples/r2-image-server/Makefile @@ -5,7 +5,7 @@ dev: .PHONY: build build: mkdir -p dist - tinygo build -o ./dist/app.wasm -target wasm ./main.go + tinygo build -o ./dist/app.wasm -target wasm ./... .PHONY: publish publish: diff --git a/examples/r2-image-server/jsutil.go b/examples/r2-image-server/jsutil.go index 5edeb77..fe871c1 100644 --- a/examples/r2-image-server/jsutil.go +++ b/examples/r2-image-server/jsutil.go @@ -29,6 +29,8 @@ func newPromise(fn js.Func) js.Value { } func awaitPromise(promiseVal js.Value) (js.Value, error) { + fmt.Println("await promise") + fmt.Println(promiseVal.Call("toString").String()) resultCh := make(chan js.Value) errCh := make(chan error) var then, catch js.Func @@ -47,8 +49,10 @@ func awaitPromise(promiseVal js.Value) (js.Value, error) { promiseVal.Call("then", then).Call("catch", catch) select { case result := <-resultCh: + fmt.Println("got result of promise") return result, nil case err := <-errCh: + fmt.Println("got error of promise") return js.Value{}, err } } diff --git a/examples/r2-image-server/main.go b/examples/r2-image-server/main.go index f03f942..d4d70bc 100644 --- a/examples/r2-image-server/main.go +++ b/examples/r2-image-server/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" "io" + "log" "net/http" "github.com/syumai/workers" @@ -11,7 +12,8 @@ import ( // bucketName is R2 bucket name defined in wrangler.toml. const bucketName = "BUCKET" -func handleErr(w http.ResponseWriter, msg string) { +func handleErr(w http.ResponseWriter, msg string, err error) { + log.Println(err) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(msg)) } @@ -19,15 +21,17 @@ func handleErr(w http.ResponseWriter, msg string) { // 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) { + fmt.Println("new R2Bucket") bucket, err := NewR2Bucket(bucketName) if err != nil { - handleErr(w, "failed to get R2Bucket\n") + handleErr(w, "failed to get R2Bucket\n", err) return } imgPath := req.URL.Path + fmt.Println("bucket.get") imgObj, err := bucket.Get(imgPath) if err != nil { - handleErr(w, "failed to get R2Object\n") + handleErr(w, "failed to get R2Object\n", err) return } w.Header().Set("Cache-Control", "public, max-age=14400") @@ -37,6 +41,7 @@ func handler(w http.ResponseWriter, req *http.Request) { contentType = *imgObj.HTTPMetadata.ContentType } w.Header().Set("Content-Type", contentType) + fmt.Println("return result") io.Copy(w, imgObj.Body) } diff --git a/examples/r2-image-server/r2bucket.go b/examples/r2-image-server/r2bucket.go index 171f522..96511b0 100644 --- a/examples/r2-image-server/r2bucket.go +++ b/examples/r2-image-server/r2bucket.go @@ -41,6 +41,7 @@ func (r *r2Bucket) Get(key string) (*R2Object, error) { if err != nil { return nil, err } + fmt.Println(v) if v.IsNull() { return nil, nil } diff --git a/examples/r2-image-server/r2object.go b/examples/r2-image-server/r2object.go index 26e97b1..9403f6d 100644 --- a/examples/r2-image-server/r2object.go +++ b/examples/r2-image-server/r2object.go @@ -41,6 +41,7 @@ func (o *R2Object) BodyUsed() (bool, error) { // 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) { + fmt.Println("toR2Object") uploaded, err := dateToTime(v.Get("uploaded")) if err != nil { return nil, fmt.Errorf("error converting uploaded: %w", err) diff --git a/examples/r2-image-server/worker.mjs b/examples/r2-image-server/worker.mjs index a0bb726..06ce572 100644 --- a/examples/r2-image-server/worker.mjs +++ b/examples/r2-image-server/worker.mjs @@ -9,9 +9,11 @@ const load = WebAssembly.instantiate(mod, go.importObject).then((instance) => { return instance; }); -export default { - async fetch(req) { - await load; - return handleRequest(req); - }, -}; +async function processRequest(event) { + const req = event.request; + await load; + console.log("finished loading"); + return handleRequest(req); +} + +addEventListener("fetch", (event) => event.respondWith(processRequest(event))) From 480f570fe3c48086db9c7cb92ba5030ca2e6c2e2 Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 29 May 2022 09:47:55 +0900 Subject: [PATCH 4/4] fix implementation of R2 example and removed debug logging --- examples/r2-image-server/jsutil.go | 7 ++----- examples/r2-image-server/main.go | 11 +++++++---- examples/r2-image-server/r2object.go | 1 - 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/examples/r2-image-server/jsutil.go b/examples/r2-image-server/jsutil.go index fe871c1..d80a77a 100644 --- a/examples/r2-image-server/jsutil.go +++ b/examples/r2-image-server/jsutil.go @@ -14,6 +14,7 @@ var ( uint8ArrayClass = global.Get("Uint8Array") errorClass = global.Get("Error") readableStreamClass = global.Get("ReadableStream") + stringClass = global.Get("String") ) func newObject() js.Value { @@ -29,8 +30,6 @@ func newPromise(fn js.Func) js.Value { } func awaitPromise(promiseVal js.Value) (js.Value, error) { - fmt.Println("await promise") - fmt.Println(promiseVal.Call("toString").String()) resultCh := make(chan js.Value) errCh := make(chan error) var then, catch js.Func @@ -49,17 +48,15 @@ func awaitPromise(promiseVal js.Value) (js.Value, error) { promiseVal.Call("then", then).Call("catch", catch) select { case result := <-resultCh: - fmt.Println("got result of promise") return result, nil case err := <-errCh: - fmt.Println("got error of promise") 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() + milliStr := stringClass.Invoke(v.Call("getTime")).String() milli, err := strconv.ParseInt(milliStr, 10, 64) if err != nil { return time.Time{}, fmt.Errorf("failed to convert Date to time.Time: %w", err) diff --git a/examples/r2-image-server/main.go b/examples/r2-image-server/main.go index d4d70bc..d825e6e 100644 --- a/examples/r2-image-server/main.go +++ b/examples/r2-image-server/main.go @@ -5,6 +5,7 @@ import ( "io" "log" "net/http" + "strings" "github.com/syumai/workers" ) @@ -21,19 +22,22 @@ 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) { - fmt.Println("new R2Bucket") bucket, err := NewR2Bucket(bucketName) if err != nil { handleErr(w, "failed to get R2Bucket\n", err) return } - imgPath := req.URL.Path - fmt.Println("bucket.get") + imgPath := strings.TrimPrefix(req.URL.Path, "/") imgObj, err := bucket.Get(imgPath) if err != nil { handleErr(w, "failed to get R2Object\n", err) return } + if imgObj == nil { + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(fmt.Sprintf("image not found: %s", imgPath))) + return + } w.Header().Set("Cache-Control", "public, max-age=14400") w.Header().Set("ETag", fmt.Sprintf("W/%s", imgObj.HTTPETag)) contentType := "application/octet-stream" @@ -41,7 +45,6 @@ func handler(w http.ResponseWriter, req *http.Request) { contentType = *imgObj.HTTPMetadata.ContentType } w.Header().Set("Content-Type", contentType) - fmt.Println("return result") io.Copy(w, imgObj.Body) } diff --git a/examples/r2-image-server/r2object.go b/examples/r2-image-server/r2object.go index 9403f6d..26e97b1 100644 --- a/examples/r2-image-server/r2object.go +++ b/examples/r2-image-server/r2object.go @@ -41,7 +41,6 @@ func (o *R2Object) BodyUsed() (bool, error) { // 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) { - fmt.Println("toR2Object") uploaded, err := dateToTime(v.Get("uploaded")) if err != nil { return nil, fmt.Errorf("error converting uploaded: %w", err)