mirror of
https://github.com/syumai/workers.git
synced 2025-03-10 17:29:11 +00:00
Merge pull request #3 from syumai/move-r2-impl
move R2 implementation to main package
This commit is contained in:
commit
855f9c2a2a
@ -9,7 +9,12 @@
|
|||||||
* [x] serve http.Handler
|
* [x] serve http.Handler
|
||||||
* [ ] environment variables (WIP)
|
* [ ] environment variables (WIP)
|
||||||
* [ ] KV (WIP)
|
* [ ] KV (WIP)
|
||||||
* [ ] R2 (WIP)
|
* [ ] R2 - Partially supported
|
||||||
|
- [x] Head
|
||||||
|
- [x] Get
|
||||||
|
- [ ] Put
|
||||||
|
- [ ] Delete
|
||||||
|
- [x] List
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
@ -9,9 +9,12 @@ const load = WebAssembly.instantiate(mod, go.importObject).then((instance) => {
|
|||||||
return instance;
|
return instance;
|
||||||
});
|
});
|
||||||
|
|
||||||
export default {
|
async function processRequest(event) {
|
||||||
async fetch(req) {
|
const req = event.request;
|
||||||
await load;
|
await load;
|
||||||
return handleRequest(req);
|
return handleRequest(req);
|
||||||
},
|
}
|
||||||
};
|
|
||||||
|
addEventListener("fetch", (event) => {
|
||||||
|
event.respondWith(processRequest(event));
|
||||||
|
})
|
||||||
|
@ -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
|
## Example
|
||||||
|
|
||||||
* https://simple-json-server.syumai.workers.dev
|
* https://r2-image-server.syumai.workers.dev/syumai.png
|
||||||
|
|
||||||
### 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
|
## Development
|
||||||
|
|
||||||
@ -32,8 +15,6 @@ This project requires these tools to be installed globally.
|
|||||||
|
|
||||||
* wrangler
|
* wrangler
|
||||||
* tinygo
|
* tinygo
|
||||||
* [easyjson](https://github.com/mailru/easyjson)
|
|
||||||
- `go install github.com/mailru/easyjson/...@latest`
|
|
||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
|
@ -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<string, string> 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
|
|
||||||
}
|
|
@ -22,7 +22,7 @@ func handleErr(w http.ResponseWriter, msg string, err error) {
|
|||||||
// This example is based on implementation in syumai/workers-playground
|
// 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
|
// * https://github.com/syumai/workers-playground/blob/e32881648ccc055e3690a0d9c750a834261c333e/r2-image-viewer/src/index.ts#L30
|
||||||
func handler(w http.ResponseWriter, req *http.Request) {
|
func handler(w http.ResponseWriter, req *http.Request) {
|
||||||
bucket, err := NewR2Bucket(bucketName)
|
bucket, err := workers.NewR2Bucket(bucketName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handleErr(w, "failed to get R2Bucket\n", err)
|
handleErr(w, "failed to get R2Bucket\n", err)
|
||||||
return
|
return
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -12,8 +12,9 @@ const load = WebAssembly.instantiate(mod, go.importObject).then((instance) => {
|
|||||||
async function processRequest(event) {
|
async function processRequest(event) {
|
||||||
const req = event.request;
|
const req = event.request;
|
||||||
await load;
|
await load;
|
||||||
console.log("finished loading");
|
|
||||||
return handleRequest(req);
|
return handleRequest(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
addEventListener("fetch", (event) => event.respondWith(processRequest(event)))
|
addEventListener("fetch", (event) => {
|
||||||
|
event.respondWith(processRequest(event));
|
||||||
|
})
|
||||||
|
78
jsutil.go
78
jsutil.go
@ -1,6 +1,11 @@
|
|||||||
package workers
|
package workers
|
||||||
|
|
||||||
import "syscall/js"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"syscall/js"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
global = js.Global()
|
global = js.Global()
|
||||||
@ -12,6 +17,7 @@ var (
|
|||||||
uint8ArrayClass = global.Get("Uint8Array")
|
uint8ArrayClass = global.Get("Uint8Array")
|
||||||
errorClass = global.Get("Error")
|
errorClass = global.Get("Error")
|
||||||
readableStreamClass = global.Get("ReadableStream")
|
readableStreamClass = global.Get("ReadableStream")
|
||||||
|
stringClass = global.Get("String")
|
||||||
)
|
)
|
||||||
|
|
||||||
func newObject() js.Value {
|
func newObject() js.Value {
|
||||||
@ -30,3 +36,73 @@ func newPromise(fn js.Func) js.Value {
|
|||||||
func arrayFrom(v js.Value) js.Value {
|
func arrayFrom(v js.Value) js.Value {
|
||||||
return arrayClass.Call("from", v)
|
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<string, string> 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
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package workers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package workers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
@ -1,4 +1,4 @@
|
|||||||
package main
|
package workers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
Loading…
x
Reference in New Issue
Block a user