mirror of
https://github.com/syumai/workers.git
synced 2025-03-11 09:49:12 +00:00
Merge pull request #1 from syumai/add-r2-image-server-example
add R2 image server example
This commit is contained in:
commit
c3790aa88e
12
examples/r2-image-server/Makefile
Normal file
12
examples/r2-image-server/Makefile
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
.PHONY: dev
|
||||||
|
dev:
|
||||||
|
wrangler dev
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build:
|
||||||
|
mkdir -p dist
|
||||||
|
tinygo build -o ./dist/app.wasm -target wasm ./...
|
||||||
|
|
||||||
|
.PHONY: publish
|
||||||
|
publish:
|
||||||
|
wrangler publish
|
52
examples/r2-image-server/README.md
Normal file
52
examples/r2-image-server/README.md
Normal file
@ -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
|
7
examples/r2-image-server/go.mod
Normal file
7
examples/r2-image-server/go.mod
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
module github.com/syumai/workers/examples/r2-image-server
|
||||||
|
|
||||||
|
go 1.18
|
||||||
|
|
||||||
|
require github.com/syumai/workers v0.0.0-00010101000000-000000000000
|
||||||
|
|
||||||
|
replace github.com/syumai/workers => ../../
|
0
examples/r2-image-server/go.sum
Normal file
0
examples/r2-image-server/go.sum
Normal file
100
examples/r2-image-server/jsutil.go
Normal file
100
examples/r2-image-server/jsutil.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
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
|
||||||
|
}
|
53
examples/r2-image-server/main.go
Normal file
53
examples/r2-image-server/main.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/syumai/workers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bucketName is R2 bucket name defined in wrangler.toml.
|
||||||
|
const bucketName = "BUCKET"
|
||||||
|
|
||||||
|
func handleErr(w http.ResponseWriter, msg string, err error) {
|
||||||
|
log.Println(err)
|
||||||
|
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", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
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))
|
||||||
|
}
|
74
examples/r2-image-server/r2bucket.go
Normal file
74
examples/r2-image-server/r2bucket.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
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) (*R2Object, error)
|
||||||
|
Get(key string) (*R2Object, error)
|
||||||
|
Put(key string, value io.Reader) error
|
||||||
|
Delete(key string) error
|
||||||
|
List() (*R2Objects, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type r2Bucket struct {
|
||||||
|
instance js.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ R2Bucket = &r2Bucket{}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
fmt.Println(v)
|
||||||
|
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) Delete(key string) error {
|
||||||
|
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) {
|
||||||
|
inst := js.Global().Get(varName)
|
||||||
|
if inst.IsUndefined() {
|
||||||
|
return nil, fmt.Errorf("%s is undefined", varName)
|
||||||
|
}
|
||||||
|
return &r2Bucket{instance: inst}, nil
|
||||||
|
}
|
95
examples/r2-image-server/r2object.go
Normal file
95
examples/r2-image-server/r2object.go
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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
|
||||||
|
ETag string
|
||||||
|
HTTPETag string
|
||||||
|
Uploaded time.Time
|
||||||
|
HTTPMetadata R2HTTPMetadata
|
||||||
|
CustomMetadata map[string]string
|
||||||
|
// Body is a body of R2Object.
|
||||||
|
// This value becomes nil when `Head` method of R2Bucket is called.
|
||||||
|
Body io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: implement
|
||||||
|
// - https://github.com/cloudflare/workers-types/blob/3012f263fb1239825e5f0061b267c8650d01b717/index.d.ts#L1106
|
||||||
|
// func (o *R2Object) WriteHTTPMetadata(headers http.Header) {
|
||||||
|
// }
|
||||||
|
|
||||||
|
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"))
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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(),
|
||||||
|
ETag: v.Get("etag").String(),
|
||||||
|
HTTPETag: v.Get("httpEtag").String(),
|
||||||
|
Uploaded: uploaded,
|
||||||
|
HTTPMetadata: r2Meta,
|
||||||
|
CustomMetadata: strRecordToMap(v.Get("customMetadata")),
|
||||||
|
Body: body,
|
||||||
|
}, 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
|
||||||
|
}
|
40
examples/r2-image-server/r2objects.go
Normal file
40
examples/r2-image-server/r2objects.go
Normal file
@ -0,0 +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
|
||||||
|
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
|
||||||
|
}
|
140
examples/r2-image-server/stream.go
Normal file
140
examples/r2-image-server/stream.go
Normal file
@ -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)
|
||||||
|
}
|
19
examples/r2-image-server/worker.mjs
Normal file
19
examples/r2-image-server/worker.mjs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function processRequest(event) {
|
||||||
|
const req = event.request;
|
||||||
|
await load;
|
||||||
|
console.log("finished loading");
|
||||||
|
return handleRequest(req);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener("fetch", (event) => event.respondWith(processRequest(event)))
|
14
examples/r2-image-server/wrangler.toml
Normal file
14
examples/r2-image-server/wrangler.toml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
name = "r2-image-server"
|
||||||
|
main = "./worker.mjs"
|
||||||
|
compatibility_date = "2022-05-13"
|
||||||
|
compatibility_flags = [
|
||||||
|
"streams_enable_constructors"
|
||||||
|
]
|
||||||
|
|
||||||
|
[build]
|
||||||
|
command = "make build"
|
||||||
|
|
||||||
|
[[r2_buckets]]
|
||||||
|
binding = 'BUCKET'
|
||||||
|
bucket_name = 'r2-image-viewer'
|
||||||
|
preview_bucket_name = 'r2-image-viewer-dev'
|
Loading…
x
Reference in New Issue
Block a user