Skip to content

Commit

Permalink
Merge pull request #6 from syumai/fix-put-delete-impl
Browse files Browse the repository at this point in the history
fix implementation of put and delete of R2
  • Loading branch information
syumai authored May 29, 2022
2 parents 02f63bd + e4c098f commit 1cf0183
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 29 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@
* [ ] R2 - Partially supported
- [x] Head
- [x] Get
- [ ] Put
- [ ] Delete
- [x] Put (load all bytes to memory)
- [ ] Put (stream)
- [x] Delete
- [x] List
- [ ] Options for R2 methods
* [ ] environment variables (WIP)
Expand Down
20 changes: 16 additions & 4 deletions examples/r2-image-server/README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
# r2-image-server

* An example server which returns image from Cloudflare R2.
* This server is implemented in Go and compiled with tinygo.
* An example server of R2.
* This server can store / load / delete images in R2.

## Example
## Usage

* https://r2-image-server.syumai.workers.dev/syumai.png
### Endpoints

* **GET `/images/{key}`**
- Get an image object at the `key` and returns it.
* **POST `/images/{key}`**
- Create an image object at the `key` and uploads image.
- Request body must be binary and request header must have `Content-Type`.
* **DELETE `/images/{key}`**
- Delete an image object at the `key`.

## Development

* See the following documents for details on how to use R2.
- https://developers.cloudflare.com/r2/runtime-apis
- https://pkg.go.dev/github.com/syumai/workers

### Requirements

This project requires these tools to be installed globally.
Expand Down
86 changes: 78 additions & 8 deletions examples/r2-image-server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io"
"log"
"net/http"
"os"
"strings"

"github.com/syumai/workers"
Expand All @@ -16,26 +17,61 @@ const bucketName = "BUCKET"
func handleErr(w http.ResponseWriter, msg string, err error) {
log.Println(err)
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", "text/plain")
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) {
type server struct {
bucket workers.R2Bucket
}

func newServer() (*server, error) {
// delete image object from R2
bucket, err := workers.NewR2Bucket(bucketName)
if err != nil {
handleErr(w, "failed to get R2Bucket\n", err)
return nil, err
}
return &server{bucket: bucket}, nil
}

func (s *server) post(w http.ResponseWriter, req *http.Request, key string) {
objects, err := s.bucket.List()
if err != nil {
handleErr(w, "failed to list R2Objects\n", err)
return
}
imgPath := strings.TrimPrefix(req.URL.Path, "/")
imgObj, err := bucket.Get(imgPath)
for _, obj := range objects.Objects {
if obj.Key == key {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "key %s already exists\n", key)
return
}
}
_, err = s.bucket.Put(key, req.Body, &workers.R2PutOptions{
HTTPMetadata: workers.R2HTTPMetadata{
ContentType: req.Header.Get("Content-Type"),
},
CustomMetadata: map[string]string{"custom-key": "custom-value"},
})
if err != nil {
handleErr(w, "failed to put R2Object\n", err)
return
}
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("successfully uploaded image"))
}

func (s *server) get(w http.ResponseWriter, req *http.Request, key string) {
// get image object from R2
imgObj, err := s.bucket.Get(key)
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)))
w.Write([]byte(fmt.Sprintf("image not found: %s", key)))
return
}
w.Header().Set("Cache-Control", "public, max-age=14400")
Expand All @@ -48,6 +84,40 @@ func handler(w http.ResponseWriter, req *http.Request) {
io.Copy(w, imgObj.Body)
}

func (s *server) delete(w http.ResponseWriter, req *http.Request, key string) {
// delete image object from R2
if err := s.bucket.Delete(key); err != nil {
handleErr(w, "failed to delete R2Object\n", err)
return
}
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte("successfully deleted image"))
}

func (s *server) routeHandler(w http.ResponseWriter, req *http.Request) {
key := strings.TrimPrefix(req.URL.Path, "/")
switch req.Method {
case "GET":
s.get(w, req, key)
return
case "DELETE":
s.delete(w, req, key)
return
case "POST":
s.post(w, req, key)
return
default:
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("url not found\n"))
return
}
}

func main() {
workers.Serve(http.HandlerFunc(handler))
s, err := newServer()
if err != nil {
fmt.Fprintf(os.Stderr, "failed to start server: %v", err)
os.Exit(1)
}
workers.Serve(http.HandlerFunc(s.routeHandler))
}
14 changes: 3 additions & 11 deletions jsutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package workers

import (
"fmt"
"strconv"
"syscall/js"
"time"
)
Expand All @@ -17,9 +16,7 @@ var (
uint8ArrayClass = global.Get("Uint8Array")
errorClass = global.Get("Error")
readableStreamClass = global.Get("ReadableStream")
stringClass = global.Get("String")
dateClass = global.Get("Date")
numberClass = global.Get("Number")
)

func newObject() js.Value {
Expand Down Expand Up @@ -96,16 +93,11 @@ func maybeDate(v js.Value) (time.Time, error) {

// 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
milli := v.Call("getTime").Float()
return time.UnixMilli(int64(milli)), nil
}

// timeToDate converts Go side's time.Time into Date object.
func timeToDate(t time.Time) js.Value {
milliStr := strconv.FormatInt(t.UnixMilli(), 10)
return dateClass.New(numberClass.Call(milliStr))
return dateClass.New(t.UnixMilli())
}
15 changes: 14 additions & 1 deletion r2bucket.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,24 @@ func (opts *R2PutOptions) toJS() js.Value {
}

// Put returns the result of `put` call to R2Bucket.
// * This method copies all bytes into memory for implementation restriction.
// * Body field of *R2Object is always nil for Put call.
// * if a network error happens, returns error.
func (r *r2Bucket) Put(key string, value io.ReadCloser, opts *R2PutOptions) (*R2Object, error) {
/* TODO: implement this in FixedLengthStream: https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/#fixedlengthstream
body := convertReaderToReadableStream(value)
p := r.instance.Call("put", key, body, opts.toJS())
streams := fixedLengthStreamClass.New(contentLength)
rs := streams.Get("readable")
body.Call("pipeTo", streams.Get("writable"))
*/
b, err := io.ReadAll(value)
if err != nil {
return nil, err
}
defer value.Close()
ua := newUint8Array(len(b))
js.CopyBytesToJS(ua, b)
p := r.instance.Call("put", key, ua.Get("buffer"), opts.toJS())
v, err := awaitPromise(p)
if err != nil {
return nil, err
Expand Down
6 changes: 3 additions & 3 deletions r2objects.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,15 @@ func toR2Objects(v js.Value) (*R2Objects, error) {
}
objects[i] = obj
}
prefixesVal := objectsVal.Get("delimitedPrefixes")
prefixesVal := v.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")),
Truncated: v.Get("truncated").Bool(),
Cursor: maybeString(v.Get("cursor")),
DelimitedPrefixes: prefixes,
}, nil
}

0 comments on commit 1cf0183

Please sign in to comment.