Merge lp:~nick-craig-wood/goamz/list-buckets into lp:~gophers/goamz/trunk

Proposed by Nick Craig-Wood
Status: Needs review
Proposed branch: lp:~nick-craig-wood/goamz/list-buckets
Merge into: lp:~gophers/goamz/trunk
Diff against target: 429 lines (+173/-34)
12 files modified
aws/aws.go (+7/-1)
aws/aws_test.go (+9/-0)
ec2/ec2_test.go (+4/-0)
exp/mturk/mturk_test.go (+4/-0)
exp/sdb/sdb_test.go (+4/-0)
exp/sns/sns_test.go (+4/-0)
iam/iam_test.go (+6/-2)
s3/multi.go (+3/-2)
s3/responses_test.go (+19/-0)
s3/s3.go (+69/-27)
s3/s3_test.go (+30/-0)
testutil/http.go (+14/-2)
To merge this branch: bzr merge lp:~nick-craig-wood/goamz/list-buckets
Reviewer Review Type Date Requested Status
The Go Language Gophers Pending
Review via email: mp+146925@code.launchpad.net

Description of the change

s3: Implement ListBuckets method

https://codereview.appspot.com/7305051/

To post a comment you must log in.
Revision history for this message
Gustavo Niemeyer (niemeyer) wrote :

Looking good, thanks for this. Just a few trivial details to sort out.

https://codereview.appspot.com/7305051/diff/1/s3/s3.go
File s3/s3.go (right):

https://codereview.appspot.com/7305051/diff/1/s3/s3.go#newcode43
s3/s3.go:43: // The following fields are only set on Buckets returned
from the ListBuckets call
s/Buckets/buckets/

https://codereview.appspot.com/7305051/diff/1/s3/s3.go#newcode45
s3/s3.go:45: CreationDate string // Date the bucket was created, e.g.,
2009-02-03T16:45:09.000Z
Seems like this should be a native time.Time.

