Skip to content

Commit

Permalink
Swift: Add "bulk-delete" URL argument support (#1930)
Browse files Browse the repository at this point in the history
  • Loading branch information
kayrus committed Apr 19, 2020
1 parent 65804fe commit e547035
Show file tree
Hide file tree
Showing 14 changed files with 373 additions and 43 deletions.
37 changes: 37 additions & 0 deletions acceptance/openstack/objectstorage/v1/containers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,40 @@ func TestListAllContainers(t *testing.T) {
}
th.AssertEquals(t, numContainers, len(containerNamesList))
}

func TestBulkDeleteContainers(t *testing.T) {
client, err := clients.NewObjectStorageV1Client()
if err != nil {
t.Fatalf("Unable to create client: %v", err)
}

numContainers := 20

// Create a slice of random container names.
cNames := make([]string, numContainers)
for i := 0; i < numContainers; i++ {
cNames[i] = tools.RandomString("test&happy?-", 8)
}

// Create numContainers containers.
for i := 0; i < len(cNames); i++ {
res := containers.Create(client, cNames[i], nil)
th.AssertNoErr(t, res.Err)
}

expectedResp := containers.BulkDeleteResponse{
ResponseStatus: "200 OK",
Errors: [][]string{},
NumberDeleted: numContainers,
}

resp, err := containers.BulkDelete(client, cNames).Extract()
th.AssertNoErr(t, err)
tools.PrintResource(t, *resp)
th.AssertDeepEquals(t, *resp, expectedResp)

for _, c := range cNames {
_, err = containers.Get(client, c, nil).Extract()
th.AssertErr(t, err)
}
}
84 changes: 84 additions & 0 deletions acceptance/openstack/objectstorage/v1/objects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,87 @@ func TestObjectsListSubdir(t *testing.T) {
th.AssertEquals(t, allObjects[0], cSubdir2+"/")
t.Logf("%#v\n", allObjects)
}

func TestObjectsBulkDelete(t *testing.T) {
client, err := clients.NewObjectStorageV1Client()
if err != nil {
t.Fatalf("Unable to create client: %v", err)
}

// Create a random subdirectory name.
cSubdir1 := tools.RandomString("don't worry & be happy?-", 8)
cSubdir2 := tools.RandomString("don't worry & be happy?-", 8)

// Make a slice of length numObjects to hold the random object names.
oNames1 := make([]string, numObjects)
for i := 0; i < len(oNames1); i++ {
oNames1[i] = cSubdir1 + "/" + tools.RandomString("stranger?things-", 8)
}

oNames2 := make([]string, numObjects)
for i := 0; i < len(oNames2); i++ {
oNames2[i] = cSubdir2 + "/" + tools.RandomString("freddy's coming for you?-", 8)
}

// Create a container to hold the test objects.
cName := tools.RandomString("test&happy?-", 8)
_, err = containers.Create(client, cName, nil).Extract()
th.AssertNoErr(t, err)

// Defer deletion of the container until after testing.
defer func() {
t.Logf("Deleting container %s", cName)
res := containers.Delete(client, cName)
th.AssertNoErr(t, res.Err)
}()

// Create a slice of buffers to hold the test object content.
oContents1 := make([]*bytes.Buffer, numObjects)
for i := 0; i < numObjects; i++ {
oContents1[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10)))
createOpts := objects.CreateOpts{
Content: oContents1[i],
}
res := objects.Create(client, cName, oNames1[i], createOpts)
th.AssertNoErr(t, res.Err)
}

expectedResp := objects.BulkDeleteResponse{
ResponseStatus: "200 OK",
Errors: [][]string{},
NumberDeleted: numObjects * 2,
}

oContents2 := make([]*bytes.Buffer, numObjects)
for i := 0; i < numObjects; i++ {
oContents2[i] = bytes.NewBuffer([]byte(tools.RandomString("", 10)))
createOpts := objects.CreateOpts{
Content: oContents2[i],
}
res := objects.Create(client, cName, oNames2[i], createOpts)
th.AssertNoErr(t, res.Err)
}

// Delete the objects after testing.
resp, err := objects.BulkDelete(client, cName, append(oNames1, oNames2...)).Extract()
th.AssertNoErr(t, err)
th.AssertDeepEquals(t, *resp, expectedResp)

// Verify deletion
listOpts := objects.ListOpts{
Full: true,
Delimiter: "/",
}

allPages, err := objects.List(client, cName, listOpts).AllPages()
if err != nil {
t.Fatal(err)
}

allObjects, err := objects.ExtractNames(allPages)
if err != nil {
t.Fatal(err)
}

