use generic parameter building functions; pagination in unit tests
diff --git a/acceptance/openstack/storage/v1/containers_test.go b/acceptance/openstack/storage/v1/containers_test.go
index 29f6312..3cc367b 100644
--- a/acceptance/openstack/storage/v1/containers_test.go
+++ b/acceptance/openstack/storage/v1/containers_test.go
@@ -8,6 +8,7 @@
"github.com/rackspace/gophercloud/acceptance/tools"
"github.com/rackspace/gophercloud/openstack/storage/v1/containers"
+ "github.com/rackspace/gophercloud/pagination"
)
// numContainers is the number of containers to create for testing.
@@ -18,7 +19,6 @@
client, err := newClient()
if err != nil {
t.Error(err)
- return
}
// Create a slice of random container names.
@@ -29,80 +29,65 @@
// Create numContainers containers.
for i := 0; i < len(cNames); i++ {
- _, err := containers.Create(client, containers.CreateOpts{
- Name: cNames[i],
- })
+ _, err := containers.Create(client, cNames[i], containers.CreateOpts{})
if err != nil {
t.Error(err)
- return
}
}
// Delete the numContainers containers after function completion.
defer func() {
for i := 0; i < len(cNames); i++ {
- err = containers.Delete(client, containers.DeleteOpts{
- Name: cNames[i],
- })
+ err = containers.Delete(client, cNames[i])
if err != nil {
t.Error(err)
- return
}
}
}()
// List the numContainer names that were just created. To just list those,
// the 'prefix' parameter is used.
- lr, err := containers.List(client, containers.ListOpts{
- Full: false,
- Params: map[string]string{
- "prefix": "gophercloud-test-container-",
- },
+ pager := containers.List(client, containers.ListOpts{Full: true, Prefix: "gophercloud-test-container-"})
+ if pager.Err != nil {
+ t.Error(err)
+ return
+ }
+ err = pager.EachPage(func(page pagination.Page) (bool, error) {
+ containerList, err := containers.ExtractInfo(page)
+ if err != nil {
+ t.Error(err)
+ }
+ for _, n := range containerList {
+ t.Logf("Container: Name [%s] Count [%d] Bytes [%d]",
+ n["name"], int(n["count"].(float64)), int(n["bytes"].(float64)))
+ }
+
+ return true, nil
})
if err != nil {
t.Error(err)
- return
- }
- // Extract the names from the 'List' response.
- cns, err := containers.ExtractNames(lr)
- if err != nil {
- t.Error(err)
- return
- }
- if len(cns) != len(cNames) {
- t.Errorf("Expected %d names and got %d:\nExpected:%v\nActual:%v", len(cNames), len(cns), cNames, cns)
- return
}
// List the info for the numContainer containers that were created.
- lr, err = containers.List(client, containers.ListOpts{
- Full: true,
- Params: map[string]string{
- "prefix": "gophercloud-test-container-",
- },
+ pager = containers.List(client, containers.ListOpts{Full: false, Prefix: "gophercloud-test-container-"})
+ err = pager.EachPage(func(page pagination.Page) (bool, error) {
+ containerList, err := containers.ExtractNames(page)
+ if err != nil {
+ return false, err
+ }
+ for _, n := range containerList {
+ t.Logf("Container: Name [%s]", n)
+ }
+
+ return true, nil
})
if err != nil {
t.Error(err)
- return
- }
- // Extract the info from the 'List' response.
- cis, err := containers.ExtractInfo(lr)
- if err != nil {
- t.Error(err)
- return
- }
- if len(cis) != len(cNames) {
- t.Errorf("Expected %d containers and got %d", len(cNames), len(cis))
- return
}
// Update one of the numContainer container metadata.
- err = containers.Update(client, containers.UpdateOpts{
- Name: cNames[0],
- Metadata: metadata,
- })
+ err = containers.Update(client, cNames[0], containers.UpdateOpts{Metadata: metadata})
if err != nil {
t.Error(err)
- return
}
// After the tests are done, delete the metadata that was set.
defer func() {
@@ -110,30 +95,20 @@
for k := range metadata {
tempMap[k] = ""
}
- err = containers.Update(client, containers.UpdateOpts{
- Name: cNames[0],
- Metadata: tempMap,
- })
+ err = containers.Update(client, cNames[0], containers.UpdateOpts{Metadata: tempMap})
if err != nil {
t.Error(err)
- return
}
}()
// Retrieve a container's metadata.
- gr, err := containers.Get(client, containers.GetOpts{
- Name: cNames[0],
- })
+ cm, err := containers.Get(client, cNames[0]).ExtractMetadata()
if err != nil {
t.Error(err)
- return
}
- // Extract the metadata from the 'Get' response.
- cm := containers.ExtractMetadata(gr)
for k := range metadata {
if cm[k] != metadata[strings.Title(k)] {
t.Errorf("Expected custom metadata with key: %s", k)
- return
}
}
}
diff --git a/acceptance/openstack/storage/v1/objects_test.go b/acceptance/openstack/storage/v1/objects_test.go
index 066a223..239b4d5 100644
--- a/acceptance/openstack/storage/v1/objects_test.go
+++ b/acceptance/openstack/storage/v1/objects_test.go
@@ -33,18 +33,14 @@
// Create a container to hold the test objects.
cName := tools.RandomString("test-container-", 8)
- _, err = containers.Create(client, containers.CreateOpts{
- Name: cName,
- })
+ _, err = containers.Create(client, cName, containers.CreateOpts{})
if err != nil {
t.Error(err)
return
}
// Defer deletion of the container until after testing.
defer func() {
- err = containers.Delete(client, containers.DeleteOpts{
- Name: cName,
- })
+ err = containers.Delete(client, cName)
if err != nil {
t.Error(err)
return
@@ -55,11 +51,7 @@
oContents := make([]*bytes.Buffer, numObjects)
for i := 0; i < numObjects; i++ {
oContents[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10)))
- err = objects.Create(client, objects.CreateOpts{
- Container: cName,
- Name: oNames[i],
- Content: oContents[i],
- })
+ err = objects.Create(client, cName, oNames[i], oContents[i], objects.CreateOpts{})
if err != nil {
t.Error(err)
return
@@ -68,14 +60,11 @@
// Delete the objects after testing.
defer func() {
for i := 0; i < numObjects; i++ {
- err = objects.Delete(client, objects.DeleteOpts{
- Container: cName,
- Name: oNames[i],
- })
+ err = objects.Delete(client, cName, oNames[i], objects.DeleteOpts{})
}
}()
- pager := objects.List(client, objects.ListOpts{Full: false, Container: cName})
+ pager := objects.List(client, cName, objects.ListOpts{Full: false})
ons := make([]string, 0, len(oNames))
err = pager.EachPage(func(page pagination.Page) (bool, error) {
names, err := objects.ExtractNames(page)
@@ -95,7 +84,7 @@
return
}
- pager = objects.List(client, objects.ListOpts{Full: true, Container: cName})
+ pager = objects.List(client, cName, objects.ListOpts{Full: true})
ois := make([]objects.Object, 0, len(oNames))
err = pager.EachPage(func(page pagination.Page) (bool, error) {
info, err := objects.ExtractInfo(page)
@@ -117,42 +106,20 @@
}
// Copy the contents of one object to another.
- err = objects.Copy(client, objects.CopyOpts{
- Container: cName,
- Name: oNames[0],
- NewContainer: cName,
- NewName: oNames[1],
- })
+ err = objects.Copy(client, cName, oNames[0], objects.CopyOpts{Destination: cName + "/" + oNames[1]})
if err != nil {
t.Error(err)
return
}
// Download one of the objects that was created above.
- dr, err := objects.Download(client, objects.DownloadOpts{
- Container: cName,
- Name: oNames[1],
- })
+ o1Content, err := objects.Download(client, cName, oNames[0], objects.DownloadOpts{}).ExtractContent()
if err != nil {
t.Error(err)
return
}
- // Extract the content from the 'Download' response of object.
- o2Content, err := objects.ExtractContent(dr)
- if err != nil {
- t.Error(err)
- }
// Download the another object that was create above.
- dr, err = objects.Download(client, objects.DownloadOpts{
- Container: cName,
- Name: oNames[0],
- })
- if err != nil {
- t.Error(err)
- return
- }
- // Extract the content from the 'Download' response of other object.
- o1Content, err := objects.ExtractContent(dr)
+ o2Content, err := objects.Download(client, cName, oNames[1], objects.DownloadOpts{}).ExtractContent()
if err != nil {
t.Error(err)
return
@@ -164,11 +131,7 @@
}
// Update an object's metadata.
- err = objects.Update(client, objects.UpdateOpts{
- Container: cName,
- Name: oNames[0],
- Metadata: metadata,
- })
+ err = objects.Update(client, cName, oNames[0], objects.UpdateOpts{Metadata: metadata})
if err != nil {
t.Error(err)
return
@@ -179,11 +142,7 @@
for k := range metadata {
tempMap[k] = ""
}
- err = objects.Update(client, objects.UpdateOpts{
- Container: cName,
- Name: oNames[0],
- Metadata: tempMap,
- })
+ err = objects.Update(client, cName, oNames[0], objects.UpdateOpts{Metadata: tempMap})
if err != nil {
t.Error(err)
return
@@ -191,13 +150,11 @@
}()
// Retrieve an object's metadata.
- gr, err := objects.Get(client, objects.GetOpts{})
+ om, err := objects.Get(client, cName, oNames[0], objects.GetOpts{}).ExtractMetadata()
if err != nil {
t.Error(err)
return
}
- // Extract the custom metadata from the 'Get' response.
- om := objects.ExtractMetadata(gr)
for k := range metadata {
if om[k] != metadata[strings.Title(k)] {
t.Errorf("Expected custom metadata with key: %s", k)
diff --git a/openstack/storage/v1/containers/containers.go b/openstack/storage/v1/containers/containers.go
deleted file mode 100644
index 9ae344b..0000000
--- a/openstack/storage/v1/containers/containers.go
+++ /dev/null
@@ -1,99 +0,0 @@
-package containers
-
-import (
- "fmt"
- "strings"
-
- "github.com/rackspace/gophercloud/pagination"
-)
-
-// Container is a structure that holds information related to a storage container.
-type Container map[string]interface{}
-
-// ListOpts is a structure that holds parameters for listing containers.
-type ListOpts struct {
- Full bool
- Params map[string]string
-}
-
-// CreateOpts is a structure that holds parameters for creating a container.
-type CreateOpts struct {
- Name string
- Metadata map[string]string
- Headers map[string]string
-}
-
-// DeleteOpts is a structure that holds parameters for deleting a container.
-type DeleteOpts struct {
- Name string
- Params map[string]string
-}
-
-// UpdateOpts is a structure that holds parameters for updating, creating, or deleting a
-// container's metadata.
-type UpdateOpts struct {
- Name string
- Metadata map[string]string
- Headers map[string]string
-}
-
-// GetOpts is a structure that holds parameters for getting a container's metadata.
-type GetOpts struct {
- Name string
-}
-
-// ExtractInfo is a function that takes a ListResult and returns the containers' information.
-func ExtractInfo(page pagination.Page) ([]Container, error) {
- untyped := page.(ListResult).Body.([]interface{})
- results := make([]Container, len(untyped))
- for index, each := range untyped {
- results[index] = Container(each.(map[string]interface{}))
- }
- return results, nil
-}
-
-// ExtractNames is a function that takes a ListResult and returns the containers' names.
-func ExtractNames(page pagination.Page) ([]string, error) {
- casted := page.(ListResult)
- ct := casted.Header.Get("Content-Type")
-
- switch {
- case strings.HasPrefix(ct, "application/json"):
- parsed, err := ExtractInfo(page)
- if err != nil {
- return nil, err
- }
-
- names := make([]string, 0, len(parsed))
- for _, container := range parsed {
- names = append(names, container["name"].(string))
- }
- return names, nil
- case strings.HasPrefix(ct, "text/plain"):
- names := make([]string, 0, 50)
-
- body := string(page.(ListResult).Body.([]uint8))
- for _, name := range strings.Split(body, "\n") {
- if len(name) > 0 {
- names = append(names, name)
- }
- }
-
- return names, nil
- default:
- return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
- }
-}
-
-// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
-// and returns the custom metadata associated with the container.
-func ExtractMetadata(gr GetResult) map[string]string {
- metadata := make(map[string]string)
- for k, v := range gr.Header {
- if strings.HasPrefix(k, "X-Container-Meta-") {
- key := strings.TrimPrefix(k, "X-Container-Meta-")
- metadata[key] = v[0]
- }
- }
- return metadata
-}
diff --git a/openstack/storage/v1/containers/containers_test.go b/openstack/storage/v1/containers/containers_test.go
deleted file mode 100644
index 3296bb1..0000000
--- a/openstack/storage/v1/containers/containers_test.go
+++ /dev/null
@@ -1,78 +0,0 @@
-package containers
-
-import (
- "bytes"
- "encoding/json"
- "io/ioutil"
- "net/http"
- "reflect"
- "testing"
-)
-
-
-func TestExtractContainerMetadata(t *testing.T) {
- getResult := &http.Response{}
-
- expected := map[string]string{}
-
- actual := ExtractMetadata(getResult)
-
- if !reflect.DeepEqual(expected, actual) {
- t.Errorf("Expected: %+v\nActual:%+v", expected, actual)
- }
-}
-
-func TestExtractContainerInfo(t *testing.T) {
- responseBody := `
- [
- {
- "count": 3,
- "bytes": 2000,
- "name": "artemis"
- },
- {
- "count": 1,
- "bytes": 450,
- "name": "diana"
- }
- ]
- `
-
- listResult := &http.Response{
- Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)),
- }
-
- var expected []Container
- err := json.Unmarshal([]byte(responseBody), &expected)
- if err != nil {
- t.Errorf("Error unmarshaling JSON: %s", err)
- }
-
- actual, err := ExtractInfo(listResult)
- if err != nil {
- t.Errorf("Error extracting containers info: %s", err)
- }
-
- if !reflect.DeepEqual(expected, actual) {
- t.Errorf("\nExpected: %+v\nActual: %+v", expected, actual)
- }
-}
-
-func TestExtractConatinerNames(t *testing.T) {
- responseBody := "artemis\ndiana\n"
-
- listResult := &http.Response{
- Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)),
- }
-
- expected := []string{"artemis", "diana"}
-
- actual, err := ExtractNames(listResult)
- if err != nil {
- t.Errorf("Error extracting container names: %s", err)
- }
-
- if !reflect.DeepEqual(expected, actual) {
- t.Errorf("Expected: %+v\nActual:%+v", expected, actual)
- }
-}
diff --git a/openstack/storage/v1/containers/requests.go b/openstack/storage/v1/containers/requests.go
index a5435a2..3a6a265 100644
--- a/openstack/storage/v1/containers/requests.go
+++ b/openstack/storage/v1/containers/requests.go
@@ -1,57 +1,39 @@
package containers
import (
- "net/http"
-
"github.com/racker/perigee"
"github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/openstack/utils"
"github.com/rackspace/gophercloud/pagination"
)
-// ListResult is a *http.Response that is returned from a call to the List function.
-type ListResult struct {
- pagination.MarkerPageBase
+// ListOpts is a structure that holds options for listing containers.
+type ListOpts struct {
+ Full bool
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ EndMarker string `q:"end_marker"`
+ Format string `q:"format"`
+ Prefix string `q:"prefix"`
+ Delimiter []byte `q:"delimiter"`
}
-// IsEmpty returns true if a ListResult contains no container names.
-func (r ListResult) IsEmpty() (bool, error) {
- names, err := ExtractNames(r)
- if err != nil {
- return true, err
- }
- return len(names) == 0, nil
-}
-
-// LastMarker returns the last container name in a ListResult.
-func (r ListResult) LastMarker() (string, error) {
- names, err := ExtractNames(r)
- if err != nil {
- return "", err
- }
- if len(names) == 0 {
- return "", nil
- }
- return names[len(names)-1], nil
-}
-
-// GetResult is a *http.Response that is returned from a call to the Get function.
-type GetResult *http.Response
-
// List is a function that retrieves all objects in a container. It also returns the details
// for the account. To extract just the container information or names, pass the ListResult
// response to the ExtractInfo or ExtractNames function, respectively.
func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
var headers map[string]string
- query := utils.BuildQuery(opts.Params)
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
if !opts.Full {
- headers = map[string]string{"Content-Type": "text/plain"}
+ headers = map[string]string{"Accept": "text/plain"}
}
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
- p := ListResult{pagination.MarkerPageBase{LastHTTPResponse: r}}
+ p := ContainerPage{pagination.MarkerPageBase{LastHTTPResponse: r}}
p.MarkerPageBase.Owner = p
return p
}
@@ -62,13 +44,30 @@
return pager
}
-// Create is a function that creates a new container.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) (Container, error) {
- var ci Container
+// CreateOpts is a structure that holds parameters for creating a container.
+type CreateOpts struct {
+ Metadata map[string]string
+ ContainerRead string `h:"X-Container-Read"`
+ ContainerSyncTo string `h:"X-Container-Sync-To"`
+ ContainerSyncKey string `h:"X-Container-Sync-Key"`
+ ContainerWrite string `h:"X-Container-Write"`
+ ContentType string `h:"Content-Type"`
+ DetectContentType bool `h:"X-Detect-Content-Type"`
+ IfNoneMatch string `h:"If-None-Match"`
+ VersionsLocation string `h:"X-Versions-Location"`
+}
+// Create is a function that creates a new container.
+func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOpts) (Container, error) {
+ var container Container
h := c.Provider.AuthenticatedHeaders()
- for k, v := range opts.Headers {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return container, err
+ }
+
+ for k, v := range headers {
h[k] = v
}
@@ -76,38 +75,49 @@
h["X-Container-Meta-"+k] = v
}
- url := containerURL(c, opts.Name)
- _, err := perigee.Request("PUT", url, perigee.Options{
+ _, err = perigee.Request("PUT", containerURL(c, containerName), perigee.Options{
MoreHeaders: h,
OkCodes: []int{201, 204},
})
if err == nil {
- ci = Container{
- "name": opts.Name,
- }
+ container = Container{"name": containerName}
}
- return ci, err
+ return container, err
}
// Delete is a function that deletes a container.
-func Delete(c *gophercloud.ServiceClient, opts DeleteOpts) error {
- h := c.Provider.AuthenticatedHeaders()
-
- query := utils.BuildQuery(opts.Params)
-
- url := containerURL(c, opts.Name) + query
- _, err := perigee.Request("DELETE", url, perigee.Options{
- MoreHeaders: h,
+func Delete(c *gophercloud.ServiceClient, containerName string) error {
+ _, err := perigee.Request("DELETE", containerURL(c, containerName), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
return err
}
+// UpdateOpts is a structure that holds parameters for updating, creating, or deleting a
+// container's metadata.
+type UpdateOpts struct {
+ Metadata map[string]string
+ ContainerRead string `h:"X-Container-Read"`
+ ContainerSyncTo string `h:"X-Container-Sync-To"`
+ ContainerSyncKey string `h:"X-Container-Sync-Key"`
+ ContainerWrite string `h:"X-Container-Write"`
+ ContentType string `h:"Content-Type"`
+ DetectContentType bool `h:"X-Detect-Content-Type"`
+ RemoveVersionsLocation string `h:"X-Remove-Versions-Location"`
+ VersionsLocation string `h:"X-Versions-Location"`
+}
+
// Update is a function that creates, updates, or deletes a container's metadata.
-func Update(c *gophercloud.ServiceClient, opts UpdateOpts) error {
+func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOpts) error {
h := c.Provider.AuthenticatedHeaders()
- for k, v := range opts.Headers {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return err
+ }
+
+ for k, v := range headers {
h[k] = v
}
@@ -115,8 +125,8 @@
h["X-Container-Meta-"+k] = v
}
- url := containerURL(c, opts.Name)
- _, err := perigee.Request("POST", url, perigee.Options{
+ url := containerURL(c, containerName)
+ _, err = perigee.Request("POST", url, perigee.Options{
MoreHeaders: h,
OkCodes: []int{204},
})
@@ -125,13 +135,13 @@
// Get is a function that retrieves the metadata of a container. To extract just the custom
// metadata, pass the GetResult response to the ExtractMetadata function.
-func Get(c *gophercloud.ServiceClient, opts GetOpts) (GetResult, error) {
- h := c.Provider.AuthenticatedHeaders()
-
- url := containerURL(c, opts.Name)
- resp, err := perigee.Request("HEAD", url, perigee.Options{
- MoreHeaders: h,
+func Get(c *gophercloud.ServiceClient, containerName string) GetResult {
+ var gr GetResult
+ resp, err := perigee.Request("HEAD", containerURL(c, containerName), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
OkCodes: []int{204},
})
- return &resp.HttpResponse, err
+ gr.Err = err
+ gr.Resp = &resp.HttpResponse
+ return gr
}
diff --git a/openstack/storage/v1/containers/requests_test.go b/openstack/storage/v1/containers/requests_test.go
index 871cb21..3b59e00 100644
--- a/openstack/storage/v1/containers/requests_test.go
+++ b/openstack/storage/v1/containers/requests_test.go
@@ -1,14 +1,16 @@
package containers
import (
+ "fmt"
"net/http"
"testing"
"github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
"github.com/rackspace/gophercloud/testhelper"
)
-const (
+const (
tokenId = "abcabcabcabc"
)
@@ -29,12 +31,42 @@
testhelper.TestMethod(t, r, "GET")
testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
testhelper.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Add("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `[{'count': 0,'bytes': 0,'name': 'janeausten'},{'count': 1,'bytes': 14,'name': 'marktwain'}]`)
})
client := serviceClient()
- _, err := List(client, ListOpts{Full: true})
- if err != nil {
- t.Fatalf("Unexpected error listing containers info: %v", err)
+ count := 0
+ List(client, ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractInfo(page)
+ if err != nil {
+ t.Errorf("Failed to extract container info: %v", err)
+ return false, err
+ }
+
+ expected := []Container{
+ Container{
+ "count": 0,
+ "bytes": 0,
+ "name": "janeausten",
+ },
+ Container{
+ "count": 1,
+ "bytes": 14,
+ "name": "marktwain",
+ },
+ }
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
}
}
@@ -46,12 +78,31 @@
testhelper.TestMethod(t, r, "GET")
testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
testhelper.TestHeader(t, r, "Accept", "text/plain")
+
+ w.Header().Set("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, "")
})
client := serviceClient()
- _, err := List(client, ListOpts{})
- if err != nil {
- t.Fatalf("Unexpected error listing containers info: %v", err)
+ count := 0
+ List(client, ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractNames(page)
+ if err != nil {
+ t.Errorf("Failed to extract container names: %v", err)
+ return false, err
+ }
+
+ expected := []string{"janeausten, marktwain"}
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+
+ if count != 0 {
+ t.Fatalf("Expected 0 pages, got %d", count)
}
}
@@ -67,9 +118,7 @@
})
client := serviceClient()
- _, err := Create(client, CreateOpts{
- Name: "testContainer",
- })
+ _, err := Create(client, "testContainer", CreateOpts{})
if err != nil {
t.Fatalf("Unexpected error creating container: %v", err)
}
@@ -87,9 +136,7 @@
})
client := serviceClient()
- err := Delete(client, DeleteOpts{
- Name: "testContainer",
- })
+ err := Delete(client, "testContainer")
if err != nil {
t.Fatalf("Unexpected error deleting container: %v", err)
}
@@ -107,9 +154,7 @@
})
client := serviceClient()
- err := Update(client, UpdateOpts{
- Name: "testContainer",
- })
+ err := Update(client, "testContainer", UpdateOpts{})
if err != nil {
t.Fatalf("Unexpected error updating container metadata: %v", err)
}
@@ -124,12 +169,13 @@
testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
testhelper.TestHeader(t, r, "Accept", "application/json")
w.WriteHeader(http.StatusNoContent)
+ fmt.Fprintf(w, `
+
+ `)
})
client := serviceClient()
- _, err := Get(client, GetOpts{
- Name: "testContainer",
- })
+ _, err := Get(client, "testContainer").ExtractMetadata()
if err != nil {
t.Fatalf("Unexpected error getting container metadata: %v", err)
}
diff --git a/openstack/storage/v1/containers/results.go b/openstack/storage/v1/containers/results.go
new file mode 100644
index 0000000..6a93b4d
--- /dev/null
+++ b/openstack/storage/v1/containers/results.go
@@ -0,0 +1,134 @@
+package containers
+
+import (
+ "fmt"
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "net/http"
+ "strings"
+)
+
+type Container map[string]interface{}
+
+type commonResult struct {
+ gophercloud.CommonResult
+}
+
+func (r GetResult) Extract() (*Container, error) {
+ if r.Err != nil {
+ return nil, r.Err
+ }
+
+ var res struct {
+ Container *Container
+ }
+
+ err := mapstructure.Decode(r.Resp, &res)
+ if err != nil {
+ return nil, fmt.Errorf("Error decoding Object Storage Container: %v", err)
+ }
+
+ return res.Container, nil
+}
+
+type CreateResult struct {
+ commonResult
+}
+
+// GetResult represents the result of a get operation.
+type GetResult struct {
+ Resp *http.Response
+ Err error
+}
+
+// UpdateResult represents the result of an update operation.
+type UpdateResult commonResult
+
+// DeleteResult represents the result of a delete operation.
+type DeleteResult commonResult
+
+// ListResult is a *http.Response that is returned from a call to the List function.
+type ContainerPage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no container names.
+func (r ContainerPage) IsEmpty() (bool, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return true, err
+ }
+ return len(names) == 0, nil
+}
+
+// LastMarker returns the last container name in a ListResult.
+func (r ContainerPage) LastMarker() (string, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return "", err
+ }
+ if len(names) == 0 {
+ return "", nil
+ }
+ return names[len(names)-1], nil
+}
+
+// ExtractInfo is a function that takes a ListResult and returns the containers' information.
+func ExtractInfo(page pagination.Page) ([]Container, error) {
+ untyped := page.(ContainerPage).Body.([]interface{})
+ results := make([]Container, len(untyped))
+ for index, each := range untyped {
+ results[index] = Container(each.(map[string]interface{}))
+ }
+ return results, nil
+}
+
+// ExtractNames is a function that takes a ListResult and returns the containers' names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+ casted := page.(ContainerPage)
+ ct := casted.Header.Get("Content-Type")
+
+ switch {
+ case strings.HasPrefix(ct, "application/json"):
+ parsed, err := ExtractInfo(page)
+ if err != nil {
+ return nil, err
+ }
+
+ names := make([]string, 0, len(parsed))
+ for _, container := range parsed {
+ names = append(names, container["name"].(string))
+ }
+ return names, nil
+ case strings.HasPrefix(ct, "text/plain"):
+ names := make([]string, 0, 50)
+
+ body := string(page.(ContainerPage).Body.([]uint8))
+ for _, name := range strings.Split(body, "\n") {
+ if len(name) > 0 {
+ names = append(names, name)
+ }
+ }
+
+ return names, nil
+ default:
+ return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
+ }
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the container.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+ metadata := make(map[string]string)
+ for k, v := range gr.Resp.Header {
+ if strings.HasPrefix(k, "X-Container-Meta-") {
+ key := strings.TrimPrefix(k, "X-Container-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata, nil
+}
diff --git a/openstack/storage/v1/objects/objects.go b/openstack/storage/v1/objects/objects.go
deleted file mode 100644
index cd248c8..0000000
--- a/openstack/storage/v1/objects/objects.go
+++ /dev/null
@@ -1,139 +0,0 @@
-package objects
-
-import (
- "fmt"
- "io"
- "io/ioutil"
- "strings"
-
- "github.com/rackspace/gophercloud/pagination"
-)
-
-// Object is a structure that holds information related to a storage object.
-type Object map[string]interface{}
-
-// ListOpts is a structure that holds parameters for listing objects.
-type ListOpts struct {
- Container string
- Full bool
- Params map[string]string
-}
-
-// DownloadOpts is a structure that holds parameters for downloading an object.
-type DownloadOpts struct {
- Container string
- Name string
- Headers map[string]string
- Params map[string]string
-}
-
-// CreateOpts is a structure that holds parameters for creating an object.
-type CreateOpts struct {
- Container string
- Name string
- Content io.Reader
- Metadata map[string]string
- Headers map[string]string
- Params map[string]string
-}
-
-// CopyOpts is a structure that holds parameters for copying one object to another.
-type CopyOpts struct {
- Container string
- Name string
- NewContainer string
- NewName string
- Metadata map[string]string
- Headers map[string]string
-}
-
-// DeleteOpts is a structure that holds parameters for deleting an object.
-type DeleteOpts struct {
- Container string
- Name string
- Params map[string]string
-}
-
-// GetOpts is a structure that holds parameters for getting an object's metadata.
-type GetOpts struct {
- Container string
- Name string
- Params map[string]string
-}
-
-// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
-// object's metadata.
-type UpdateOpts struct {
- Container string
- Name string
- Metadata map[string]string
- Headers map[string]string
-}
-
-// ExtractInfo is a function that takes a page of objects and returns their full information.
-func ExtractInfo(page pagination.Page) ([]Object, error) {
- untyped := page.(ListResult).Body.([]interface{})
- results := make([]Object, len(untyped))
- for index, each := range untyped {
- results[index] = Object(each.(map[string]interface{}))
- }
- return results, nil
-}
-
-// ExtractNames is a function that takes a page of objects and returns only their names.
-func ExtractNames(page pagination.Page) ([]string, error) {
- casted := page.(ListResult)
- ct := casted.Header.Get("Content-Type")
-
- switch {
- case strings.HasPrefix(ct, "application/json"):
- parsed, err := ExtractInfo(page)
- if err != nil {
- return nil, err
- }
-
- names := make([]string, 0, len(parsed))
- for _, object := range parsed {
- names = append(names, object["name"].(string))
- }
- return names, nil
- case strings.HasPrefix(ct, "text/plain"):
- names := make([]string, 0, 50)
-
- body := string(page.(ListResult).Body.([]uint8))
- for _, name := range strings.Split(body, "\n") {
- if len(name) > 0 {
- names = append(names, name)
- }
- }
-
- return names, nil
- default:
- return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
- }
-}
-
-// ExtractContent is a function that takes a DownloadResult (of type *http.Response)
-// and returns the object's content.
-func ExtractContent(dr DownloadResult) ([]byte, error) {
- var body []byte
- defer dr.Body.Close()
- body, err := ioutil.ReadAll(dr.Body)
- if err != nil {
- return body, fmt.Errorf("Error trying to read DownloadResult body: %v", err)
- }
- return body, nil
-}
-
-// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
-// and returns the custom metadata associated with the object.
-func ExtractMetadata(gr GetResult) map[string]string {
- metadata := make(map[string]string)
- for k, v := range gr.Header {
- if strings.HasPrefix(k, "X-Object-Meta-") {
- key := strings.TrimPrefix(k, "X-Object-Meta-")
- metadata[key] = v[0]
- }
- }
- return metadata
-}
diff --git a/openstack/storage/v1/objects/objects_test.go b/openstack/storage/v1/objects/objects_test.go
deleted file mode 100644
index 31fa837..0000000
--- a/openstack/storage/v1/objects/objects_test.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package objects
-
-import (
- "bytes"
- "encoding/json"
- "io/ioutil"
- "net/http"
- "reflect"
- "testing"
-)
-
-func TestExtractObjectMetadata(t *testing.T) {
- getResult := &http.Response{}
-
- expected := map[string]string{}
-
- actual := ExtractMetadata(getResult)
-
- if !reflect.DeepEqual(expected, actual) {
- t.Errorf("Expected: %+v\nActual:%+v", expected, actual)
- }
-}
-
-func TestExtractContent(t *testing.T) {
- responseBody := "'Twas brillig, and the slithy toves"
- downloadResult := &http.Response{
- Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)),
- }
- expected := []byte("'Twas brillig, and the slithy toves")
- actual, err := ExtractContent(downloadResult)
- if err != nil {
- t.Errorf("Error extracting object content: %s", err)
- }
- if !reflect.DeepEqual(actual, expected) {
- t.Errorf("Expected: %+v\nActual:%+v", expected, actual)
- }
-}
-
-func TestExtractObjectInfo(t *testing.T) {
- responseBody := `
- [
- {
- "hash": "451e372e48e0f6b1114fa0724aa79fa1",
- "last_modified": "2014-01-15T16:41:49.390270",
- "bytes": 14,
- "name": "goodbye",
- "content_type": "application/octet-stream"
- },
- {
- "hash": "ed076287532e86365e841e92bfc50d8c",
- "last_modified": "2014-01-15T16:37:43.427570",
- "bytes": 12,
- "name": "helloworld",
- "content_type": "application/octet-stream"
- }
- ]
- `
-
- listResult := &http.Response{
- Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)),
- }
-
- var expected []Object
- err := json.Unmarshal([]byte(responseBody), &expected)
- if err != nil {
- t.Errorf("Error unmarshaling JSON: %s", err)
- }
-
- actual, err := ExtractInfo(listResult)
- if err != nil {
- t.Errorf("Error extracting objects info: %s", err)
- }
-
- if !reflect.DeepEqual(expected, actual) {
- t.Errorf("Expected: %+v\nActual: %+v", expected, actual)
- }
-}
-
-func TestExtractObjectNames(t *testing.T) {
- responseBody := "goodbye\nhelloworld\n"
-
- listResult := &http.Response{
- Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)),
- }
-
- expected := []string{"goodbye", "helloworld"}
-
- actual, err := ExtractNames(listResult)
- if err != nil {
- t.Errorf("Error extracting object names: %s", err)
- }
-
- if !reflect.DeepEqual(expected, actual) {
- t.Errorf("Expected: %+v\nActual:%+v", expected, actual)
- }
-}
diff --git a/openstack/storage/v1/objects/requests.go b/openstack/storage/v1/objects/requests.go
index 7b00127..363843c 100644
--- a/openstack/storage/v1/objects/requests.go
+++ b/openstack/storage/v1/objects/requests.go
@@ -2,97 +2,132 @@
import (
"fmt"
- "net/http"
+ "io"
+ "time"
"github.com/racker/perigee"
"github.com/rackspace/gophercloud"
- "github.com/rackspace/gophercloud/openstack/utils"
"github.com/rackspace/gophercloud/pagination"
)
-// ListResult is a single page of objects that is returned from a call to the List function.
-type ListResult struct {
- pagination.MarkerPageBase
+// ListOpts is a structure that holds parameters for listing objects.
+type ListOpts struct {
+ Full bool
+ Limit int `q:"limit"`
+ Marker string `q:"marker"`
+ EndMarker string `q:"end_marker"`
+ Format string `q:"format"`
+ Prefix string `q:"prefix"`
+ Delimiter [1]byte `q:"delimiter"`
+ Path string `q:"path"`
}
-// IsEmpty returns true if a ListResult contains no object names.
-func (r ListResult) IsEmpty() (bool, error) {
- names, err := ExtractNames(r)
- if err != nil {
- return true, err
- }
- return len(names) == 0, nil
-}
-
-// LastMarker returns the last object name in a ListResult.
-func (r ListResult) LastMarker() (string, error) {
- names, err := ExtractNames(r)
- if err != nil {
- return "", err
- }
- if len(names) == 0 {
- return "", nil
- }
- return names[len(names)-1], nil
-}
-
-// DownloadResult is a *http.Response that is returned from a call to the Download function.
-type DownloadResult *http.Response
-
-// GetResult is a *http.Response that is returned from a call to the Get function.
-type GetResult *http.Response
-
// List is a function that retrieves all objects in a container. It also returns the details
// for the container. To extract only the object information or names, pass the ListResult
// response to the ExtractInfo or ExtractNames function, respectively.
-func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
+func List(c *gophercloud.ServiceClient, containerName string, opts ListOpts) pagination.Pager {
var headers map[string]string
- query := utils.BuildQuery(opts.Params)
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ fmt.Printf("Error building query string: %v", err)
+ return pagination.Pager{Err: err}
+ }
if !opts.Full {
- headers = map[string]string{"Content-Type": "text/plain"}
+ headers = map[string]string{"Accept": "text/plain"}
}
createPage := func(r pagination.LastHTTPResponse) pagination.Page {
- p := ListResult{pagination.MarkerPageBase{LastHTTPResponse: r}}
+ p := ObjectPage{pagination.MarkerPageBase{LastHTTPResponse: r}}
p.MarkerPageBase.Owner = p
return p
}
- url := containerURL(c, opts.Container) + query
+ url := containerURL(c, containerName) + query
pager := pagination.NewPager(c, url, createPage)
pager.Headers = headers
return pager
}
+// DownloadOpts is a structure that holds parameters for downloading an object.
+type DownloadOpts struct {
+ IfMatch string `h:"If-Match"`
+ IfModifiedSince time.Time `h:"If-Modified-Since"`
+ IfNoneMatch string `h:"If-None-Match"`
+ IfUnmodifiedSince time.Time `h:"If-Unmodified-Since"`
+ Range string `h:"Range"`
+ Expires string `q:"expires"`
+ MultipartManifest string `q:"multipart-manifest"`
+ Signature string `q:"signature"`
+}
+
// Download is a function that retrieves the content and metadata for an object.
// To extract just the content, pass the DownloadResult response to the ExtractContent
// function.
-func Download(c *gophercloud.ServiceClient, opts DownloadOpts) (DownloadResult, error) {
+func Download(c *gophercloud.ServiceClient, containerName, objectName string, opts DownloadOpts) DownloadResult {
+ var dr DownloadResult
+
h := c.Provider.AuthenticatedHeaders()
- for k, v := range opts.Headers {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ dr.Err = err
+ return dr
+ }
+
+ for k, v := range headers {
h[k] = v
}
- query := utils.BuildQuery(opts.Params)
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ dr.Err = err
+ return dr
+ }
- url := objectURL(c, opts.Container, opts.Name) + query
+ url := objectURL(c, containerName, objectName) + query
resp, err := perigee.Request("GET", url, perigee.Options{
MoreHeaders: h,
OkCodes: []int{200},
})
- return &resp.HttpResponse, err
+ dr.Err = err
+ dr.Resp = &resp.HttpResponse
+ return dr
+}
+
+// CreateOpts is a structure that holds parameters for creating an object.
+type CreateOpts struct {
+ Metadata map[string]string
+ ContentDisposition string `h:"Content-Disposition"`
+ ContentEncoding string `h:"Content-Encoding"`
+ ContentLength int `h:"Content-Length"`
+ ContentType string `h:"Content-Type"`
+ CopyFrom string `h:"X-Copy-From"`
+ DeleteAfter int `h:"X-Delete-After"`
+ DeleteAt int `h:"X-Delete-At"`
+ DetectContentType string `h:"X-Detect-Content-Type"`
+ ETag string `h:"ETag"`
+ IfNoneMatch string `h:"If-None-Match"`
+ ObjectManifest string `h:"X-Object-Manifest"`
+ TransferEncoding string `h:"Transfer-Encoding"`
+ Expires string `q:"expires"`
+ MultipartManifest string `q:"multiple-manifest"`
+ Signature string `q:"signature"`
}
// Create is a function that creates a new object or replaces an existing object.
-func Create(c *gophercloud.ServiceClient, opts CreateOpts) error {
+func Create(c *gophercloud.ServiceClient, containerName, objectName string, content io.Reader, opts CreateOpts) error {
var reqBody []byte
h := c.Provider.AuthenticatedHeaders()
- for k, v := range opts.Headers {
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil
+ }
+
+ for k, v := range headers {
h[k] = v
}
@@ -100,9 +135,11 @@
h["X-Object-Meta-"+k] = v
}
- query := utils.BuildQuery(opts.Params)
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return err
+ }
- content := opts.Content
if content != nil {
reqBody = make([]byte, 0)
_, err := content.Read(reqBody)
@@ -111,8 +148,8 @@
}
}
- url := objectURL(c, opts.Container, opts.Name) + query
- _, err := perigee.Request("PUT", url, perigee.Options{
+ url := objectURL(c, containerName, objectName) + query
+ _, err = perigee.Request("PUT", url, perigee.Options{
ReqBody: reqBody,
MoreHeaders: h,
OkCodes: []int{201},
@@ -120,56 +157,24 @@
return err
}
+// CopyOpts is a structure that holds parameters for copying one object to another.
+type CopyOpts struct {
+ Metadata map[string]string
+ ContentDisposition string `h:"Content-Disposition"`
+ ContentEncoding string `h:"Content-Encoding"`
+ ContentType string `h:"Content-Type"`
+ Destination string `h:"Destination,required"`
+}
+
// Copy is a function that copies one object to another.
-func Copy(c *gophercloud.ServiceClient, opts CopyOpts) error {
+func Copy(c *gophercloud.ServiceClient, containerName, objectName string, opts CopyOpts) error {
h := c.Provider.AuthenticatedHeaders()
- for k, v := range opts.Metadata {
- h["X-Object-Meta-"+k] = v
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return err
}
-
- h["Destination"] = fmt.Sprintf("/%s/%s", opts.NewContainer, opts.NewName)
-
- url := objectURL(c, opts.Container, opts.Name)
- _, err := perigee.Request("COPY", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{201},
- })
- return err
-}
-
-// Delete is a function that deletes an object.
-func Delete(c *gophercloud.ServiceClient, opts DeleteOpts) error {
- h := c.Provider.AuthenticatedHeaders()
-
- query := utils.BuildQuery(opts.Params)
-
- url := objectURL(c, opts.Container, opts.Name) + query
- _, err := perigee.Request("DELETE", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{204},
- })
- return err
-}
-
-// Get is a function that retrieves the metadata of an object. To extract just the custom
-// metadata, pass the GetResult response to the ExtractMetadata function.
-func Get(c *gophercloud.ServiceClient, opts GetOpts) (GetResult, error) {
- h := c.Provider.AuthenticatedHeaders()
-
- url := objectURL(c, opts.Container, opts.Name)
- resp, err := perigee.Request("HEAD", url, perigee.Options{
- MoreHeaders: h,
- OkCodes: []int{204},
- })
- return &resp.HttpResponse, err
-}
-
-// Update is a function that creates, updates, or deletes an object's metadata.
-func Update(c *gophercloud.ServiceClient, opts UpdateOpts) error {
- h := c.Provider.AuthenticatedHeaders()
-
- for k, v := range opts.Headers {
+ for k, v := range headers {
h[k] = v
}
@@ -177,8 +182,93 @@
h["X-Object-Meta-"+k] = v
}
- url := objectURL(c, opts.Container, opts.Name)
- _, err := perigee.Request("POST", url, perigee.Options{
+ url := objectURL(c, containerName, objectName)
+ _, err = perigee.Request("COPY", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{201},
+ })
+ return err
+}
+
+// DeleteOpts is a structure that holds parameters for deleting an object.
+type DeleteOpts struct {
+ MultipartManifest string `q:"multipart-manifest"`
+}
+
+// Delete is a function that deletes an object.
+func Delete(c *gophercloud.ServiceClient, containerName, objectName string, opts DeleteOpts) error {
+ h := c.Provider.AuthenticatedHeaders()
+
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return err
+ }
+
+ url := objectURL(c, containerName, objectName) + query
+ _, err = perigee.Request("DELETE", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ return err
+}
+
+// GetOpts is a structure that holds parameters for getting an object's metadata.
+type GetOpts struct {
+ Expires string `q:"expires"`
+ Signature string `q:"signature"`
+}
+
+// Get is a function that retrieves the metadata of an object. To extract just the custom
+// metadata, pass the GetResult response to the ExtractMetadata function.
+func Get(c *gophercloud.ServiceClient, containerName, objectName string, opts GetOpts) GetResult {
+ var gr GetResult
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ gr.Err = err
+ return gr
+ }
+
+ url := objectURL(c, containerName, objectName) + query
+ resp, err := perigee.Request("HEAD", url, perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{200, 204},
+ })
+ gr.Err = err
+ gr.Resp = &resp.HttpResponse
+ return gr
+}
+
+// UpdateOpts is a structure that holds parameters for updating, creating, or deleting an
+// object's metadata.
+type UpdateOpts struct {
+ Metadata map[string]string
+ ContentDisposition string `h:"Content-Disposition"`
+ ContentEncoding string `h:"Content-Encoding"`
+ ContentType string `h:"Content-Type"`
+ DeleteAfter int `h:"X-Delete-After"`
+ DeleteAt int `h:"X-Delete-At"`
+ DetectContentType bool `h:"X-Detect-Content-Type"`
+}
+
+// Update is a function that creates, updates, or deletes an object's metadata.
+func Update(c *gophercloud.ServiceClient, containerName, objectName string, opts UpdateOpts) error {
+ h := c.Provider.AuthenticatedHeaders()
+
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return nil
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Object-Meta-"+k] = v
+ }
+
+ url := objectURL(c, containerName, objectName)
+ _, err = perigee.Request("POST", url, perigee.Options{
MoreHeaders: h,
OkCodes: []int{202},
})
diff --git a/openstack/storage/v1/objects/requests_test.go b/openstack/storage/v1/objects/requests_test.go
index ca829ae..c3047c6 100644
--- a/openstack/storage/v1/objects/requests_test.go
+++ b/openstack/storage/v1/objects/requests_test.go
@@ -2,14 +2,16 @@
import (
"bytes"
+ "fmt"
"net/http"
"testing"
"github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
"github.com/rackspace/gophercloud/testhelper"
)
-const (
+const (
tokenId = "abcabcabcabc"
)
@@ -22,24 +24,26 @@
}
}
-func TestDownloadObject(t * testing.T) {
+func TestDownloadObject(t *testing.T) {
testhelper.SetupHTTP()
defer testhelper.TeardownHTTP()
-
+
testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "GET")
testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, "Successful download with Gophercloud")
})
client := serviceClient()
- _, err := Download(client, DownloadOpts{
- Container: "testContainer",
- Name: "testObject",
- })
+ content, err := Download(client, "testContainer", "testObject", DownloadOpts{}).ExtractContent()
if err != nil {
t.Fatalf("Unexpected error downloading object: %v", err)
}
+ if string(content) != "Successful download with Gophercloud" {
+ t.Errorf("Expected %s, got %s", "Successful download with Gophercloud", content)
+ }
}
func TestListObjectInfo(t *testing.T) {
@@ -49,16 +53,39 @@
testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
testhelper.TestMethod(t, r, "GET")
testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
- testhelper.TestHeader(t, r, "Accept", "application/json")
+
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, `[{'hash': '451e372e48e0f6b1114fa0724aa79fa1','last_modified': '2014-01-15T16:41:49.390270','bytes': 14,'name': 'goodbye','content_type': 'application/octet-stream'}]`)
})
client := serviceClient()
- _, err := List(client, ListOpts{
- Full: true,
- Container: "testContainer",
+ count := 0
+ List(client, "testContainer", ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractInfo(page)
+ if err != nil {
+ t.Errorf("Failed to extract object info: %v", err)
+ return false, err
+ }
+
+ expected := []Object{
+ Object{
+ "hash": "451e372e48e0f6b1114fa0724aa79fa1",
+ "last_modified": "2014-01-15T16:41:49.390270",
+ "bytes": 14,
+ "name": "goodbye",
+ "content_type": "application/octet-stream",
+ },
+ }
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
})
- if err != nil {
- t.Fatalf("Unexpected error listing objects info: %v", err)
+
+ if count != 1 {
+ t.Errorf("Expected 1 page, got %d", count)
}
}
@@ -70,17 +97,33 @@
testhelper.TestMethod(t, r, "GET")
testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
testhelper.TestHeader(t, r, "Accept", "text/plain")
+
+ w.Header().Add("Content-Type", "text/plain")
+ w.WriteHeader(http.StatusOK)
+ fmt.Fprintf(w, "")
})
client := serviceClient()
- _, err := List(client, ListOpts{
- Container: "testContainer",
+ count := 0
+ List(client, "testContainer", ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ count++
+ actual, err := ExtractNames(page)
+ if err != nil {
+ t.Errorf("Failed to extract object names: %v", err)
+ return false, err
+ }
+
+ expected := []string{"helloworld", "goodbye"}
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
})
- if err != nil {
- t.Fatalf("Unexpected error listing object names: %v", err)
+
+ if count != 0 {
+ t.Fatalf("Expected 0 pages, got %d", count)
}
}
-
func TestCreateObject(t *testing.T) {
testhelper.SetupHTTP()
defer testhelper.TeardownHTTP()
@@ -93,11 +136,8 @@
})
client := serviceClient()
- err := Create(client, CreateOpts{
- Content: bytes.NewBufferString("Did gyre and gimble in the wabe:"),
- Container: "testContainer",
- Name: "testObject",
- })
+ content := bytes.NewBufferString("Did gyre and gimble in the wabe")
+ err := Create(client, "testContainer", "testObject", content, CreateOpts{})
if err != nil {
t.Fatalf("Unexpected error creating object: %v", err)
}
@@ -116,12 +156,7 @@
})
client := serviceClient()
- err := Copy(client, CopyOpts{
- NewContainer: "newTestContainer",
- NewName: "newTestObject",
- Container: "testContainer",
- Name: "testObject",
- })
+ err := Copy(client, "testContainer", "testObject", CopyOpts{Destination: "/newTestContainer/newTestObject"})
if err != nil {
t.Fatalf("Unexpected error copying object: %v", err)
}
@@ -139,10 +174,7 @@
})
client := serviceClient()
- err := Delete(client, DeleteOpts{
- Container: "testContainer",
- Name: "testObject",
- })
+ err := Delete(client, "testContainer", "testObject", DeleteOpts{})
if err != nil {
t.Fatalf("Unexpected error deleting object: %v", err)
}
@@ -161,17 +193,13 @@
})
client := serviceClient()
- err := Update(client, UpdateOpts{
- Container: "testContainer",
- Name: "testObject",
- Metadata: metadata,
- })
+ err := Update(client, "testContainer", "testObject", UpdateOpts{Metadata: metadata})
if err != nil {
t.Fatalf("Unexpected error updating object metadata: %v", err)
}
}
-func TestGetContainer(t *testing.T) {
+func TestGetObject(t *testing.T) {
testhelper.SetupHTTP()
defer testhelper.TeardownHTTP()
@@ -183,11 +211,10 @@
})
client := serviceClient()
- _, err := Get(client, GetOpts{
- Container: "testContainer",
- Name: "testObject",
- })
+ expected := metadata
+ actual, err := Get(client, "testContainer", "testObject", GetOpts{}).ExtractMetadata()
if err != nil {
t.Fatalf("Unexpected error getting object metadata: %v", err)
}
+ testhelper.CheckDeepEquals(t, expected, actual)
}
diff --git a/openstack/storage/v1/objects/results.go b/openstack/storage/v1/objects/results.go
new file mode 100644
index 0000000..8808d03
--- /dev/null
+++ b/openstack/storage/v1/objects/results.go
@@ -0,0 +1,125 @@
+package objects
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Object is a structure that holds information related to a storage object.
+type Object map[string]interface{}
+
+// ListResult is a single page of objects that is returned from a call to the List function.
+type ObjectPage struct {
+ pagination.MarkerPageBase
+}
+
+// IsEmpty returns true if a ListResult contains no object names.
+func (r ObjectPage) IsEmpty() (bool, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return true, err
+ }
+ return len(names) == 0, nil
+}
+
+// LastMarker returns the last object name in a ListResult.
+func (r ObjectPage) LastMarker() (string, error) {
+ names, err := ExtractNames(r)
+ if err != nil {
+ return "", err
+ }
+ if len(names) == 0 {
+ return "", nil
+ }
+ return names[len(names)-1], nil
+}
+
+// DownloadResult is a *http.Response that is returned from a call to the Download function.
+type DownloadResult struct {
+ Resp *http.Response
+ Err error
+}
+
+// GetResult is a *http.Response that is returned from a call to the Get function.
+type GetResult struct {
+ Resp *http.Response
+ Err error
+}
+
+// ExtractInfo is a function that takes a page of objects and returns their full information.
+func ExtractInfo(page pagination.Page) ([]Object, error) {
+ untyped := page.(ObjectPage).Body.([]interface{})
+ results := make([]Object, len(untyped))
+ for index, each := range untyped {
+ results[index] = Object(each.(map[string]interface{}))
+ }
+ return results, nil
+}
+
+// ExtractNames is a function that takes a page of objects and returns only their names.
+func ExtractNames(page pagination.Page) ([]string, error) {
+ casted := page.(ObjectPage)
+ ct := casted.Header.Get("Content-Type")
+
+ switch {
+ case strings.HasPrefix(ct, "application/json"):
+ parsed, err := ExtractInfo(page)
+ if err != nil {
+ return nil, err
+ }
+
+ names := make([]string, 0, len(parsed))
+ for _, object := range parsed {
+ names = append(names, object["name"].(string))
+ }
+ return names, nil
+ case strings.HasPrefix(ct, "text/plain"):
+ names := make([]string, 0, 50)
+
+ body := string(page.(ObjectPage).Body.([]uint8))
+ for _, name := range strings.Split(body, "\n") {
+ if len(name) > 0 {
+ names = append(names, name)
+ }
+ }
+
+ return names, nil
+ default:
+ return nil, fmt.Errorf("Cannot extract names from response with content-type: [%s]", ct)
+ }
+}
+
+// ExtractContent is a function that takes a DownloadResult (of type *http.Response)
+// and returns the object's content.
+func (dr DownloadResult) ExtractContent() ([]byte, error) {
+ if dr.Err != nil {
+ return nil, nil
+ }
+ var body []byte
+ defer dr.Resp.Body.Close()
+ body, err := ioutil.ReadAll(dr.Resp.Body)
+ if err != nil {
+ return body, fmt.Errorf("Error trying to read DownloadResult body: %v", err)
+ }
+ return body, nil
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metadata associated with the object.
+func (gr GetResult) ExtractMetadata() (map[string]string, error) {
+ if gr.Err != nil {
+ return nil, gr.Err
+ }
+ metadata := make(map[string]string)
+ for k, v := range gr.Resp.Header {
+ if strings.HasPrefix(k, "X-Object-Meta-") {
+ key := strings.TrimPrefix(k, "X-Object-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata, nil
+}