vendor: add qingstor-sdk-go for QingStor
This commit is contained in:
+303
@@ -0,0 +1,303 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/pengsrc/go-shared/convert"
|
||||
"github.com/pengsrc/go-shared/json"
|
||||
|
||||
"github.com/yunify/qingstor-sdk-go/request/data"
|
||||
"github.com/yunify/qingstor-sdk-go/utils"
|
||||
)
|
||||
|
||||
// BaseBuilder is the base builder for all services.
|
||||
type BaseBuilder struct {
|
||||
parsedURL string
|
||||
parsedProperties *map[string]string
|
||||
parsedParams *map[string]string
|
||||
parsedHeaders *map[string]string
|
||||
parsedBodyString string
|
||||
parsedBody io.Reader
|
||||
|
||||
operation *data.Operation
|
||||
input *reflect.Value
|
||||
}
|
||||
|
||||
// BuildHTTPRequest builds http request with an operation and an input.
|
||||
func (b *BaseBuilder) BuildHTTPRequest(o *data.Operation, i *reflect.Value) (*http.Request, error) {
|
||||
b.operation = o
|
||||
b.input = i
|
||||
|
||||
_, err := b.parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.build()
|
||||
}
|
||||
|
||||
func (b *BaseBuilder) build() (*http.Request, error) {
|
||||
httpRequest, err := http.NewRequest(b.operation.RequestMethod, b.parsedURL, b.parsedBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = b.setupHeaders(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return httpRequest, nil
|
||||
}
|
||||
|
||||
func (b *BaseBuilder) parse() (*BaseBuilder, error) {
|
||||
err := b.parseRequestParamsAndHeaders()
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
err = b.parseRequestBody()
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
err = b.parseRequestProperties()
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
err = b.parseRequestURL()
|
||||
if err != nil {
|
||||
return b, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *BaseBuilder) parseRequestParamsAndHeaders() error {
|
||||
requestParams := map[string]string{}
|
||||
requestHeaders := map[string]string{}
|
||||
maps := map[string](map[string]string){
|
||||
"params": requestParams,
|
||||
"headers": requestHeaders,
|
||||
}
|
||||
|
||||
b.parsedParams = &requestParams
|
||||
b.parsedHeaders = &requestHeaders
|
||||
|
||||
if !b.input.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := b.input.Elem()
|
||||
if !fields.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < fields.NumField(); i++ {
|
||||
tagName := fields.Type().Field(i).Tag.Get("name")
|
||||
tagLocation := fields.Type().Field(i).Tag.Get("location")
|
||||
if tagDefault := fields.Type().Field(i).Tag.Get("default"); tagDefault != "" {
|
||||
maps[tagLocation][tagName] = tagDefault
|
||||
}
|
||||
if tagName != "" && tagLocation != "" && maps[tagLocation] != nil {
|
||||
switch value := fields.Field(i).Interface().(type) {
|
||||
case *string:
|
||||
if value != nil {
|
||||
maps[tagLocation][tagName] = *value
|
||||
}
|
||||
case *int:
|
||||
if value != nil {
|
||||
maps[tagLocation][tagName] = strconv.Itoa(int(*value))
|
||||
}
|
||||
case *int64:
|
||||
if value != nil {
|
||||
maps[tagLocation][tagName] = strconv.FormatInt(int64(*value), 10)
|
||||
}
|
||||
case *bool:
|
||||
case *time.Time:
|
||||
if value != nil {
|
||||
formatString := fields.Type().Field(i).Tag.Get("format")
|
||||
format := ""
|
||||
switch formatString {
|
||||
case "RFC 822":
|
||||
format = convert.RFC822
|
||||
case "ISO 8601":
|
||||
format = convert.ISO8601
|
||||
}
|
||||
maps[tagLocation][tagName] = convert.TimeToString(*value, format)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseBuilder) parseRequestBody() error {
|
||||
requestData := map[string]interface{}{}
|
||||
|
||||
if !b.input.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := b.input.Elem()
|
||||
if !fields.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := 0; i < fields.NumField(); i++ {
|
||||
location := fields.Type().Field(i).Tag.Get("location")
|
||||
if location == "elements" {
|
||||
name := fields.Type().Field(i).Tag.Get("name")
|
||||
requestData[name] = fields.Field(i).Interface()
|
||||
}
|
||||
}
|
||||
|
||||
if len(requestData) != 0 {
|
||||
dataValue, err := json.Encode(requestData, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.parsedBodyString = string(dataValue)
|
||||
b.parsedBody = strings.NewReader(b.parsedBodyString)
|
||||
(*b.parsedHeaders)["Content-Type"] = "application/json"
|
||||
} else {
|
||||
value := fields.FieldByName("Body")
|
||||
if value.IsValid() {
|
||||
switch value.Interface().(type) {
|
||||
case string:
|
||||
if value.String() != "" {
|
||||
b.parsedBodyString = value.String()
|
||||
b.parsedBody = strings.NewReader(value.String())
|
||||
}
|
||||
case io.Reader:
|
||||
if value.Interface().(io.Reader) != nil {
|
||||
b.parsedBody = value.Interface().(io.Reader)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseBuilder) parseRequestProperties() error {
|
||||
propertiesMap := map[string]string{}
|
||||
b.parsedProperties = &propertiesMap
|
||||
|
||||
if b.operation.Properties != nil {
|
||||
fields := reflect.ValueOf(b.operation.Properties).Elem()
|
||||
if fields.IsValid() {
|
||||
for i := 0; i < fields.NumField(); i++ {
|
||||
switch value := fields.Field(i).Interface().(type) {
|
||||
case *string:
|
||||
if value != nil {
|
||||
propertiesMap[fields.Type().Field(i).Tag.Get("name")] = *value
|
||||
}
|
||||
case *int:
|
||||
if value != nil {
|
||||
numberString := strconv.Itoa(int(*value))
|
||||
propertiesMap[fields.Type().Field(i).Tag.Get("name")] = numberString
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseBuilder) parseRequestURL() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseBuilder) setupHeaders(httpRequest *http.Request) error {
|
||||
if b.parsedHeaders != nil {
|
||||
|
||||
for headerKey, headerValue := range *b.parsedHeaders {
|
||||
if headerKey == "X-QS-Fetch-Source" {
|
||||
// header X-QS-Fetch-Source is a URL to fetch.
|
||||
// We should first parse this URL.
|
||||
requestURL, err := url.Parse(headerValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid HTTP header value: %s", headerValue)
|
||||
}
|
||||
headerValue = requestURL.String()
|
||||
} else {
|
||||
for _, r := range headerValue {
|
||||
if r > unicode.MaxASCII {
|
||||
headerValue = utils.URLQueryEscape(headerValue)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
httpRequest.Header.Set(headerKey, headerValue)
|
||||
}
|
||||
}
|
||||
|
||||
if httpRequest.Header.Get("Content-Length") == "" {
|
||||
var length int64
|
||||
switch body := b.parsedBody.(type) {
|
||||
case nil:
|
||||
length = 0
|
||||
case io.Seeker:
|
||||
//start, err := body.Seek(0, io.SeekStart)
|
||||
start, err := body.Seek(0, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//end, err := body.Seek(0, io.SeekEnd)
|
||||
end, err := body.Seek(0, 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//body.Seek(0, io.SeekStart)
|
||||
body.Seek(0, 0)
|
||||
length = end - start
|
||||
default:
|
||||
return errors.New("Can not get Content-Length")
|
||||
}
|
||||
if length > 0 {
|
||||
httpRequest.ContentLength = length
|
||||
httpRequest.Header.Set("Content-Length", strconv.Itoa(int(length)))
|
||||
} else {
|
||||
httpRequest.Header.Set("Content-Length", "0")
|
||||
}
|
||||
}
|
||||
length, err := strconv.Atoi(httpRequest.Header.Get("Content-Length"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpRequest.ContentLength = int64(length)
|
||||
|
||||
if httpRequest.Header.Get("Date") == "" {
|
||||
httpRequest.Header.Set("Date", convert.TimeToString(time.Now(), convert.RFC822))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yunify/qingstor-sdk-go/config"
|
||||
"github.com/yunify/qingstor-sdk-go/request/data"
|
||||
)
|
||||
|
||||
type FakeProperties struct {
|
||||
A *string `name:"a"`
|
||||
B *string `name:"b"`
|
||||
CD *int `name:"c-d"`
|
||||
}
|
||||
type FakeInput struct {
|
||||
ParamA *string `location:"params" name:"a"`
|
||||
ParamB *string `location:"params" name:"b"`
|
||||
ParamCD *int `location:"params" name:"c_d" default:"1024"`
|
||||
HeaderA *string `location:"headers" name:"A"`
|
||||
HeaderB *time.Time `location:"headers" name:"B" format:"RFC 822"`
|
||||
HeaderCD *int `location:"headers" name:"C-D"`
|
||||
ElementA *string `location:"elements" name:"a"`
|
||||
ElementB *string `location:"elements" name:"b"`
|
||||
ElementCD *int64 `location:"elements" name:"cd"`
|
||||
Body *string `localtion:"body"`
|
||||
}
|
||||
|
||||
func (i *FakeInput) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func String(v string) *string {
|
||||
return &v
|
||||
}
|
||||
|
||||
func Int(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
func Int64(v int64) *int64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
func Time(v time.Time) *time.Time {
|
||||
return &v
|
||||
}
|
||||
|
||||
func TestBaseBuilder_BuildHTTPRequest(t *testing.T) {
|
||||
conf, err := config.NewDefault()
|
||||
assert.Nil(t, err)
|
||||
|
||||
tz, err := time.LoadLocation("Asia/Shanghai")
|
||||
assert.Nil(t, err)
|
||||
|
||||
builder := BaseBuilder{}
|
||||
operation := &data.Operation{
|
||||
Config: conf,
|
||||
APIName: "This is API name",
|
||||
ServiceName: "Base",
|
||||
Properties: &FakeProperties{
|
||||
A: String("property_a"),
|
||||
B: String("property_b"),
|
||||
CD: Int(0),
|
||||
},
|
||||
RequestMethod: "GET",
|
||||
RequestURI: "/hello/<a>/<c-d>/<b>/world",
|
||||
StatusCodes: []int{
|
||||
200,
|
||||
201,
|
||||
},
|
||||
}
|
||||
inputValue := reflect.ValueOf(&FakeInput{
|
||||
ParamA: String("param_a"),
|
||||
ParamCD: Int(1024),
|
||||
HeaderA: String("header_a"),
|
||||
HeaderB: Time(time.Date(2016, 9, 1, 15, 30, 0, 0, tz)),
|
||||
ElementA: String("element_a"),
|
||||
ElementB: String("element_b"),
|
||||
ElementCD: Int64(0),
|
||||
Body: String("This is body string"),
|
||||
})
|
||||
httpRequest, err := builder.BuildHTTPRequest(operation, &inputValue)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, &map[string]string{
|
||||
"a": "property_a",
|
||||
"b": "property_b",
|
||||
"c-d": "0",
|
||||
}, builder.parsedProperties)
|
||||
assert.Equal(t, &map[string]string{
|
||||
"a": "param_a",
|
||||
"c_d": "1024",
|
||||
}, builder.parsedParams)
|
||||
assert.Equal(t, &map[string]string{
|
||||
"A": "header_a",
|
||||
"B": "Thu, 01 Sep 2016 07:30:00 GMT",
|
||||
"Content-Type": "application/json",
|
||||
}, builder.parsedHeaders)
|
||||
assert.NotNil(t, httpRequest.Header.Get("Date"))
|
||||
assert.Equal(t, "40", httpRequest.Header.Get("Content-Length"))
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
buffer.ReadFrom(httpRequest.Body)
|
||||
httpRequest.Body.Close()
|
||||
assert.Equal(t, "{\"a\":\"element_a\",\"b\":\"element_b\",\"cd\":0}", buffer.String())
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/pengsrc/go-shared/convert"
|
||||
|
||||
"github.com/yunify/qingstor-sdk-go"
|
||||
"github.com/yunify/qingstor-sdk-go/logger"
|
||||
"github.com/yunify/qingstor-sdk-go/request/data"
|
||||
"github.com/yunify/qingstor-sdk-go/utils"
|
||||
)
|
||||
|
||||
// QingStorBuilder is the request builder for QingStor service.
|
||||
type QingStorBuilder struct {
|
||||
baseBuilder *BaseBuilder
|
||||
}
|
||||
|
||||
// BuildHTTPRequest builds http request with an operation and an input.
|
||||
func (qb *QingStorBuilder) BuildHTTPRequest(o *data.Operation, i *reflect.Value) (*http.Request, error) {
|
||||
qb.baseBuilder = &BaseBuilder{}
|
||||
qb.baseBuilder.operation = o
|
||||
qb.baseBuilder.input = i
|
||||
|
||||
_, err := qb.baseBuilder.parse()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = qb.parseURL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpRequest, err := http.NewRequest(qb.baseBuilder.operation.RequestMethod,
|
||||
qb.baseBuilder.parsedURL, qb.baseBuilder.parsedBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = qb.baseBuilder.setupHeaders(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = qb.setupHeaders(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf(
|
||||
"Built QingStor request: [%d] %s",
|
||||
convert.StringToUnixTimestamp(httpRequest.Header.Get("Date"), convert.RFC822),
|
||||
httpRequest.URL.String()),
|
||||
)
|
||||
|
||||
logger.Info(fmt.Sprintf(
|
||||
"QingStor request headers: [%d] %s",
|
||||
convert.StringToUnixTimestamp(httpRequest.Header.Get("Date"), convert.RFC822),
|
||||
fmt.Sprint(httpRequest.Header)),
|
||||
)
|
||||
|
||||
if qb.baseBuilder.parsedBodyString != "" {
|
||||
logger.Info(fmt.Sprintf(
|
||||
"QingStor request body string: [%d] %s",
|
||||
convert.StringToUnixTimestamp(httpRequest.Header.Get("Date"), convert.RFC822),
|
||||
qb.baseBuilder.parsedBodyString),
|
||||
)
|
||||
}
|
||||
|
||||
return httpRequest, nil
|
||||
}
|
||||
|
||||
func (qb *QingStorBuilder) parseURL() error {
|
||||
config := qb.baseBuilder.operation.Config
|
||||
|
||||
zone := (*qb.baseBuilder.parsedProperties)["zone"]
|
||||
port := strconv.Itoa(config.Port)
|
||||
endpoint := config.Protocol + "://" + config.Host + ":" + port
|
||||
if zone != "" {
|
||||
endpoint = config.Protocol + "://" + zone + "." + config.Host + ":" + port
|
||||
}
|
||||
|
||||
requestURI := qb.baseBuilder.operation.RequestURI
|
||||
for key, value := range *qb.baseBuilder.parsedProperties {
|
||||
endpoint = strings.Replace(endpoint, "<"+key+">", utils.URLQueryEscape(value), -1)
|
||||
requestURI = strings.Replace(requestURI, "<"+key+">", utils.URLQueryEscape(value), -1)
|
||||
}
|
||||
requestURI = regexp.MustCompile(`/+`).ReplaceAllString(requestURI, "/")
|
||||
|
||||
requestURL, err := url.Parse(endpoint + requestURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if qb.baseBuilder.parsedParams != nil {
|
||||
queryValue := requestURL.Query()
|
||||
for key, value := range *qb.baseBuilder.parsedParams {
|
||||
queryValue.Set(key, value)
|
||||
}
|
||||
requestURL.RawQuery = queryValue.Encode()
|
||||
}
|
||||
|
||||
qb.baseBuilder.parsedURL = requestURL.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qb *QingStorBuilder) setupHeaders(httpRequest *http.Request) error {
|
||||
method := httpRequest.Method
|
||||
if method == "POST" || method == "PUT" || method == "DELETE" {
|
||||
if httpRequest.Header.Get("Content-Type") == "" {
|
||||
mimeType := mime.TypeByExtension(path.Ext(httpRequest.URL.Path))
|
||||
if mimeType != "" {
|
||||
httpRequest.Header.Set("Content-Type", mimeType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if httpRequest.Header.Get("User-Agent") == "" {
|
||||
version := fmt.Sprintf(`Go v%s`, strings.Replace(runtime.Version(), "go", "", -1))
|
||||
system := fmt.Sprintf(`%s_%s_%s`, runtime.GOOS, runtime.GOARCH, runtime.Compiler)
|
||||
ua := fmt.Sprintf(`qingstor-sdk-go/%s (%s; %s)`, sdk.Version, version, system)
|
||||
if qb.baseBuilder.operation.Config.AdditionalUserAgent != "" {
|
||||
ua = fmt.Sprintf(`%s %s`, ua, qb.baseBuilder.operation.Config.AdditionalUserAgent)
|
||||
}
|
||||
httpRequest.Header.Set("User-Agent", ua)
|
||||
}
|
||||
|
||||
if qb.baseBuilder.operation.APIName == "Delete Multiple Objects" {
|
||||
buffer := &bytes.Buffer{}
|
||||
buffer.ReadFrom(httpRequest.Body)
|
||||
httpRequest.Body = ioutil.NopCloser(bytes.NewReader(buffer.Bytes()))
|
||||
|
||||
md5Value := md5.Sum(buffer.Bytes())
|
||||
httpRequest.Header.Set("Content-MD5", base64.StdEncoding.EncodeToString(md5Value[:]))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package builder
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yunify/qingstor-sdk-go/config"
|
||||
"github.com/yunify/qingstor-sdk-go/request/data"
|
||||
)
|
||||
|
||||
type ObjectSubServiceProperties struct {
|
||||
BucketName *string `json:"bucket-name" name:"bucket-name"`
|
||||
ObjectKey *string `json:"object-key" name:"object-key"`
|
||||
Zone *string `json:"zone" name:"zone"`
|
||||
}
|
||||
type GetObjectInput struct {
|
||||
IfMatch *string `json:"If-Match" name:"If-Match" location:"headers"`
|
||||
IfModifiedSince *time.Time `json:"If-Modified-Since" name:"If-Modified-Since" format:"RFC 822" location:"headers"`
|
||||
IfNoneMatch *string `json:"If-None-Match" name:"If-None-Match" location:"headers"`
|
||||
IfUnmodifiedSince time.Time `json:"If-Unmodified-Since" name:"If-Unmodified-Since" format:"RFC 822" location:"headers"`
|
||||
// Specified range of the Object
|
||||
Range *string `json:"Range" name:"Range" location:"headers"`
|
||||
}
|
||||
|
||||
func (i *GetObjectInput) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestQingStorBuilder_BuildHTTPRequest(t *testing.T) {
|
||||
conf, err := config.NewDefault()
|
||||
assert.Nil(t, err)
|
||||
conf.Host = "qingstor.dev"
|
||||
|
||||
tz, err := time.LoadLocation("Asia/Shanghai")
|
||||
assert.Nil(t, err)
|
||||
|
||||
qsBuilder := &QingStorBuilder{}
|
||||
operation := &data.Operation{
|
||||
Config: conf,
|
||||
APIName: "GET Object",
|
||||
ServiceName: "QingStor",
|
||||
Properties: &ObjectSubServiceProperties{
|
||||
BucketName: String("test"),
|
||||
ObjectKey: String("path/to/key.txt"),
|
||||
Zone: String("beta"),
|
||||
},
|
||||
RequestMethod: "GET",
|
||||
RequestURI: "/<bucket-name>/<object-key>",
|
||||
StatusCodes: []int{
|
||||
201,
|
||||
},
|
||||
}
|
||||
inputValue := reflect.ValueOf(&GetObjectInput{
|
||||
IfModifiedSince: Time(time.Date(2016, 9, 1, 15, 30, 0, 0, tz)),
|
||||
Range: String("100-"),
|
||||
})
|
||||
httpRequest, err := qsBuilder.BuildHTTPRequest(operation, &inputValue)
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, httpRequest.Header.Get("Date"))
|
||||
assert.Equal(t, "0", httpRequest.Header.Get("Content-Length"))
|
||||
assert.Equal(t, "", httpRequest.Header.Get("If-Match"))
|
||||
assert.Equal(t, "Thu, 01 Sep 2016 07:30:00 GMT", httpRequest.Header.Get("If-Modified-Since"))
|
||||
assert.Equal(t, "100-", httpRequest.Header.Get("Range"))
|
||||
assert.Equal(t, "https://beta.qingstor.dev:443/test/path/to/key.txt", httpRequest.URL.String())
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package data
|
||||
|
||||
// Input defines the interfaces that input should implement.
|
||||
type Input interface {
|
||||
Validation
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/yunify/qingstor-sdk-go/config"
|
||||
)
|
||||
|
||||
// Operation stores information of an operation.
|
||||
type Operation struct {
|
||||
Config *config.Config
|
||||
Properties interface{}
|
||||
|
||||
APIName string
|
||||
ServiceName string
|
||||
|
||||
RequestMethod string
|
||||
RequestURI string
|
||||
|
||||
StatusCodes []int
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package data
|
||||
|
||||
// Validation defines the validate interface.
|
||||
type Validation interface {
|
||||
Validate() error
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ParameterRequiredError indicates that the required parameter is missing.
|
||||
type ParameterRequiredError struct {
|
||||
ParameterName string
|
||||
ParentName string
|
||||
}
|
||||
|
||||
// Error returns the description of ParameterRequiredError.
|
||||
func (e ParameterRequiredError) Error() string {
|
||||
return fmt.Sprintf(`"%s" is required in "%s"`, e.ParameterName, e.ParentName)
|
||||
}
|
||||
|
||||
// ParameterValueNotAllowedError indicates that the parameter value is not allowed.
|
||||
type ParameterValueNotAllowedError struct {
|
||||
ParameterName string
|
||||
ParameterValue string
|
||||
AllowedValues []string
|
||||
}
|
||||
|
||||
// Error returns the description of ParameterValueNotAllowedError.
|
||||
func (e ParameterValueNotAllowedError) Error() string {
|
||||
allowedValues := []string{}
|
||||
for _, value := range e.AllowedValues {
|
||||
allowedValues = append(allowedValues, "\""+value+"\"")
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
`"%s" value "%s" is not allowed, should be one of %s`,
|
||||
e.ParameterName,
|
||||
e.ParameterValue,
|
||||
strings.Join(allowedValues, ", "))
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
// QingStorError stores information of an QingStor error response.
|
||||
type QingStorError struct {
|
||||
StatusCode int
|
||||
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"request_id"`
|
||||
ReferenceURL string `json:"url"`
|
||||
}
|
||||
|
||||
// Error returns the description of QingStor error response.
|
||||
func (qse QingStorError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"QingStor Error: StatusCode \"%d\", Code \"%s\", Message \"%s\", Request ID \"%s\", Reference URL \"%s\"",
|
||||
qse.StatusCode, qse.Code, qse.Message, qse.RequestID, qse.ReferenceURL)
|
||||
}
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/pengsrc/go-shared/convert"
|
||||
|
||||
"github.com/yunify/qingstor-sdk-go/logger"
|
||||
"github.com/yunify/qingstor-sdk-go/request/builder"
|
||||
"github.com/yunify/qingstor-sdk-go/request/data"
|
||||
"github.com/yunify/qingstor-sdk-go/request/signer"
|
||||
"github.com/yunify/qingstor-sdk-go/request/unpacker"
|
||||
)
|
||||
|
||||
// A Request can build, sign, send and unpack API request.
|
||||
type Request struct {
|
||||
Operation *data.Operation
|
||||
Input *reflect.Value
|
||||
Output *reflect.Value
|
||||
|
||||
HTTPRequest *http.Request
|
||||
HTTPResponse *http.Response
|
||||
}
|
||||
|
||||
// New create a Request from given Operation, Input and Output.
|
||||
// It returns a Request.
|
||||
func New(o *data.Operation, i data.Input, x interface{}) (*Request, error) {
|
||||
input := reflect.ValueOf(i)
|
||||
if input.IsValid() && input.Elem().IsValid() {
|
||||
err := i.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
output := reflect.ValueOf(x)
|
||||
|
||||
return &Request{
|
||||
Operation: o,
|
||||
Input: &input,
|
||||
Output: &output,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Send sends API request.
|
||||
// It returns error if error occurred.
|
||||
func (r *Request) Send() error {
|
||||
err := r.check()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.sign()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.send()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.unpack()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign sign the API request by setting the authorization header.
|
||||
// It returns error if error occurred.
|
||||
func (r *Request) Sign() error {
|
||||
err := r.check()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.sign()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SignQuery sign the API request by appending query string.
|
||||
// It returns error if error occurred.
|
||||
func (r *Request) SignQuery(timeoutSeconds int) error {
|
||||
err := r.check()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = r.signQuery(int(time.Now().Unix()) + timeoutSeconds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Request) check() error {
|
||||
if r.Operation.Config.AccessKeyID == "" {
|
||||
return errors.New("access key not provided")
|
||||
}
|
||||
|
||||
if r.Operation.Config.SecretAccessKey == "" {
|
||||
return errors.New("secret access key not provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Request) build() error {
|
||||
b := &builder.QingStorBuilder{}
|
||||
httpRequest, err := b.BuildHTTPRequest(r.Operation, r.Input)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.HTTPRequest = httpRequest
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Request) sign() error {
|
||||
s := &signer.QingStorSigner{
|
||||
AccessKeyID: r.Operation.Config.AccessKeyID,
|
||||
SecretAccessKey: r.Operation.Config.SecretAccessKey,
|
||||
}
|
||||
err := s.WriteSignature(r.HTTPRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Request) signQuery(expires int) error {
|
||||
s := &signer.QingStorSigner{
|
||||
AccessKeyID: r.Operation.Config.AccessKeyID,
|
||||
SecretAccessKey: r.Operation.Config.SecretAccessKey,
|
||||
}
|
||||
err := s.WriteQuerySignature(r.HTTPRequest, expires)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Request) send() error {
|
||||
var response *http.Response
|
||||
var err error
|
||||
|
||||
if r.Operation.Config.Connection == nil {
|
||||
return errors.New("connection not initialized")
|
||||
}
|
||||
|
||||
retries := r.Operation.Config.ConnectionRetries + 1
|
||||
for {
|
||||
if retries > 0 {
|
||||
logger.Info(fmt.Sprintf(
|
||||
"Sending request: [%d] %s %s",
|
||||
convert.StringToUnixTimestamp(r.HTTPRequest.Header.Get("Date"), convert.RFC822),
|
||||
r.Operation.RequestMethod,
|
||||
r.HTTPRequest.Host,
|
||||
))
|
||||
|
||||
response, err = r.Operation.Config.Connection.Do(r.HTTPRequest)
|
||||
if err == nil {
|
||||
retries = 0
|
||||
} else {
|
||||
retries--
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.HTTPResponse = response
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Request) unpack() error {
|
||||
u := &unpacker.QingStorUnpacker{}
|
||||
err := u.UnpackHTTPRequest(r.Operation, r.HTTPResponse, r.Output)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/yunify/qingstor-sdk-go/config"
|
||||
"github.com/yunify/qingstor-sdk-go/logger"
|
||||
"github.com/yunify/qingstor-sdk-go/request/data"
|
||||
"github.com/yunify/qingstor-sdk-go/request/errors"
|
||||
)
|
||||
|
||||
type SomeActionProperties struct {
|
||||
A *string `json:"a" name:"a"`
|
||||
B *string `json:"b" name:"b"`
|
||||
CD *string `json:"c-d" name:"c-d"`
|
||||
}
|
||||
|
||||
type SomeActionInput struct {
|
||||
Date *time.Time `json:"Date" name:"Date" format:"RFC 822" location:"headers"`
|
||||
IfModifiedSince *time.Time `json:"If-Modified-Since" name:"If-Modified-Since" format:"RFC 822" location:"headers"`
|
||||
Range *string `json:"Range" name:"Range" location:"headers"`
|
||||
UploadID *string `json:"upload_id" name:"upload_id" location:"params"`
|
||||
Count *int `json:"count" name:"count" location:"elements"`
|
||||
}
|
||||
|
||||
func (s *SomeActionInput) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type SomeActionOutput struct {
|
||||
StatusCode *int `location:"statusCode"`
|
||||
Error *errors.QingStorError
|
||||
RequestID *string `location:"requestID"`
|
||||
}
|
||||
|
||||
func String(v string) *string {
|
||||
return &v
|
||||
}
|
||||
|
||||
func Int(v int) *int {
|
||||
return &v
|
||||
}
|
||||
|
||||
func Time(v time.Time) *time.Time {
|
||||
return &v
|
||||
}
|
||||
|
||||
func TestRequest_Send(t *testing.T) {
|
||||
conf, err := config.New("ACCESS_KEY_ID", "SECRET_ACCESS_KEY")
|
||||
assert.Nil(t, err)
|
||||
logger.SetLevel("warn")
|
||||
|
||||
operation := &data.Operation{
|
||||
Config: conf,
|
||||
Properties: &SomeActionProperties{
|
||||
A: String("aaa"),
|
||||
B: String("bbb"),
|
||||
CD: String("ccc-ddd"),
|
||||
},
|
||||
APIName: "Some Action",
|
||||
RequestMethod: "GET",
|
||||
RequestURI: "/<a>/<b>/<c-d>",
|
||||
StatusCodes: []int{
|
||||
200, // OK
|
||||
206, // Partial content
|
||||
304, // Not modified
|
||||
412, // Precondition failed
|
||||
},
|
||||
}
|
||||
|
||||
output := &SomeActionOutput{}
|
||||
r, err := New(operation, &SomeActionInput{
|
||||
Date: Time(time.Date(2016, 9, 1, 15, 30, 0, 0, time.UTC)),
|
||||
IfModifiedSince: Time(time.Date(2016, 9, 1, 15, 30, 0, 0, time.UTC)),
|
||||
Range: String("100-"),
|
||||
UploadID: String("0"),
|
||||
Count: Int(23),
|
||||
}, output)
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = r.build()
|
||||
assert.Nil(t, err)
|
||||
|
||||
err = r.sign()
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Equal(t, r.HTTPRequest.URL.String(), "https://qingstor.com:443/aaa/bbb/ccc-ddd?upload_id=0")
|
||||
assert.Equal(t, r.HTTPRequest.Header.Get("Range"), "100-")
|
||||
assert.Equal(t, r.HTTPRequest.Header.Get("If-Modified-Since"), "Thu, 01 Sep 2016 15:30:00 GMT")
|
||||
assert.Equal(t, r.HTTPRequest.Header.Get("Content-Length"), "12")
|
||||
assert.Equal(t, r.HTTPRequest.Header.Get("Authorization"), "QS ACCESS_KEY_ID:pA7G9qo4iQ6YHu7p4fX9Wcg4V9S6Mcgvz7p/0wEdz78=")
|
||||
|
||||
httpResponse := &http.Response{Header: http.Header{}}
|
||||
httpResponse.StatusCode = 400
|
||||
httpResponse.Header.Set("Content-Type", "application/json")
|
||||
responseString := `{
|
||||
"code": "bad_request",
|
||||
"message": "Invalid argument(s) or invalid argument value(s)",
|
||||
"request_id": "1e588695254aa08cf7a43f612e6ce14b",
|
||||
"url": "http://docs.qingcloud.com/object_storage/api/object/get.html"
|
||||
}`
|
||||
httpResponse.Body = ioutil.NopCloser(bytes.NewReader([]byte(responseString)))
|
||||
assert.Nil(t, err)
|
||||
r.HTTPResponse = httpResponse
|
||||
|
||||
err = r.unpack()
|
||||
assert.NotNil(t, err)
|
||||
|
||||
switch e := err.(type) {
|
||||
case *errors.QingStorError:
|
||||
assert.Equal(t, "bad_request", e.Code)
|
||||
assert.Equal(t, "1e588695254aa08cf7a43f612e6ce14b", e.RequestID)
|
||||
}
|
||||
}
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package signer
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/pengsrc/go-shared/convert"
|
||||
|
||||
"github.com/yunify/qingstor-sdk-go/logger"
|
||||
"github.com/yunify/qingstor-sdk-go/utils"
|
||||
)
|
||||
|
||||
// QingStorSigner is the http request signer for QingStor service.
|
||||
type QingStorSigner struct {
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
}
|
||||
|
||||
// WriteSignature calculates signature and write it to http request header.
|
||||
func (qss *QingStorSigner) WriteSignature(request *http.Request) error {
|
||||
authorization, err := qss.BuildSignature(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request.Header.Set("Authorization", authorization)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteQuerySignature calculates signature and write it to http request url.
|
||||
func (qss *QingStorSigner) WriteQuerySignature(request *http.Request, expires int) error {
|
||||
query, err := qss.BuildQuerySignature(request, expires)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if request.URL.RawQuery != "" {
|
||||
query = "?" + request.URL.RawQuery + "&" + query
|
||||
} else {
|
||||
query = "?" + query
|
||||
}
|
||||
|
||||
newRequest, err := http.NewRequest(request.Method,
|
||||
request.URL.Scheme+"://"+request.URL.Host+utils.URLQueryEscape(request.URL.Path)+query, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.URL = newRequest.URL
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildSignature calculates the signature string.
|
||||
func (qss *QingStorSigner) BuildSignature(request *http.Request) (string, error) {
|
||||
stringToSign, err := qss.BuildStringToSign(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
h := hmac.New(sha256.New, []byte(qss.SecretAccessKey))
|
||||
h.Write([]byte(stringToSign))
|
||||
|
||||
signature := strings.TrimSpace(base64.StdEncoding.EncodeToString(h.Sum(nil)))
|
||||
authorization := "QS " + qss.AccessKeyID + ":" + signature
|
||||
|
||||
logger.Debug(fmt.Sprintf(
|
||||
"QingStor authorization: [%d] %s",
|
||||
convert.StringToUnixTimestamp(request.Header.Get("Date"), convert.RFC822),
|
||||
authorization),
|
||||
)
|
||||
|
||||
return authorization, nil
|
||||
}
|
||||
|
||||
// BuildQuerySignature calculates the signature string for query.
|
||||
func (qss *QingStorSigner) BuildQuerySignature(request *http.Request, expires int) (string, error) {
|
||||
stringToSign, err := qss.BuildQueryStringToSign(request, expires)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
h := hmac.New(sha256.New, []byte(qss.SecretAccessKey))
|
||||
h.Write([]byte(stringToSign))
|
||||
|
||||
signature := strings.TrimSpace(base64.StdEncoding.EncodeToString(h.Sum(nil)))
|
||||
signature = utils.URLQueryEscape(signature)
|
||||
query := fmt.Sprintf(
|
||||
"access_key_id=%s&expires=%d&signature=%s",
|
||||
qss.AccessKeyID, expires, signature,
|
||||
)
|
||||
|
||||
logger.Debug(fmt.Sprintf(
|
||||
"QingStor query signature: [%d] %s",
|
||||
convert.StringToUnixTimestamp(request.Header.Get("Date"), convert.RFC822),
|
||||
query,
|
||||
))
|
||||
|
||||
return query, nil
|
||||
}
|
||||
|
||||
// BuildStringToSign build the string to sign.
|
||||
func (qss *QingStorSigner) BuildStringToSign(request *http.Request) (string, error) {
|
||||
stringToSign := fmt.Sprintf(
|
||||
"%s\n%s\n%s\n%s\n",
|
||||
request.Method,
|
||||
request.Header.Get("Content-MD5"),
|
||||
request.Header.Get("Content-Type"),
|
||||
request.Header.Get("Date"),
|
||||
)
|
||||
|
||||
stringToSign += qss.buildCanonicalizedHeaders(request)
|
||||
canonicalizedResource, err := qss.buildCanonicalizedResource(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stringToSign += canonicalizedResource
|
||||
|
||||
logger.Debug(fmt.Sprintf(
|
||||
"QingStor string to sign: [%d] %s",
|
||||
convert.StringToUnixTimestamp(request.Header.Get("Date"), convert.RFC822),
|
||||
stringToSign,
|
||||
))
|
||||
|
||||
return stringToSign, nil
|
||||
}
|
||||
|
||||
// BuildQueryStringToSign build the string to sign for query.
|
||||
func (qss *QingStorSigner) BuildQueryStringToSign(request *http.Request, expires int) (string, error) {
|
||||
stringToSign := fmt.Sprintf(
|
||||
"%s\n%s\n%s\n%d\n",
|
||||
request.Method,
|
||||
request.Header.Get("Content-MD5"),
|
||||
request.Header.Get("Content-Type"),
|
||||
expires,
|
||||
)
|
||||
|
||||
stringToSign += qss.buildCanonicalizedHeaders(request)
|
||||
canonicalizedResource, err := qss.buildCanonicalizedResource(request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stringToSign += canonicalizedResource
|
||||
|
||||
logger.Debug(fmt.Sprintf(
|
||||
"QingStor query string to sign: [%d] %s",
|
||||
convert.StringToUnixTimestamp(request.Header.Get("Date"), convert.RFC822),
|
||||
stringToSign,
|
||||
))
|
||||
|
||||
return stringToSign, nil
|
||||
}
|
||||
|
||||
func (qss *QingStorSigner) buildCanonicalizedHeaders(request *http.Request) string {
|
||||
keys := []string{}
|
||||
for key := range request.Header {
|
||||
if strings.HasPrefix(strings.ToLower(key), "x-qs-") {
|
||||
keys = append(keys, strings.TrimSpace(strings.ToLower(key)))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
canonicalizedHeaders := ""
|
||||
for _, key := range keys {
|
||||
canonicalizedHeaders += key + ":" + strings.TrimSpace(request.Header.Get(key)) + "\n"
|
||||
}
|
||||
|
||||
return canonicalizedHeaders
|
||||
}
|
||||
|
||||
func (qss *QingStorSigner) buildCanonicalizedResource(request *http.Request) (string, error) {
|
||||
path := utils.URLQueryEscape(request.URL.Path)
|
||||
query := request.URL.Query()
|
||||
|
||||
keys := []string{}
|
||||
for key := range query {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
parts := []string{}
|
||||
for _, key := range keys {
|
||||
values := query[key]
|
||||
if qss.paramsToSign(key) {
|
||||
if len(values) > 0 {
|
||||
if values[0] != "" {
|
||||
value := strings.TrimSpace(strings.Join(values, ""))
|
||||
value, err := utils.URLQueryUnescape(value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parts = append(parts, key+"="+value)
|
||||
} else {
|
||||
parts = append(parts, key)
|
||||
}
|
||||
} else {
|
||||
parts = append(parts, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
joinedParts := strings.Join(parts, "&")
|
||||
if joinedParts != "" {
|
||||
path = path + "?" + joinedParts
|
||||
}
|
||||
|
||||
logger.Debug(fmt.Sprintf(
|
||||
"QingStor canonicalized resource: [%d] %s",
|
||||
convert.StringToUnixTimestamp(request.Header.Get("Date"), convert.RFC822),
|
||||
path,
|
||||
))
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func (qss *QingStorSigner) paramsToSign(key string) bool {
|
||||
keysMap := map[string]bool{
|
||||
"acl": true,
|
||||
"cors": true,
|
||||
"delete": true,
|
||||
"mirror": true,
|
||||
"part_number": true,
|
||||
"policy": true,
|
||||
"stats": true,
|
||||
"upload_id": true,
|
||||
"uploads": true,
|
||||
"response-expires": true,
|
||||
"response-cache-control": true,
|
||||
"response-content-type": true,
|
||||
"response-content-language": true,
|
||||
"response-content-encoding": true,
|
||||
"response-content-disposition": true,
|
||||
}
|
||||
|
||||
return keysMap[key]
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package signer
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pengsrc/go-shared/convert"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestQingStorSignerWriteSignature(t *testing.T) {
|
||||
url := "https://qingstor.com/?acl&upload_id=fde133b5f6d932cd9c79bac3c7318da1&part_number=0&other=abc"
|
||||
httpRequest, err := http.NewRequest("GET", url, nil)
|
||||
httpRequest.Header.Set("Date", convert.TimeToString(time.Time{}, convert.RFC822))
|
||||
httpRequest.Header.Set("X-QS-Test-2", "Test 2")
|
||||
httpRequest.Header.Set("X-QS-Test-1", "Test 1")
|
||||
assert.Nil(t, err)
|
||||
|
||||
s := QingStorSigner{
|
||||
AccessKeyID: "ENV_ACCESS_KEY_ID",
|
||||
SecretAccessKey: "ENV_SECRET_ACCESS_KEY",
|
||||
}
|
||||
|
||||
err = s.WriteSignature(httpRequest)
|
||||
assert.Nil(t, err)
|
||||
|
||||
signature := "QS ENV_ACCESS_KEY_ID:bvglZF9iMOv1RaCTxPYWxexmt1UN2m5WKngYnhDEp2c="
|
||||
assert.Equal(t, signature, httpRequest.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func TestQingStorSignerWriteSignatureChinese(t *testing.T) {
|
||||
url := "https://zone.qingstor.com/bucket-name/中文"
|
||||
httpRequest, err := http.NewRequest("GET", url, nil)
|
||||
httpRequest.Header.Set("Date", convert.TimeToString(time.Time{}, convert.RFC822))
|
||||
assert.Nil(t, err)
|
||||
|
||||
s := QingStorSigner{
|
||||
AccessKeyID: "ENV_ACCESS_KEY_ID",
|
||||
SecretAccessKey: "ENV_SECRET_ACCESS_KEY",
|
||||
}
|
||||
|
||||
err = s.WriteSignature(httpRequest)
|
||||
assert.Nil(t, err)
|
||||
|
||||
signature := "QS ENV_ACCESS_KEY_ID:XsTXX50kzqBf92zLG1aIUIJmZ0hqIHoaHgkumwnV3fs="
|
||||
assert.Equal(t, signature, httpRequest.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
func TestQingStorSignerWriteQuerySignature(t *testing.T) {
|
||||
url := "https://qingstor.com/?acl&upload_id=fde133b5f6d932cd9c79bac3c7318da1&part_number=0"
|
||||
httpRequest, err := http.NewRequest("GET", url, nil)
|
||||
httpRequest.Header.Set("Date", convert.TimeToString(time.Time{}, convert.RFC822))
|
||||
httpRequest.Header.Set("X-QS-Test-2", "Test 2")
|
||||
httpRequest.Header.Set("X-QS-Test-1", "Test 1")
|
||||
assert.Nil(t, err)
|
||||
|
||||
s := QingStorSigner{
|
||||
AccessKeyID: "ENV_ACCESS_KEY_ID",
|
||||
SecretAccessKey: "ENV_SECRET_ACCESS_KEY",
|
||||
}
|
||||
|
||||
err = s.WriteQuerySignature(httpRequest, 3600)
|
||||
assert.Nil(t, err)
|
||||
|
||||
targetURL := "https://qingstor.com/?acl&upload_id=fde133b5f6d932cd9c79bac3c7318da1&part_number=0&access_key_id=ENV_ACCESS_KEY_ID&expires=3600&signature=GRL3p3NOgHR9CQygASvyo344vdnO1hFke6ZvQ5mDVHM="
|
||||
assert.Equal(t, httpRequest.URL.String(), targetURL)
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package unpacker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/pengsrc/go-shared/convert"
|
||||
"github.com/pengsrc/go-shared/json"
|
||||
|
||||
"github.com/yunify/qingstor-sdk-go/logger"
|
||||
"github.com/yunify/qingstor-sdk-go/request/data"
|
||||
)
|
||||
|
||||
// BaseUnpacker is the base unpacker for all services.
|
||||
type BaseUnpacker struct {
|
||||
operation *data.Operation
|
||||
httpResponse *http.Response
|
||||
output *reflect.Value
|
||||
}
|
||||
|
||||
// UnpackHTTPRequest unpacks http response with an operation and an output.
|
||||
func (b *BaseUnpacker) UnpackHTTPRequest(o *data.Operation, r *http.Response, x *reflect.Value) error {
|
||||
b.operation = o
|
||||
b.httpResponse = r
|
||||
b.output = x
|
||||
|
||||
err := b.exposeStatusCode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.parseResponseHeaders()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.parseResponseBody()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = b.parseResponseElements()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseUnpacker) exposeStatusCode() error {
|
||||
value := b.output.Elem().FieldByName("StatusCode")
|
||||
if value.IsValid() {
|
||||
switch value.Interface().(type) {
|
||||
case *int:
|
||||
logger.Info(fmt.Sprintf(
|
||||
"QingStor response status code: [%d] %d",
|
||||
convert.StringToUnixTimestamp(b.httpResponse.Header.Get("Date"), convert.RFC822),
|
||||
b.httpResponse.StatusCode,
|
||||
))
|
||||
value.Set(reflect.ValueOf(&b.httpResponse.StatusCode))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseUnpacker) parseResponseHeaders() error {
|
||||
logger.Info(fmt.Sprintf(
|
||||
"QingStor response headers: [%d] %s",
|
||||
convert.StringToUnixTimestamp(b.httpResponse.Header.Get("Date"), convert.RFC822),
|
||||
fmt.Sprint(b.httpResponse.Header),
|
||||
))
|
||||
|
||||
if b.isResponseRight() {
|
||||
fields := b.output.Elem()
|
||||
for i := 0; i < fields.NumField(); i++ {
|
||||
field := fields.Field(i)
|
||||
fieldTagName := fields.Type().Field(i).Tag.Get("name")
|
||||
fieldTagLocation := fields.Type().Field(i).Tag.Get("location")
|
||||
fieldStringValue := b.httpResponse.Header.Get(fieldTagName)
|
||||
|
||||
if fieldTagName != "" && fieldTagLocation == "headers" {
|
||||
switch field.Interface().(type) {
|
||||
case *string:
|
||||
field.Set(reflect.ValueOf(&fieldStringValue))
|
||||
case *int:
|
||||
intValue, err := strconv.Atoi(fieldStringValue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.Set(reflect.ValueOf(&intValue))
|
||||
case *int64:
|
||||
int64Value, err := strconv.ParseInt(fieldStringValue, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.Set(reflect.ValueOf(&int64Value))
|
||||
case *bool:
|
||||
case *time.Time:
|
||||
formatString := fields.Type().Field(i).Tag.Get("format")
|
||||
format := ""
|
||||
switch formatString {
|
||||
case "RFC 822":
|
||||
format = convert.RFC822
|
||||
case "ISO 8601":
|
||||
format = convert.ISO8601
|
||||
}
|
||||
timeValue, err := convert.StringToTime(fieldStringValue, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
field.Set(reflect.ValueOf(&timeValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseUnpacker) parseResponseBody() error {
|
||||
if b.isResponseRight() {
|
||||
value := b.output.Elem().FieldByName("Body")
|
||||
if value.IsValid() {
|
||||
switch value.Type().String() {
|
||||
case "string":
|
||||
buffer := &bytes.Buffer{}
|
||||
buffer.ReadFrom(b.httpResponse.Body)
|
||||
b.httpResponse.Body.Close()
|
||||
|
||||
logger.Info(fmt.Sprintf(
|
||||
"QingStor response body string: [%d] %s",
|
||||
convert.StringToUnixTimestamp(b.httpResponse.Header.Get("Date"), convert.RFC822),
|
||||
string(buffer.Bytes()),
|
||||
))
|
||||
|
||||
value.SetString(string(buffer.Bytes()))
|
||||
case "io.ReadCloser":
|
||||
value.Set(reflect.ValueOf(b.httpResponse.Body))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseUnpacker) parseResponseElements() error {
|
||||
if b.isResponseRight() {
|
||||
if b.httpResponse.Header.Get("Content-Type") == "application/json" {
|
||||
buffer := &bytes.Buffer{}
|
||||
buffer.ReadFrom(b.httpResponse.Body)
|
||||
b.httpResponse.Body.Close()
|
||||
|
||||
logger.Info(fmt.Sprintf(
|
||||
"QingStor response body string: [%d] %s",
|
||||
convert.StringToUnixTimestamp(b.httpResponse.Header.Get("Date"), convert.RFC822),
|
||||
string(buffer.Bytes()),
|
||||
))
|
||||
|
||||
_, err := json.Decode(buffer.Bytes(), b.output.Interface())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BaseUnpacker) isResponseRight() bool {
|
||||
rightStatusCodes := b.operation.StatusCodes
|
||||
if len(rightStatusCodes) == 0 {
|
||||
rightStatusCodes = append(rightStatusCodes, 200)
|
||||
}
|
||||
|
||||
flag := false
|
||||
for _, statusCode := range rightStatusCodes {
|
||||
if statusCode == b.httpResponse.StatusCode {
|
||||
flag = true
|
||||
}
|
||||
}
|
||||
|
||||
return flag
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package unpacker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yunify/qingstor-sdk-go/request/data"
|
||||
)
|
||||
|
||||
func StringValue(v *string) string {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func IntValue(v *int) int {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func Int64Value(v *int64) int64 {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func TimeValue(v *time.Time) time.Time {
|
||||
if v != nil {
|
||||
return *v
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func TestBaseUnpacker_UnpackHTTPRequest(t *testing.T) {
|
||||
type FakeOutput struct {
|
||||
StatusCode *int
|
||||
|
||||
A *string `location:"elements" json:"a" name:"a"`
|
||||
B *string `location:"elements" json:"b" name:"b"`
|
||||
CD *int `location:"elements" json:"cd" name:"cd"`
|
||||
EF *int64 `location:"elements" json:"ef" name:"ef"`
|
||||
}
|
||||
|
||||
httpResponse := &http.Response{Header: http.Header{}}
|
||||
httpResponse.StatusCode = 200
|
||||
httpResponse.Header.Set("Content-Type", "application/json")
|
||||
responseString := `{"a": "el_a", "b": "el_b", "cd": 1024, "ef": 2048}`
|
||||
httpResponse.Body = ioutil.NopCloser(bytes.NewReader([]byte(responseString)))
|
||||
|
||||
output := &FakeOutput{}
|
||||
outputValue := reflect.ValueOf(output)
|
||||
unpacker := BaseUnpacker{}
|
||||
err := unpacker.UnpackHTTPRequest(&data.Operation{}, httpResponse, &outputValue)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 200, IntValue(output.StatusCode))
|
||||
assert.Equal(t, "el_a", StringValue(output.A))
|
||||
assert.Equal(t, "el_b", StringValue(output.B))
|
||||
assert.Equal(t, 1024, IntValue(output.CD))
|
||||
assert.Equal(t, int64(2048), Int64Value(output.EF))
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package unpacker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/http"
|
||||
"reflect"
|
||||
|
||||
"github.com/pengsrc/go-shared/json"
|
||||
|
||||
"github.com/yunify/qingstor-sdk-go/request/data"
|
||||
"github.com/yunify/qingstor-sdk-go/request/errors"
|
||||
)
|
||||
|
||||
// QingStorUnpacker is the response unpacker for QingStor service.
|
||||
type QingStorUnpacker struct {
|
||||
baseUnpacker *BaseUnpacker
|
||||
}
|
||||
|
||||
// UnpackHTTPRequest unpack the http response with an operation, http response and an output.
|
||||
func (qu *QingStorUnpacker) UnpackHTTPRequest(o *data.Operation, r *http.Response, x *reflect.Value) error {
|
||||
qu.baseUnpacker = &BaseUnpacker{}
|
||||
err := qu.baseUnpacker.UnpackHTTPRequest(o, r, x)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = qu.parseError()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qu *QingStorUnpacker) parseError() error {
|
||||
if !qu.baseUnpacker.isResponseRight() {
|
||||
if qu.baseUnpacker.httpResponse.Header.Get("Content-Type") == "application/json" {
|
||||
buffer := &bytes.Buffer{}
|
||||
buffer.ReadFrom(qu.baseUnpacker.httpResponse.Body)
|
||||
qu.baseUnpacker.httpResponse.Body.Close()
|
||||
|
||||
qsError := &errors.QingStorError{}
|
||||
if buffer.Len() > 0 {
|
||||
_, err := json.Decode(buffer.Bytes(), qsError)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
qsError.StatusCode = qu.baseUnpacker.httpResponse.StatusCode
|
||||
|
||||
return qsError
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Copyright (C) 2016 Yunify, Inc.
|
||||
// +-------------------------------------------------------------------------
|
||||
// | Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// | you may not use this work except in compliance with the License.
|
||||
// | You may obtain a copy of the License in the LICENSE file, or at:
|
||||
// |
|
||||
// | http://www.apache.org/licenses/LICENSE-2.0
|
||||
// |
|
||||
// | Unless required by applicable law or agreed to in writing, software
|
||||
// | distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// | See the License for the specific language governing permissions and
|
||||
// | limitations under the License.
|
||||
// +-------------------------------------------------------------------------
|
||||
|
||||
package unpacker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yunify/qingstor-sdk-go/request/data"
|
||||
"github.com/yunify/qingstor-sdk-go/request/errors"
|
||||
)
|
||||
|
||||
func TestQingStorUnpacker_UnpackHTTPRequest(t *testing.T) {
|
||||
type Bucket struct {
|
||||
// Created time of the Bucket
|
||||
Created *time.Time `json:"created" name:"created" format:"RFC 822"`
|
||||
// QingCloud Zone ID
|
||||
Location *string `json:"location" name:"location"`
|
||||
// Bucket name
|
||||
Name *string `json:"name" name:"name"`
|
||||
// URL to access the Bucket
|
||||
URL *string `json:"url" name:"url"`
|
||||
}
|
||||
|
||||
type ListBucketsOutput struct {
|
||||
StatusCode *int `location:"statusCode"`
|
||||
Error *errors.QingStorError
|
||||
RequestID *string `location:"requestID"`
|
||||
|
||||
XTestHeader *string `json:"X-Test-Header" name:"X-Test-Header" location:"headers"`
|
||||
XTestTime *time.Time `json:"X-Test-Time" name:"X-Test-Time" format:"RFC 822" location:"headers"`
|
||||
|
||||
// Buckets information
|
||||
Buckets []*Bucket `json:"buckets" name:"buckets"`
|
||||
// Bucket count
|
||||
Count *int `json:"count" name:"count"`
|
||||
}
|
||||
|
||||
httpResponse := &http.Response{Header: http.Header{
|
||||
"X-Test-Header": []string{"test-header"},
|
||||
"X-Test-Time": []string{"Thu, 01 Sep 2016 07:30:00 GMT"},
|
||||
}}
|
||||
httpResponse.StatusCode = 200
|
||||
httpResponse.Header.Set("Content-Type", "application/json")
|
||||
responseString := `{
|
||||
"count": 2,
|
||||
"buckets": [
|
||||
{
|
||||
"name": "test-bucket",
|
||||
"location": "pek3a",
|
||||
"url": "https://test-bucket.pek3a.qingstor.com",
|
||||
"created": "2015-07-11T04:45:57Z"
|
||||
},
|
||||
{
|
||||
"name": "test-photos",
|
||||
"location": "pek3a",
|
||||
"url": "https://test-photos.pek3a.qingstor.com",
|
||||
"created": "2015-07-12T09:40:32Z"
|
||||
}
|
||||
]
|
||||
}`
|
||||
httpResponse.Body = ioutil.NopCloser(bytes.NewReader([]byte(responseString)))
|
||||
|
||||
output := &ListBucketsOutput{}
|
||||
outputValue := reflect.ValueOf(output)
|
||||
unpacker := QingStorUnpacker{}
|
||||
err := unpacker.UnpackHTTPRequest(&data.Operation{}, httpResponse, &outputValue)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test-header", StringValue(output.XTestHeader))
|
||||
assert.Equal(t, time.Date(2016, 9, 1, 7, 30, 0, 0, time.UTC), TimeValue(output.XTestTime))
|
||||
assert.Equal(t, 2, IntValue(output.Count))
|
||||
assert.Equal(t, "test-bucket", StringValue(output.Buckets[0].Name))
|
||||
assert.Equal(t, "pek3a", StringValue(output.Buckets[0].Location))
|
||||
assert.Equal(t, time.Date(2015, 7, 12, 9, 40, 32, 0, time.UTC), TimeValue(output.Buckets[1].Created))
|
||||
}
|
||||
|
||||
func TestQingStorUnpacker_UnpackHTTPRequestWithError(t *testing.T) {
|
||||
type ListBucketsOutput struct {
|
||||
StatusCode *int `location:"statusCode"`
|
||||
Error *errors.QingStorError
|
||||
RequestID *string `location:"requestID"`
|
||||
}
|
||||
|
||||
httpResponse := &http.Response{Header: http.Header{}}
|
||||
httpResponse.StatusCode = 400
|
||||
httpResponse.Header.Set("Content-Type", "application/json")
|
||||
responseString := `{
|
||||
"code": "bad_request",
|
||||
"message": "Invalid argument(s) or invalid argument value(s)",
|
||||
"request_id": "aa08cf7a43f611e5886952542e6ce14b",
|
||||
"url": "http://docs.qingcloud.com/object_storage/api/bucket/get.html"
|
||||
}`
|
||||
httpResponse.Body = ioutil.NopCloser(bytes.NewReader([]byte(responseString)))
|
||||
|
||||
output := &ListBucketsOutput{}
|
||||
outputValue := reflect.ValueOf(output)
|
||||
unpacker := QingStorUnpacker{}
|
||||
err := unpacker.UnpackHTTPRequest(&data.Operation{}, httpResponse, &outputValue)
|
||||
assert.NotNil(t, err)
|
||||
switch e := err.(type) {
|
||||
case *errors.QingStorError:
|
||||
assert.Equal(t, "bad_request", e.Code)
|
||||
assert.Equal(t, "aa08cf7a43f611e5886952542e6ce14b", e.RequestID)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user