diff --git a/.changelog/1335.txt b/.changelog/1335.txt new file mode 100644 index 00000000000..13b520854ae --- /dev/null +++ b/.changelog/1335.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +images: adds ability to upload image by url +``` diff --git a/images.go b/images.go index b93b2f46de5..59209ad1f78 100644 --- a/images.go +++ b/images.go @@ -37,6 +37,7 @@ type Image struct { // 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{} @@ -45,33 +46,43 @@ type UploadImageParams struct { // 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 { - return errors.New("a file to upload must be specified") + if b.File == nil && b.URL == "" { + return errors.New("a file or url to upload must be specified") } - name := b.Name - part, err := mpw.CreateFormFile("file", name) - if err != nil { - return err - } - _, err = io.Copy(part, b.File) - if err != nil { + + 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() - 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") + err := mpw.WriteField("requireSignedURLs", "true") if err != nil { return err } } if b.Metadata != nil { - part, err = mpw.CreateFormField("metadata") + part, err := mpw.CreateFormField("metadata") if err != nil { return err } @@ -151,6 +162,10 @@ func (api *API) UploadImage(ctx context.Context, rc *ResourceContainer, params U 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{} diff --git a/images_test.go b/images_test.go index 3640d9f3131..43cd8ad3498 100644 --- a/images_test.go +++ b/images_test.go @@ -96,6 +96,61 @@ func TestUploadImage(t *testing.T) { } } +func TestUploadImageByUrl(t *testing.T) { + setup() + defer teardown() + + handler := func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method) + + u, err := parseImageMultipartUpload(r) + if !assert.NoError(t, err) { + w.WriteHeader(http.StatusBadRequest) + return + } + assert.Equal(t, u.RequireSignedURLs, true) + assert.Equal(t, u.Metadata, map[string]interface{}{"meta": "metaID"}) + assert.Equal(t, u.Url, "https://www.images-elsewhere.com/avatar.png") + + w.Header().Set("content-type", "application/json") + fmt.Fprintf(w, `{ + "success": true, + "errors": [], + "messages": [], + "result": { + "id": "ZxR0pLaXRldlBtaFhhO2FiZGVnaA", + "filename": "avatar.png", + "metadata": { + "meta": "metaID" + }, + "requireSignedURLs": true, + "variants": [ + "https://imagedelivery.net/MTt4OTd0b0w5aj/ZxR0pLaXRldlBtaFhhO2FiZGVnaA/hero", + "https://imagedelivery.net/MTt4OTd0b0w5aj/ZxR0pLaXRldlBtaFhhO2FiZGVnaA/original", + "https://imagedelivery.net/MTt4OTd0b0w5aj/ZxR0pLaXRldlBtaFhhO2FiZGVnaA/thumbnail" + ], + "uploaded": "2014-01-02T02:20:00Z" + } + } + `) + } + + mux.HandleFunc("/accounts/"+testAccountID+"/images/v1", handler) + want := expectedImageStruct + + actual, err := client.UploadImage(context.Background(), AccountIdentifier(testAccountID), UploadImageParams{ + URL: "https://www.images-elsewhere.com/avatar.png", + RequireSignedURLs: true, + Metadata: map[string]interface{}{ + "meta": "metaID", + }, + }) + + if assert.NoError(t, err) { + assert.Equal(t, want, actual) + } +} + func TestUpdateImage(t *testing.T) { setup() defer teardown() @@ -197,6 +252,20 @@ func TestCreateImageDirectUploadURL(t *testing.T) { } } +func TestCreateImageConflictingTypes(t *testing.T) { + setup() + defer teardown() + + _, err := client.UploadImage(context.Background(), AccountIdentifier(testAccountID), UploadImageParams{ + URL: "https://example.com/foo.jpg", + File: fakeFile{ + Buffer: bytes.NewBufferString("this is definitely an image"), + }, + }) + + assert.Error(t, err) +} + func TestCreateImageDirectUploadURLV2(t *testing.T) { setup() defer teardown() @@ -387,6 +456,7 @@ type imageMultipartUpload struct { // this is for testing, never read an entire file into memory, // especially when being done on a per-http request basis. File []byte + Url string RequireSignedURLs bool Metadata map[string]interface{} } @@ -418,15 +488,27 @@ func parseImageMultipartUpload(r *http.Request) (imageMultipartUpload, error) { } } - f, _, err := r.FormFile("file") - if err != nil { - return u, err - } - defer f.Close() + if _, ok := r.MultipartForm.Value["url"]; ok { + urlBytes, err := getImageFormValue(r, "url") + if err != nil { + if !strings.HasPrefix(err.Error(), "no value found for key") { + return u, err + } + } + if urlBytes != nil { + u.Url = string(urlBytes) + } + } else { + f, _, err := r.FormFile("file") + if err != nil { + return u, err + } + defer f.Close() - u.File, err = io.ReadAll(f) - if err != nil { - return u, err + u.File, err = io.ReadAll(f) + if err != nil { + return u, err + } } return u, nil