th.AssertEquals(t, len(allObjects), 0)
}
8 changes: 4 additions & 4 deletions openstack/objectstorage/v1/accounts/results.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ func (r *UpdateHeader) UnmarshalJSON(b []byte) error {
// Extract will return a struct of headers returned from a call to Get. To
// obtain a map of headers, call the Extract method on the GetResult.
func (r UpdateResult) Extract() (*UpdateHeader, error) {
var s *UpdateHeader
var s UpdateHeader
err := r.ExtractInto(&s)
return s, err
return &s, err
}

// GetHeader represents the headers returned in the response from a Get request.
Expand Down Expand Up @@ -157,9 +157,9 @@ type GetResult struct {

// Extract will return a struct of headers returned from a call to Get.
func (r GetResult) Extract() (*GetHeader, error) {
var s *GetHeader
var s GetHeader
err := r.ExtractInto(&s)
return s, err
return &s, err
}

// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
Expand Down
36 changes: 32 additions & 4 deletions openstack/objectstorage/v1/containers/requests.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package containers

import (
"net/url"
"strings"

"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/pagination"
)
Expand Down Expand Up @@ -104,7 +107,7 @@ func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsB
h[k] = v
}
}
resp, err := c.Request("PUT", createURL(c, containerName), &gophercloud.RequestOpts{
resp, err := c.Request("PUT", createURL(c, url.QueryEscape(containerName)), &gophercloud.RequestOpts{
MoreHeaders: h,
OkCodes: []int{201, 202, 204},
})
Expand All @@ -116,9 +119,34 @@ func Create(c *gophercloud.ServiceClient, containerName string, opts CreateOptsB
return
}

// BulkDelete is a function that bulk deletes containers.
func BulkDelete(c *gophercloud.ServiceClient, containers []string) (r BulkDeleteResult) {
// urlencode container names to be on the safe side
// https://github.com/openstack/swift/blob/stable/train/swift/common/middleware/bulk.py#L160
// https://github.com/openstack/swift/blob/stable/train/swift/common/swob.py#L302
encodedContainers := make([]string, len(containers))
for i, v := range containers {
encodedContainers[i] = url.QueryEscape(v)
}
resp, err := c.Request("POST", bulkDeleteURL(c), &gophercloud.RequestOpts{
RawBody: strings.NewReader(strings.Join(encodedContainers, "\n") + "\n"),
MoreHeaders: map[string]string{
"Accept": "application/json",
"Content-Type": "text/plain",
},
OkCodes: []int{200},
})
if resp != nil {
r.Header = resp.Header
r.Body = resp.Body
}
r.Err = err
return
}

// Delete is a function that deletes a container.
func Delete(c *gophercloud.ServiceClient, containerName string) (r DeleteResult) {
_, r.Err = c.Delete(deleteURL(c, containerName), nil)
_, r.Err = c.Delete(deleteURL(c, url.QueryEscape(containerName)), nil)
return
}

Expand Down Expand Up @@ -180,7 +208,7 @@ func Update(c *gophercloud.ServiceClient, containerName string, opts UpdateOptsB
h[k] = v
}
}
resp, err := c.Request("POST", updateURL(c, containerName), &gophercloud.RequestOpts{
resp, err := c.Request("POST", updateURL(c, url.QueryEscape(containerName)), &gophercloud.RequestOpts{
MoreHeaders: h,
OkCodes: []int{201, 202, 204},
})
Expand Down Expand Up @@ -223,7 +251,7 @@ func Get(c *gophercloud.ServiceClient, containerName string, opts GetOptsBuilder
h[k] = v
}
}
resp, err := c.Head(getURL(c, containerName), &gophercloud.RequestOpts{
resp, err := c.Head(getURL(c, url.QueryEscape(containerName)), &gophercloud.RequestOpts{
MoreHeaders: h,
OkCodes: []int{200, 204},
})
Expand Down
40 changes: 31 additions & 9 deletions openstack/objectstorage/v1/containers/results.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,9 @@ type GetResult struct {

// Extract will return a struct of headers returned from a call to Get.
func (r GetResult) Extract() (*GetHeader, error) {
var s *GetHeader
var s GetHeader
err := r.ExtractInto(&s)
return s, err
return &s, err
}

// ExtractMetadata is a function that takes a GetResult (of type *http.Response)
Expand Down Expand Up @@ -238,9 +238,9 @@ type CreateResult struct {
// Extract will return a struct of headers returned from a call to Create.
// To extract the headers from the HTTP response, call its Extract method.
func (r CreateResult) Extract() (*CreateHeader, error) {
var s *CreateHeader
var s CreateHeader
err := r.ExtractInto(&s)
return s, err
return &s, err
}

// UpdateHeader represents the headers returned in the response from a Update
Expand Down Expand Up @@ -289,9 +289,9 @@ type UpdateResult struct {

// Extract will return a struct of headers returned from a call to Update.
func (r UpdateResult) Extract() (*UpdateHeader, error) {
var s *UpdateHeader
var s UpdateHeader
err := r.ExtractInto(&s)
return s, err
return &s, err
}

// DeleteHeader represents the headers returned in the response from a Delete
Expand Down Expand Up @@ -333,14 +333,36 @@ func (r *DeleteHeader) UnmarshalJSON(b []byte) error {
}

// DeleteResult represents the result of a delete operation. To extract the
// the headers from the HTTP response, call its Extract method.
// headers from the HTTP response, call its Extract method.
type DeleteResult struct {
gophercloud.HeaderResult
}

// Extract will return a struct of headers returned from a call to Delete.
func (r DeleteResult) Extract() (*DeleteHeader, error) {
var s *DeleteHeader
var s DeleteHeader
err := r.ExtractInto(&s)
return s, err
return &s, err
}

type BulkDeleteResponse struct {
ResponseStatus string `json:"Response Status"`
ResponseBody string `json:"Response Body"`
Errors [][]string `json:"Errors"`
NumberDeleted int `json:"Number Deleted"`
NumberNotFound int `json:"Number Not Found"`
}

// BulkDeleteResult represents the result of a bulk delete operation. To extract
// the response object from the HTTP response, call its Extract method.
type BulkDeleteResult struct {
gophercloud.Result
}

// Extract will return a BulkDeleteResponse struct returned from a BulkDelete
// call.
func (r BulkDeleteResult) Extract() (*BulkDeleteResponse, error) {
var s BulkDeleteResponse
err := r.ExtractInto(&s)
return &s, err
}
29 changes: 29 additions & 0 deletions openstack/objectstorage/v1/containers/testing/fixtures.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,35 @@ func HandleDeleteContainerSuccessfully(t *testing.T) {
})
}

const bulkDeleteResponse = `
{
"Response Status": "foo",
"Response Body": "bar",
"Errors": [],
"Number Deleted": 2,
"Number Not Found": 0
}
`

// HandleBulkDeleteSuccessfully creates an HTTP handler at `/` on the test
// handler mux that responds with a `Delete` response.
func HandleBulkDeleteSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestHeader(t, r, "Accept", "application/json")
th.TestHeader(t, r, "Content-Type", "text/plain")
th.TestFormValues(t, r, map[string]string{
"bulk-delete": "true",
})
th.TestBody(t, r, "testContainer1\ntestContainer2\n")

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, bulkDeleteResponse)
})
}

// HandleUpdateContainerSuccessfully creates an HTTP handler at `/testContainer` on the test handler mux that
// responds with a `Update` response.
func HandleUpdateContainerSuccessfully(t *testing.T) {
Expand Down
27 changes: 22 additions & 5 deletions openstack/objectstorage/v1/containers/testing/requests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func TestCreateContainer(t *testing.T) {
TransID: "tx554ed59667a64c61866f1-0058b4ba37",
}
actual, err := res.Extract()
th.CheckNoErr(t, err)
th.AssertNoErr(t, err)
th.AssertDeepEquals(t, expected, actual)
}

Expand All @@ -105,7 +105,24 @@ func TestDeleteContainer(t *testing.T) {
HandleDeleteContainerSuccessfully(t)

res := containers.Delete(fake.ServiceClient(), "testContainer")
th.CheckNoErr(t, res.Err)
th.AssertNoErr(t, res.Err)
}

func TestBulkDelete(t *testing.T) {
th.SetupHTTP()
defer th.TeardownHTTP()
HandleBulkDeleteSuccessfully(t)

expected := containers.BulkDeleteResponse{
ResponseStatus: "foo",
ResponseBody: "bar",
NumberDeleted: 2,
Errors: [][]string{},
}

resp, err := containers.BulkDelete(fake.ServiceClient(), []string{"testContainer1", "testContainer2"}).Extract()
th.AssertNoErr(t, err)
th.AssertDeepEquals(t, expected, *resp)
}

func TestUpdateContainer(t *testing.T) {
Expand All @@ -115,7 +132,7 @@ func TestUpdateContainer(t *testing.T) {

options := &containers.UpdateOpts{Metadata: map[string]string{"foo": "bar"}}
res := containers.Update(fake.ServiceClient(), "testContainer", options)
th.CheckNoErr(t, res.Err)
th.AssertNoErr(t, res.Err)
}

func TestGetContainer(t *testing.T) {
Expand All @@ -128,7 +145,7 @@ func TestGetContainer(t *testing.T) {
}
res := containers.Get(fake.ServiceClient(), "testContainer", getOpts)
_, err := res.ExtractMetadata()
th.CheckNoErr(t, err)
th.AssertNoErr(t, err)

expected := &containers.GetHeader{
AcceptRanges: "bytes",
Expand All @@ -142,6 +159,6 @@ func TestGetContainer(t *testing.T) {
StoragePolicy: "test_policy",
}
actual, err := res.Extract()
th.CheckNoErr(t, err)
th.AssertNoErr(t, err)
th.AssertDeepEquals(t, expected, actual)
}
Loading

0 comments on commit e547035

Please sign in to comment.