Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adds method to create image v2 direct upload url #1322

Merged
merged 10 commits into from
Jun 30, 2023
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