Skip to content

Commit

Permalink
SendFile (#23)
Browse files Browse the repository at this point in the history
* Add SendFile and SendImage

* Add DeleteFile\DeleteImage
  • Loading branch information
bogdan-d authored Oct 28, 2019
1 parent 10a1ed1 commit b81d2ba
Show file tree
Hide file tree
Showing 8 changed files with 740 additions and 321 deletions.
45 changes: 45 additions & 0 deletions channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package stream_chat //nolint: golint

import (
"errors"
"io"
"net/http"
"net/url"
"path"
Expand Down Expand Up @@ -325,6 +326,50 @@ func (c *Client) CreateChannel(chanType, chanID, userID string, data map[string]
return ch, err
}

type SendFileRequest struct {
Reader io.Reader `json:"-"`
// name of the file would be stored
FileName string
// User object; required
User *User
// file content type, required for SendImage
ContentType string
}

// SendFile sends file to the channel. Returns file url or error
func (ch *Channel) SendFile(request SendFileRequest) (string, error) {
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "file")

return ch.client.sendFile(p, request)
}

// SendFile sends image to the channel. Returns file url or error
func (ch *Channel) SendImage(request SendFileRequest) (string, error) {
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "image")

return ch.client.sendFile(p, request)
}

// DeleteFile removes uploaded file
func (ch *Channel) DeleteFile(location string) error {
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "file")

var params = url.Values{}
params.Set("url", location)

return ch.client.makeRequest(http.MethodDelete, p, params, nil, nil)
}

// DeleteImage removes uploaded image
func (ch *Channel) DeleteImage(location string) error {
p := path.Join("channels", url.PathEscape(ch.Type), url.PathEscape(ch.ID), "image")

var params = url.Values{}
params.Set("url", location)

return ch.client.makeRequest(http.MethodDelete, p, params, nil, nil)
}

