From 77d0d257d399943eb0db969e8319fd3e093a905d Mon Sep 17 00:00:00 2001 From: Vartan Benohanian Date: Mon, 14 Sep 2020 10:32:39 -0400 Subject: [PATCH] Add methods to upload images to a subreddit Signed-off-by: Vartan Benohanian --- reddit/subreddit.go | 194 ++++++++++++++++++++++-------- reddit/subreddit_test.go | 250 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 381 insertions(+), 63 deletions(-) diff --git a/reddit/subreddit.go b/reddit/subreddit.go index 695c104..490b8e8 100644 --- a/reddit/subreddit.go +++ b/reddit/subreddit.go @@ -6,8 +6,12 @@ import ( "encoding/json" "errors" "fmt" + "io" + "mime/multipart" "net/http" "net/url" + "os" + "path/filepath" "strings" "github.com/google/go-querystring/query" @@ -840,54 +844,6 @@ func (s *SubredditService) UpdateStyleSheet(ctx context.Context, subreddit, styl return s.client.Do(ctx, req, nil) } -// RemoveHeaderImage removes the subreddit's custom header image. -// The call succeeds even if there's no header image. -func (s *SubredditService) RemoveHeaderImage(ctx context.Context, subreddit string) (*Response, error) { - path := fmt.Sprintf("r/%s/api/delete_sr_header", subreddit) - - form := url.Values{} - form.Set("api_type", "json") - - req, err := s.client.NewRequest(http.MethodPost, path, form) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// RemoveMobileIcon removes the subreddit's custom mobile icon. -// The call succeeds even if there's no mobile icon. -func (s *SubredditService) RemoveMobileIcon(ctx context.Context, subreddit string) (*Response, error) { - path := fmt.Sprintf("r/%s/api/delete_sr_icon", subreddit) - - form := url.Values{} - form.Set("api_type", "json") - - req, err := s.client.NewRequest(http.MethodPost, path, form) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// RemoveMobileBanner removes the subreddit's custom mobile banner. -// The call succeeds even if there's no mobile banner. -func (s *SubredditService) RemoveMobileBanner(ctx context.Context, subreddit string) (*Response, error) { - path := fmt.Sprintf("r/%s/api/delete_sr_banner", subreddit) - - form := url.Values{} - form.Set("api_type", "json") - - req, err := s.client.NewRequest(http.MethodPost, path, form) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - // RemoveImage removes an image from the subreddit's custom image set. // The call succeeds even if the named image does not exist. func (s *SubredditService) RemoveImage(ctx context.Context, subreddit, imageName string) (*Response, error) { @@ -904,3 +860,145 @@ func (s *SubredditService) RemoveImage(ctx context.Context, subreddit, imageName return s.client.Do(ctx, req, nil) } + +// RemoveHeader removes the subreddit's current header image. +// The call succeeds even if there's no header image. +func (s *SubredditService) RemoveHeader(ctx context.Context, subreddit string) (*Response, error) { + path := fmt.Sprintf("r/%s/api/delete_sr_header", subreddit) + + form := url.Values{} + form.Set("api_type", "json") + + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// RemoveMobileHeader removes the subreddit's current mobile header. +// The call succeeds even if there's no mobile header. +func (s *SubredditService) RemoveMobileHeader(ctx context.Context, subreddit string) (*Response, error) { + path := fmt.Sprintf("r/%s/api/delete_sr_banner", subreddit) + + form := url.Values{} + form.Set("api_type", "json") + + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// RemoveMobileIcon removes the subreddit's current mobile icon. +// The call succeeds even if there's no mobile icon. +func (s *SubredditService) RemoveMobileIcon(ctx context.Context, subreddit string) (*Response, error) { + path := fmt.Sprintf("r/%s/api/delete_sr_icon", subreddit) + + form := url.Values{} + form.Set("api_type", "json") + + req, err := s.client.NewRequest(http.MethodPost, path, form) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +func (s *SubredditService) uploadImage(ctx context.Context, subreddit, imagePath, imageType, imageName string) (string, *Response, error) { + file, err := os.Open(imagePath) + if err != nil { + return "", nil, err + } + defer file.Close() + + form := url.Values{} + form.Set("upload_type", imageType) + form.Set("name", imageName) + form.Set("img_type", "png") + + ext := filepath.Ext(file.Name()) + if strings.EqualFold(ext, ".jpg") { + form.Set("img_type", "jpg") + } + + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + + for k := range form { + writer.WriteField(k, form.Get(k)) + } + + part, err := writer.CreateFormFile("file", file.Name()) + if err != nil { + return "", nil, err + } + + _, err = io.Copy(part, file) + if err != nil { + return "", nil, err + } + + err = writer.Close() + if err != nil { + return "", nil, err + } + + path := fmt.Sprintf("r/%s/api/upload_sr_img", subreddit) + u, err := s.client.BaseURL.Parse(path) + if err != nil { + return "", nil, err + } + + req, err := http.NewRequest(http.MethodPost, u.String(), body) + if err != nil { + return "", nil, err + } + req.Header.Set(headerContentType, writer.FormDataContentType()) + + root := new(struct { + Errors []string `json:"errors"` + ErrorValues []string `json:"errors_values"` + ImageSource string `json:"img_src"` + }) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return "", resp, err + } + + if len(root.ErrorValues) > 0 { + err = fmt.Errorf("could not upload image: %s", strings.Join(root.ErrorValues, "; ")) + return "", resp, err + } + + return root.ImageSource, resp, nil +} + +// UploadImage uploads an image to the subreddit. +// If an image with the image name already exists, it it replaced. +// A successful call returns a link to the uploaded image. +func (s *SubredditService) UploadImage(ctx context.Context, subreddit, imagePath, imageName string) (string, *Response, error) { + return s.uploadImage(ctx, subreddit, imagePath, "img", imageName) +} + +// UploadHeader uploads an image to be user as the subreddit's header image. +// A successful call returns a link to the uploaded image. +func (s *SubredditService) UploadHeader(ctx context.Context, subreddit, imagePath, imageName string) (string, *Response, error) { + return s.uploadImage(ctx, subreddit, imagePath, "header", imageName) +} + +// UploadMobileHeader uploads an image to be user as the subreddit's mobile header image. +// A successful call returns a link to the uploaded image. +func (s *SubredditService) UploadMobileHeader(ctx context.Context, subreddit, imagePath, imageName string) (string, *Response, error) { + return s.uploadImage(ctx, subreddit, imagePath, "banner", imageName) +} + +// UploadMobileIcon uploads an image to be user as the subreddit's mobile icon. +// A successful call returns a link to the uploaded image. +func (s *SubredditService) UploadMobileIcon(ctx context.Context, subreddit, imagePath, imageName string) (string, *Response, error) { + return s.uploadImage(ctx, subreddit, imagePath, "icon", imageName) +} diff --git a/reddit/subreddit_test.go b/reddit/subreddit_test.go index 3c86156..3648ece 100644 --- a/reddit/subreddit_test.go +++ b/reddit/subreddit_test.go @@ -1,9 +1,13 @@ package reddit import ( + "bytes" "fmt" + "io" + "io/ioutil" "net/http" "net/url" + "os" "strings" "testing" "time" @@ -1227,7 +1231,27 @@ func TestSubredditService_UpdateStyleSheet(t *testing.T) { require.NoError(t, err) } -func TestSubredditService_RemoveHeaderImage(t *testing.T) { +func TestSubredditService_RemoveImage(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/delete_sr_img", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + form.Set("img_name", "testimage") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.Subreddit.RemoveImage(ctx, "testsubreddit", "testimage") + require.NoError(t, err) +} + +func TestSubredditService_RemoveHeader(t *testing.T) { client, mux, teardown := setup() defer teardown() @@ -1242,7 +1266,26 @@ func TestSubredditService_RemoveHeaderImage(t *testing.T) { require.Equal(t, form, r.PostForm) }) - _, err := client.Subreddit.RemoveHeaderImage(ctx, "testsubreddit") + _, err := client.Subreddit.RemoveHeader(ctx, "testsubreddit") + require.NoError(t, err) +} + +func TestSubredditService_RemoveMobileHeader(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + mux.HandleFunc("/r/testsubreddit/api/delete_sr_banner", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + form := url.Values{} + form.Set("api_type", "json") + + err := r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + }) + + _, err := client.Subreddit.RemoveMobileHeader(ctx, "testsubreddit") require.NoError(t, err) } @@ -1265,41 +1308,218 @@ func TestSubredditService_RemoveMobileIcon(t *testing.T) { require.NoError(t, err) } -func TestSubredditService_RemoveMobileBanner(t *testing.T) { +func TestSubredditService_UploadImage(t *testing.T) { client, mux, teardown := setup() defer teardown() - mux.HandleFunc("/r/testsubreddit/api/delete_sr_banner", func(w http.ResponseWriter, r *http.Request) { + imageFile, err := ioutil.TempFile("/tmp", "emoji*.png") + require.NoError(t, err) + defer func() { + imageFile.Close() + os.Remove(imageFile.Name()) + }() + + _, err = imageFile.WriteString("this is a test") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/api/upload_sr_img", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) - form := url.Values{} - form.Set("api_type", "json") + _, file, err := r.FormFile("file") + require.NoError(t, err) - err := r.ParseForm() + rdr, err := file.Open() + require.NoError(t, err) + + buf := new(bytes.Buffer) + _, err = io.Copy(buf, rdr) + require.NoError(t, err) + require.Equal(t, "this is a test", buf.String()) + + form := url.Values{} + form.Set("upload_type", "img") + form.Set("name", "testname") + form.Set("img_type", "png") + + err = r.ParseForm() require.NoError(t, err) require.Equal(t, form, r.PostForm) + + fmt.Fprint(w, `{ + "img_src": "https://example.com/test.png" + }`) }) - _, err := client.Subreddit.RemoveMobileBanner(ctx, "testsubreddit") + link, _, err := client.Subreddit.UploadImage(ctx, "testsubreddit", imageFile.Name(), "testname") require.NoError(t, err) + require.Equal(t, "https://example.com/test.png", link) } -func TestSubredditService_RemoveImage(t *testing.T) { +func TestSubredditService_UploadHeader(t *testing.T) { client, mux, teardown := setup() defer teardown() - mux.HandleFunc("/r/testsubreddit/api/delete_sr_img", func(w http.ResponseWriter, r *http.Request) { + imageFile, err := ioutil.TempFile("/tmp", "emoji*.png") + require.NoError(t, err) + defer func() { + imageFile.Close() + os.Remove(imageFile.Name()) + }() + + _, err = imageFile.WriteString("this is a test") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/api/upload_sr_img", func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPost, r.Method) - form := url.Values{} - form.Set("api_type", "json") - form.Set("img_name", "testimage") + _, file, err := r.FormFile("file") + require.NoError(t, err) - err := r.ParseForm() + rdr, err := file.Open() + require.NoError(t, err) + + buf := new(bytes.Buffer) + _, err = io.Copy(buf, rdr) + require.NoError(t, err) + require.Equal(t, "this is a test", buf.String()) + + form := url.Values{} + form.Set("upload_type", "header") + form.Set("name", "testname") + form.Set("img_type", "png") + + err = r.ParseForm() require.NoError(t, err) require.Equal(t, form, r.PostForm) + + fmt.Fprint(w, `{ + "img_src": "https://example.com/test.png" + }`) }) - _, err := client.Subreddit.RemoveImage(ctx, "testsubreddit", "testimage") + link, _, err := client.Subreddit.UploadHeader(ctx, "testsubreddit", imageFile.Name(), "testname") require.NoError(t, err) + require.Equal(t, "https://example.com/test.png", link) +} + +func TestSubredditService_UploadMobileHeader(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + imageFile, err := ioutil.TempFile("/tmp", "emoji*.png") + require.NoError(t, err) + defer func() { + imageFile.Close() + os.Remove(imageFile.Name()) + }() + + _, err = imageFile.WriteString("this is a test") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/api/upload_sr_img", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + _, file, err := r.FormFile("file") + require.NoError(t, err) + + rdr, err := file.Open() + require.NoError(t, err) + + buf := new(bytes.Buffer) + _, err = io.Copy(buf, rdr) + require.NoError(t, err) + require.Equal(t, "this is a test", buf.String()) + + form := url.Values{} + form.Set("upload_type", "banner") + form.Set("name", "testname") + form.Set("img_type", "png") + + err = r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + + fmt.Fprint(w, `{ + "img_src": "https://example.com/test.png" + }`) + }) + + link, _, err := client.Subreddit.UploadMobileHeader(ctx, "testsubreddit", imageFile.Name(), "testname") + require.NoError(t, err) + require.Equal(t, "https://example.com/test.png", link) +} + +func TestSubredditService_UploadMobileIcon(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + imageFile, err := ioutil.TempFile("/tmp", "emoji*.jpg") + require.NoError(t, err) + defer func() { + imageFile.Close() + os.Remove(imageFile.Name()) + }() + + _, err = imageFile.WriteString("this is a test") + require.NoError(t, err) + + mux.HandleFunc("/r/testsubreddit/api/upload_sr_img", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + _, file, err := r.FormFile("file") + require.NoError(t, err) + + rdr, err := file.Open() + require.NoError(t, err) + + buf := new(bytes.Buffer) + _, err = io.Copy(buf, rdr) + require.NoError(t, err) + require.Equal(t, "this is a test", buf.String()) + + form := url.Values{} + form.Set("upload_type", "icon") + form.Set("name", "testname") + form.Set("img_type", "jpg") + + err = r.ParseForm() + require.NoError(t, err) + require.Equal(t, form, r.PostForm) + + fmt.Fprint(w, `{ + "img_src": "https://example.com/test.jpg" + }`) + }) + + link, _, err := client.Subreddit.UploadMobileIcon(ctx, "testsubreddit", imageFile.Name(), "testname") + require.NoError(t, err) + require.Equal(t, "https://example.com/test.jpg", link) +} + +func TestSubredditService_UploadImage_Error(t *testing.T) { + client, mux, teardown := setup() + defer teardown() + + imageFile, err := ioutil.TempFile("/tmp", "emoji*.jpg") + require.NoError(t, err) + defer func() { + imageFile.Close() + os.Remove(imageFile.Name()) + }() + + mux.HandleFunc("/r/testsubreddit/api/upload_sr_img", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + fmt.Fprint(w, `{ + "errors_values": [ + "error one", + "error two" + ] + }`) + }) + + _, _, err = client.Subreddit.UploadImage(ctx, "testsubreddit", "does-not-exist.jpg", "testname") + require.EqualError(t, err, "open does-not-exist.jpg: no such file or directory") + + _, _, err = client.Subreddit.UploadImage(ctx, "testsubreddit", imageFile.Name(), "testname") + require.EqualError(t, err, "could not upload image: error one; error two") }