Merge lp:~garethwatts/goamz/goamz into lp:goamz

Proposed by Gareth Watts
Status: Needs review
Proposed branch: lp:~garethwatts/goamz/goamz
Merge into: lp:goamz
Diff against target: 269 lines (+172/-9)
4 files modified
s3/responses_test.go (+14/-0)
s3/s3.go (+112/-9)
s3/s3_test.go (+36/-0)
s3/sign.go (+10/-0)
To merge this branch: bzr merge lp:~garethwatts/goamz/goamz
Reviewer Review Type Date Requested Status
goamz maintainers Pending
Review via email: mp+196410@code.launchpad.net

Description of the change

Adds support for deleting up to 1,000 objects from S3 per request using S3's Delete Multiple Objects interface (http://goo.gl/W5ICVJ )

I used this to selectively delete over 4 billion objects from S3 last night fwiw

To post a comment you must log in.

Unmerged revisions

44. By Gareth Watts

Add s3.DelMulti() to delete multiple objects in one request

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 's3/responses_test.go'
--- s3/responses_test.go 2013-02-12 04:50:11 +0000
+++ s3/responses_test.go 2013-11-23 00:42:56 +0000
@@ -196,3 +196,17 @@
196 <HostId>kjhwqk</HostId>196 <HostId>kjhwqk</HostId>
197</Error>197</Error>
198`198`
199
200var DelMultiResultDump = `
201<?xml version="1.0" encoding="UTF-8"?>
202<DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
203 <Deleted>
204 <Key>Nelson</Key>
205 </Deleted>
206 <Error>
207 <Key>Neo</Key>
208 <Code>AccessDenied</Code>
209 <Message>Access Denied</Message>
210 </Error>
211</DeleteResult>
212`
199213
=== modified file 's3/s3.go'
--- s3/s3.go 2013-08-15 15:33:14 +0000
+++ s3/s3.go 2013-11-23 00:42:56 +0000
@@ -220,6 +220,97 @@
220 return b.S3.query(req, nil)220 return b.S3.query(req, nil)
221}221}
222222
223// The DelObject type holds the key name and optional version identifier of an object
224// to be deleted from S3.
225type DelObject struct {
226 Key string
227 VersionId string `xml:",omitempty"`
228}
229
230// The DelErr type holds the deletion error for a specific object returned as part of
231// the DelResp response from DelMulti.
232type DelErr struct {
233 Key string
234 VersionId string
235 Code string
236 Message string
237}
238
239// The DelSuccess type holds details of a successful object deletion fom S3 as part of
240// the DelResp response from DelMulti.
241type DelSuccess struct {
242 Key string
243 VersionId string
244 DeleteMarker bool
245 DeleteMarkerVersionId string
246}
247
248// The DelResp type holds the result of a DelMulti operation.
249type DelResp struct {
250 // All objects that were successfully deleted.
251 // Note this will always be empty if DelMulti is called with quiet=True.
252 Deleted []DelSuccess
253 // All objects that failed to be deleted.
254 Error []DelErr
255}
256
257type delRequest struct {
258 XMLName xml.Name `xml:"Delete"`
259 Quiet bool
260 Object []DelObject
261}
262
263// DelMultiLimit defines the maximum number of objects S3 supports for a DelMulti operation.
264const DelMultiLimit = 1000
265
266// DelMulti deletes up to 1,000 objects from the S3 bucket.
267//
268// Set quiet to true to return only errors in the result from S3.
269//
270// See http://goo.gl/W5ICVJ for details.
271func (b *Bucket) DelMulti(objects []DelObject, quiet bool) (result *DelResp, err error) {
272 if len(objects) > DelMultiLimit {
273 // this is a programming error really; perhaps it should be a panic.
274 return nil, fmt.Errorf("attempted to delete %d objects. maximum is %d.", len(objects), DelMultiLimit)
275 }
276 req := &delRequest{Object: objects, Quiet: quiet}
277
278 reqXml, err := xml.Marshal(req)
279 if err != nil {
280 return nil, err
281 }
282
283 reqXml = append([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"), reqXml...)
284
285 headers := http.Header{
286 "Content-Type": []string{"text/xml"},
287 }
288
289 params := map[string][]string{
290 "delete": {""},
291 }
292
293 result = &DelResp{}
294 for attempt := attempts.Start(); attempt.Next(); {
295 req := &request{
296 method: "POST",
297 bucket: b.Name,
298 path: "/",
299 params: params,
300 headers: headers,
301 signpayload: reqXml,
302 }
303 err = b.S3.query(req, result)
304 if !shouldRetry(err) {
305 break
306 }
307 }
308 if err != nil {
309 return nil, err
310 }
311 return result, nil
312}
313
223// The ListResp type holds the results of a List bucket operation.314// The ListResp type holds the results of a List bucket operation.
224type ListResp struct {315type ListResp struct {
225 Name string316 Name string
@@ -370,15 +461,16 @@
370}461}
371462
372type request struct {463type request struct {
373 method string464 method string
374 bucket string465 bucket string
375 path string466 path string
376 signpath string467 signpath string
377 params url.Values468 params url.Values
378 headers http.Header469 headers http.Header
379 baseurl string470 baseurl string
380 payload io.Reader471 payload io.Reader
381 prepared bool472 signpayload []byte // use instead of payload to auto-set Content-MD5
473 prepared bool
382}474}
383475
384func (req *request) url() (*url.URL, error) {476func (req *request) url() (*url.URL, error) {
@@ -428,6 +520,10 @@
428 }520 }
429 req.params = params521 req.params = params
430 req.headers = headers522 req.headers = headers
523 if req.signpayload != nil {
524 // Add Content-MD5 and Content-Length headers
525 signBody(req.signpayload, headers)
526 }
431 if !strings.HasPrefix(req.path, "/") {527 if !strings.HasPrefix(req.path, "/") {
432 req.path = "/" + req.path528 req.path = "/" + req.path
433 }529 }
@@ -485,10 +581,17 @@
485 hreq.ContentLength, _ = strconv.ParseInt(v[0], 10, 64)581 hreq.ContentLength, _ = strconv.ParseInt(v[0], 10, 64)
486 delete(req.headers, "Content-Length")582 delete(req.headers, "Content-Length")
487 }583 }
584 if req.signpayload != nil {
585 req.payload = bytes.NewBuffer(req.signpayload)
586 }
488 if req.payload != nil {587 if req.payload != nil {
489 hreq.Body = ioutil.NopCloser(req.payload)588 hreq.Body = ioutil.NopCloser(req.payload)
490 }589 }
491590
591 if debug {
592 dump, _ := httputil.DumpRequestOut(&hreq, true)
593 log.Printf("{ <- %s\n", dump)
594 }
492 hresp, err := http.DefaultClient.Do(&hreq)595 hresp, err := http.DefaultClient.Do(&hreq)
493 if err != nil {596 if err != nil {
494 return nil, err597 return nil, err
495598
=== modified file 's3/s3_test.go'
--- s3/s3_test.go 2013-08-15 13:18:02 +0000
+++ s3/s3_test.go 2013-11-23 00:42:56 +0000
@@ -4,6 +4,7 @@
4 "bytes"4 "bytes"
5 "io/ioutil"5 "io/ioutil"
6 "net/http"6 "net/http"
7 "strconv"
7 "testing"8 "testing"
89
9 "launchpad.net/goamz/aws"10 "launchpad.net/goamz/aws"
@@ -206,6 +207,41 @@
206 c.Assert(req.Header["Date"], Not(Equals), "")207 c.Assert(req.Header["Date"], Not(Equals), "")
207}208}
208209
210// Delete Multiple Objects docs: http://goo.gl/W5ICVJ
211
212func (s *S) TestDelMultiObjects(c *C) {
213 testServer.Response(200, nil, DelMultiResultDump)
214
215 b := s.s3.Bucket("quotes")
216
217 entries := []s3.DelObject{
218 s3.DelObject{"Nelson", ""},
219 s3.DelObject{"Neo", "12345"},
220 }
221 data, err := b.DelMulti(entries, false)
222 c.Assert(err, IsNil)
223 c.Assert(data, NotNil)
224
225 req := testServer.WaitRequest()
226 expected := `<?xml version="1.0" encoding="UTF-8"?>
227<Delete><Quiet>false</Quiet><Object><Key>Nelson</Key></Object><Object><Key>Neo</Key><VersionId>12345</VersionId></Object></Delete>`
228
229 c.Assert(req.Method, Equals, "POST")
230 c.Assert(req.URL.Path, Equals, "/quotes/")
231 c.Assert(req.URL.RawQuery, Equals, "delete=")
232 c.Assert(req.Header["Date"], Not(Equals), "")
233 c.Assert(req.Header["Content-MD5"], Not(Equals), "")
234 c.Assert(req.Header["Content-Length"], DeepEquals, []string{strconv.Itoa(len(expected))})
235
236 reqbody, err := ioutil.ReadAll(req.Body)
237 c.Assert(string(reqbody), Equals, expected)
238
239 c.Assert(data.Deleted, HasLen, 1)
240 c.Assert(data.Error, HasLen, 1)
241 c.Assert(data.Deleted[0], DeepEquals, s3.DelSuccess{"Nelson", "", false, ""})
242 c.Assert(data.Error[0], DeepEquals, s3.DelErr{"Neo", "", "AccessDenied", "Access Denied"})
243}
244
209// Bucket List Objects docs: http://goo.gl/YjQTc245// Bucket List Objects docs: http://goo.gl/YjQTc
210246
211func (s *S) TestList(c *C) {247func (s *S) TestList(c *C) {
212248
=== modified file 's3/sign.go'
--- s3/sign.go 2012-07-19 13:51:13 +0000
+++ s3/sign.go 2013-11-23 00:42:56 +0000
@@ -2,11 +2,13 @@
22
3import (3import (
4 "crypto/hmac"4 "crypto/hmac"
5 "crypto/md5"
5 "crypto/sha1"6 "crypto/sha1"
6 "encoding/base64"7 "encoding/base64"
7 "launchpad.net/goamz/aws"8 "launchpad.net/goamz/aws"
8 "log"9 "log"
9 "sort"10 "sort"
11 "strconv"
10 "strings"12 "strings"
11)13)
1214
@@ -17,6 +19,7 @@
1719
18var s3ParamsToSign = map[string]bool{20var s3ParamsToSign = map[string]bool{
19 "acl": true,21 "acl": true,
22 "delete": true,
20 "location": true,23 "location": true,
21 "logging": true,24 "logging": true,
22 "notification": true,25 "notification": true,
@@ -110,3 +113,10 @@
110 log.Printf("Signature: %q", signature)113 log.Printf("Signature: %q", signature)
111 }114 }
112}115}
116
117func signBody(body []byte, headers map[string][]string) {
118 digest := md5.New()
119 digest.Write(body)
120 headers["Content-MD5"] = []string{base64.StdEncoding.EncodeToString(digest.Sum(nil))}
121 headers["Content-Length"] = []string{strconv.Itoa(len(body))}
122}

Subscribers

People subscribed via source and target branches