diff --git a/examples/r2-image-server/main.go b/examples/r2-image-server/main.go index d1d4e5a..0313a67 100644 --- a/examples/r2-image-server/main.go +++ b/examples/r2-image-server/main.go @@ -41,8 +41,8 @@ func handler(w http.ResponseWriter, req *http.Request) { 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 + if imgObj.HTTPMetadata.ContentType != "" { + contentType = imgObj.HTTPMetadata.ContentType } w.Header().Set("Content-Type", contentType) io.Copy(w, imgObj.Body) diff --git a/jsutil.go b/jsutil.go index 030ebc1..e84b891 100644 --- a/jsutil.go +++ b/jsutil.go @@ -18,6 +18,8 @@ var ( errorClass = global.Get("Error") readableStreamClass = global.Get("ReadableStream") stringClass = global.Get("String") + dateClass = global.Get("Date") + numberClass = global.Get("Number") ) func newObject() js.Value { @@ -77,24 +79,19 @@ func strRecordToMap(v js.Value) map[string]string { } // maybeString returns string value of given JavaScript value or returns nil if the value is undefined. -func maybeString(v js.Value) *string { +func maybeString(v js.Value) string { if v.IsUndefined() { - return nil + return "" } - s := v.String() - return &s + return v.String() } // 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) { +func maybeDate(v js.Value) (time.Time, error) { if v.IsUndefined() { - return nil, nil + return time.Time{}, nil } - d, err := dateToTime(v) - if err != nil { - return nil, err - } - return &d, nil + return dateToTime(v) } // dateToTime converts JavaScript side's Data object into time.Time. @@ -106,3 +103,9 @@ func dateToTime(v js.Value) (time.Time, error) { } return time.UnixMilli(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)) +} diff --git a/r2bucket.go b/r2bucket.go index 87fc6de..23b9ab2 100644 --- a/r2bucket.go +++ b/r2bucket.go @@ -12,7 +12,7 @@ import ( type R2Bucket interface { Head(key string) (*R2Object, error) Get(key string) (*R2Object, error) - Put(key string, value io.Reader) error + Put(key string, value io.ReadCloser, opts *R2PutOptions) (*R2Object, error) Delete(key string) error List() (*R2Objects, error) } @@ -66,12 +66,57 @@ func (r *r2Bucket) Get(key string) (*R2Object, error) { return toR2Object(v) } -func (r *r2Bucket) Put(key string, value io.Reader) error { - panic("implement me") +type R2PutOptions struct { + HTTPMetadata R2HTTPMetadata + CustomMetadata map[string]string + MD5 string } +func (opts *R2PutOptions) toJS() js.Value { + if opts == nil { + return js.Undefined() + } + obj := newObject() + if opts.HTTPMetadata != (R2HTTPMetadata{}) { + obj.Set("httpMetadata", opts.HTTPMetadata.toJS()) + } + if opts.CustomMetadata != nil { + // convert map[string]string to map[string]any. + // This makes the map convertible to JS. + // see: https://pkg.go.dev/syscall/js#ValueOf + customMeta := make(map[string]any, len(opts.CustomMetadata)) + for k, v := range opts.CustomMetadata { + customMeta[k] = v + } + obj.Set("customMetadata", customMeta) + } + if opts.MD5 != "" { + obj.Set("md5", opts.MD5) + } + return obj +} + +// Put returns the result of `put` call to R2Bucket. +// * 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) { + body := convertReaderToReadableStream(value) + p := r.instance.Call("put", key, body, opts.toJS()) + v, err := awaitPromise(p) + if err != nil { + return nil, err + } + return toR2Object(v) +} + +// Delete returns the result of `delete` call to R2Bucket. +// * if a network error happens, returns error. func (r *r2Bucket) Delete(key string) error { - panic("implement me") + p := r.instance.Call("delete", key) + if _, err := awaitPromise(p); err != nil { + return err + } + return nil } // List returns the result of `list` call to R2Bucket. diff --git a/r2object.go b/r2object.go index 560383d..d6b07fc 100644 --- a/r2object.go +++ b/r2object.go @@ -71,12 +71,12 @@ func toR2Object(v js.Value) (*R2Object, error) { // R2HTTPMetadata represents metadata of R2Object. // * 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 + ContentType string + ContentLanguage string + ContentDisposition string + ContentEncoding string + CacheControl string + CacheExpiry time.Time } func toR2HTTPMetadata(v js.Value) (R2HTTPMetadata, error) { @@ -93,3 +93,23 @@ func toR2HTTPMetadata(v js.Value) (R2HTTPMetadata, error) { CacheExpiry: cacheExpiry, }, nil } + +func (md *R2HTTPMetadata) toJS() js.Value { + obj := newObject() + kv := map[string]string{ + "contentType": md.ContentType, + "contentLanguage": md.ContentLanguage, + "contentDisposition": md.ContentDisposition, + "contentEncoding": md.ContentEncoding, + "cacheControl": md.CacheControl, + } + for k, v := range kv { + if v != "" { + obj.Set(k, v) + } + } + if !md.CacheExpiry.IsZero() { + obj.Set("cacheExpiry", timeToDate(md.CacheExpiry)) + } + return obj +}