Skip to content

Commit

Permalink
Merge pull request #1322 from cloudflare/feature/images-v2-direct-upload
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobbednarz committed Jun 30, 2023
2 parents 8c81839 + 768c9b2 commit b4b8ba9
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 42 deletions.
31 changes: 31 additions & 0 deletions .changelog/1322.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
```release-note:enhancement
images: adds support for v2 when uploading images directly
```

```release-note:breaking-change
images: updated method signatures of `UploadImage` to match newer conventions and standards
```

```release-note:breaking-change
images: updated method signatures of `UpdateImage` to match newer conventions and standards
```

```release-note:breaking-change
images: updated method signatures of `ListImages` to match newer conventions and standards
```

```release-note:breaking-change
images: updated method signatures of `DeleteImage` to match newer conventions and standards
```

```release-note:breaking-change
images: renamed `ImageDetails` to `GetImage` to match library conventions
```

```release-note:breaking-change
images: renamed `BaseImage` to `GetBaseImage` to match library conventions
```

```release-note:breaking-change
images: renamed `ImagesStats` to `GetImagesStats` to match library conventions
```
164 changes: 134 additions & 30 deletions images.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ import (
"time"
)

var (
ErrInvalidImagesAPIVersion = errors.New("invalid images API version")
ErrMissingImageID = errors.New("required image ID missing")
)

type ImagesAPIVersion string

const (
ImagesAPIVersionV1 ImagesAPIVersion = "v1"
ImagesAPIVersionV2 ImagesAPIVersion = "v2"
)

