files upload support

This commit is contained in:
Pavel 2019-09-04 11:38:21 +03:00
parent ddf8ee7cef
commit 2f06836bd4
6 changed files with 609 additions and 9 deletions

View File

@ -1,8 +1,10 @@
package v5 package v5
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"io/ioutil" "io/ioutil"
"log" "log"
"net/http" "net/http"
@ -12,6 +14,7 @@ import (
"time" "time"
"github.com/google/go-querystring/query" "github.com/google/go-querystring/query"
"github.com/pkg/errors"
"github.com/retailcrm/api-client-go/errs" "github.com/retailcrm/api-client-go/errs"
) )
@ -73,23 +76,46 @@ func (c *Client) GetRequest(urlWithParameters string, versioned ...bool) ([]byte
return res, resp.StatusCode, failure return res, resp.StatusCode, failure
} }
// PostRequest implements POST Request // PostRequest implements POST Request with generic body data
func (c *Client) PostRequest(url string, postParams url.Values) ([]byte, int, *errs.Failure) { func (c *Client) PostRequest(uri string, postData interface{}, contType ...string) ([]byte, int, *errs.Failure) {
var res []byte var (
var prefix = "/api/v5" res []byte
contentType string
reader io.Reader
)
prefix := "/api/v5"
failure := &errs.Failure{} failure := &errs.Failure{}
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s%s", c.URL, prefix, url), strings.NewReader(postParams.Encode())) if len(contType) > 0 {
contentType = contType[0]
} else {
contentType = "application/x-www-form-urlencoded"
}
switch postData.(type) {
case url.Values:
reader = strings.NewReader(postData.(url.Values).Encode())
default:
if i, ok := postData.(io.Reader); ok {
reader = i
} else {
failure.SetRuntimeError(errors.New("postData should be url.Values or implement io.Reader"))
return []byte{}, 0, failure
}
}
req, err := http.NewRequest("POST", fmt.Sprintf("%s%s%s", c.URL, prefix, uri), reader)
if err != nil { if err != nil {
failure.SetRuntimeError(err) failure.SetRuntimeError(err)
return res, 0, failure return res, 0, failure
} }
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", contentType)
req.Header.Set("X-API-KEY", c.Key) req.Header.Set("X-API-KEY", c.Key)
if c.Debug { if c.Debug {
log.Printf("API Request: %s %s %s", url, c.Key, postParams.Encode()) log.Printf("API Request: %s %s", uri, c.Key)
} }
resp, err := c.httpClient.Do(req) resp, err := c.httpClient.Do(req)
@ -3941,6 +3967,228 @@ func (c *Client) CostEdit(id int, cost CostRecord, site ...string) (CreateRespon
return resp, status, nil return resp, status, nil
} }
// Files returns files list
//
// For more information see https://help.retailcrm.pro/Developers/ApiVersion5#get--api-v5-files
//
// Example:
//
// var client = v5.New("https://demo.url", "09jIJ")
//
// data, status, err := client.Files(FilesRequest{
// Filter: FilesFilter{
// Filename: "image.jpeg",
// },
// })
//
// if err.Error() != "" {
// fmt.Printf("%v", err.RuntimeErr)
// }
//
// if status >= http.StatusBadRequest {
// fmt.Printf("%v", err.ApiErr())
// }
func (c *Client) Files(files FilesRequest) (FilesResponse, int, *errs.Failure) {
var resp FilesResponse
params, _ := query.Values(files)
data, status, err := c.GetRequest(fmt.Sprintf("/files?%s", params.Encode()))
if err != nil && err.Error() != "" {
return resp, status, err
}
json.Unmarshal(data, &resp)
if resp.Success == false {
buildErr(data, err)
return resp, status, err
}
return resp, status, nil
}
// FileUpload uploads file to retailCRM
//
// For more information see https://help.retailcrm.pro/Developers/ApiVersion5#get--api-v5-files
//
// Example:
//
// var client = v5.New("https://demo.url", "09jIJ")
//
// file, err := os.Open("file.jpg")
// if err != nil {
// fmt.Print(err)
// }
//
// data, status, err := client.FileUpload(file)
//
// if err.Error() != "" {
// fmt.Printf("%v", err.RuntimeErr)
// }
//
// if status >= http.StatusBadRequest {
// fmt.Printf("%v", err.ApiErr())
// }
func (c *Client) FileUpload(reader io.Reader) (FileUploadResponse, int, *errs.Failure) {
var resp FileUploadResponse
data, status, err := c.PostRequest(fmt.Sprintf("/files/upload?apiKey=%s", c.Key), reader, "application/octet-stream")
if err != nil && err.Error() != "" {
return resp, status, err
}
json.Unmarshal(data, &resp)
if resp.Success == false {
buildErr(data, err)
return resp, status, err
}
return resp, status, nil
}
// File returns a file info
//
// For more information see https://help.retailcrm.pro/Developers/ApiVersion5#get--api-v5-files
//
// Example:
//
// var client = v5.New("https://demo.url", "09jIJ")
//
// data, status, err := client.File(112)
//
// if err.Error() != "" {
// fmt.Printf("%v", err.RuntimeErr)
// }
//
// if status >= http.StatusBadRequest {
// fmt.Printf("%v", err.ApiErr())
// }
//
// if data.Success == true {
// fmt.Printf("%v\n", data.File)
// }
func (c *Client) File(id int) (FileResponse, int, *errs.Failure) {
var resp FileResponse
data, status, err := c.GetRequest(fmt.Sprintf("/files/%d", id))
if err.Error() != "" {
return resp, status, err
}
json.Unmarshal(data, &resp)
if resp.Success == false {
buildErr(data, err)
return resp, status, err
}
return resp, status, nil
}
// FileDelete removes file from retailCRM
//
// For more information see https://help.retailcrm.pro/Developers/ApiVersion5#get--api-v5-files
//
// Example:
//
// var client = v5.New("https://demo.url", "09jIJ")
//
// data, status, err := client.FileDelete(123)
//
// if err.Error() != "" {
// fmt.Printf("%v", err.RuntimeErr)
// }
//
// if status >= http.StatusBadRequest {
// fmt.Printf("%v", err.ApiErr())
// }
func (c *Client) FileDelete(id int) (SuccessfulResponse, int, *errs.Failure) {
var resp SuccessfulResponse
data, status, err := c.PostRequest(fmt.Sprintf("/files/%d/delete", id), strings.NewReader(""))
if err != nil && err.Error() != "" {
return resp, status, err
}
json.Unmarshal(data, &resp)
if resp.Success == false {
buildErr(data, err)
return resp, status, err
}
return resp, status, nil
}
// FileDownload downloads file from retailCRM
//
// For more information see https://help.retailcrm.pro/Developers/ApiVersion5#get--api-v5-files
//
// Example:
//
// var client = v5.New("https://demo.url", "09jIJ")
//
// fileData, status, err := client.FileDownload(123)
//
// if err.Error() != "" {
// fmt.Printf("%v", err.RuntimeErr)
// }
//
// if status >= http.StatusBadRequest {
// fmt.Printf("%v", err.ApiErr())
// }
func (c *Client) FileDownload(id int) (io.ReadCloser, int, *errs.Failure) {
data, status, err := c.GetRequest(fmt.Sprintf("/files/%d/download", id))
if status != http.StatusOK {
return nil, status, err
}
closer := ioutil.NopCloser(bytes.NewReader(data))
return closer, status, nil
}
// FileEdit edits file name and relations with orders and customers in retailCRM
//
// For more information see https://help.retailcrm.pro/Developers/ApiVersion5#get--api-v5-files
//
// Example:
//
// var client = v5.New("https://demo.url", "09jIJ")
//
// data, status, err := client.FileEdit(123, File{Filename: "image2.jpg"})
//
// if err.Error() != "" {
// fmt.Printf("%v", err.RuntimeErr)
// }
//
// if status >= http.StatusBadRequest {
// fmt.Printf("%v", err.ApiErr())
// }
func (c *Client) FileEdit(id int, file File) (FileResponse, int, *errs.Failure) {
var resp FileResponse
req, _ := json.Marshal(file)
data, status, err := c.PostRequest(fmt.Sprintf("/files/%d/edit", id), bytes.NewReader(req))
if err != nil && err.Error() != "" {
return resp, status, err
}
json.Unmarshal(data, &resp)
if resp.Success == false {
buildErr(data, err)
return resp, status, err
}
return resp, status, nil
}
// CustomFields returns list of custom fields // CustomFields returns list of custom fields
// //
// For more information see http://www.retailcrm.pro/docs/Developers/ApiVersion5#get--api-v5-custom-fields // For more information see http://www.retailcrm.pro/docs/Developers/ApiVersion5#get--api-v5-custom-fields

