diff --git a/README.md b/README.md index 0315bf2..190ee14 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/examples/r2-image-server/main.go b/examples/r2-image-server/main.go index 0313a67..dfe176a 100644 --- a/examples/r2-image-server/main.go +++ b/examples/r2-image-server/main.go @@ -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)) } diff --git a/jsutil.go b/jsutil.go index e84b891..6affd51 100644 --- a/jsutil.go +++ b/jsutil.go @@ -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()) } diff --git a/r2bucket.go b/r2bucket.go index d379c8c..1fbc6b5 100644 --- a/r2bucket.go +++ b/r2bucket.go @@ -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 diff --git a/r2objects.go b/r2objects.go index df14f8a..ac279d0 100644 --- a/r2objects.go +++ b/r2objects.go @@ -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 }