fix implementation of put and delete of R2

This commit is contained in:
syumai 2022-05-29 21:57:44 +09:00
parent 02f63bd14c
commit 19537376e1
5 changed files with 102 additions and 26 deletions

View File

@ -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)

View File

@ -5,6 +5,7 @@ import (
"io"
"log"
"net/http"
"os"
"strings"
"github.com/syumai/workers"
@ -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")
@ -48,6 +84,39 @@ func handler(w http.ResponseWriter, req *http.Request) {
io.Copy(w, imgObj.Body)
}
func main() {
workers.Serve(http.HandlerFunc(handler))
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)
default:
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("url not found\n"))
return
}
}
func main() {
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))
}

View File

@ -2,7 +2,6 @@ package workers
import (
"fmt"
"strconv"
"syscall/js"
"time"
)
@ -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 {
@ -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())
}

View File

@ -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

View File

@ -19,6 +19,7 @@ type R2Objects struct {
// 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) {
global.Get("console").Call("log", global.Get("JSON").Call("stringify", v, nil, 2))
objectsVal := v.Get("objects")
objects := make([]*R2Object, objectsVal.Length())
for i := 0; i < len(objects); i++ {
@ -28,15 +29,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
}