Skip to content

Commit 1cf0183

Browse files
authored
Merge pull request #6 from syumai/fix-put-delete-impl
fix implementation of put and delete of R2
2 parents 02f63bd + e4c098f commit 1cf0183

File tree

6 files changed

+117
-29
lines changed

6 files changed

+117
-29
lines changed

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
* [ ] R2 - Partially supported
1111
- [x] Head
1212
- [x] Get
13-
- [ ] Put
14-
- [ ] Delete
13+
- [x] Put (load all bytes to memory)
14+
- [ ] Put (stream)
15+
- [x] Delete
1516
- [x] List
1617
- [ ] Options for R2 methods
1718
* [ ] environment variables (WIP)

examples/r2-image-server/README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
# r2-image-server
22

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

6-
## Example
6+
## Usage
77

8-
* https://r2-image-server.syumai.workers.dev/syumai.png
8+
### Endpoints
9+
10+
* **GET `/images/{key}`**
11+
- Get an image object at the `key` and returns it.
12+
* **POST `/images/{key}`**
13+
- Create an image object at the `key` and uploads image.
14+
- Request body must be binary and request header must have `Content-Type`.
15+
* **DELETE `/images/{key}`**
16+
- Delete an image object at the `key`.
917

1018
## Development
1119

20+
* See the following documents for details on how to use R2.
21+
- https://developers.cloudflare.com/r2/runtime-apis
22+
- https://pkg.go.dev/github.com/syumai/workers
23+
1224
### Requirements
1325

1426
This project requires these tools to be installed globally.

examples/r2-image-server/main.go

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66
"log"
77
"net/http"
8+
"os"
89
"strings"
910

1011
"github.com/syumai/workers"
@@ -16,26 +17,61 @@ const bucketName = "BUCKET"
1617
func handleErr(w http.ResponseWriter, msg string, err error) {
1718
log.Println(err)
1819
w.WriteHeader(http.StatusInternalServerError)
20+
w.Header().Set("Content-Type", "text/plain")
1921
w.Write([]byte(msg))
2022
}
2123

