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
1=== modified file 's3/responses_test.go'
2--- s3/responses_test.go 2013-02-12 04:50:11 +0000
3+++ s3/responses_test.go 2013-11-23 00:42:56 +0000
4@@ -196,3 +196,17 @@
5 <HostId>kjhwqk</HostId>
6 </Error>
7 `
8+
9+var DelMultiResultDump = `
10+<?xml version="1.0" encoding="UTF-8"?>
11+<DeleteResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
12+ <Deleted>
13+ <Key>Nelson</Key>
14+ </Deleted>
15+ <Error>
16+ <Key>Neo</Key>
17+ <Code>AccessDenied</Code>
18+ <Message>Access Denied</Message>
19+ </Error>
20+</DeleteResult>
21+`
22
23=== modified file 's3/s3.go'
24--- s3/s3.go 2013-08-15 15:33:14 +0000
25+++ s3/s3.go 2013-11-23 00:42:56 +0000
26@@ -220,6 +220,97 @@
27 return b.S3.query(req, nil)
28 }
29
30+// The DelObject type holds the key name and optional version identifier of an object
31+// to be deleted from S3.
32+type DelObject struct {
33+ Key string
34+ VersionId string `xml:",omitempty"`
35+}
36+
37+// The DelErr type holds the deletion error for a specific object returned as part of
38+// the DelResp response from DelMulti.
39+type DelErr struct {
40+ Key string
41+ VersionId string
42+ Code string
43+ Message string
44+}
45+
46+// The DelSuccess type holds details of a successful object deletion fom S3 as part of
47+// the DelResp response from DelMulti.
48+type DelSuccess struct {
49+ Key string
50+ VersionId string
51+ DeleteMarker bool
52+ DeleteMarkerVersionId string
53+}
54+
55+// The DelResp type holds the result of a DelMulti operation.
56+type DelResp struct {
57+ // All objects that were successfully deleted.
58+ // Note this will always be empty if DelMulti is called with quiet=True.
59+ Deleted []DelSuccess
60+ // All objects that failed to be deleted.
61+ Error []DelErr
62+}
63+
64+type delRequest struct {
65+ XMLName xml.Name `xml:"Delete"`
66+ Quiet bool
67+ Object []DelObject
68+}
69+
70+// DelMultiLimit defines the maximum number of objects S3 supports for a DelMulti operation.
71+const DelMultiLimit = 1000
72+
73+// DelMulti deletes up to 1,000 objects from the S3 bucket.
74+//
75+// Set quiet to true to return only errors in the result from S3.
76+//
77+// See http://goo.gl/W5ICVJ for details.
78+func (b *Bucket) DelMulti(objects []DelObject, quiet bool) (result *DelResp, err error) {
79+ if len(objects) > DelMultiLimit {
80+ // this is a programming error really; perhaps it should be a panic.
81+ return nil, fmt.Errorf("attempted to delete %d objects. maximum is %d.", len(objects), DelMultiLimit)
82+ }
83+ req := &delRequest{Object: objects, Quiet: quiet}
84+
85+ reqXml, err := xml.Marshal(req)
86+ if err != nil {
87+ return nil, err
88+ }
89+
90+ reqXml = append([]byte("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"), reqXml...)
91+
92+ headers := http.Header{
93+ "Content-Type": []string{"text/xml"},
94+ }
95+
96+ params := map[string][]string{
97+ "delete": {""},
98+ }
99+
100+ result = &DelResp{}
101+ for attempt := attempts.Start(); attempt.Next(); {
102+ req := &request{
103+ method: "POST",
104+ bucket: b.Name,
105+ path: "/",
106+ params: params,
107+ headers: headers,
108+ signpayload: reqXml,
109+ }
110+ err = b.S3.query(req, result)
111+ if !shouldRetry(err) {
112+ break
113+ }
114+ }
115+ if err != nil {
116+ return nil, err
117+ }
118+ return result, nil
119+}
120+
121 // The ListResp type holds the results of a List bucket operation.
122 type ListResp struct {
123 Name string
124@@ -370,15 +461,16 @@
125 }
126
127 type request struct {
128- method string
129- bucket string
130- path string
131- signpath string
132- params url.Values
133- headers http.Header
134- baseurl string
135- payload io.Reader
136- prepared bool
137+ method string
138+ bucket string
139+ path string
140+ signpath string
141+ params url.Values
142+ headers http.Header
143+ baseurl string
144+ payload io.Reader
145+ signpayload []byte // use instead of payload to auto-set Content-MD5
146+ prepared bool
147 }
148
149 func (req *request) url() (*url.URL, error) {
150@@ -428,6 +520,10 @@
151 }
152 req.params = params
153 req.headers = headers
154+ if req.signpayload != nil {
155+ // Add Content-MD5 and Content-Length headers
156+ signBody(req.signpayload, headers)
157+ }
158 if !strings.HasPrefix(req.path, "/") {
159 req.path = "/" + req.path
160 }
161@@ -485,10 +581,17 @@
162 hreq.ContentLength, _ = strconv.ParseInt(v[0], 10, 64)
163 delete(req.headers, "Content-Length")
164 }
165+ if req.signpayload != nil {
166+ req.payload = bytes.NewBuffer(req.signpayload)
167+ }
168 if req.payload != nil {
169 hreq.Body = ioutil.NopCloser(req.payload)
170 }
171
172+ if debug {
173+ dump, _ := httputil.DumpRequestOut(&hreq, true)
174+ log.Printf("{ <- %s\n", dump)
175+ }
176 hresp, err := http.DefaultClient.Do(&hreq)
177 if err != nil {
178 return nil, err
179
180=== modified file 's3/s3_test.go'
181--- s3/s3_test.go 2013-08-15 13:18:02 +0000
182+++ s3/s3_test.go 2013-11-23 00:42:56 +0000
183@@ -4,6 +4,7 @@
184 "bytes"
185 "io/ioutil"
186 "net/http"
187+ "strconv"
188 "testing"
189
190 "launchpad.net/goamz/aws"
191@@ -206,6 +207,41 @@
192 c.Assert(req.Header["Date"], Not(Equals), "")
193 }
194
195+// Delete Multiple Objects docs: http://goo.gl/W5ICVJ
196+
197+func (s *S) TestDelMultiObjects(c *C) {
198+ testServer.Response(200, nil, DelMultiResultDump)
199+
200+ b := s.s3.Bucket("quotes")
201+
202+ entries := []s3.DelObject{
203+ s3.DelObject{"Nelson", ""},
204+ s3.DelObject{"Neo", "12345"},
205+ }
206+ data, err := b.DelMulti(entries, false)
207+ c.Assert(err, IsNil)
208+ c.Assert(data, NotNil)
209+
210+ req := testServer.WaitRequest()
211+ expected := `<?xml version="1.0" encoding="UTF-8"?>
212+<Delete><Quiet>false</Quiet><Object><Key>Nelson</Key></Object><Object><Key>Neo</Key><VersionId>12345</VersionId></Object></Delete>`
213+
214+ c.Assert(req.Method, Equals, "POST")
215+ c.Assert(req.URL.Path, Equals, "/quotes/")
216+ c.Assert(req.URL.RawQuery, Equals, "delete=")
217+ c.Assert(req.Header["Date"], Not(Equals), "")
218+ c.Assert(req.Header["Content-MD5"], Not(Equals), "")
219+ c.Assert(req.Header["Content-Length"], DeepEquals, []string{strconv.Itoa(len(expected))})
220+
221+ reqbody, err := ioutil.ReadAll(req.Body)
222+ c.Assert(string(reqbody), Equals, expected)
223+
224+ c.Assert(data.Deleted, HasLen, 1)
225+ c.Assert(data.Error, HasLen, 1)
226+ c.Assert(data.Deleted[0], DeepEquals, s3.DelSuccess{"Nelson", "", false, ""})
227+ c.Assert(data.Error[0], DeepEquals, s3.DelErr{"Neo", "", "AccessDenied", "Access Denied"})
228+}
229+
230 // Bucket List Objects docs: http://goo.gl/YjQTc
231
232 func (s *S) TestList(c *C) {
233
234=== modified file 's3/sign.go'
235--- s3/sign.go 2012-07-19 13:51:13 +0000
236+++ s3/sign.go 2013-11-23 00:42:56 +0000
237@@ -2,11 +2,13 @@
238
239 import (
240 "crypto/hmac"
241+ "crypto/md5"
242 "crypto/sha1"
243 "encoding/base64"
244 "launchpad.net/goamz/aws"
245 "log"
246 "sort"
247+ "strconv"
248 "strings"
249 )
250
251@@ -17,6 +19,7 @@
252
253 var s3ParamsToSign = map[string]bool{
254 "acl": true,
255+ "delete": true,
256 "location": true,
257 "logging": true,
258 "notification": true,
259@@ -110,3 +113,10 @@
260 log.Printf("Signature: %q", signature)
261 }
262 }
263+
264+func signBody(body []byte, headers map[string][]string) {
265+ digest := md5.New()
266+ digest.Write(body)
267+ headers["Content-MD5"] = []string{base64.StdEncoding.EncodeToString(digest.Sum(nil))}
268+ headers["Content-Length"] = []string{strconv.Itoa(len(body))}
269+}

Subscribers

People subscribed via source and target branches