rename directory from 'storage' to 'objectStorage'; add fix for handling 'text/html' content-type response from 'ListNames'
diff --git a/openstack/objectStorage/v1/accounts/accounts.go b/openstack/objectStorage/v1/accounts/accounts.go
new file mode 100644
index 0000000..c460e45
--- /dev/null
+++ b/openstack/objectStorage/v1/accounts/accounts.go
@@ -0,0 +1,30 @@
+package accounts
+
+import (
+ "strings"
+)
+
+// UpdateOpts is a structure that contains parameters for updating, creating, or deleting an
+// account's metadata.
+type UpdateOpts struct {
+ Metadata map[string]string
+ Headers map[string]string
+}
+
+// GetOpts is a structure that contains parameters for getting an account's metadata.
+type GetOpts struct {
+ Headers map[string]string
+}
+
+// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
+// and returns the custom metatdata associated with the account.
+func ExtractMetadata(gr GetResult) map[string]string {
+ metadata := make(map[string]string)
+ for k, v := range gr.Header {
+ if strings.HasPrefix(k, "X-Account-Meta-") {
+ key := strings.TrimPrefix(k, "X-Account-Meta-")
+ metadata[key] = v[0]
+ }
+ }
+ return metadata
+}
diff --git a/openstack/objectStorage/v1/accounts/accounts_test.go b/openstack/objectStorage/v1/accounts/accounts_test.go
new file mode 100644
index 0000000..2c2a84a
--- /dev/null
+++ b/openstack/objectStorage/v1/accounts/accounts_test.go
@@ -0,0 +1,19 @@
+package accounts
+
+import (
+ "net/http"
+ "reflect"
+ "testing"
+)
+
+func TestExtractAccountMetadata(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)
+ }
+}
diff --git a/openstack/objectStorage/v1/accounts/doc.go b/openstack/objectStorage/v1/accounts/doc.go
new file mode 100644
index 0000000..5c94e1a
--- /dev/null
+++ b/openstack/objectStorage/v1/accounts/doc.go
@@ -0,0 +1,5 @@
+/* The accounts package defines operations performed on an object-storage account.
+
+Reference: http://developer.openstack.org/api-ref-objectstorage-v1.html#storage_account_services
+*/
+package accounts
diff --git a/openstack/objectStorage/v1/accounts/requests.go b/openstack/objectStorage/v1/accounts/requests.go
new file mode 100644
index 0000000..c738573
--- /dev/null
+++ b/openstack/objectStorage/v1/accounts/requests.go
@@ -0,0 +1,46 @@
+package accounts
+
+import (
+ "net/http"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+)
+
+// GetResult is a *http.Response that is returned from a call to the Get function.
+type GetResult *http.Response
+
+// Update is a function that creates, updates, or deletes an account's metadata.
+func Update(c *gophercloud.ServiceClient, opts UpdateOpts) error {
+ h := c.Provider.AuthenticatedHeaders()
+
+ for k, v := range opts.Headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Account-Meta-"+k] = v
+ }
+
+ _, err := perigee.Request("POST", accountURL(c), perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ return err
+}
+
+// Get is a function that retrieves an account's metadata. 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()
+
+ for k, v := range opts.Headers {
+ h[k] = v
+ }
+
+ resp, err := perigee.Request("HEAD", accountURL(c), perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ return &resp.HttpResponse, err
+}
diff --git a/openstack/objectStorage/v1/accounts/requests_test.go b/openstack/objectStorage/v1/accounts/requests_test.go
new file mode 100644
index 0000000..e1450d4
--- /dev/null
+++ b/openstack/objectStorage/v1/accounts/requests_test.go
@@ -0,0 +1,55 @@
+package accounts
+
+import (
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const tokenId = "abcabcabcabc"
+
+var metadata = map[string]string{"gophercloud-test": "accounts"}
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenId},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestUpdateAccount(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "X-Account-Meta-Gophercloud-Test", "accounts")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ err := Update(client, UpdateOpts{Metadata: metadata})
+ if err != nil {
+ t.Fatalf("Unable to update account: %v", err)
+ }
+}
+
+func TestGetAccount(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "HEAD")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ _, err := Get(client, GetOpts{})
+ if err != nil {
+ t.Fatalf("Unable to get account metadata: %v", err)
+ }
+}
diff --git a/openstack/objectStorage/v1/accounts/urls.go b/openstack/objectStorage/v1/accounts/urls.go
new file mode 100644
index 0000000..53b1343
--- /dev/null
+++ b/openstack/objectStorage/v1/accounts/urls.go
@@ -0,0 +1,8 @@
+package accounts
+
+import "github.com/rackspace/gophercloud"
+
+// accountURL returns the URI for making Account requests.
+func accountURL(c *gophercloud.ServiceClient) string {
+ return c.Endpoint
+}
diff --git a/openstack/objectStorage/v1/accounts/urls_test.go b/openstack/objectStorage/v1/accounts/urls_test.go
new file mode 100644
index 0000000..f127a5e
--- /dev/null
+++ b/openstack/objectStorage/v1/accounts/urls_test.go
@@ -0,0 +1,17 @@
+package accounts
+
+import (
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+)
+
+func TestAccountURL(t *testing.T) {
+ client := gophercloud.ServiceClient{
+ Endpoint: "http://localhost:5000/v3/",
+ }
+ url := accountURL(&client)
+ if url != "http://localhost:5000/v3/" {
+ t.Errorf("Unexpected service URL generated: [%s]", url)
+ }
+}
diff --git a/openstack/objectStorage/v1/containers/doc.go b/openstack/objectStorage/v1/containers/doc.go
new file mode 100644
index 0000000..9b6ac17
--- /dev/null
+++ b/openstack/objectStorage/v1/containers/doc.go
@@ -0,0 +1,5 @@
+/* The containers package defines operations performed on an object-storage container.
+
+Reference: http://developer.openstack.org/api-ref-objectstorage-v1.html#storage_container_services
+*/
+package containers
diff --git a/openstack/objectStorage/v1/containers/requests.go b/openstack/objectStorage/v1/containers/requests.go
new file mode 100644
index 0000000..013c6ff
--- /dev/null
+++ b/openstack/objectStorage/v1/containers/requests.go
@@ -0,0 +1,146 @@
+package containers
+
+import (
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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"`
+}
+
+// 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, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return pagination.Pager{Err: err}
+ }
+
+ if !opts.Full {
+ headers = map[string]string{"Accept": "text/plain", "Content-Type": "text/plain"}
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ p := ContainerPage{pagination.MarkerPageBase{LastHTTPResponse: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ url := accountURL(c) + query
+ pager := pagination.NewPager(c, url, createPage)
+ pager.Headers = headers
+ return pager
+}
+
+// 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()
+
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return container, err
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+
+ _, err = perigee.Request("PUT", containerURL(c, containerName), perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{201, 204},
+ })
+ if err == nil {
+ container = Container{Name: containerName}
+ }
+ return container, err
+}
+
+// Delete is a function that deletes a container.
+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, containerName string, opts UpdateOpts) error {
+ h := c.Provider.AuthenticatedHeaders()
+
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return err
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ for k, v := range opts.Metadata {
+ h["X-Container-Meta-"+k] = v
+ }
+
+ url := containerURL(c, containerName)
+ _, err = perigee.Request("POST", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{204},
+ })
+ return err
+}
+
+// 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, containerName string) GetResult {
+ var gr GetResult
+ resp, err := perigee.Request("HEAD", containerURL(c, containerName), perigee.Options{
+ MoreHeaders: c.Provider.AuthenticatedHeaders(),
+ OkCodes: []int{204},
+ })
+ gr.Err = err
+ gr.Resp = &resp.HttpResponse
+ return gr
+}
diff --git a/openstack/objectStorage/v1/containers/requests_test.go b/openstack/objectStorage/v1/containers/requests_test.go
new file mode 100644
index 0000000..1c9ee11
--- /dev/null
+++ b/openstack/objectStorage/v1/containers/requests_test.go
@@ -0,0 +1,198 @@
+package containers
+
+import (
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const (
+ tokenId = "abcabcabcabc"
+)
+
+var metadata = map[string]string{"gophercloud-test": "containers"}
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenId},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+func TestListContainerInfo(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/", 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().Add("Content-Type", "application/json")
+ r.ParseForm()
+ fmt.Printf("r: %+v\n", r)
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `[
+ {
+ "count": 0,
+ "bytes": 0,
+ "name": "janeausten"
+ },
+ {
+ "count": 1,
+ "bytes": 14,
+ "name": "marktwain"
+ }
+ ]`)
+ case "marktwain":
+ fmt.Fprintf(w, `[]`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ List(client, ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+ 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
+ })
+}
+
+func TestListContainerNames(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/", 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", "text/plain")
+
+ w.Header().Set("Content-Type", "text/plain")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, "janeausten\nmarktwain\n")
+ case "marktwain":
+ fmt.Fprintf(w, ``)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ List(client, ListOpts{Full: false}).EachPage(func(page pagination.Page) (bool, error) {
+ 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
+ })
+}
+
+func TestCreateContainer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PUT")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ _, err := Create(client, "testContainer", CreateOpts{})
+ if err != nil {
+ t.Fatalf("Unexpected error creating container: %v", err)
+ }
+}
+
+func TestDeleteContainer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ err := Delete(client, "testContainer")
+ if err != nil {
+ t.Fatalf("Unexpected error deleting container: %v", err)
+ }
+}
+
+func TestUpateContainer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ err := Update(client, "testContainer", UpdateOpts{})
+ if err != nil {
+ t.Fatalf("Unexpected error updating container metadata: %v", err)
+ }
+}
+
+func TestGetContainer(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "HEAD")
+ 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, "testContainer").ExtractMetadata()
+ if err != nil {
+ t.Fatalf("Unexpected error getting container metadata: %v", err)
+ }
+}
diff --git a/openstack/objectStorage/v1/containers/results.go b/openstack/objectStorage/v1/containers/results.go
new file mode 100644
index 0000000..5df99ed
--- /dev/null
+++ b/openstack/objectStorage/v1/containers/results.go
@@ -0,0 +1,142 @@
+package containers
+
+import (
+ "fmt"
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "net/http"
+ "strings"
+)
+
+type Container struct {
+ Bytes int `json:"bytes" mapstructure:"bytes"`
+ Count int `json:"count" mapstructure:"count"`
+ Name string `json:"name" mapstructure:"name"`
+}
+
+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 {
+ container := each.(map[string]interface{})
+ err := mapstructure.Decode(container, &results[index])
+ if err != nil {
+ return results, err
+ }
+ }
+ 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)
+ }
+ 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/objectStorage/v1/containers/urls.go b/openstack/objectStorage/v1/containers/urls.go
new file mode 100644
index 0000000..2a06f95
--- /dev/null
+++ b/openstack/objectStorage/v1/containers/urls.go
@@ -0,0 +1,13 @@
+package containers
+
+import "github.com/rackspace/gophercloud"
+
+// accountURL returns the URI used to list Containers.
+func accountURL(c *gophercloud.ServiceClient) string {
+ return c.Endpoint
+}
+
+// containerURL returns the URI for making Container requests.
+func containerURL(c *gophercloud.ServiceClient, container string) string {
+ return c.ServiceURL(container)
+}
diff --git a/openstack/objectStorage/v1/containers/urls_test.go b/openstack/objectStorage/v1/containers/urls_test.go
new file mode 100644
index 0000000..da37bf6
--- /dev/null
+++ b/openstack/objectStorage/v1/containers/urls_test.go
@@ -0,0 +1,29 @@
+package containers
+
+import (
+ "testing"
+ "github.com/rackspace/gophercloud"
+)
+
+func TestAccountURL(t *testing.T) {
+ client := gophercloud.ServiceClient{
+ Endpoint: "http://localhost:5000/v1/",
+ }
+ expected := "http://localhost:5000/v1/"
+ actual := accountURL(&client)
+ if actual != expected {
+ t.Errorf("Unexpected service URL generated: [%s]", actual)
+ }
+
+}
+
+func TestContainerURL(t *testing.T) {
+ client := gophercloud.ServiceClient{
+ Endpoint: "http://localhost:5000/v1/",
+ }
+ expected := "http://localhost:5000/v1/testContainer"
+ actual := containerURL(&client, "testContainer")
+ if actual != expected {
+ t.Errorf("Unexpected service URL generated: [%s]", actual)
+ }
+}
diff --git a/openstack/objectStorage/v1/objects/doc.go b/openstack/objectStorage/v1/objects/doc.go
new file mode 100644
index 0000000..2a7461b
--- /dev/null
+++ b/openstack/objectStorage/v1/objects/doc.go
@@ -0,0 +1,5 @@
+/* The objects package defines operations performed on an object-storage object.
+
+Reference: http://developer.openstack.org/api-ref-objectstorage-v1.html#storage_object_services
+*/
+package objects
diff --git a/openstack/objectStorage/v1/objects/requests.go b/openstack/objectStorage/v1/objects/requests.go
new file mode 100644
index 0000000..1622ff7
--- /dev/null
+++ b/openstack/objectStorage/v1/objects/requests.go
@@ -0,0 +1,276 @@
+package objects
+
+import (
+ "fmt"
+ "io"
+ "time"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// 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"`
+}
+
+// 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, containerName string, opts ListOpts) pagination.Pager {
+ var headers map[string]string
+
+ 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{"Accept": "text/plain", "Content-Type": "text/plain"}
+ }
+
+ createPage := func(r pagination.LastHTTPResponse) pagination.Page {
+ p := ObjectPage{pagination.MarkerPageBase{LastHTTPResponse: r}}
+ p.MarkerPageBase.Owner = p
+ return p
+ }
+
+ 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, containerName, objectName string, opts DownloadOpts) DownloadResult {
+ var dr DownloadResult
+
+ h := c.Provider.AuthenticatedHeaders()
+
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ dr.Err = err
+ return dr
+ }
+
+ for k, v := range headers {
+ h[k] = v
+ }
+
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ dr.Err = err
+ return dr
+ }
+
+ url := objectURL(c, containerName, objectName) + query
+ resp, err := perigee.Request("GET", url, perigee.Options{
+ MoreHeaders: h,
+ OkCodes: []int{200},
+ })
+ 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, containerName, objectName string, content io.Reader, opts CreateOpts) error {
+ var reqBody []byte
+
+ 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
+ }
+
+ query, err := gophercloud.BuildQueryString(opts)
+ if err != nil {
+ return err
+ }
+
+ if content != nil {
+ reqBody = make([]byte, 0)
+ _, err := content.Read(reqBody)
+ if err != nil {
+ return err
+ }
+ }
+
+ url := objectURL(c, containerName, objectName) + query
+ _, err = perigee.Request("PUT", url, perigee.Options{
+ ReqBody: reqBody,
+ MoreHeaders: h,
+ OkCodes: []int{201},
+ })
+ 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, containerName, objectName string, opts CopyOpts) error {
+ h := c.Provider.AuthenticatedHeaders()
+
+ headers, err := gophercloud.BuildHeaders(opts)
+ if err != nil {
+ return err
+ }
+ 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("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},
+ })
+ return err
+}
diff --git a/openstack/objectStorage/v1/objects/requests_test.go b/openstack/objectStorage/v1/objects/requests_test.go
new file mode 100644
index 0000000..0c61550
--- /dev/null
+++ b/openstack/objectStorage/v1/objects/requests_test.go
@@ -0,0 +1,224 @@
+package objects
+
+import (
+ "bytes"
+ "fmt"
+ "net/http"
+ "testing"
+
+ "github.com/rackspace/gophercloud"
+ "github.com/rackspace/gophercloud/pagination"
+ "github.com/rackspace/gophercloud/testhelper"
+)
+
+const (
+ tokenId = "abcabcabcabc"
+)
+
+var metadata = map[string]string{"Gophercloud-Test": "objects"}
+
+func serviceClient() *gophercloud.ServiceClient {
+ return &gophercloud.ServiceClient{
+ Provider: &gophercloud.ProviderClient{TokenID: tokenId},
+ Endpoint: testhelper.Endpoint(),
+ }
+}
+
+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()
+ 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) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "GET")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+
+ w.Header().Set("Content-Type", "application/json")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, `[{'hash': '451e372e48e0f6b1114fa0724aa79fa1','last_modified': '2009-11-10 23:00:00 +0000 UTC','bytes': 14,'name': 'goodbye','content_type': 'application/octet-stream'}]`)
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ List(client, "testContainer", ListOpts{Full: true}).EachPage(func(page pagination.Page) (bool, error) {
+ actual, err := ExtractInfo(page)
+ if err != nil {
+ t.Errorf("Failed to extract object info: %v", err)
+ return false, err
+ }
+
+ expected := []Object{
+ Object{
+ Hash: "451e372e48e0f6b1114fa0724aa79fa1",
+ LastModified: "2009-11-10 23:00:00 +0000 UTC",
+ Bytes: 14,
+ Name: "goodbye",
+ ContentType: "application/octet-stream",
+ },
+ }
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+}
+
+func TestListObjectNames(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ 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", "text/plain")
+
+ w.Header().Add("Content-Type", "text/plain")
+ r.ParseForm()
+ marker := r.Form.Get("marker")
+ switch marker {
+ case "":
+ fmt.Fprintf(w, "goodbye\n")
+ case "goodbye":
+ fmt.Fprintf(w, "")
+ default:
+ t.Fatalf("Unexpected marker: [%s]", marker)
+ }
+ })
+
+ client := serviceClient()
+ List(client, "testContainer", ListOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+ actual, err := ExtractNames(page)
+ if err != nil {
+ t.Errorf("Failed to extract object names: %v", err)
+ return false, err
+ }
+
+ expected := []string{"goodbye"}
+
+ testhelper.CheckDeepEquals(t, expected, actual)
+
+ return true, nil
+ })
+}
+
+func TestCreateObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "PUT")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ client := serviceClient()
+ 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)
+ }
+}
+
+func TestCopyObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "COPY")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "Destination", "/newTestContainer/newTestObject")
+ w.WriteHeader(http.StatusCreated)
+ })
+
+ client := serviceClient()
+ err := Copy(client, "testContainer", "testObject", CopyOpts{Destination: "/newTestContainer/newTestObject"})
+ if err != nil {
+ t.Fatalf("Unexpected error copying object: %v", err)
+ }
+}
+
+func TestDeleteObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "DELETE")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ err := Delete(client, "testContainer", "testObject", DeleteOpts{})
+ if err != nil {
+ t.Fatalf("Unexpected error deleting object: %v", err)
+ }
+}
+
+func TestUpateObjectMetadata(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "POST")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ testhelper.TestHeader(t, r, "X-Object-Meta-Gophercloud-Test", "objects")
+ w.WriteHeader(http.StatusAccepted)
+ })
+
+ client := serviceClient()
+ err := Update(client, "testContainer", "testObject", UpdateOpts{Metadata: metadata})
+ if err != nil {
+ t.Fatalf("Unexpected error updating object metadata: %v", err)
+ }
+}
+
+func TestGetObject(t *testing.T) {
+ testhelper.SetupHTTP()
+ defer testhelper.TeardownHTTP()
+
+ testhelper.Mux.HandleFunc("/testContainer/testObject", func(w http.ResponseWriter, r *http.Request) {
+ testhelper.TestMethod(t, r, "HEAD")
+ testhelper.TestHeader(t, r, "X-Auth-Token", tokenId)
+ testhelper.TestHeader(t, r, "Accept", "application/json")
+ w.Header().Add("X-Object-Meta-Gophercloud-Test", "objects")
+ w.WriteHeader(http.StatusNoContent)
+ })
+
+ client := serviceClient()
+ 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/objectStorage/v1/objects/results.go b/openstack/objectStorage/v1/objects/results.go
new file mode 100644
index 0000000..14435b9
--- /dev/null
+++ b/openstack/objectStorage/v1/objects/results.go
@@ -0,0 +1,138 @@
+package objects
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/rackspace/gophercloud/pagination"
+)
+
+// Object is a structure that holds information related to a storage object.
+type Object struct {
+ Bytes int `json:"bytes" mapstructure:"bytes"`
+ ContentType string `json:"content_type" mapstructure:"content_type"`
+ Hash string `json:"hash" mapstructure:"hash"`
+ LastModified string `json:"last_modified" mapstructure:"last_modified"`
+ Name string `json:"name" mapstructure:"name"`
+}
+
+// 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 {
+ object := each.(map[string]interface{})
+ err := mapstructure.Decode(object, &results[index])
+ if err != nil {
+ return results, err
+ }
+ }
+ 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)
+ }
+
+ 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
+ case strings.HasPrefix(ct, "text/html"):
+ return []string{}, 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
+}
diff --git a/openstack/objectStorage/v1/objects/urls.go b/openstack/objectStorage/v1/objects/urls.go
new file mode 100644
index 0000000..a377960
--- /dev/null
+++ b/openstack/objectStorage/v1/objects/urls.go
@@ -0,0 +1,13 @@
+package objects
+
+import "github.com/rackspace/gophercloud"
+
+// objectURL returns the URI for making Object requests.
+func objectURL(c *gophercloud.ServiceClient, container, object string) string {
+ return c.ServiceURL(container, object)
+}
+
+// containerURL returns the URI for making Container requests.
+func containerURL(c *gophercloud.ServiceClient, container string) string {
+ return c.ServiceURL(container)
+}
diff --git a/openstack/objectStorage/v1/objects/urls_test.go b/openstack/objectStorage/v1/objects/urls_test.go
new file mode 100644
index 0000000..89d1cb1
--- /dev/null
+++ b/openstack/objectStorage/v1/objects/urls_test.go
@@ -0,0 +1,28 @@
+package objects
+
+import (
+ "testing"
+ "github.com/rackspace/gophercloud"
+)
+
+func TestContainerURL(t *testing.T) {
+ client := gophercloud.ServiceClient{
+ Endpoint: "http://localhost:5000/v1/",
+ }
+ expected := "http://localhost:5000/v1/testContainer"
+ actual := containerURL(&client, "testContainer")
+ if actual != expected {
+ t.Errorf("Unexpected service URL generated: %s", actual)
+ }
+}
+
+func TestObjectURL(t *testing.T) {
+ client := gophercloud.ServiceClient{
+ Endpoint: "http://localhost:5000/v1/",
+ }
+ expected := "http://localhost:5000/v1/testContainer/testObject"
+ actual := objectURL(&client, "testContainer", "testObject")
+ if actual != expected {
+ t.Errorf("Unexpected service URL generated: %s", actual)
+ }
+}