diff options
| -rw-r--r-- | account_test.go | 9 | ||||
| -rw-r--r-- | headers.go | 129 | ||||
| -rw-r--r-- | headers_test.go | 8 | ||||
| -rw-r--r-- | request.go | 35 |
4 files changed, 97 insertions, 84 deletions
diff --git a/account_test.go b/account_test.go index 83567f9..7a07fa4 100644 --- a/account_test.go +++ b/account_test.go @@ -18,7 +18,10 @@ package schwift -import "testing" +import ( + "net/http" + "testing" +) func TestAccountBasic(t *testing.T) { testWithAccount(t, func(a *Account) { @@ -37,7 +40,7 @@ func TestAccountBasic(t *testing.T) { func TestAccountMetadata(t *testing.T) { testWithAccount(t, func(a *Account) { err := a.Post(AccountHeaders{ - Metadata: map[string]string{"schwift-test": "first"}, + Metadata: http.Header{"Schwift-Test": {"first"}}, }, nil) if !expectError(t, err, nil) { t.FailNow() @@ -47,6 +50,6 @@ func TestAccountMetadata(t *testing.T) { if !expectError(t, err, nil) { t.FailNow() } - expectString(t, hdr.Metadata["schwift-test"], "first") + expectString(t, hdr.Metadata.Get("schwift-test"), "first") }) } @@ -21,6 +21,7 @@ package schwift import ( "fmt" "net/http" + "net/textproto" "reflect" "strconv" "strings" @@ -32,10 +33,10 @@ import ( //headers, as noted in the tags next to each field. Well-known metadata headers //can be accessed in a type-safe way using the methods on this type. type AccountHeaders struct { - BytesUsed uint64 `schwift:"ro,X-Account-Bytes-Used"` - ContainerCount uint64 `schwift:"ro,X-Account-Container-Count"` - ObjectCount uint64 `schwift:"ro,X-Account-Object-Count"` - Metadata map[string]string `schwift:"rw,X-Account-Meta-,X-Remove-Account-Meta-"` + BytesUsed uint64 `schwift:"ro,X-Account-Bytes-Used"` + ContainerCount uint64 `schwift:"ro,X-Account-Container-Count"` + ObjectCount uint64 `schwift:"ro,X-Account-Object-Count"` + Metadata http.Header `schwift:"rw,X-Account-Meta-"` Raw http.Header } @@ -44,7 +45,6 @@ func (a AccountHeaders) QuotaBytes() UnsignedIntField { return UnsignedIntField{ a.Metadata, "X-Account-Meta-", "Quota-Bytes", - false, } } @@ -53,7 +53,6 @@ func (a AccountHeaders) TempURLKey() StringField { return StringField{ a.Metadata, "X-Account-Meta-", "Temp-URL-Key", - false, } } @@ -62,7 +61,6 @@ func (a AccountHeaders) TempURLKey2() StringField { return StringField{ a.Metadata, "X-Account-Meta-", "Temp-URL-Key-2", - false, } } @@ -78,31 +76,26 @@ func (a AccountHeaders) TempURLKey2() StringField { // headers.TempURLKey().Set(value + " changed") // headers.TempURLKey().Clear() type StringField struct { - metadata map[string]string - prefix string - key string - clearByDeleting bool + metadata http.Header + prefix string + key string } //Get returns the value for this key, or the empty string if the key does not exist. func (f StringField) Get() string { - return f.metadata[f.key] + return f.metadata.Get(f.key) } //Set writes a new value for this key into the original AccountHeaders, //ContainerHeaders or ObjectHeaders instance. func (f StringField) Set(value string) { - f.metadata[f.key] = value + f.metadata.Set(f.key, value) } //Clear removes this key from the original AccountHeaders, ContainerHeaders or //ObjectHeaders instance. func (f StringField) Clear() { - if f.clearByDeleting { - delete(f.metadata, f.key) - } else { - f.metadata[f.key] = "" - } + f.metadata.Set(f.key, "") } //UnsignedIntField is a helper type used in the interface of AccountHeaders, @@ -110,19 +103,30 @@ func (f StringField) Clear() { // // var headers AccountHeaders // ... -// value, err := headers.QuotaBytes().Get() -// headers.QuotaBytes().Set(value * 2) +// if headers.QuotaBytes.Exists() { +// value, err := headers.QuotaBytes().Get() +// headers.QuotaBytes().Set(value * 2) +// } +// .... // headers.QuotaBytes().Clear() type UnsignedIntField struct { - metadata map[string]string - prefix string - key string - clearByDeleting bool + metadata http.Header + prefix string + key string +} + +//Exists returns whether there is a value for this key. +func (f UnsignedIntField) Exists() bool { + return f.metadata.Get(f.key) != "" } //Get returns the value for this key, or 0 if the key does not exist. func (f UnsignedIntField) Get() (uint64, error) { - value, err := strconv.ParseUint(f.metadata[f.key], 10, 64) + str := f.metadata.Get(f.key) + if str == "" { + return 0, nil + } + value, err := strconv.ParseUint(str, 10, 64) if err != nil { err = MalformedHeaderError{Key: f.prefix + f.key, ParseError: err} } @@ -132,17 +136,13 @@ func (f UnsignedIntField) Get() (uint64, error) { //Set writes a new value for this key into the original AccountHeaders, //ContainerHeaders or ObjectHeaders instance. func (f UnsignedIntField) Set(value uint64) { - f.metadata[f.key] = strconv.FormatUint(value, 10) + f.metadata.Set(f.key, strconv.FormatUint(value, 10)) } //Clear removes this key from the original AccountHeaders, ContainerHeaders or //ObjectHeaders instance. func (f UnsignedIntField) Clear() { - if f.clearByDeleting { - delete(f.metadata, f.key) - } else { - f.metadata[f.key] = "" - } + f.metadata.Set(f.key, "") } //////////////////////////////////////////////////////////////////////////////// @@ -151,8 +151,8 @@ func (f UnsignedIntField) Clear() { func parseHeaders(hdr http.Header, target interface{}) error { return foreachField(target, func(fieldPtr interface{}, info fieldInfo) error { //populate the .Raw field that all input types share - if ptr, ok := fieldPtr.(*http.Header); ok { - *ptr = hdr + if info.FieldName == "Raw" { + *(fieldPtr.(*http.Header)) = hdr return nil } @@ -171,16 +171,17 @@ func parseHeaders(hdr http.Header, target interface{}) error { return MalformedHeaderError{info.HeaderName, err} } *fieldPtr = value - case *map[string]string: + case *http.Header: //collect all headers with a prefix equal to `headerName` - values := make(map[string]string) - for key, value := range hdr { - if len(value) > 0 && strings.HasPrefix(key, info.HeaderName) { + result := make(http.Header) + for key, values := range hdr { + key = textproto.CanonicalMIMEHeaderKey(key) + if len(values) > 0 && strings.HasPrefix(key, info.HeaderName) { key = strings.TrimPrefix(key, info.HeaderName) - values[key] = value[0] + result[key] = values } } - *fieldPtr = values + *fieldPtr = result default: panic(fmt.Sprintf("parseHeaders: cannot handle field type %T", fieldPtr)) } @@ -190,7 +191,7 @@ func parseHeaders(hdr http.Header, target interface{}) error { } func compileHeaders(headers interface{}, opts *RequestOptions) RequestOptions { - hdr := make(map[string]string) + hdr := make(http.Header) foreachField(headers, func(fieldPtr interface{}, info fieldInfo) error { //skip over fields without schwift field tag, and readonly fields @@ -201,22 +202,33 @@ func compileHeaders(headers interface{}, opts *RequestOptions) RequestOptions { //decode header value into field depending on type switch fieldPtr := fieldPtr.(type) { case *string: - hdr[info.HeaderName] = *fieldPtr + hdr.Set(info.HeaderName, *fieldPtr) case *uint64: - hdr[info.HeaderName] = strconv.FormatUint(*fieldPtr, 10) - case *map[string]string: - for key, val := range *fieldPtr { - if val == "" { - if info.RemoveHeaderName == "" { - //RemoveHeaderName is used by account and container metadata: e.g. - //"X-Account-Meta-Foo: bar" is reverted by "X-Remove-Account-Meta-Foo: x" - hdr[info.RemoveHeaderName+key] = "x" + hdr.Set(info.HeaderName, strconv.FormatUint(*fieldPtr, 10)) + case *http.Header: + for key, values := range *fieldPtr { + //Swift only supports one value per metadata field + value := "" + if len(values) > 0 { + value = values[0] + } + + //empty string means that this key shall be removed + if value == "" { + //for object metadata, a key is removed by just omitting it... + if info.HeaderName != "X-Object-Meta-" { + //...for container and account metadata, a key is removed by + //setting its value to the empty string + hdr.Set(info.HeaderName+key, "") } else { //for object metadata, you just leave out the metadata fields that //you want to clear, so we do nothing } } else { - hdr[info.HeaderName+key] = val + //NOTE: The spec says that `value` needs to be percent-encoded, but + //neither python-swiftclient nor ncw/swift do so. If in doubt, we + //follow the de-facto standards rather than the spec. + hdr.Set(info.HeaderName+key, value) } } default: @@ -237,9 +249,9 @@ func compileHeaders(headers interface{}, opts *RequestOptions) RequestOptions { } type fieldInfo struct { - Access string - HeaderName string - RemoveHeaderName string + FieldName string + Access string + HeaderName string } func foreachField(value interface{}, callback func(fieldPtr interface{}, info fieldInfo) error) error { @@ -255,14 +267,13 @@ func foreachField(value interface{}, callback func(fieldPtr interface{}, info fi fieldPtr := rv.Field(idx).Addr().Interface() //decode schwift:"<access>,<header-name>" tag - tagValues := strings.SplitN(fieldType.Tag.Get("schwift"), ",", 3) - var fieldInfo fieldInfo - if len(tagValues) >= 2 { + tagValues := strings.SplitN(fieldType.Tag.Get("schwift"), ",", 2) + fieldInfo := fieldInfo{ + FieldName: fieldType.Name, + } + if len(tagValues) == 2 { fieldInfo.Access = tagValues[0] fieldInfo.HeaderName = tagValues[1] - if len(tagValues) >= 3 { - fieldInfo.RemoveHeaderName = tagValues[2] - } } err := callback(fieldPtr, fieldInfo) diff --git a/headers_test.go b/headers_test.go index 62bc066..2d1d804 100644 --- a/headers_test.go +++ b/headers_test.go @@ -31,7 +31,6 @@ func TestParseAccountHeadersSuccess(t *testing.T) { "X-Account-Container-Count": {"23"}, "X-Account-Meta-Quota-Bytes": {"1048576"}, "X-Account-Meta-foo": {"bar"}, - "X-Account-Meta-FOO": {"baz"}, }, &headers) expectError(t, err, nil) @@ -43,10 +42,9 @@ func TestParseAccountHeadersSuccess(t *testing.T) { expectError(t, err, nil) expectUint64(t, value, 1048576) - //metadata keys are case-insensitive (wtf Swift) - expectString(t, headers.Metadata["foo"], "bar") - expectString(t, headers.Metadata["Foo"], "") - expectString(t, headers.Metadata["FOO"], "baz") + expectString(t, headers.Metadata.Get("foo"), "bar") + expectString(t, headers.Metadata.Get("Foo"), "bar") + expectString(t, headers.Metadata.Get("FOO"), "bar") } //TODO TestParseAccountHeadersError @@ -48,6 +48,7 @@ type Request struct { ContainerName string //empty for requests on accounts ObjectName string //empty for requests on accounts/containers Options RequestOptions + Body io.Reader //ExpectStatusCodes can be left empty to disable this check, otherwise //schwift.UnexpectedStatusCodeError may be returned. ExpectStatusCodes []int @@ -55,7 +56,7 @@ type Request struct { //RequestOptions contains additional headers and values for request. type RequestOptions struct { - Headers map[string]string + Headers http.Header Values url.Values } @@ -90,30 +91,31 @@ func (r Request) Do(client *gophercloud.ServiceClient) (*http.Response, error) { } func (r Request) do(client *gophercloud.ServiceClient, afterReauth bool) (*http.Response, error) { + provider := client.ProviderClient + //build URL uri, err := r.URL(client, r.Options.Values) if err != nil { return nil, err } - //override gophercloud's error handling - opts := &gophercloud.RequestOpts{OkCodes: okCodes} + //build request + req, err := http.NewRequest(r.Method, uri, r.Body) + if err != nil { + return nil, err + } - //override gophercloud's default headers - opts.MoreHeaders = map[string]string{ - "Accept": "", - "Content-Type": "", + req.Header.Set("User-Agent", provider.UserAgent.Join()) + for key, values := range r.Options.Headers { + req.Header[key] = values } - for key, value := range r.Options.Headers { - opts.MoreHeaders[key] = value + for key, value := range provider.AuthenticatedHeaders() { + req.Header.Set(key, value) } - resp, err := client.ProviderClient.Request(r.Method, uri, opts) + resp, err := provider.HTTPClient.Do(req) if err != nil { - if resp.StatusCode == 204 { - return resp, drainResponseBody(resp) - } - return resp, nil + return nil, err } //return success if error code matches expectation @@ -127,14 +129,13 @@ func (r Request) do(client *gophercloud.ServiceClient, afterReauth bool) (*http. } } - //since we override gophercloud's error handling, we need to handle token - //expiry ourselves + //detect expired token if resp.StatusCode == http.StatusUnauthorized && !afterReauth { err := drainResponseBody(resp) if err != nil { return nil, err } - err = client.Reauthenticate(resp.Request.Header.Get("X-Auth-Token")) + err = provider.Reauthenticate(resp.Request.Header.Get("X-Auth-Token")) if err != nil { return nil, err } |
