Skip to content

Commit b138cfa

Browse files
authored
Merge pull request #764 from matthewpi/feature/images
add support for Cloudflare Images
2 parents 9a6a709 + a19385e commit b138cfa

File tree

4 files changed

+700
-11
lines changed

4 files changed

+700
-11
lines changed

cloudflare.go

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -183,31 +183,28 @@ func (api *API) makeRequestWithAuthType(ctx context.Context, method, uri string,
183183
}
184184

185185
func (api *API) makeRequestWithAuthTypeAndHeaders(ctx context.Context, method, uri string, params interface{}, authType int, headers http.Header) ([]byte, error) {
186-
// Replace nil with a JSON object if needed
187-
var jsonBody []byte
186+
var reqBody io.Reader
188187
var err error
189188

190189
if params != nil {
191-
if paramBytes, ok := params.([]byte); ok {
192-
jsonBody = paramBytes
190+
if r, ok := params.(io.Reader); ok {
191+
reqBody = r
192+
} else if paramBytes, ok := params.([]byte); ok {
193+
reqBody = bytes.NewReader(paramBytes)
193194
} else {
195+
var jsonBody []byte
194196
jsonBody, err = json.Marshal(params)
195197
if err != nil {
196198
return nil, errors.Wrap(err, "error marshalling params to JSON")
197199
}
200+
reqBody = bytes.NewReader(jsonBody)
198201
}
199-
} else {
200-
jsonBody = nil
201202
}
202203

203204
var resp *http.Response
204205
var respErr error
205-
var reqBody io.Reader
206206
var respBody []byte
207207
for i := 0; i <= api.retryPolicy.MaxRetries; i++ {
208-
if jsonBody != nil {
209-
reqBody = bytes.NewReader(jsonBody)
210-
}
211208
if i > 0 {
212209
// expect the backoff introduced here on errored requests to dominate the effect of rate limiting
213210
// don't need a random component here as the rate limiter should do something similar
@@ -426,6 +423,7 @@ type Logger interface {
426423

427424
// ReqOption is a functional option for configuring API requests.
428425
type ReqOption func(opt *reqOption)
426+
429427
type reqOption struct {
430428
params url.Values
431429
}

images.go

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
package cloudflare
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"mime/multipart"
10+
"net/http"
11+
"net/url"
12+
"strconv"
13+
"time"
14+
15+
"github.com/pkg/errors"
16+
)
17+
18+
// Image represents a Cloudflare Image.
19+
type Image struct {
20+
ID string `json:"id"`
21+
Filename string `json:"filename"`
22+
Metadata map[string]interface{} `json:"metadata,omitempty"`
23+
RequireSignedURLs bool `json:"requireSignedURLs"`
24+
Variants []string `json:"variants"`
25+
Uploaded time.Time `json:"uploaded"`
26+
}
27+
28+
// ImageUploadRequest is the data required for an Image Upload request.
29+
type ImageUploadRequest struct {
30+
File io.ReadCloser
31+
Name string
32+
RequireSignedURLs bool
33+
Metadata map[string]interface{}
34+
}
35+
36+
// write writes the image upload data to a multipart writer, so
37+
// it can be used in an HTTP request.
38+
func (b ImageUploadRequest) write(mpw *multipart.Writer) error {
39+
if b.File == nil {
40+
return errors.New("a file to upload must be specified")
41+
}
42+
name := b.Name
43+
part, err := mpw.CreateFormFile("file", name)
44+
if err != nil {
45+
return err
46+
}
47+
_, err = io.Copy(part, b.File)
48+
if err != nil {
49+
_ = b.File.Close()
50+
return err
51+
}
52+
_ = b.File.Close()
53+
54+
// According to the Cloudflare docs, this field defaults to false.
55+
// For simplicity, we will only send it if the value is true, however
56+
// if the default is changed to true, this logic will need to be updated.
57+
if b.RequireSignedURLs {
58+
err = mpw.WriteField("requireSignedURLs", "true")
59+
if err != nil {
60+
return err
61+
}
62+
}
63+
64+
if b.Metadata != nil {
65+
part, err = mpw.CreateFormField("metadata")
66+
if err != nil {
67+
return err
68+
}
69+
err = json.NewEncoder(part).Encode(b.Metadata)
70+
if err != nil {
71+
return err
72+
}
73+
}
74+
75+
return nil
76+
}
77+
78+
// ImageUpdateRequest is the data required for an UpdateImage request.
79+
type ImageUpdateRequest struct {
80+
RequireSignedURLs bool `json:"requireSignedURLs"`
81+
Metadata map[string]interface{} `json:"metadata,omitempty"`
82+
}
83+
84+
// ImageDirectUploadURLRequest is the data required for a CreateImageDirectUploadURL request.
85+
type ImageDirectUploadURLRequest struct {
86+
Expiry time.Time `json:"expiry"`
87+
}
88+
89+
// ImageDirectUploadURLResponse is the API response for a direct image upload url.
90+
type ImageDirectUploadURLResponse struct {
91+
Result ImageDirectUploadURL `json:"result"`
92+
Response
93+
}
94+
95+
// ImageDirectUploadURL .
96+
type ImageDirectUploadURL struct {
97+
ID string `json:"id"`
98+
UploadURL string `json:"uploadURL"`
99+
}
100+
101+
// ImagesListResponse is the API response for listing all images.
102+
type ImagesListResponse struct {
103+
Result struct {
104+
Images []Image `json:"images"`
105+
} `json:"result"`
106+
Response
107+
}
108+
109+
// ImageDetailsResponse is the API response for getting an image's details.
110+
type ImageDetailsResponse struct {
111+
Result Image `json:"result"`
112+
Response
113+
}
114+
115+
// ImagesStatsResponse is the API response for image stats.
116+
type ImagesStatsResponse struct {
117+
Result struct {
118+
Count ImagesStatsCount `json:"count"`
119+
} `json:"result"`
120+
Response
121+
}
122+
123+
// ImagesStatsCount is the stats attached to a ImagesStatsResponse.
124+
type ImagesStatsCount struct {
125+
Current int64 `json:"current"`
126+
Allowed int64 `json:"allowed"`
127+
}
128+
129+
// UploadImage uploads a single image.
130+
//
131+
// API Reference: https://api.cloudflare.com/#cloudflare-images-upload-an-image-using-a-single-http-request
132+
func (api *API) UploadImage(ctx context.Context, accountID string, upload ImageUploadRequest) (Image, error) {
133+
uri := fmt.Sprintf("/accounts/%s/images/v1", accountID)
134+
135+
body := &bytes.Buffer{}
136+
w := multipart.NewWriter(body)
137+
if err := upload.write(w); err != nil {
138+
_ = w.Close()
139+
return Image{}, errors.Wrap(err, "error writing multipart body")
140+
}
141+
_ = w.Close()
142+
143+
res, err := api.makeRequestWithAuthTypeAndHeaders(
144+
ctx,
145+
http.MethodPost,
146+
uri,
147+
body,
148+
api.authType,
149+
http.Header{
150+
"Accept": []string{"application/json"},
151+
"Content-Type": []string{w.FormDataContentType()},
152+
},
153+
)
154+
if err != nil {
155+
return Image{}, err
156+
}
157+
158+
var imageDetailsResponse ImageDetailsResponse
159+
err = json.Unmarshal(res, &imageDetailsResponse)
160+
if err != nil {
161+
return Image{}, errors.Wrap(err, errUnmarshalError)
162+
}
163+
return imageDetailsResponse.Result, nil
164+
}
165+
166+
// UpdateImage updates an existing image's metadata.
167+
//
168+
// API Reference: https://api.cloudflare.com/#cloudflare-images-update-image
169+
func (api *API) UpdateImage(ctx context.Context, accountID string, id string, image ImageUpdateRequest) (Image, error) {
170+
uri := fmt.Sprintf("/accounts/%s/images/v1/%s", accountID, id)
171+
172+
res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, image)
173+
if err != nil {
174+
return Image{}, err
175+
}
176+
177+
var imageDetailsResponse ImageDetailsResponse
178+
err = json.Unmarshal(res, &imageDetailsResponse)
179+
if err != nil {
180+
return Image{}, errors.Wrap(err, errUnmarshalError)
181+
}
182+
return imageDetailsResponse.Result, nil
183+
}
184+
185+
// CreateImageDirectUploadURL creates an authenticated direct upload url.
186+
//
187+
// API Reference: https://api.cloudflare.com/#cloudflare-images-create-authenticated-direct-upload-url
188+
func (api *API) CreateImageDirectUploadURL(ctx context.Context, accountID string, params ImageDirectUploadURLRequest) (ImageDirectUploadURL, error) {
189+
uri := fmt.Sprintf("/accounts/%s/images/v1/direct_upload", accountID)
190+
191+
res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
192+
if err != nil {
193+
return ImageDirectUploadURL{}, err
194+
}
195+
196+
var imageDirectUploadURLResponse ImageDirectUploadURLResponse
197+
err = json.Unmarshal(res, &imageDirectUploadURLResponse)
198+
if err != nil {
199+
return ImageDirectUploadURL{}, errors.Wrap(err, errUnmarshalError)
200+
}
201+
return imageDirectUploadURLResponse.Result, nil
202+
}
203+
204+
// ListImages lists all images.
205+
//
206+
// API Reference: https://api.cloudflare.com/#cloudflare-images-list-images
207+
func (api *API) ListImages(ctx context.Context, accountID string, pageOpts PaginationOptions) ([]Image, error) {
208+
v := url.Values{}
209+
if pageOpts.PerPage > 0 {
210+
v.Set("per_page", strconv.Itoa(pageOpts.PerPage))
211+
}
212+
if pageOpts.Page > 0 {
213+
v.Set("page", strconv.Itoa(pageOpts.Page))
214+
}
215+
216+
uri := fmt.Sprintf("/accounts/%s/images/v1", accountID)
217+
if len(v) > 0 {
218+
uri = fmt.Sprintf("%s?%s", uri, v.Encode())
219+
}
220+
221+
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
222+
if err != nil {
223+
return []Image{}, err
224+
}
225+
226+
var imagesListResponse ImagesListResponse
227+
err = json.Unmarshal(res, &imagesListResponse)
228+
if err != nil {
229+
return []Image{}, errors.Wrap(err, errUnmarshalError)
230+
}
231+
return imagesListResponse.Result.Images, nil
232+
}
233+
234+
// ImageDetails gets the details of an uploaded image.
235+
//
236+
// API Reference: https://api.cloudflare.com/#cloudflare-images-image-details
237+
func (api *API) ImageDetails(ctx context.Context, accountID string, id string) (Image, error) {
238+
uri := fmt.Sprintf("/accounts/%s/images/v1/%s", accountID, id)
239+
240+
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
241+
if err != nil {
242+
return Image{}, err
243+
}
244+
245+
var imageDetailsResponse ImageDetailsResponse
246+
err = json.Unmarshal(res, &imageDetailsResponse)
247+
if err != nil {
248+
return Image{}, errors.Wrap(err, errUnmarshalError)
249+
}
250+
return imageDetailsResponse.Result, nil
251+
}
252+
253+
// BaseImage gets the base image used to derive variants.
254+
//
255+
// API Reference: https://api.cloudflare.com/#cloudflare-images-base-image
256+
func (api *API) BaseImage(ctx context.Context, accountID string, id string) ([]byte, error) {
257+
uri := fmt.Sprintf("/accounts/%s/images/v1/%s/blob", accountID, id)
258+
259+
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
260+
if err != nil {
261+
return nil, err
262+
}
263+
return res, nil
264+
}
265+
266+
// DeleteImage deletes an image.
267+
//
268+
// API Reference: https://api.cloudflare.com/#cloudflare-images-delete-image
269+
func (api *API) DeleteImage(ctx context.Context, accountID string, id string) error {
270+
uri := fmt.Sprintf("/accounts/%s/images/v1/%s", accountID, id)
271+
272+
_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
273+
if err != nil {
274+
return err
275+
}
276+
return nil
277+
}
278+
279+
// ImagesStats gets an account's statistics for Cloudflare Images.
280+
//
281+
// API Reference: https://api.cloudflare.com/#cloudflare-images-images-usage-statistics
282+
func (api *API) ImagesStats(ctx context.Context, accountID string) (ImagesStatsCount, error) {
283+
uri := fmt.Sprintf("/accounts/%s/images/v1/stats", accountID)
284+
285+
res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
286+
if err != nil {
287+
return ImagesStatsCount{}, err
288+
}
289+
290+
var imagesStatsResponse ImagesStatsResponse
291+
err = json.Unmarshal(res, &imagesStatsResponse)
292+
if err != nil {
293+
return ImagesStatsCount{}, errors.Wrap(err, errUnmarshalError)
294+
}
295+
return imagesStatsResponse.Result.Count, nil
296+
}

0 commit comments

Comments
 (0)