View File

@ -3,12 +3,14 @@ package v5
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"math/rand" "math/rand"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strconv" "strconv"
"strings"
"testing" "testing"
"time" "time"
@ -4750,6 +4752,297 @@ func TestClient_CostsUpload_Fail(t *testing.T) {
} }
} }
func TestClient_Files(t *testing.T) {
c := client()
fileID := 14925
defer gock.Off()
gock.New(crmURL).
Get("/files").
MatchParam("filter[ids][]", strconv.Itoa(fileID)).
MatchParam("limit", "20").
MatchParam("page", "1").
Reply(200).
BodyString(`{
"success": true,
"pagination": {
"limit": 20,
"totalCount": 0,
"currentPage": 1,
"totalPageCount": 0
},
"files": []
}`)
_, status, err := c.Files(FilesRequest{
Limit: 20,
Page: 1,
Filter: FilesFilter{
Ids: []int{fileID},
},
})
if status != 200 {
t.Errorf("%v %v", err.Error(), err.ApiError())
}
}
func TestClient_FileUpload(t *testing.T) {
c := client()
file := strings.NewReader(`test file contents`)
defer gock.Off()
gock.New(crmURL).
Post("/files/upload").
Reply(200).
BodyString(`{"success": true, "file": {"id": 1}}`)
data, status, err := c.FileUpload(file)
if err.Error() != "" {
t.Errorf("%v", err.Error())
}
if status != http.StatusOK {
t.Errorf("%v", err.ApiError())
}
if data.Success != true {
t.Errorf("%v", err.ApiError())
}
if data.File.ID != 1 {
t.Error("invalid file id")
}
}
func TestClient_FileUploadFail(t *testing.T) {
c := client()
file := strings.NewReader(`test file contents`)
defer gock.Off()
gock.New(crmURL).
Post("/files/upload").
Reply(400).
BodyString(`{"success": false, "errorMsg": "error"}`)
_, status, err := c.FileUpload(file)
if err.Error() != "" {
t.Errorf("%v", err.Error())
}
if status != http.StatusBadRequest {
t.Errorf("status should be `%d`, got `%d` instead", http.StatusBadRequest, status)
}
}
func TestClient_File(t *testing.T) {
c := client()
invalidFile := 20
fileResponse := &FileResponse{
Success: true,
File: &File{
ID: 19,
Filename: "image.jpg",
Type: "image/jpeg",
CreatedAt: time.Now().String(),
Size: 10000,
Attachment: nil,
},
}
respData, errr := json.Marshal(fileResponse)
if errr != nil {
t.Errorf("%v", errr.Error())
}
defer gock.Off()
gock.New(crmURL).
Get(fmt.Sprintf("/files/%d", fileResponse.File.ID)).
Reply(200).
BodyString(string(respData))
gock.New(crmURL).
Get(fmt.Sprintf("/files/%d", invalidFile)).
Reply(404).
BodyString(`{"success": false, "errorMsg": "Not Found"}`)
s, status, err := c.File(fileResponse.File.ID)
if err.Error() != "" {
t.Errorf("%v", err.Error())
}
if status != http.StatusOK {
t.Errorf("%v", err.ApiError())
}
if s.Success != true {
t.Errorf("%v", err.ApiError())
}
if s.File.ID != fileResponse.File.ID {
t.Error("invalid response data")
}
s, status, err = c.File(invalidFile)
if err.Error() != "" {
t.Errorf("%v", err.Error())
}
if status != http.StatusNotFound {
t.Errorf("status should be `%d`, got `%d` instead", http.StatusNotFound, status)
}
}
func TestClient_FileDelete(t *testing.T) {
c := client()
successful := 19
badRequest := 20
notFound := 21
defer gock.Off()
gock.New(crmURL).
Post(fmt.Sprintf("/files/%d/delete", successful)).
Reply(200).
BodyString(`{"success": true}`)
gock.New(crmURL).
Post(fmt.Sprintf("/files/%d/delete", badRequest)).
Reply(400).
BodyString(`{"success": false, "errorMsg": "Error"}`)
gock.New(crmURL).
Post(fmt.Sprintf("/files/%d/delete", notFound)).
Reply(404).
BodyString(`{"success": false, "errorMsg": "Not Found"}`)
data, status, err := c.FileDelete(successful)
if err.Error() != "" {
t.Errorf("%v", err.Error())
}
if status != http.StatusOK {
t.Errorf("%v", err.ApiError())
}
if data.Success != true {
t.Errorf("%v", err.ApiError())
}
data, status, err = c.FileDelete(badRequest)
if err.Error() != "" {
t.Errorf("%v", err.Error())
}
if status != http.StatusBadRequest {
t.Errorf("status should be `%d`, got `%d` instead", http.StatusBadRequest, status)
}
data, status, err = c.FileDelete(notFound)
if err.Error() != "" {
t.Errorf("%v", err.Error())
}
if status != http.StatusNotFound {
t.Errorf("status should be `%d`, got `%d` instead", http.StatusNotFound, status)
}
}
func TestClient_FileDownload(t *testing.T) {
c := client()
successful := 19
fail := 20
fileData := "file data"
defer gock.Off()
gock.New(crmURL).
Get(fmt.Sprintf("/files/%d/download", successful)).
Reply(200).
BodyString(fileData)
gock.New(crmURL).
Get(fmt.Sprintf("/files/%d/download", fail)).
Reply(400).
BodyString("")
data, status, err := c.FileDownload(successful)
if err.Error() != "" {
t.Errorf("%v", err.Error())
}
if status != http.StatusOK {
t.Errorf("%v", err.ApiError())
}
fetchedByte, errr := ioutil.ReadAll(data)
if errr != nil {
t.Error(errr)
}
fetched := string(fetchedByte)
if fetched != fileData {
t.Error("file data mismatch")
}
data, status, err = c.FileDownload(fail)
if err.Error() != "" {
t.Errorf("%v", err.Error())
}
if status != http.StatusBadRequest {
t.Errorf("status should be `%d`, got `%d` instead", http.StatusBadRequest, status)
}
}
func TestClient_FileEdit(t *testing.T) {
c := client()
successful := 19
fail := 20
resp := FileResponse{
Success: true,
File: &File{Filename: "image.jpg"},
}
respData, _ := json.Marshal(resp)
defer gock.Off()
gock.New(crmURL).
Post(fmt.Sprintf("/files/%d/edit", successful)).
Reply(200).
BodyString(string(respData))
gock.New(crmURL).
Post(fmt.Sprintf("/files/%d/edit", fail)).
Reply(404).
BodyString(`{"success": false, "errorMsg": "Not Found"}`)
data, status, err := c.FileEdit(successful, *resp.File)
if err.Error() != "" {
t.Errorf("%v", err.Error())
}
if status != http.StatusOK {
t.Errorf("%v", err.ApiError())
}
if data.Success != true {
t.Errorf("%v", err.ApiError())
}
if data.File.Filename != resp.File.Filename {
t.Errorf("filename should be `%s`, got `%s` instead", resp.File.Filename, data.File.Filename)
}
data, status, err = c.FileEdit(fail, *resp.File)
if status != http.StatusNotFound {
t.Errorf("status should be `%d`, got `%d` instead", http.StatusNotFound, status)
}
}
func TestClient_CustomFields(t *testing.T) { func TestClient_CustomFields(t *testing.T) {
c := client() c := client()

View File

@ -318,6 +318,23 @@ type CostsFilter struct {
OrderExternalIds []string `url:"orderIds,omitempty,brackets"` OrderExternalIds []string `url:"orderIds,omitempty,brackets"`
} }
// FilesFilter type
type FilesFilter struct {
Ids []int `url:"ids,omitempty,brackets"`
OrderIds []int `url:"orderIds,omitempty,brackets"`
OrderExternalIds []string `url:"orderExternalIds,omitempty,brackets"`
CustomerIds []int `url:"customerIds,omitempty,brackets"`
CustomerExternalIds []string `url:"customerExternalIds,omitempty,brackets"`
CreatedAtFrom string `url:"createdAtFrom,omitempty"`
CreatedAtTo string `url:"createdAtTo,omitempty"`
SizeFrom int `url:"sizeFrom,omitempty"`
SizeTo int `url:"sizeTo,omitempty"`
Type []string `url:"type,omitempty,brackets"`
Filename string `url:"filename,omitempty"`
IsAttached string `url:"isAttached,omitempty"`
Sites []string `url:"sites,omitempty,brackets"`
}
// CustomFieldsFilter type // CustomFieldsFilter type
type CustomFieldsFilter struct { type CustomFieldsFilter struct {
Name string `url:"name,omitempty"` Name string `url:"name,omitempty"`

View File

@ -150,6 +150,13 @@ type CostsRequest struct {
Page int `url:"page,omitempty"` Page int `url:"page,omitempty"`
} }
// FilesRequest type
type FilesRequest struct {
Filter FilesFilter `url:"filter,omitempty"`
Limit int `url:"limit,omitempty"`
Page int `url:"page,omitempty"`
}
// CustomFieldsRequest type // CustomFieldsRequest type
type CustomFieldsRequest struct { type CustomFieldsRequest struct {
Filter CustomFieldsFilter `url:"filter,omitempty"` Filter CustomFieldsFilter `url:"filter,omitempty"`

View File

@ -367,6 +367,25 @@ type CostResponse struct {
Cost *Cost `json:"cost,omitempty,brackets"` Cost *Cost `json:"cost,omitempty,brackets"`
} }
// FilesResponse type
type FilesResponse struct {
Success bool `json:"success"`
Pagination *Pagination `json:"pagination,omitempty"`
Files []File `json:"files,omitempty,brackets"`
}
// FileUpload response
type FileUploadResponse struct {
Success bool `json:"success"`
File *File `json:"file,omitempty"`
}
// FileResponse type
type FileResponse struct {
Success bool `json:"success"`
File *File `json:"file,omitempty"`
}
// CustomFieldsResponse type // CustomFieldsResponse type
type CustomFieldsResponse struct { type CustomFieldsResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
@ -401,6 +420,6 @@ type CustomFieldResponse struct {
// UnitsResponse type // UnitsResponse type
type UnitsResponse struct { type UnitsResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
Units *[]Unit `json:"units,omitempty,brackets"` Units *[]Unit `json:"units,omitempty,brackets"`
} }

View File

@ -923,6 +923,22 @@ type Cost struct {
Sites []string `json:"sites,omitempty,brackets"` Sites []string `json:"sites,omitempty,brackets"`
} }
// File type
type File struct {
ID int `json:"id,omitempty"`
Filename string `json:"filename,omitempty"`
Type string `json:"type,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Size int `json:"size,omitempty"`
Attachment []Attachment `json:"attachment,omitempty"`
}
// Attachment type
type Attachment struct {
Customer *Customer `json:"customer,omitempty"`
Order *Order `json:"order,omitempty"`
}
// CustomFields type // CustomFields type
type CustomFields struct { type CustomFields struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`