// todo: cleanup this
func (ch *Channel) refresh() error {
options := map[string]interface{}{
Expand Down
71 changes: 71 additions & 0 deletions channel_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package stream_chat // nolint: golint

import (
"os"
"path"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -255,3 +257,72 @@ func TestChannel_DemoteModerators(t *testing.T) {
func TestChannel_UnBanUser(t *testing.T) {

}

func TestChannel_SendFile(t *testing.T) {
c := initClient(t)
ch := initChannel(t, c)

var url string

t.Run("Send file", func(t *testing.T) {
file, err := os.Open(path.Join("testdata", "helloworld.txt"))
if err != nil {
t.Fatal(err)
}

url, err = ch.SendFile(SendFileRequest{
Reader: file,
FileName: "HelloWorld.txt",
User: randomUser(),
})
if err != nil {
t.Fatalf("send file failed: %s", err)
}
if url == "" {
t.Fatal("upload file returned empty url")
}
})

t.Run("Delete file", func(t *testing.T) {
err := ch.DeleteFile(url)
if err != nil {
t.Fatalf("delete file failed: %s", err.Error())
}
})
}

func TestChannel_SendImage(t *testing.T) {
c := initClient(t)
ch := initChannel(t, c)

var url string

t.Run("Send image", func(t *testing.T) {
file, err := os.Open(path.Join("testdata", "helloworld.jpg"))
if err != nil {
t.Fatal(err)
}

url, err = ch.SendImage(SendFileRequest{
Reader: file,
FileName: "HelloWorld.jpg",
User: randomUser(),
ContentType: "image/jpeg",
})

if err != nil {
t.Fatalf("Send image failed: %s", err.Error())
}

if url == "" {
t.Fatal("upload image returned empty url")
}
})

t.Run("Delete image", func(t *testing.T) {
err := ch.DeleteImage(url)
if err != nil {
t.Fatalf("delete image failed: %s", err.Error())
}
})
}
161 changes: 148 additions & 13 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/textproto"
"net/url"
"os"
"strings"
"time"

"github.com/getstream/easyjson"
Expand Down Expand Up @@ -72,31 +77,52 @@ func (c *Client) requestURL(path string, values url.Values) (string, error) {
return _url.String(), nil
}

func (c *Client) makeRequest(method, path string,
params url.Values, data interface{}, result easyjson.Unmarshaler) error {
func (c *Client) newRequest(method, path string, params url.Values, data interface{}) (*http.Request, error) {
_url, err := c.requestURL(path, params)
if err != nil {
return err
return nil, err
}

var body []byte
if m, ok := data.(easyjson.Marshaler); ok {
body, err = easyjson.Marshal(m)
} else {
body, err = json.Marshal(data)
r, err := http.NewRequest(method, _url, nil)
if err != nil {
return nil, err
}

if err != nil {
return err
c.setHeaders(r)

switch t := data.(type) {
case easyjson.Marshaler:
b, err := easyjson.Marshal(t)
if err != nil {
return nil, err
}
r.Body = ioutil.NopCloser(bytes.NewReader(b))

case io.ReadCloser:
r.Body = t

case io.Reader:
r.Body = ioutil.NopCloser(t)

default:
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
r.Body = ioutil.NopCloser(bytes.NewReader(b))
}

r, err := http.NewRequest(method, _url, bytes.NewReader(body))
return r, nil
}

func (c *Client) makeRequest(method, path string, params url.Values,
data interface{}, result easyjson.Unmarshaler) error {

r, err := c.newRequest(method, path, params, data)
if err != nil {
return err
}

c.setHeaders(r)

resp, err := c.HTTP.Do(r)
if err != nil {
return err
Expand Down Expand Up @@ -138,6 +164,115 @@ func (c *Client) VerifyWebhook(body []byte, signature []byte) (valid bool) {
return hmac.Equal(signature, expectedMAC)
}

type sendFileResponse struct {
File string `json:"file"`
}

//nolint:gochecknoglobals
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")

func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}

// this adds possible to set content type
type multipartForm struct {
*multipart.Writer
}

// CreateFormFile is a convenience wrapper around CreatePart. It creates
// a new form-data header with the provided field name, file name and content type
func (form *multipartForm) CreateFormFile(fieldName, filename, contentType string) (io.Writer, error) {
h := make(textproto.MIMEHeader)

h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
escapeQuotes(fieldName), escapeQuotes(filename)))

if contentType == "" {
contentType = "application/octet-stream"
}

h.Set("Content-Type", contentType)

return form.Writer.CreatePart(h)
}

func (form *multipartForm) setData(fieldName string, data easyjson.Marshaler) error {
field, err := form.CreateFormField(fieldName)
if err != nil {
return err
}
_, err = easyjson.MarshalToWriter(data, field)
return err
}

func (form *multipartForm) setFile(fieldName string, r io.Reader, fileName, contentType string) error {
file, err := form.CreateFormFile(fieldName, fileName, contentType)
if err != nil {
return err
}
_, err = io.Copy(file, r)

return err
}

func (c *Client) sendFile(url string, opts SendFileRequest) (string, error) {
if opts.User == nil {
return "", errors.New("user is nil")
}

tmpfile, err := ioutil.TempFile("", opts.FileName)
if err != nil {
return "", err
}

defer func() {
_ = tmpfile.Close()
_ = os.Remove(tmpfile.Name())
}()

form := multipartForm{multipart.NewWriter(tmpfile)}

if err = form.setData("user", opts.User); err != nil {
return "", err
}

err = form.setFile("file", opts.Reader, opts.FileName, opts.ContentType)
if err != nil {
return "", err
}

err = form.Close()
if err != nil {
return "", err
}

if _, err = tmpfile.Seek(0, 0); err != nil {
return "", err
}

r, err := c.newRequest(http.MethodPost, url, nil, tmpfile)
if err != nil {
return "", err
}

r.Header.Set("Content-Type", form.FormDataContentType())

res, err := c.HTTP.Do(r)
if err != nil {
return "", err
}

var resp sendFileResponse
err = c.parseResponse(res, &resp)
if err != nil {
return "", err
}

return resp.File, err
}

// NewClient creates new stream chat api client
func NewClient(apiKey string, apiSecret []byte) (*Client, error) {
switch {
Expand Down
4 changes: 4 additions & 0 deletions stream_chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ type StreamChannel interface {
Query(data map[string]interface{}) error
Show(userID string) error
Hide(userID string) error
SendFile(request SendFileRequest) (url string, err error)
SendImage(request SendFileRequest) (url string, err error)
DeleteFile(location string) error
DeleteImage(location string) error

// event.go
SendEvent(event *Event, userID string) error
Expand Down
Loading

0 comments on commit b81d2ba

Please sign in to comment.