mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2024-09-20 14:56:20 +08:00
7fd6a74e0c
Co-authored-by: Josh Zhang <jzhang1@stackoverflow.com>
402 lines
12 KiB
Go
402 lines
12 KiB
Go
package cloudflare
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/goccy/go-json"
|
|
)
|
|
|
|
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"`
|
|
Filename string `json:"filename"`
|
|
Meta map[string]interface{} `json:"meta,omitempty"`
|
|
RequireSignedURLs bool `json:"requireSignedURLs"`
|
|
Variants []string `json:"variants"`
|
|
Uploaded time.Time `json:"uploaded"`
|
|
}
|
|
|
|
// UploadImageParams is the data required for an Image Upload request.
|
|
type UploadImageParams struct {
|
|
File io.ReadCloser
|
|
URL string
|
|
Name string
|
|
RequireSignedURLs bool
|
|
Metadata map[string]interface{}
|
|
}
|
|
|
|
// write writes the image upload data to a multipart writer, so
|
|
// it can be used in an HTTP request.
|
|
func (b UploadImageParams) write(mpw *multipart.Writer) error {
|
|
if b.File == nil && b.URL == "" {
|
|
return errors.New("a file or url to upload must be specified")
|
|
}
|
|
|
|
if b.File != nil {
|
|
name := b.Name
|
|
part, err := mpw.CreateFormFile("file", name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(part, b.File)
|
|
if err != nil {
|
|
_ = b.File.Close()
|
|
return err
|
|
}
|
|
_ = b.File.Close()
|
|
}
|
|
|
|
if b.URL != "" {
|
|
err := mpw.WriteField("url", b.URL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// According to the Cloudflare docs, this field defaults to false.
|
|
// For simplicity, we will only send it if the value is true, however
|
|
// if the default is changed to true, this logic will need to be updated.
|
|
if b.RequireSignedURLs {
|
|
err := mpw.WriteField("requireSignedURLs", "true")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if b.Metadata != nil {
|
|
part, err := mpw.CreateFormField("metadata")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = json.NewEncoder(part).Encode(b.Metadata)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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.
|
|
type ImageDirectUploadURLResponse struct {
|
|
Result ImageDirectUploadURL `json:"result"`
|
|
Response
|
|
}
|
|
|
|
// ImageDirectUploadURL .
|
|
type ImageDirectUploadURL struct {
|
|
ID string `json:"id"`
|
|
UploadURL string `json:"uploadURL"`
|
|
}
|
|
|
|
// ImagesListResponse is the API response for listing all images.
|
|
type ImagesListResponse struct {
|
|
Result struct {
|
|
Images []Image `json:"images"`
|
|
} `json:"result"`
|
|
Response
|
|
}
|
|
|
|
// ImageDetailsResponse is the API response for getting an image's details.
|
|
type ImageDetailsResponse struct {
|
|
Result Image `json:"result"`
|
|
Response
|
|
}
|
|
|
|
// ImagesStatsResponse is the API response for image stats.
|
|
type ImagesStatsResponse struct {
|
|
Result struct {
|
|
Count ImagesStatsCount `json:"count"`
|
|
} `json:"result"`
|
|
Response
|
|
}
|
|
|
|
// ImagesStatsCount is the stats attached to a ImagesStatsResponse.
|
|
type ImagesStatsCount struct {
|
|
Current int64 `json:"current"`
|
|
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, rc *ResourceContainer, params UploadImageParams) (Image, error) {
|
|
if rc.Level != AccountRouteLevel {
|
|
return Image{}, ErrRequiredAccountLevelResourceContainer
|
|
}
|
|
|
|
if params.File != nil && params.URL != "" {
|
|
return Image{}, errors.New("file and url uploads are mutually exclusive and can only be performed individually")
|
|
}
|
|
|
|
uri := fmt.Sprintf("/accounts/%s/images/v1", rc.Identifier)
|
|
|
|
body := &bytes.Buffer{}
|
|
w := multipart.NewWriter(body)
|
|
if err := params.write(w); err != nil {
|
|
_ = w.Close()
|
|
return Image{}, fmt.Errorf("error writing multipart body: %w", err)
|
|
}
|
|
_ = w.Close()
|
|
|
|
res, err := api.makeRequestContextWithHeaders(
|
|
ctx,
|
|
http.MethodPost,
|
|
uri,
|
|
body,
|
|
http.Header{
|
|
"Accept": []string{"application/json"},
|
|
"Content-Type": []string{w.FormDataContentType()},
|
|
},
|
|
)
|
|
if err != nil {
|
|
return Image{}, err
|
|
}
|
|
|
|
var imageDetailsResponse ImageDetailsResponse
|
|
err = json.Unmarshal(res, &imageDetailsResponse)
|
|
if err != nil {
|
|
return Image{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
|
|
}
|
|
return imageDetailsResponse.Result, nil
|
|
}
|
|
|
|
// UpdateImage updates an existing image's metadata.
|
|
//
|
|
// API Reference: https://api.cloudflare.com/#cloudflare-images-update-image
|
|
func (api *API) UpdateImage(ctx context.Context, rc *ResourceContainer, params UpdateImageParams) (Image, error) {
|
|
if rc.Level != AccountRouteLevel {
|
|
return Image{}, ErrRequiredAccountLevelResourceContainer
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
var imageDetailsResponse ImageDetailsResponse
|
|
err = json.Unmarshal(res, &imageDetailsResponse)
|
|
if err != nil {
|
|
return Image{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
|
|
}
|
|
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, 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
|
|
}
|
|
|
|
if err != nil {
|
|
return ImageDirectUploadURL{}, err
|
|
}
|
|
|
|
var imageDirectUploadURLResponse ImageDirectUploadURLResponse
|
|
err = json.Unmarshal(res, &imageDirectUploadURLResponse)
|
|
if err != nil {
|
|
return ImageDirectUploadURL{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
|
|
}
|
|
return imageDirectUploadURLResponse.Result, nil
|
|
}
|
|
|
|
// ListImages lists all images.
|
|
//
|
|
// API Reference: https://api.cloudflare.com/#cloudflare-images-list-images
|
|
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 {
|
|
return []Image{}, err
|
|
}
|
|
|
|
var imagesListResponse ImagesListResponse
|
|
err = json.Unmarshal(res, &imagesListResponse)
|
|
if err != nil {
|
|
return []Image{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
|
|
}
|
|
return imagesListResponse.Result.Images, nil
|
|
}
|
|
|
|
// GetImage gets the details of an uploaded image.
|
|
//
|
|
// API Reference: https://api.cloudflare.com/#cloudflare-images-image-details
|
|
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 {
|
|
return Image{}, err
|
|
}
|
|
|
|
var imageDetailsResponse ImageDetailsResponse
|
|
err = json.Unmarshal(res, &imageDetailsResponse)
|
|
if err != nil {
|
|
return Image{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
|
|
}
|
|
return imageDetailsResponse.Result, nil
|
|
}
|
|
|
|
// GetBaseImage gets the base image used to derive variants.
|
|
//
|
|
// API Reference: https://api.cloudflare.com/#cloudflare-images-base-image
|
|
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 {
|
|
return nil, err
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// DeleteImage deletes an image.
|
|
//
|
|
// API Reference: https://api.cloudflare.com/#cloudflare-images-delete-image
|
|
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 {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetImagesStats gets an account's statistics for Cloudflare Images.
|
|
//
|
|
// API Reference: https://api.cloudflare.com/#cloudflare-images-images-usage-statistics
|
|
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 {
|
|
return ImagesStatsCount{}, err
|
|
}
|
|
|
|
var imagesStatsResponse ImagesStatsResponse
|
|
err = json.Unmarshal(res, &imagesStatsResponse)
|
|
if err != nil {
|
|
return ImagesStatsCount{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
|
|
}
|
|
return imagesStatsResponse.Result.Count, nil
|
|
}
|