diff options
| -rw-r--r-- | account.go | 69 | ||||
| -rw-r--r-- | doc.go | 16 | ||||
| -rw-r--r-- | errors.go | 16 | ||||
| -rw-r--r-- | headers.go | 232 | ||||
| -rw-r--r-- | headers_test.go | 81 | ||||
| -rw-r--r-- | parse.go | 33 | ||||
| -rw-r--r-- | request.go | 5 |
7 files changed, 364 insertions, 88 deletions
@@ -20,7 +20,6 @@ package schwift import ( "fmt" - "net/http" "regexp" "github.com/gophercloud/gophercloud" @@ -33,7 +32,7 @@ type Account struct { baseURL string name string //cache - metadata *AccountMetadata + headers *AccountHeaders } //////////////////////////////////////////////////////////////////////////////// @@ -86,24 +85,13 @@ func (a *Account) Client() *gophercloud.ServiceClient { } //////////////////////////////////////////////////////////////////////////////// -// account metadata - -//AccountMetadata contains the metadata for an account. The `Raw` attribute -//contains the raw set of headers returned from a HEAD or GET request on the -//account. The other attributes contain the parsed values of common headers. -type AccountMetadata struct { - Exists bool - BytesUsed uint64 //from X-Account-Bytes-Used - ContainerCount uint64 //from X-Account-Container-Count - ObjectCount uint64 //from X-Account-Object-Count - //TODO account quota - Raw http.Header -} +// account headers -//Metadata returns the metadata for this account. If the account does not exist, -func (a *Account) Metadata() (AccountMetadata, error) { - if a.metadata != nil { - return *a.metadata, nil +//Headers returns the AccountHeaders for this account. If the AccountHeaders +//has not been cached yet, a HEAD request is issued on the account. +func (a *Account) Headers() (AccountHeaders, error) { + if a.headers != nil { + return *a.headers, nil } resp, err := Request{ @@ -111,36 +99,31 @@ func (a *Account) Metadata() (AccountMetadata, error) { ExpectStatusCodes: []int{200}, }.Do(a.client) if err != nil { - return AccountMetadata{}, err + return AccountHeaders{}, err } - a.metadata, err = parseAccountMetadata(resp) + var headers AccountHeaders + err = parseHeaders(resp.Header, &headers) if err != nil { - return AccountMetadata{}, err + return AccountHeaders{}, err } - return *a.metadata, nil + return *a.headers, nil } -func parseAccountMetadata(resp *http.Response) (*AccountMetadata, error) { - bytesUsed, err := parseUnsignedIntHeader(resp, "X-Account-Bytes-Used") - if err != nil { - return nil, err - } - containerCount, err := parseUnsignedIntHeader(resp, "X-Account-Container-Count") - if err != nil { - return nil, err - } - objectCount, err := parseUnsignedIntHeader(resp, "X-Account-Object-Count") - if err != nil { - return nil, err - } - return &AccountMetadata{ - Exists: true, - BytesUsed: bytesUsed, - ContainerCount: containerCount, - ObjectCount: objectCount, - Raw: resp.Header, - }, nil +//Invalidate clears the internal cache of this Account instance. The next call +//to Headers() on this instance will issue a HEAD request on the account. +func (a *Account) Invalidate() { + a.headers = nil +} + +//Post creates or updates the account using a POST request. +func (a *Account) Post(headers AccountHeaders) error { + _, err := Request{ + Method: "POST", + AdditionalHeaders: compileHeaders(headers), + ExpectStatusCodes: []int{204}, + }.Do(a.client) + return err } //////////////////////////////////////////////////////////////////////////////// @@ -25,9 +25,9 @@ It uses Gophercloud (https://github.com/gophercloud/gophercloud) for authentication, so you usually start by obtaining a gophercloud.ServiceClient for Swift like so: - authOptions, err := openstack.AuthOptionsFromEnv() + authOptions, err := openstack.AuthOptionsFromEnv() // or build a gophercloud.AuthOptions instance yourself provider, err := openstack.AuthenticatedClient(authOptions) - client, err := openstack.NewObjectStorageV1(provider, gophercloud.EndpointOpts {}) + client, err := openstack.NewObjectStorageV1(provider, gophercloud.EndpointOpts{}) Or, if you use Swift's built-in authentication instead of Keystone: @@ -44,16 +44,14 @@ API. Caching When a GET or HEAD request is sent by an Account, Container or Object instance, -the metadata associated with that thing will be stored in that instance. You -can therefore access metadata attributes directly via their accessors and -everything just works, i.e. the first call to a getter will retrieve the -metadata: +the headers associated with that thing will be stored in that instance and not +retrieved again. obj := account.Container("foo").Object("bar") - t, err := obj.LastModified() //sends HTTP request "HEAD <storage-url>/foo/bar" - assert(err == nil) - t, err := obj.LastModified() //returns cached value immediately + hdr, err := obj.Headers() //sends HTTP request "HEAD <storage-url>/foo/bar" + ... + hdr, err = obj.Headers() //returns cached values immediately If this behavior is not desired, the Invalidate() method can be used to clear caches on any Account, Container or Object instance. @@ -59,13 +59,13 @@ func (e UnexpectedStatusCodeError) Error() string { //Is checks if the given error is an UnexpectedStatusCodeError for that status //code. For example: // -// metadata, err := container.Metadata() +// info, err := container.Info() // if schwift.Is(err, http.StatusNotFound) { // // ... create container ... // } else if err != nil { // // ... report error ... // } else { -// // ... use metadata ... +// // ... use container info ... // } func Is(err error, code int) bool { if e, ok := err.(UnexpectedStatusCodeError); ok { @@ -73,3 +73,15 @@ func Is(err error, code int) bool { } return false } + +//MalformedHeaderError is generated when a response from Swift contains a +//malformed header. +type MalformedHeaderError struct { + Key string + ParseError error +} + +//Error implements the builtin/error interface. +func (e MalformedHeaderError) Error() string { + return "Bad header " + e.Key + ": " + e.ParseError.Error() +} diff --git a/headers.go b/headers.go new file mode 100644 index 0000000..0d9f0e0 --- /dev/null +++ b/headers.go @@ -0,0 +1,232 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License 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 schwift + +import ( + "fmt" + "net/http" + "reflect" + "strconv" + "strings" +) + +//AccountHeaders contains the headers for an account. The Raw attribute +//contains the original set of headers returned from a HEAD or GET request on +//the account. The other attributes contain the parsed values of common +//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-"` + Raw http.Header +} + +//QuotaBytes returns a handle to read or write the X-Account-Meta-Quota-Bytes field. +func (a AccountHeaders) QuotaBytes() UnsignedIntField { + return UnsignedIntField{ + a.Metadata, + "X-Account-Meta-", "Quota-Bytes", + false, + } +} + +//TempURLKey returns a handle to read or write the X-Account-Meta-Temp-URL-Key field. +func (a AccountHeaders) TempURLKey() StringField { + return StringField{ + a.Metadata, + "X-Account-Meta-", "Temp-URL-Key", + false, + } +} + +//TempURLKey2 returns a handle to read or write the X-Account-Meta-Temp-URL-Key-2 field. +func (a AccountHeaders) TempURLKey2() StringField { + return StringField{ + a.Metadata, + "X-Account-Meta-", "Temp-URL-Key-2", + false, + } +} + +//////////////////////////////////////////////////////////////////////////////// +// field types + +//StringField is a helper type used in the interface of AccountHeaders, +//ContainerHeaders and ObjectHeaders. For example: +// +// var headers AccountHeaders +// ... +// value := headers.TempURLKey().Get() +// headers.TempURLKey().Set(value + " changed") +// headers.TempURLKey().Clear() +type StringField struct { + metadata map[string]string + prefix string + key string + clearByDeleting bool +} + +//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] +} + +//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 +} + +//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] = "" + } +} + +//UnsignedIntField is a helper type used in the interface of AccountHeaders, +//ContainerHeaders and ObjectHeaders. For example: +// +// var headers AccountHeaders +// ... +// 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 +} + +//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) + if err != nil { + err = MalformedHeaderError{Key: f.prefix + f.key, ParseError: err} + } + return value, err +} + +//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) +} + +//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] = "" + } +} + +//////////////////////////////////////////////////////////////////////////////// +// generic parsing functions + +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 + return nil + } + + //skip over fields without schwift field tag + if info.HeaderName == "" { + return nil + } + + //decode header value into field depending on type + switch fieldPtr := fieldPtr.(type) { + case *string: + *fieldPtr = hdr.Get(info.HeaderName) + case *uint64: + value, err := strconv.ParseUint(hdr.Get(info.HeaderName), 10, 64) + if err != nil { + return MalformedHeaderError{info.HeaderName, err} + } + *fieldPtr = value + case *map[string]string: + //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) { + key = strings.TrimPrefix(key, info.HeaderName) + values[key] = value[0] + } + } + *fieldPtr = values + default: + panic(fmt.Sprintf("parseHeaders: cannot handle field type %T", fieldPtr)) + } + + return nil + }) +} + +func compileHeaders(headers interface{}) map[string]string { + panic("TODO") +} + +type fieldInfo struct { + Access string + HeaderName string + RemoveHeaderName string +} + +func foreachField(value interface{}, callback func(fieldPtr interface{}, info fieldInfo) error) error { + rv := reflect.ValueOf(value) + //unpack pointer type if necessary + if rv.Type().Kind() == reflect.Ptr { + rv = rv.Elem() + } + + //iterate over the struct fields + for idx := 0; idx < rv.NumField(); idx++ { + fieldType := rv.Type().Field(idx) + 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 { + fieldInfo.Access = tagValues[0] + fieldInfo.HeaderName = tagValues[1] + if len(tagValues) >= 3 { + fieldInfo.RemoveHeaderName = tagValues[2] + } + } + + err := callback(fieldPtr, fieldInfo) + if err != nil { + return err + } + } + + return nil +} diff --git a/headers_test.go b/headers_test.go new file mode 100644 index 0000000..6859690 --- /dev/null +++ b/headers_test.go @@ -0,0 +1,81 @@ +/****************************************************************************** +* +* Copyright 2018 Stefan Majewsky <majewsky@gmx.net> +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License 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 schwift + +import ( + "net/http" + "testing" +) + +func TestParseAccountHeadersSuccess(t *testing.T) { + var headers AccountHeaders + err := parseHeaders(http.Header{ + "X-Account-Bytes-Used": {"1234"}, + "X-Account-Object-Count": {"42"}, + "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) + expectUint64(t, headers.BytesUsed, 1234) + expectUint64(t, headers.ContainerCount, 23) + expectUint64(t, headers.ObjectCount, 42) + + value, err := headers.QuotaBytes().Get() + 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") +} + +//TODO TestParseAccountHeadersError + +func expectUint64(t *testing.T, actual uint64, expected uint64) { + t.Helper() + if actual != expected { + t.Errorf("expected value %d, got %d instead\n", expected, actual) + } +} + +func expectString(t *testing.T, actual string, expected string) { + t.Helper() + if actual != expected { + t.Errorf("expected value %d, got %d instead\n", expected, actual) + } +} + +func expectError(t *testing.T, actual error, expected *string) { + t.Helper() + if actual == nil { + if expected != nil { + t.Errorf("expected error %q, got no error\n", *expected) + } + } else { + if expected == nil { + t.Errorf("expected no error, got %q\n", actual.Error()) + } else if *expected != actual.Error() { + t.Errorf("expected error %q, got %q instead\n", *expected, actual.Error()) + } + } +} diff --git a/parse.go b/parse.go deleted file mode 100644 index cdb6e8e..0000000 --- a/parse.go +++ /dev/null @@ -1,33 +0,0 @@ -/****************************************************************************** -* -* Copyright 2018 Stefan Majewsky <majewsky@gmx.net> -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License 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 schwift - -import ( - "fmt" - "net/http" - "strconv" -) - -func parseUnsignedIntHeader(resp *http.Response, key string) (value uint64, err error) { - value, err = strconv.ParseUint(resp.Header.Get(key), 10, 64) - if err != nil { - err = fmt.Errorf("Bad header %s: %s", key, err.Error()) - } - return -} @@ -103,7 +103,10 @@ func (r Request) do(client *gophercloud.ServiceClient, afterReauth bool) (*http. resp, err := client.ProviderClient.Request(r.Method, url, opts) if err != nil { - return resp, err + if resp.StatusCode == 204 { + return resp, drainResponseBody(resp) + } + return resp, nil } //return success if error code matches expectation |