22-
// This example is based on implementation in syumai/workers-playground
23-
// * https://github.com/syumai/workers-playground/blob/e32881648ccc055e3690a0d9c750a834261c333e/r2-image-viewer/src/index.ts#L30
24-
func handler(w http.ResponseWriter, req *http.Request) {
24+
type server struct {
25+
bucket workers.R2Bucket
26+
}
27+
28+
func newServer() (*server, error) {
29+
// delete image object from R2
2530
bucket, err := workers.NewR2Bucket(bucketName)
2631
if err != nil {
27-
handleErr(w, "failed to get R2Bucket\n", err)
32+
return nil, err
33+
}
34+
return &server{bucket: bucket}, nil
35+
}
36+
37+
func (s *server) post(w http.ResponseWriter, req *http.Request, key string) {
38+
objects, err := s.bucket.List()
39+
if err != nil {
40+
handleErr(w, "failed to list R2Objects\n", err)
2841
return
2942
}
30-
imgPath := strings.TrimPrefix(req.URL.Path, "/")
31-
imgObj, err := bucket.Get(imgPath)
43+
for _, obj := range objects.Objects {
44+
if obj.Key == key {
45+
w.WriteHeader(http.StatusBadRequest)
46+
fmt.Fprintf(w, "key %s already exists\n", key)
47+
return
48+
}
49+
}
50+
_, err = s.bucket.Put(key, req.Body, &workers.R2PutOptions{
51+
HTTPMetadata: workers.R2HTTPMetadata{
52+
ContentType: req.Header.Get("Content-Type"),
53+
},
54+
CustomMetadata: map[string]string{"custom-key": "custom-value"},
55+
})
56+
if err != nil {
57+
handleErr(w, "failed to put R2Object\n", err)
58+
return
59+
}
60+
w.WriteHeader(http.StatusCreated)
61+
w.Header().Set("Content-Type", "text/plain")
62+
w.Write([]byte("successfully uploaded image"))
63+
}
64+
65+
func (s *server) get(w http.ResponseWriter, req *http.Request, key string) {
66+
// get image object from R2
67+
imgObj, err := s.bucket.Get(key)
3268
if err != nil {
3369
handleErr(w, "failed to get R2Object\n", err)
3470
return
3571
}
3672
if imgObj == nil {
3773
w.WriteHeader(http.StatusNotFound)
38-
w.Write([]byte(fmt.Sprintf("image not found: %s", imgPath)))
74+
w.Write([]byte(fmt.Sprintf("image not found: %s", key)))
3975
return
4076
}
4177
w.Header().Set("Cache-Control", "public, max-age=14400")
@@ -48,6 +84,40 @@ func handler(w http.ResponseWriter, req *http.Request) {
4884
io.Copy(w, imgObj.Body)
4985
}
5086

87+
func (s *server) delete(w http.ResponseWriter, req *http.Request, key string) {
88+
// delete image object from R2
89+
if err := s.bucket.Delete(key); err != nil {
90+
handleErr(w, "failed to delete R2Object\n", err)
91+
return
92+
}
93+
w.Header().Set("Content-Type", "text/plain")
94+
w.Write([]byte("successfully deleted image"))
95+
}
96+
97+
func (s *server) routeHandler(w http.ResponseWriter, req *http.Request) {
98+
key := strings.TrimPrefix(req.URL.Path, "/")
99+
switch req.Method {
100+
case "GET":
101+
s.get(w, req, key)
102+
return
103+
case "DELETE":
104+
s.delete(w, req, key)
105+
return
106+
case "POST":
107+
s.post(w, req, key)
108+
return
109+
default:
110+
w.WriteHeader(http.StatusNotFound)
111+
w.Write([]byte("url not found\n"))
112+
return
113+
}
114+
}
115+
51116
func main() {
52-
workers.Serve(http.HandlerFunc(handler))
117+
s, err := newServer()
118+
if err != nil {
119+
fmt.Fprintf(os.Stderr, "failed to start server: %v", err)
120+
os.Exit(1)
121+
}
122+
workers.Serve(http.HandlerFunc(s.routeHandler))
53123
}

jsutil.go

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package workers
22

33
import (
44
"fmt"
5-
"strconv"
65
"syscall/js"
76
"time"
87
)
@@ -17,9 +16,7 @@ var (
1716
uint8ArrayClass = global.Get("Uint8Array")
1817
errorClass = global.Get("Error")
1918
readableStreamClass = global.Get("ReadableStream")
20-
stringClass = global.Get("String")
2119
dateClass = global.Get("Date")
22-
numberClass = global.Get("Number")
2320
)
2421

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

9794
// dateToTime converts JavaScript side's Data object into time.Time.
9895
func dateToTime(v js.Value) (time.Time, error) {
99-
milliStr := stringClass.Invoke(v.Call("getTime")).String()
100-
milli, err := strconv.ParseInt(milliStr, 10, 64)
101-
if err != nil {
102-
return time.Time{}, fmt.Errorf("failed to convert Date to time.Time: %w", err)
103-
}
104-
return time.UnixMilli(milli), nil
96+
milli := v.Call("getTime").Float()
97+
return time.UnixMilli(int64(milli)), nil
10598
}
10699

107100
// timeToDate converts Go side's time.Time into Date object.
108101
func timeToDate(t time.Time) js.Value {
109-
milliStr := strconv.FormatInt(t.UnixMilli(), 10)
110-
return dateClass.New(numberClass.Call(milliStr))
102+
return dateClass.New(t.UnixMilli())
111103
}

r2bucket.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,24 @@ func (opts *R2PutOptions) toJS() js.Value {
9999
}
100100

101101
// Put returns the result of `put` call to R2Bucket.
102+
// * This method copies all bytes into memory for implementation restriction.
102103
// * Body field of *R2Object is always nil for Put call.
103104
// * if a network error happens, returns error.
104105
func (r *r2Bucket) Put(key string, value io.ReadCloser, opts *R2PutOptions) (*R2Object, error) {
106+
/* TODO: implement this in FixedLengthStream: https://developers.cloudflare.com/workers/runtime-apis/streams/transformstream/#fixedlengthstream
105107
body := convertReaderToReadableStream(value)
106-
p := r.instance.Call("put", key, body, opts.toJS())
108+
streams := fixedLengthStreamClass.New(contentLength)
109+
rs := streams.Get("readable")
110+
body.Call("pipeTo", streams.Get("writable"))
111+
*/
112+
b, err := io.ReadAll(value)
113+
if err != nil {
114+
return nil, err
115+
}
116+
defer value.Close()
117+
ua := newUint8Array(len(b))
118+
js.CopyBytesToJS(ua, b)
119+
p := r.instance.Call("put", key, ua.Get("buffer"), opts.toJS())
107120
v, err := awaitPromise(p)
108121
if err != nil {
109122
return nil, err

r2objects.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,15 @@ func toR2Objects(v js.Value) (*R2Objects, error) {
2828
}
2929
objects[i] = obj
3030
}
31-
prefixesVal := objectsVal.Get("delimitedPrefixes")
31+
prefixesVal := v.Get("delimitedPrefixes")
3232
prefixes := make([]string, prefixesVal.Length())
3333
for i := 0; i < len(prefixes); i++ {
3434
prefixes[i] = prefixesVal.Index(i).String()
3535
}
3636
return &R2Objects{
3737
Objects: objects,
38-
Truncated: objectsVal.Get("truncated").Bool(),
39-
Cursor: maybeString(objectsVal.Get("cursor")),
38+
Truncated: v.Get("truncated").Bool(),
39+
Cursor: maybeString(v.Get("cursor")),
4040
DelimitedPrefixes: prefixes,
4141
}, nil
4242
}

0 commit comments

Comments
 (0)