From 6b96cfcd5d0ff40234062af955db053f0a90377b Mon Sep 17 00:00:00 2001 From: syumai Date: Sun, 29 May 2022 10:02:40 +0900 Subject: [PATCH] move R2 implementation to main package --- README.md | 7 +- examples/basic-auth-server/worker.mjs | 15 +- examples/r2-image-server/README.md | 27 +--- examples/r2-image-server/jsutil.go | 100 ------------- examples/r2-image-server/main.go | 2 +- examples/r2-image-server/stream.go | 140 ------------------ examples/r2-image-server/worker.mjs | 5 +- jsutil.go | 78 +++++++++- .../r2bucket.go => r2bucket.go | 2 +- .../r2object.go => r2object.go | 2 +- .../r2objects.go => r2objects.go | 2 +- 11 files changed, 103 insertions(+), 277 deletions(-) delete mode 100644 examples/r2-image-server/jsutil.go delete mode 100644 examples/r2-image-server/stream.go rename examples/r2-image-server/r2bucket.go => r2bucket.go (98%) rename examples/r2-image-server/r2object.go => r2object.go (99%) rename examples/r2-image-server/r2objects.go => r2objects.go (98%) diff --git a/README.md b/README.md index e2ec016..42a85e9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,12 @@ * [x] serve http.Handler * [ ] environment variables (WIP) * [ ] KV (WIP) -* [ ] R2 (WIP) +* [ ] R2 - Partially supported + - [x] Head + - [x] Get + - [ ] Put + - [ ] Delete + - [x] List ## Installation diff --git a/examples/basic-auth-server/worker.mjs b/examples/basic-auth-server/worker.mjs index a0bb726..1724f16 100644 --- a/examples/basic-auth-server/worker.mjs +++ b/examples/basic-auth-server/worker.mjs @@ -9,9 +9,12 @@ 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; + return handleRequest(req); +} + +addEventListener("fetch", (event) => { + event.respondWith(processRequest(event)); +}) diff --git a/examples/r2-image-server/README.md b/examples/r2-image-server/README.md index 9bd46df..8702731 100644 --- a/examples/r2-image-server/README.md +++ b/examples/r2-image-server/README.md @@ -1,28 +1,11 @@ -# simple-json-server +# r2-image-server -* A simple HTTP JSON server implemented in Go and compiled with tinygo. +* An example server which returns image from Cloudflare R2. +* This server is 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!" -} -``` +* https://r2-image-server.syumai.workers.dev/syumai.png ## Development @@ -32,8 +15,6 @@ 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 diff --git a/examples/r2-image-server/jsutil.go b/examples/r2-image-server/jsutil.go deleted file mode 100644 index d80a77a..0000000 --- a/examples/r2-image-server/jsutil.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "fmt" - "strconv" - "syscall/js" - "time" -) - -var ( - global = js.Global() - objectClass = global.Get("Object") - promiseClass = global.Get("Promise") - uint8ArrayClass = global.Get("Uint8Array") - errorClass = global.Get("Error") - readableStreamClass = global.Get("ReadableStream") - stringClass = global.Get("String") -) - -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 := 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) - } - 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 index d825e6e..d1d4e5a 100644 --- a/examples/r2-image-server/main.go +++ b/examples/r2-image-server/main.go @@ -22,7 +22,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 := NewR2Bucket(bucketName) + bucket, err := workers.NewR2Bucket(bucketName) if err != nil { handleErr(w, "failed to get R2Bucket\n", err) return diff --git a/examples/r2-image-server/stream.go b/examples/r2-image-server/stream.go deleted file mode 100644 index fa0a525..0000000 --- a/examples/r2-image-server/stream.go +++ /dev/null @@ -1,140 +0,0 @@ -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 index 06ce572..1724f16 100644 --- a/examples/r2-image-server/worker.mjs +++ b/examples/r2-image-server/worker.mjs @@ -12,8 +12,9 @@ const load = WebAssembly.instantiate(mod, go.importObject).then((instance) => { async function processRequest(event) { const req = event.request; await load; - console.log("finished loading"); return handleRequest(req); } -addEventListener("fetch", (event) => event.respondWith(processRequest(event))) +addEventListener("fetch", (event) => { + event.respondWith(processRequest(event)); +}) diff --git a/jsutil.go b/jsutil.go index 6bc5063..030ebc1 100644 --- a/jsutil.go +++ b/jsutil.go @@ -1,6 +1,11 @@ package workers -import "syscall/js" +import ( + "fmt" + "strconv" + "syscall/js" + "time" +) var ( global = js.Global() @@ -12,6 +17,7 @@ var ( uint8ArrayClass = global.Get("Uint8Array") errorClass = global.Get("Error") readableStreamClass = global.Get("ReadableStream") + stringClass = global.Get("String") ) func newObject() js.Value { @@ -30,3 +36,73 @@ func newPromise(fn js.Func) js.Value { func arrayFrom(v js.Value) js.Value { return arrayClass.Call("from", v) } + +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 + } +} + +// 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 +} + +// dateToTime converts JavaScript side's Data object into time.Time. +func dateToTime(v js.Value) (time.Time, error) { + 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) + } + return time.UnixMilli(milli), nil +} diff --git a/examples/r2-image-server/r2bucket.go b/r2bucket.go similarity index 98% rename from examples/r2-image-server/r2bucket.go rename to r2bucket.go index 96511b0..694ea98 100644 --- a/examples/r2-image-server/r2bucket.go +++ b/r2bucket.go @@ -1,4 +1,4 @@ -package main +package workers import ( "fmt" diff --git a/examples/r2-image-server/r2object.go b/r2object.go similarity index 99% rename from examples/r2-image-server/r2object.go rename to r2object.go index 26e97b1..71d69e7 100644 --- a/examples/r2-image-server/r2object.go +++ b/r2object.go @@ -1,4 +1,4 @@ -package main +package workers import ( "errors" diff --git a/examples/r2-image-server/r2objects.go b/r2objects.go similarity index 98% rename from examples/r2-image-server/r2objects.go rename to r2objects.go index f6bac6b..f5a3270 100644 --- a/examples/r2-image-server/r2objects.go +++ b/r2objects.go @@ -1,4 +1,4 @@ -package main +package workers import ( "fmt"