aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Majewsky <majewsky@gmx.net>2018-01-29 21:19:39 +0100
committerStefan Majewsky <majewsky@gmx.net>2018-01-29 21:43:45 +0100
commitcad4a10319b98dd15c0a74d0fea13a2da4a0d3cc (patch)
tree0703764e2d3a94fce3a553720f2a182e57612de2
parent3834e49c90c39f4c95e3b9e7bb52b35204a75625 (diff)
downloadgo-schwift-cad4a10319b98dd15c0a74d0fea13a2da4a0d3cc.tar.gz
lay down the full Account API
-rw-r--r--account.go69
-rw-r--r--doc.go16
-rw-r--r--errors.go16
-rw-r--r--headers.go232
-rw-r--r--headers_test.go81
-rw-r--r--parse.go33
-rw-r--r--request.go5
7 files changed, 364 insertions, 88 deletions
diff --git a/account.go b/account.go
index d89299f..a2bb6a7 100644
--- a/account.go
+++ b/account.go
@@ -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
}
////////////////////////////////////////////////////////////////////////////////
diff --git a/doc.go b/doc.go
index d4cb6bd..1fe46e4 100644
--- a/doc.go
+++ b/doc.go
@@ -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.
diff --git a/errors.go b/errors.go
index 62c5792..d4b3470 100644
--- a/errors.go
+++ b/errors.go
@@ -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
-}
diff --git a/request.go b/request.go
index c6bef7b..5a2d4e2 100644
--- a/request.go
+++ b/request.go
@@ -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