https://codereview.appspot.com/7305051/diff/1/s3/s3.go#newcode74
s3/s3.go:74: type BucketInfo struct {
s/BucketInfo/bucketInfo/

This is used only internally, so doesn't have to be exposed in the
public API.

https://codereview.appspot.com/7305051/diff/1/s3/s3.go#newcode80
s3/s3.go:80: type Buckets struct {
s/Buckets/bucketInfoResp/, for the same reason.

The doc also needs fixing.

https://codereview.appspot.com/7305051/diff/1/s3/s3.go#newcode444
s3/s3.go:444: if req.baseurl == "" {
That logic is changed in trunk, both due to retries and to fix a bug.
Trunk needs to be merged back so we can see what the actual change
needed is here.

https://codereview.appspot.com/7305051/

Revision history for this message
Nick Craig-Wood (nick-craig-wood) wrote :

Arg, I've messed something up - probably this branch!

bzr diff -c38 | diffstat
 responses_test.go | 19 +++++++++++++++
 s3.go | 68 ++++++++++++++++++++++++++++++++++++++++++------------
 s3_test.go | 29 +++++++++++++++++++++++
 3 files changed, 102 insertions(+), 14 deletions(-)

Will try a new branch

Unmerged revisions

38. By Nick Craig-Wood

Implement S3.List to list all Buckets

37. By Ian Booth

Support EC2_ env variables

Allow the user to specify their auth credentials using EC2_ style
environment variables. I ran go fmt and it also made some changes.

The tests failed to run due to a test http server not being closed when
each test suite finished. I added a fix for this also, so now all tests
pass when go test ./... is run from the project's root directory.

R=jameinel, Danilo
CC=
https://codereview.appspot.com/9545045

36. By Gustavo Niemeyer

s3: close http response Body properly

The interface of the run method was error prone.

R=dfc, anacrolix
CC=
https://codereview.appspot.com/9611047

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'aws/aws.go'
2--- aws/aws.go 2012-11-13 23:59:04 +0000
3+++ aws/aws.go 2013-06-27 11:56:25 +0000
4@@ -162,10 +162,16 @@
5
6 // EnvAuth creates an Auth based on environment information.
7 // The AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment
8-// variables are used.
9+// variables are used as the first preference, but EC2_ACCESS_KEY
10+// and EC2_SECRET_KEY environment variables are also supported.
11 func EnvAuth() (auth Auth, err error) {
12 auth.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID")
13 auth.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY")
14+ // We fallback to EC2_ env variables if the AWS_ variants are not used.
15+ if auth.AccessKey == "" && auth.SecretKey == "" {
16+ auth.AccessKey = os.Getenv("EC2_ACCESS_KEY")
17+ auth.SecretKey = os.Getenv("EC2_SECRET_KEY")
18+ }
19 if auth.AccessKey == "" {
20 err = errors.New("AWS_ACCESS_KEY_ID not found in environment")
21 }
22
23=== modified file 'aws/aws_test.go'
24--- aws/aws_test.go 2013-01-31 14:52:05 +0000
25+++ aws/aws_test.go 2013-06-27 11:56:25 +0000
26@@ -52,6 +52,15 @@
27 c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
28 }
29
30+func (s *S) TestEnvAuthLegacy(c *C) {
31+ os.Clearenv()
32+ os.Setenv("EC2_SECRET_KEY", "secret")
33+ os.Setenv("EC2_ACCESS_KEY", "access")
34+ auth, err := aws.EnvAuth()
35+ c.Assert(err, IsNil)
36+ c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
37+}
38+
39 func (s *S) TestEncode(c *C) {
40 c.Assert(aws.Encode("foo"), Equals, "foo")
41 c.Assert(aws.Encode("/"), Equals, "%2F")
42
43=== modified file 'ec2/ec2_test.go'
44--- ec2/ec2_test.go 2013-01-31 14:52:05 +0000
45+++ ec2/ec2_test.go 2013-06-27 11:56:25 +0000
46@@ -26,6 +26,10 @@
47 s.ec2 = ec2.New(auth, aws.Region{EC2Endpoint: testServer.URL})
48 }
49
50+func (s *S) TearDownSuite(c *C) {
51+ testServer.Stop()
52+}
53+
54 func (s *S) TearDownTest(c *C) {
55 testServer.Flush()
56 }
57
58=== modified file 'exp/mturk/mturk_test.go'
59--- exp/mturk/mturk_test.go 2013-01-31 14:52:05 +0000
60+++ exp/mturk/mturk_test.go 2013-06-27 11:56:25 +0000
61@@ -35,6 +35,10 @@
62 }
63 }
64
65+func (s *S) TearDownSuite(c *C) {
66+ testServer.Stop()
67+}
68+
69 func (s *S) TearDownTest(c *C) {
70 testServer.Flush()
71 }
72
73=== modified file 'exp/sdb/sdb_test.go'
74--- exp/sdb/sdb_test.go 2013-01-31 14:52:05 +0000
75+++ exp/sdb/sdb_test.go 2013-06-27 11:56:25 +0000
76@@ -26,6 +26,10 @@
77 s.sdb = sdb.New(auth, aws.Region{SDBEndpoint: testServer.URL})
78 }
79
80+func (s *S) TearDownSuite(c *C) {
81+ testServer.Stop()
82+}
83+
84 func (s *S) TearDownTest(c *C) {
85 testServer.Flush()
86 }
87
88=== modified file 'exp/sns/sns_test.go'
89--- exp/sns/sns_test.go 2013-01-31 14:52:05 +0000
90+++ exp/sns/sns_test.go 2013-06-27 11:56:25 +0000
91@@ -26,6 +26,10 @@
92 s.sns = sns.New(auth, aws.Region{SNSEndpoint: testServer.URL})
93 }
94
95+func (s *S) TearDownSuite(c *C) {
96+ testServer.Stop()
97+}
98+
99 func (s *S) TearDownTest(c *C) {
100 testServer.Flush()
101 }
102
103=== modified file 'iam/iam_test.go'
104--- iam/iam_test.go 2013-03-02 02:11:38 +0000
105+++ iam/iam_test.go 2013-06-27 11:56:25 +0000
106@@ -27,6 +27,10 @@
107 s.iam = iam.New(auth, aws.Region{IAMEndpoint: testServer.URL})
108 }
109
110+func (s *S) TearDownSuite(c *C) {
111+ testServer.Stop()
112+}
113+
114 func (s *S) TearDownTest(c *C) {
115 testServer.Flush()
116 }
117@@ -146,8 +150,8 @@
118 {
119 Path: "/division_abc/subdivision_xyz/product_1234/",
120 Name: "Managers",
121- Id: "AGPIODR4TAW7CSEXAMPLE",
122- Arn: "arn:aws:iam::123456789012:group/division_abc/subdivision_xyz/product_1234/Managers",
123+ Id: "AGPIODR4TAW7CSEXAMPLE",
124+ Arn: "arn:aws:iam::123456789012:group/division_abc/subdivision_xyz/product_1234/Managers",
125 },
126 }
127 c.Assert(resp.Groups, DeepEquals, expected)
128
129=== modified file 's3/multi.go'
130--- s3/multi.go 2013-02-13 12:20:38 +0000
131+++ s3/multi.go 2013-06-27 11:56:25 +0000
132@@ -175,14 +175,15 @@
133 if err != nil {
134 return Part{}, err
135 }
136- resp, err := m.Bucket.S3.run(req, nil)
137+ hresp, err := m.Bucket.S3.run(req)
138 if shouldRetry(err) && attempt.HasNext() {
139 continue
140 }
141 if err != nil {
142 return Part{}, err
143 }
144- etag := resp.Header.Get("ETag")
145+ hresp.Body.Close()
146+ etag := hresp.Header.Get("ETag")
147 if etag == "" {
148 return Part{}, errors.New("part upload succeeded with no ETag")
149 }
150
151=== modified file 's3/responses_test.go'
152--- s3/responses_test.go 2013-02-12 04:50:11 +0000
153+++ s3/responses_test.go 2013-06-27 11:56:25 +0000
154@@ -196,3 +196,22 @@
155 <HostId>kjhwqk</HostId>
156 </Error>
157 `
158+
159+var GetBucketsListResultDump = `
160+<ListAllMyBucketsResult xmlns="http://doc.s3.amazonaws.com/2006-03-01">
161+ <Owner>
162+ <ID>bcaf1ffd86f461ca5fb16fd081034f</ID>
163+ <DisplayName>webfile</DisplayName>
164+ </Owner>
165+ <Buckets>
166+ <Bucket>
167+ <Name>quotes</Name>
168+ <CreationDate>2006-02-03T16:45:09.000Z</CreationDate>
169+ </Bucket>
170+ <Bucket>
171+ <Name>samples</Name>
172+ <CreationDate>2006-02-03T16:41:58.000Z</CreationDate>
173+ </Bucket>
174+ </Buckets>
175+</ListAllMyBucketsResult>
176+`
177
178=== modified file 's3/s3.go'
179--- s3/s3.go 2013-02-11 17:15:59 +0000
180+++ s3/s3.go 2013-06-27 11:56:25 +0000
181@@ -40,6 +40,9 @@
182 type Bucket struct {
183 *S3
184 Name string
185+ // The following fields are only set on buckets returned from the ListBuckets call
186+ Owner Owner // Owner of the bucket
187+ CreationDate time.Time // When the bucket was created
188 }
189
190 // The Owner type represents the owner of the object in an S3 bucket.
191@@ -64,7 +67,44 @@
192 if s3.Region.S3BucketEndpoint != "" || s3.Region.S3LowercaseBucket {
193 name = strings.ToLower(name)
194 }
195- return &Bucket{s3, name}
196+ return &Bucket{S3: s3, Name: name}
197+}
198+
199+// bucketsResp is returned by S3.Buckets
200+type bucketsResp struct {
201+ Owner Owner
202+ Buckets []struct {
203+ Name string
204+ CreationDate string // Date the bucket was created, e.g., 2009-02-03T16:45:09.000Z
205+ } `xml:"Buckets>Bucket"`
206+}
207+
208+// List returns a list of all buckets owned by the sender
209+//
210+// See http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTServiceGET.html
211+func (s3 *S3) ListBuckets() (buckets []*Bucket, err error) {
212+ req := &request{
213+ method: "GET",
214+ bucket: "",
215+ path: "",
216+ }
217+ result := &bucketsResp{}
218+ err = s3.query(req, result)
219+ if err != nil {
220+ return nil, err
221+ }
222+ buckets = make([]*Bucket, len(result.Buckets))
223+ for i := range buckets {
224+ info := result.Buckets[i]
225+ b := s3.Bucket(info.Name)
226+ b.CreationDate, err = time.Parse(time.RFC3339Nano, info.CreationDate)
227+ if err != nil {
228+ return nil, err
229+ }
230+ b.Owner = result.Owner
231+ buckets[i] = b
232+ }
233+ return buckets, nil
234 }
235
236 var createBucketConfiguration = `<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
237@@ -156,14 +196,14 @@
238 return nil, err
239 }
240 for attempt := attempts.Start(); attempt.Next(); {
241- resp, err := b.S3.run(req, nil)
242+ hresp, err := b.S3.run(req)
243 if shouldRetry(err) && attempt.HasNext() {
244 continue
245 }
246 if err != nil {
247 return nil, err
248 }
249- return resp.Body, nil
250+ return hresp.Body, nil
251 }
252 panic("unreachable")
253 }
254@@ -382,10 +422,18 @@
255 // body will be unmarshalled on it.
256 func (s3 *S3) query(req *request, resp interface{}) error {
257 err := s3.prepare(req)
258- if err == nil {
259- _, err = s3.run(req, resp)
260- }
261- return err
262+ if err != nil {
263+ return err
264+ }
265+ hresp, err := s3.run(req)
266+ if err != nil {
267+ return err
268+ }
269+ if resp != nil {
270+ err = xml.NewDecoder(hresp.Body).Decode(resp)
271+ }
272+ hresp.Body.Close()
273+ return nil
274 }
275
276 // prepare sets up req to be delivered to S3.
277@@ -410,21 +458,21 @@
278 req.path = "/" + req.path
279 }
280 req.signpath = req.path
281- if req.bucket != "" {
282- req.baseurl = s3.Region.S3BucketEndpoint
283- if req.baseurl == "" {
284- // Use the path method to address the bucket.
285- req.baseurl = s3.Region.S3Endpoint
286+ req.baseurl = s3.Region.S3BucketEndpoint
287+ if req.baseurl == "" {
288+ // Use the path method to address the bucket.
289+ req.baseurl = s3.Region.S3Endpoint
290+ if req.bucket != "" {
291 req.path = "/" + req.bucket + req.path
292- } else {
293- // Just in case, prevent injection.
294- if strings.IndexAny(req.bucket, "/:@") >= 0 {
295- return fmt.Errorf("bad S3 bucket: %q", req.bucket)
296- }
297- req.baseurl = strings.Replace(req.baseurl, "${bucket}", req.bucket, -1)
298- }
299- req.signpath = "/" + req.bucket + req.signpath
300+ }
301+ } else {
302+ // Just in case, prevent injection.
303+ if strings.IndexAny(req.bucket, "/:@") >= 0 {
304+ return fmt.Errorf("bad S3 bucket: %q", req.bucket)
305+ }
306+ req.baseurl = strings.Replace(req.baseurl, "${bucket}", req.bucket, -1)
307 }
308+ req.signpath = "/" + req.bucket + req.signpath
309 }
310
311 // Always sign again as it's not clear how far the
312@@ -440,9 +488,7 @@
313 }
314
315 // run sends req and returns the http response from the server.
316-// If resp is not nil, the XML data contained in the response
317-// body will be unmarshalled on it.
318-func (s3 *S3) run(req *request, resp interface{}) (*http.Response, error) {
319+func (s3 *S3) run(req *request) (*http.Response, error) {
320 if debug {
321 log.Printf("Running S3 request: %#v", req)
322 }
323@@ -480,10 +526,6 @@
324 if hresp.StatusCode != 200 && hresp.StatusCode != 204 {
325 return nil, buildError(hresp)
326 }
327- if resp != nil {
328- err = xml.NewDecoder(hresp.Body).Decode(resp)
329- hresp.Body.Close()
330- }
331 return hresp, err
332 }
333
334
335=== modified file 's3/s3_test.go'
336--- s3/s3_test.go 2013-02-12 04:50:11 +0000
337+++ s3/s3_test.go 2013-06-27 11:56:25 +0000
338@@ -4,6 +4,7 @@
339 "bytes"
340 "io/ioutil"
341 "net/http"
342+ "net/url"
343 "testing"
344
345 "launchpad.net/goamz/aws"
346@@ -33,6 +34,7 @@
347
348 func (s *S) TearDownSuite(c *C) {
349 s3.SetAttemptStrategy(nil)
350+ testServer.Stop()
351 }
352
353 func (s *S) SetUpTest(c *C) {
354@@ -275,3 +277,31 @@
355 c.Assert(len(data.Contents), Equals, 0)
356 c.Assert(data.CommonPrefixes, DeepEquals, []string{"photos/2006/feb/", "photos/2006/jan/"})
357 }
358+
359+// List buckets docs: See http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTServiceGET.html
360+
361+func (s *S) TestListBuckets(c *C) {
362+ testServer.Response(200, nil, GetBucketsListResultDump)
363+
364+ buckets, err := s.s3.ListBuckets()
365+ c.Assert(err, IsNil)
366+
367+ req := testServer.WaitRequest()
368+ c.Assert(req.Method, Equals, "GET")
369+ c.Assert(req.URL.Path, Equals, "/")
370+ c.Assert(req.Header["Date"], Not(Equals), "")
371+ c.Assert(req.Form, DeepEquals, url.Values{})
372+
373+ c.Assert(len(buckets), Equals, 2)
374+
375+ c.Assert(buckets[0].Name, Equals, "quotes")
376+ c.Assert(buckets[0].CreationDate, Equals, time.Date(2006, 2, 3, 16, 45, 9, 0, time.UTC))
377+ c.Assert(buckets[0].Owner.ID, Equals, "bcaf1ffd86f461ca5fb16fd081034f")
378+ c.Assert(buckets[0].Owner.DisplayName, Equals, "webfile")
379+
380+ c.Assert(buckets[1].Name, Equals, "samples")
381+ c.Assert(buckets[1].CreationDate, Equals, time.Date(2006, 2, 3, 16, 41, 58, 0, time.UTC))
382+ c.Assert(buckets[1].Owner.ID, Equals, "bcaf1ffd86f461ca5fb16fd081034f")
383+ c.Assert(buckets[1].Owner.DisplayName, Equals, "webfile")
384+
385+}
386
387=== modified file 'testutil/http.go'
388--- testutil/http.go 2013-02-01 05:55:19 +0000
389+++ testutil/http.go 2013-06-27 11:56:25 +0000
390@@ -15,6 +15,7 @@
391 URL string
392 Timeout time.Duration
393 started bool
394+ listener net.Listener
395 request chan *http.Request
396 response chan ResponseFunc
397 }
398@@ -42,11 +43,11 @@
399 if err != nil {
400 panic(err)
401 }
402- l, err := net.Listen("tcp", u.Host)
403+ s.listener, err = net.Listen("tcp", u.Host)
404 if err != nil {
405 panic(err)
406 }
407- go http.Serve(l, s)
408+ go http.Serve(s.listener, s)
409
410 s.Response(203, nil, "")
411 for {
412@@ -60,6 +61,17 @@
413 s.WaitRequest() // Consume dummy request.
414 }
415
416+func (s *HTTPServer) Stop() {
417+ if s.listener == nil {
418+ return
419+ }
420+ err := s.listener.Close()
421+ if err != nil {
422+ panic(err)
423+ }
424+ s.listener = nil
425+}
426+
427 // Flush discards all pending requests and responses.
428 func (s *HTTPServer) Flush() {
429 for {

Subscribers

People subscribed via source and target branches