// Image represents a Cloudflare Image.
type Image struct {
ID string `json:"id"`
Expand All @@ -22,8 +34,8 @@ type Image struct {
Uploaded time.Time `json:"uploaded"`
}

// ImageUploadRequest is the data required for an Image Upload request.
type ImageUploadRequest struct {
// UploadImageParams is the data required for an Image Upload request.
type UploadImageParams struct {
File io.ReadCloser
Name string
RequireSignedURLs bool
Expand All @@ -32,7 +44,7 @@ type ImageUploadRequest struct {

// write writes the image upload data to a multipart writer, so
// it can be used in an HTTP request.
func (b ImageUploadRequest) write(mpw *multipart.Writer) error {
func (b UploadImageParams) write(mpw *multipart.Writer) error {
if b.File == nil {
return errors.New("a file to upload must be specified")
}
Expand Down Expand Up @@ -72,15 +84,19 @@ func (b ImageUploadRequest) write(mpw *multipart.Writer) error {
return nil
}

// ImageUpdateRequest is the data required for an UpdateImage request.
type ImageUpdateRequest struct {
// UpdateImageParams is the data required for an UpdateImage request.
type UpdateImageParams struct {
ID string `json:"-"`
RequireSignedURLs bool `json:"requireSignedURLs"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}

// ImageDirectUploadURLRequest is the data required for a CreateImageDirectUploadURL request.
type ImageDirectUploadURLRequest struct {
Expiry time.Time `json:"expiry"`
// CreateImageDirectUploadURLParams is the data required for a CreateImageDirectUploadURL request.
type CreateImageDirectUploadURLParams struct {
Version ImagesAPIVersion `json:"-"`
Expiry *time.Time `json:"expiry,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
RequireSignedURLs *bool `json:"requireSignedURLs,omitempty"`
}

// ImageDirectUploadURLResponse is the API response for a direct image upload url.
Expand Down Expand Up @@ -123,15 +139,23 @@ type ImagesStatsCount struct {
Allowed int64 `json:"allowed"`
}

type ListImagesParams struct {
ResultInfo
}

// UploadImage uploads a single image.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-upload-an-image-using-a-single-http-request
func (api *API) UploadImage(ctx context.Context, accountID string, upload ImageUploadRequest) (Image, error) {
uri := fmt.Sprintf("/accounts/%s/images/v1", accountID)
func (api *API) UploadImage(ctx context.Context, rc *ResourceContainer, params UploadImageParams) (Image, error) {
if rc.Level != AccountRouteLevel {
return Image{}, ErrRequiredAccountLevelResourceContainer
}

uri := fmt.Sprintf("/accounts/%s/images/v1", rc.Identifier)

body := &bytes.Buffer{}
w := multipart.NewWriter(body)
if err := upload.write(w); err != nil {
if err := params.write(w); err != nil {
_ = w.Close()
return Image{}, fmt.Errorf("error writing multipart body: %w", err)
}
Expand Down Expand Up @@ -162,10 +186,14 @@ func (api *API) UploadImage(ctx context.Context, accountID string, upload ImageU
// UpdateImage updates an existing image's metadata.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-update-image
func (api *API) UpdateImage(ctx context.Context, accountID string, id string, image ImageUpdateRequest) (Image, error) {
uri := fmt.Sprintf("/accounts/%s/images/v1/%s", accountID, id)
func (api *API) UpdateImage(ctx context.Context, rc *ResourceContainer, params UpdateImageParams) (Image, error) {
if rc.Level != AccountRouteLevel {
return Image{}, ErrRequiredAccountLevelResourceContainer
}

res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, image)
uri := fmt.Sprintf("/accounts/%s/images/v1/%s", rc.Identifier, params.ID)

res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
if err != nil {
return Image{}, err
}
Expand All @@ -178,13 +206,73 @@ func (api *API) UpdateImage(ctx context.Context, accountID string, id string, im
return imageDetailsResponse.Result, nil
}

var imagesMultipartBoundary = "----CloudflareImagesGoClientBoundary"

// CreateImageDirectUploadURL creates an authenticated direct upload url.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-create-authenticated-direct-upload-url
func (api *API) CreateImageDirectUploadURL(ctx context.Context, accountID string, params ImageDirectUploadURLRequest) (ImageDirectUploadURL, error) {
uri := fmt.Sprintf("/accounts/%s/images/v1/direct_upload", accountID)
func (api *API) CreateImageDirectUploadURL(ctx context.Context, rc *ResourceContainer, params CreateImageDirectUploadURLParams) (ImageDirectUploadURL, error) {
if rc.Level != AccountRouteLevel {
return ImageDirectUploadURL{}, ErrRequiredAccountLevelResourceContainer
}

if params.Version != "" && params.Version != ImagesAPIVersionV1 && params.Version != ImagesAPIVersionV2 {
return ImageDirectUploadURL{}, ErrInvalidImagesAPIVersion
}

var err error
var uri string
var res []byte
switch params.Version {
case ImagesAPIVersionV2:
uri = fmt.Sprintf("/%s/%s/images/%s/direct_upload", rc.Level, rc.Identifier, params.Version)
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
if err := writer.SetBoundary(imagesMultipartBoundary); err != nil {
return ImageDirectUploadURL{}, fmt.Errorf("error setting multipart boundary")
}

if *params.RequireSignedURLs {
if err = writer.WriteField("requireSignedURLs", "true"); err != nil {
return ImageDirectUploadURL{}, fmt.Errorf("error writing requireSignedURLs field: %w", err)
}
}
if !params.Expiry.IsZero() {
if err = writer.WriteField("expiry", params.Expiry.Format(time.RFC3339)); err != nil {
return ImageDirectUploadURL{}, fmt.Errorf("error writing expiry field: %w", err)
}
}
if params.Metadata != nil {
var metadataBytes []byte
if metadataBytes, err = json.Marshal(params.Metadata); err != nil {
return ImageDirectUploadURL{}, fmt.Errorf("error marshalling metadata to JSON: %w", err)
}
if err = writer.WriteField("metadata", string(metadataBytes)); err != nil {
return ImageDirectUploadURL{}, fmt.Errorf("error writing metadata field: %w", err)
}
}
if err = writer.Close(); err != nil {
return ImageDirectUploadURL{}, fmt.Errorf("error closing multipart writer: %w", err)
}

res, err = api.makeRequestContextWithHeaders(
ctx,
http.MethodPost,
uri,
body,
http.Header{
"Accept": []string{"application/json"},
"Content-Type": []string{writer.FormDataContentType()},
},
)
case ImagesAPIVersionV1:
case "":
uri = fmt.Sprintf("/%s/%s/images/%s/direct_upload", rc.Level, rc.Identifier, ImagesAPIVersionV1)
res, err = api.makeRequestContext(ctx, http.MethodPost, uri, params)
default:
return ImageDirectUploadURL{}, ErrInvalidImagesAPIVersion
}

res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
if err != nil {
return ImageDirectUploadURL{}, err
}
Expand All @@ -200,8 +288,8 @@ func (api *API) CreateImageDirectUploadURL(ctx context.Context, accountID string
// ListImages lists all images.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-list-images
func (api *API) ListImages(ctx context.Context, accountID string, pageOpts PaginationOptions) ([]Image, error) {
uri := buildURI(fmt.Sprintf("/accounts/%s/images/v1", accountID), pageOpts)
func (api *API) ListImages(ctx context.Context, rc *ResourceContainer, params ListImagesParams) ([]Image, error) {
uri := buildURI(fmt.Sprintf("/accounts/%s/images/v1", rc.Identifier), params)

res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
Expand All @@ -216,11 +304,15 @@ func (api *API) ListImages(ctx context.Context, accountID string, pageOpts Pagin
return imagesListResponse.Result.Images, nil
}

// ImageDetails gets the details of an uploaded image.
// GetImage gets the details of an uploaded image.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-image-details
func (api *API) ImageDetails(ctx context.Context, accountID string, id string) (Image, error) {
uri := fmt.Sprintf("/accounts/%s/images/v1/%s", accountID, id)
func (api *API) GetImage(ctx context.Context, rc *ResourceContainer, id string) (Image, error) {
if rc.Level != AccountRouteLevel {
return Image{}, ErrRequiredAccountLevelResourceContainer
}

uri := fmt.Sprintf("/accounts/%s/images/v1/%s", rc.Identifier, id)

res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
Expand All @@ -235,11 +327,15 @@ func (api *API) ImageDetails(ctx context.Context, accountID string, id string) (
return imageDetailsResponse.Result, nil
}

// BaseImage gets the base image used to derive variants.
// GetBaseImage gets the base image used to derive variants.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-base-image
func (api *API) BaseImage(ctx context.Context, accountID string, id string) ([]byte, error) {
uri := fmt.Sprintf("/accounts/%s/images/v1/%s/blob", accountID, id)
func (api *API) GetBaseImage(ctx context.Context, rc *ResourceContainer, id string) ([]byte, error) {
if rc.Level != AccountRouteLevel {
return []byte{}, ErrRequiredAccountLevelResourceContainer
}

uri := fmt.Sprintf("/accounts/%s/images/v1/%s/blob", rc.Identifier, id)

res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
Expand All @@ -251,8 +347,12 @@ func (api *API) BaseImage(ctx context.Context, accountID string, id string) ([]b
// DeleteImage deletes an image.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-delete-image
func (api *API) DeleteImage(ctx context.Context, accountID string, id string) error {
uri := fmt.Sprintf("/accounts/%s/images/v1/%s", accountID, id)
func (api *API) DeleteImage(ctx context.Context, rc *ResourceContainer, id string) error {
if rc.Level != AccountRouteLevel {
return ErrRequiredAccountLevelResourceContainer
}

uri := fmt.Sprintf("/accounts/%s/images/v1/%s", rc.Identifier, id)

_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
if err != nil {
Expand All @@ -261,11 +361,15 @@ func (api *API) DeleteImage(ctx context.Context, accountID string, id string) er
return nil
}

// ImagesStats gets an account's statistics for Cloudflare Images.
// GetImagesStats gets an account's statistics for Cloudflare Images.
//
// API Reference: https://api.cloudflare.com/#cloudflare-images-images-usage-statistics
func (api *API) ImagesStats(ctx context.Context, accountID string) (ImagesStatsCount, error) {
uri := fmt.Sprintf("/accounts/%s/images/v1/stats", accountID)
func (api *API) GetImagesStats(ctx context.Context, rc *ResourceContainer) (ImagesStatsCount, error) {
if rc.Level != AccountRouteLevel {
return ImagesStatsCount{}, ErrRequiredAccountLevelResourceContainer
}

uri := fmt.Sprintf("/accounts/%s/images/v1/stats", rc.Identifier)

res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
if err != nil {
Expand Down

0 comments on commit b4b8ba9

Please sign in to comment.