aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--account_test.go9
-rw-r--r--headers.go129
-rw-r--r--headers_test.go8
-rw-r--r--request.go35
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")
})
}
diff --git a/headers.go b/headers.go
index 94bd767..c2acc9b 100644
--- a/headers.go
+++ b/headers.go
@@ -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
diff --git a/request.go b/request.go
index fb4b596..79a0296 100644
--- a/request.go
+++ b/request.go
@@ -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
}