diff options
| author | Stefan Majewsky <majewsky@gmx.net> | 2018-02-04 23:29:09 +0100 |
|---|---|---|
| committer | Stefan Majewsky <majewsky@gmx.net> | 2018-02-04 23:29:09 +0100 |
| commit | fafac750baa70812432020784cc5ba2cc23224cc (patch) | |
| tree | 2b0100470a44a7db1550fc7989257c35c5ac808d | |
| parent | 698cd6aaf4e4fd235d5af904376c89f8faf177f7 (diff) | |
| download | go-schwift-fafac750baa70812432020784cc5ba2cc23224cc.tar.gz | |
add Container
| -rw-r--r-- | account.go | 36 | ||||
| -rw-r--r-- | account_test.go | 20 | ||||
| -rw-r--r-- | container.go | 172 | ||||
| -rw-r--r-- | container_test.go | 57 | ||||
| -rw-r--r-- | errors.go | 7 | ||||
| -rw-r--r-- | headers.go | 11 | ||||
| -rw-r--r-- | headers_test.go | 23 |
7 files changed, 294 insertions, 32 deletions
@@ -35,9 +35,6 @@ type Account struct { headers *AccountHeaders } -//////////////////////////////////////////////////////////////////////////////// -// interface to Gophercloud, endpoint inspection and manipulation - var endpointURLRegexp = regexp.MustCompile(`^(.*/)v1/(.*)/$`) //AccountFromClient takes a gophercloud.ServiceClient which wraps a Swift @@ -84,9 +81,6 @@ func (a *Account) Client() *gophercloud.ServiceClient { return a.client } -//////////////////////////////////////////////////////////////////////////////// -// account headers - //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) { @@ -117,11 +111,11 @@ func (a *Account) Invalidate() { a.headers = nil } -//Post creates or updates the account using a POST request. To set arbitrary -//request headers (and to add URL parameters, pass a non-nil *RequestOptions. +//Update updates the account using a POST request. To set arbitrary request +//headers (and to add URL parameters), pass a non-nil *RequestOptions. // -//A successful POST request implies Invalidate() since it changes account metadata. -func (a *Account) Post(headers AccountHeaders, opts *RequestOptions) error { +//A successful POST request implies Invalidate() since it may change metadata. +func (a *Account) Update(headers AccountHeaders, opts *RequestOptions) error { _, err := Request{ Method: "POST", Options: compileHeaders(&headers, opts), @@ -133,5 +127,23 @@ func (a *Account) Post(headers AccountHeaders, opts *RequestOptions) error { return err } -//////////////////////////////////////////////////////////////////////////////// -// container listing +//Create creates the account using a PUT request. To set arbitrary request +//headers (and to add URL parameters), pass a non-nil *RequestOptions. +// +//Note that this operation is only available to reseller admins, not to regular +//users. +// +//A successful PUT request implies Invalidate() since it may change metadata. +func (a *Account) Create(headers AccountHeaders, opts *RequestOptions) error { + _, err := Request{ + Method: "POST", + Options: compileHeaders(&headers, opts), + ExpectStatusCodes: []int{201, 202}, + }.Do(a.client) + if err == nil { + a.Invalidate() + } + return err +} + +// TODO container listing diff --git a/account_test.go b/account_test.go index 76185d4..5515777 100644 --- a/account_test.go +++ b/account_test.go @@ -25,7 +25,7 @@ import ( func TestAccountBasic(t *testing.T) { testWithAccount(t, func(a *Account) { hdr, err := a.Headers() - if !expectError(t, err, nil) { + if !expectError(t, err, "") { t.FailNow() } //There are not a lot of things we can test here (besides testing that @@ -39,18 +39,18 @@ func TestAccountBasic(t *testing.T) { func TestAccountMetadata(t *testing.T) { testWithAccount(t, func(a *Account) { //test creating some metadata - err := a.Post(AccountHeaders{ + err := a.Update(AccountHeaders{ Metadata: NewMetadata( "schwift-test1", "first", "schwift-test2", "second", ), }, nil) - if !expectError(t, err, nil) { + if !expectError(t, err, "") { t.FailNow() } hdr, err := a.Headers() - if !expectError(t, err, nil) { + if !expectError(t, err, "") { t.FailNow() } expectString(t, hdr.Metadata.Get("schwift-test1"), "first") @@ -59,15 +59,15 @@ func TestAccountMetadata(t *testing.T) { //test deleting some metadata m := make(Metadata) m.Clear("schwift-test1") - err = a.Post(AccountHeaders{ + err = a.Update(AccountHeaders{ Metadata: m, }, nil) - if !expectError(t, err, nil) { + if !expectError(t, err, "") { t.FailNow() } hdr, err = a.Headers() - if !expectError(t, err, nil) { + if !expectError(t, err, "") { t.FailNow() } expectString(t, hdr.Metadata.Get("schwift-test1"), "") @@ -76,15 +76,15 @@ func TestAccountMetadata(t *testing.T) { //test updating some metadata m = make(Metadata) m.Set("schwift-test2", "changed") - err = a.Post(AccountHeaders{ + err = a.Update(AccountHeaders{ Metadata: m, }, nil) - if !expectError(t, err, nil) { + if !expectError(t, err, "") { t.FailNow() } hdr, err = a.Headers() - if !expectError(t, err, nil) { + if !expectError(t, err, "") { t.FailNow() } expectString(t, hdr.Metadata.Get("schwift-test1"), "") diff --git a/container.go b/container.go new file mode 100644 index 0000000..9fcfe2b --- /dev/null +++ b/container.go @@ -0,0 +1,172 @@ +/****************************************************************************** +* +* 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" +) + +//Container represents a Swift container. +type Container struct { + a *Account + name string + //cache + headers *ContainerHeaders +} + +//Container returns a handle to the container with the given name within this +//account. This function does not issue any HTTP requests, and therefore cannot +//ensure that the container exists. Use the Exists() function to check for the +//container's existence, or chain this function with the EnsureExists() +//function like so: +// +// container, err := account.Container("documents").EnsureExists() +func (a *Account) Container(name string) *Container { + return &Container{a: a, name: name} +} + +//Account returns a handle to the account this container is stored in. +func (c *Container) Account() *Account { + return c.a +} + +//Name returns the container name. +func (c *Container) Name() string { + return c.name +} + +//Exists checks if this container exists, potentially by issuing a HEAD request +//if no Headers() have been cached yet. +func (c *Container) Exists() (bool, error) { + _, err := c.Headers() + if Is(err, http.StatusNotFound) { + return false, nil + } else if err != nil { + return false, err + } + return true, nil +} + +//Headers returns the ContainerHeaders for this account. If the ContainerHeaders +//has not been cached yet, a HEAD request is issued on the account. +func (c *Container) Headers() (ContainerHeaders, error) { + if c.headers != nil { + return *c.headers, nil + } + + resp, err := Request{ + Method: "HEAD", + ContainerName: c.name, + ExpectStatusCodes: []int{204}, + }.Do(c.a.client) + if err != nil { + return ContainerHeaders{}, err + } + + var headers ContainerHeaders + err = parseHeaders(resp.Header, &headers) + if err != nil { + return ContainerHeaders{}, err + } + c.headers = &headers + return *c.headers, nil +} + +//Update updates the container using a POST request. To set arbitrary request +//headers (and to add URL parameters), pass a non-nil *RequestOptions. +// +//If you are not sure whether the container exists, use Create() instead. +// +//A successful POST request implies Invalidate() since it may change metadata. +func (c *Container) Update(headers ContainerHeaders, opts *RequestOptions) error { + _, err := Request{ + Method: "POST", + ContainerName: c.name, + Options: compileHeaders(&headers, opts), + ExpectStatusCodes: []int{204}, + }.Do(c.a.client) + if err == nil { + c.Invalidate() + } + return err +} + +//Create creates the container using a PUT request. To set arbitrary request +//headers (and to add URL parameters), pass a non-nil *RequestOptions. +// +//This function can be used regardless of whether the container exists or not. +// +//A successful PUT request implies Invalidate() since it may change metadata. +func (c *Container) Create(headers ContainerHeaders, opts *RequestOptions) error { + _, err := Request{ + Method: "PUT", + ContainerName: c.name, + Options: compileHeaders(&headers, opts), + ExpectStatusCodes: []int{201, 202}, + }.Do(c.a.client) + if err == nil { + c.Invalidate() + } + return err +} + +//Delete deletes the container using a DELETE request. To set arbitrary request +//headers (and to add URL parameters), pass a non-nil *RequestOptions. +// +//This operation fails with http.StatusConflict if the container is not empty. +// +//This operation fails with http.StatusNotFound if the container does not exist. +// +//A successful DELETE request implies Invalidate(). +func (c *Container) Delete(headers ContainerHeaders, opts *RequestOptions) error { + _, err := Request{ + Method: "DELETE", + ContainerName: c.name, + Options: compileHeaders(&headers, opts), + ExpectStatusCodes: []int{204}, + }.Do(c.a.client) + if err == nil { + c.Invalidate() + } + return err +} + +//Invalidate clears the internal cache of this Container instance. The next call +//to Headers() on this instance will issue a HEAD request on the account. +func (c *Container) Invalidate() { + c.headers = nil +} + +//EnsureExists issues a PUT request on this container. +//If the container does not exist yet, it will be created by this call. +//If the container exists already, this call does not change it. +//This function returns the same container again, because its intended use is +//with freshly constructed Container instances like so: +// +// container, err := account.Container("documents").EnsureExists() +func (c *Container) EnsureExists() (*Container, error) { + _, err := Request{ + Method: "PUT", + ContainerName: c.name, + ExpectStatusCodes: []int{201, 202}, + }.Do(c.a.client) + return c, err +} + +// TODO object listing diff --git a/container_test.go b/container_test.go new file mode 100644 index 0000000..7c67ed5 --- /dev/null +++ b/container_test.go @@ -0,0 +1,57 @@ +/****************************************************************************** +* +* 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 ( + "crypto/rand" + "encoding/hex" + "net/http" + "testing" +) + +func TestContainerExistence(t *testing.T) { + testWithAccount(t, func(a *Account) { + c := a.Container(getRandomName()) + + exists, err := c.Exists() + expectError(t, err, "") + expectBool(t, exists, false) + + _, err = c.Headers() + expectError(t, err, "expected 204 response, got 404 instead") + expectBool(t, Is(err, http.StatusNotFound), true) + expectBool(t, Is(err, http.StatusNoContent), false) + + err = c.Create(ContainerHeaders{}, nil) + expectError(t, err, "") + + exists, err = c.Exists() + expectError(t, err, "") + expectBool(t, exists, true) + }) +} + +func getRandomName() string { + var buf [16]byte + _, err := rand.Read(buf[:]) + if err != nil { + panic(err.Error()) + } + return hex.EncodeToString(buf[:]) +} @@ -49,11 +49,14 @@ func (e UnexpectedStatusCodeError) Error() string { for idx, code := range e.ExpectedStatusCodes { codeStrs[idx] = strconv.Itoa(code) } - return fmt.Sprintf("expected %s response, got %d instead: %s", + msg := fmt.Sprintf("expected %s response, got %d instead", strings.Join(codeStrs, "/"), e.ActualResponse.StatusCode, - string(e.ResponseBody), ) + if len(e.ResponseBody) > 0 { + msg += ": " + string(e.ResponseBody) + } + return msg } //Is checks if the given error is an UnexpectedStatusCodeError for that status @@ -64,6 +64,17 @@ func (a AccountHeaders) TempURLKey2() StringField { } } +//ContainerHeaders 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 ContainerHeaders struct { + Metadata Metadata `schwift:"rw,X-Container-Meta-"` + Raw http.Header + //TODO map well-known headers +} + //////////////////////////////////////////////////////////////////////////////// // field types diff --git a/headers_test.go b/headers_test.go index 2d1d804..b54181c 100644 --- a/headers_test.go +++ b/headers_test.go @@ -33,13 +33,13 @@ func TestParseAccountHeadersSuccess(t *testing.T) { "X-Account-Meta-foo": {"bar"}, }, &headers) - expectError(t, err, nil) + expectError(t, err, "") expectUint64(t, headers.BytesUsed, 1234) expectUint64(t, headers.ContainerCount, 23) expectUint64(t, headers.ObjectCount, 42) value, err := headers.QuotaBytes().Get() - expectError(t, err, nil) + expectError(t, err, "") expectUint64(t, value, 1048576) expectString(t, headers.Metadata.Get("foo"), "bar") @@ -49,6 +49,13 @@ func TestParseAccountHeadersSuccess(t *testing.T) { //TODO TestParseAccountHeadersError +func expectBool(t *testing.T, actual bool, expected bool) { + t.Helper() + if actual != expected { + t.Errorf("expected value %#v, got %#v instead\n", expected, actual) + } +} + func expectUint64(t *testing.T, actual uint64, expected uint64) { t.Helper() if actual != expected { @@ -63,19 +70,19 @@ func expectString(t *testing.T, actual string, expected string) { } } -func expectError(t *testing.T, actual error, expected *string) (ok bool) { +func expectError(t *testing.T, actual error, expected string) (ok bool) { t.Helper() if actual == nil { - if expected != nil { - t.Errorf("expected error %q, got no error\n", *expected) + if expected != "" { + t.Errorf("expected error %q, got no error\n", expected) return false } } else { - if expected == nil { + if expected == "" { t.Errorf("expected no error, got %q\n", actual.Error()) return false - } else if *expected != actual.Error() { - t.Errorf("expected error %q, got %q instead\n", *expected, actual.Error()) + } else if expected != actual.Error() { + t.Errorf("expected error %q, got %q instead\n", expected, actual.Error()) return false } } |
