Merge lp:~allenap/gwacl/list-files-by-prefix into lp:gwacl

Proposed by Gavin Panella
Status: Merged
Approved by: Gavin Panella
Approved revision: 112
Merged at revision: 106
Proposed branch: lp:~allenap/gwacl/list-files-by-prefix
Merge into: lp:gwacl
Diff against target: 467 lines (+250/-94)
5 files modified
example/storage/run.go (+5/-1)
storage.go (+35/-0)
storage_base.go (+17/-41)
storage_base_test.go (+52/-52)
storage_test.go (+141/-0)
To merge this branch: bzr merge lp:~allenap/gwacl/list-files-by-prefix
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+168479@code.launchpad.net

Commit message

List blobs matching a prefix.

Description of the change

This branch's mission - and it's chosen to accept it - is to add
prefix support to the List Blobs API call via GWACL, where `prefix` is
defined as:

  Optional. Filters the results to return only blobs whose names begin
  with the specified prefix.

As far as I am aware, it's not possible to have "proper" optional
arguments with Go, so I've created the ListBlobsRequest struct to
provide this behaviour. It'll also make it easy to add support for
maxresults, include, timeout, etc. down the road.

However, first I had some refactoring to do: the List Blobs API call
was implemented in private getListBlobsBatch function, and ListBlobs
was a wrapper that was repeatedly calling getListBlobsBatch to get all
blobs. I think the layering is good, but masking the foundational API
call is limiting, and premature.

To address this I've:

- moved ListBlobs from storage_base.go to storage.go and renamed it
  ListAllBlobs.

- getListBlobsBatch is now ListBlobs.

- Both functions take the new ListBlobsRequest struct as their only
  argument.

A similar problem exists in ListContainers and getListContainersBatch,
but that can be addressed in a separate branch if the approach taken
here is acceptable.

I've also updated the storage example so that a prefix can be passed
in, and verified that it works.

To post a comment you must log in.
Revision history for this message
Julian Edwards (julian-edwards) wrote :

(You gotta wonder why Go doesn't have default args :/)

The first thing that popped into my head was, do you think that we should put mandatory parameters as separate args to the ListAllBlobs function, and reserve ListBlobsRequest for optional params only? It seems as though you don't check for the mandatory params anyway, but it would obviate that param checking and is more explicit.

Second thing: the tests nearly all have the same set-up code for the fake transport. I think we need a factored test helper for this as all the setup detracts from the test code's intention. I don't think it should be done in this branch though, a follow-up one is better.

Also - I am wondering if we need some factory methods to create all these huge blobs of XML we're passing around? It's not entirely clear what we need though.

Everything else looks pretty good to me, thanks for all the extra test cases!

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

> (You gotta wonder why Go doesn't have default args :/)
>
> The first thing that popped into my head was, do you think that we should put
> mandatory parameters as separate args to the ListAllBlobs function, and
> reserve ListBlobsRequest for optional params only?  It seems as though you
> don't check for the mandatory params anyway, but it would obviate that param
> checking and is more explicit.

Now that we have a struct to represent the request, I think it should
be the place that it's represented. We could add a Validate() method
to the struct to check always-required parameters or something, but
I'm more inclined to just throw at Azure whatever is passed in, and
let Azure do the checking. Let's make the fewest assumptions about
Azure possible. It's less work too.

>
> Second thing: the tests nearly all have the same set-up code for the fake
> transport.  I think we need a factored test helper for this as all the setup
> detracts from the test code's intention.  I don't think it should be done in
> this branch though, a follow-up one is better.

Yeah, it's ugly. We can extract some of that stuff, but it may come
with a loss of clarity. I've seen enough of this code for now. Given
time I may come back to it and see a good way to clean this up; the
same goes for any of us.

>
> Also - I am wondering if we need some factory methods to create all these huge
> blobs of XML we're passing around? It's not entirely clear what we need
> though.

Yeah :-/ Same goes as for the set-up.

>
> Everything else looks pretty good to me, thanks for all the extra test cases!

Thanks for the review!

Revision history for this message
Julian Edwards (julian-edwards) wrote :

On Tuesday 11 Jun 2013 13:30:38 you wrote:
> > (You gotta wonder why Go doesn't have default args :/)
> >
> > The first thing that popped into my head was, do you think that we should
> > put mandatory parameters as separate args to the ListAllBlobs function,
> > and reserve ListBlobsRequest for optional params only? It seems as
> > though you don't check for the mandatory params anyway, but it would
> > obviate that param checking and is more explicit.
>
> Now that we have a struct to represent the request, I think it should
> be the place that it's represented. We could add a Validate() method
> to the struct to check always-required parameters or something, but
> I'm more inclined to just throw at Azure whatever is passed in, and
> let Azure do the checking. Let's make the fewest assumptions about
> Azure possible. It's less work too.

My only worry with that is that it break the "fail as early as possible"
paradigm. The later you fail, the harder it is to work out the source of the
problem. Especially so with Go.

Please seriously consider adding some validation. It's tied to the API
version used anyway so it's not going to suddenly break at the other end.

>
> > Second thing: the tests nearly all have the same set-up code for the fake
> > transport. I think we need a factored test helper for this as all the
> > setup detracts from the test code's intention. I don't think it should
> > be done in this branch though, a follow-up one is better.
>
> Yeah, it's ugly. We can extract some of that stuff, but it may come
> with a loss of clarity.

I don't think clarity can get much worse from what it is now :)

> I've seen enough of this code for now. Given
> time I may come back to it and see a good way to clean this up; the
> same goes for any of us.

Ok, please file a tech debt bug.

>
> > Also - I am wondering if we need some factory methods to create all these
> > huge blobs of XML we're passing around? It's not entirely clear what we
> > need though.
>
> Yeah :-/ Same goes as for the set-up.
>
> > Everything else looks pretty good to me, thanks for all the extra test
> > cases!
> Thanks for the review!

I'd like to say it was a pleasure looking at all this Go. I'd really like to.

:^)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'example/storage/run.go'
2--- example/storage/run.go 2013-04-18 15:03:42 +0000
3+++ example/storage/run.go 2013-06-10 15:57:40 +0000
4@@ -21,6 +21,7 @@
5 var key string
6 var filename string
7 var container string
8+var prefix string
9
10 func checkContainerAndFilename() error {
11 if container == "" || filename == "" {
12@@ -38,6 +39,7 @@
13 flag.StringVar(&key, "key", "", "A valid storage account key (base64 encoded)")
14 flag.StringVar(&container, "container", "", "Name of the container to use")
15 flag.StringVar(&filename, "file", "", "File containing blob to upload/download")
16+ flag.StringVar(&prefix, "prefix", "", "Prefix to match when listing blobs")
17 flag.Parse()
18
19 if account == "" || key == "" {
20@@ -102,7 +104,9 @@
21 }
22
23 func list(storage gwacl.StorageContext) {
24- res, err := storage.ListBlobs(container)
25+ request := &gwacl.ListBlobsRequest{
26+ Container: container, Prefix: prefix}
27+ res, err := storage.ListAllBlobs(request)
28 if err != nil {
29 dumpError(err)
30 return
31
32=== modified file 'storage.go'
33--- storage.go 2013-04-22 16:17:23 +0000
34+++ storage.go 2013-06-10 15:57:40 +0000
35@@ -11,6 +11,7 @@
36 "io"
37 . "launchpad.net/gwacl/logging"
38 "strconv"
39+ "strings"
40 )
41
42 // UploadBlockBlob uses PutBlock and PutBlockList API operations to upload
43@@ -45,3 +46,37 @@
44 Debugf("Committing %d blocks.\n", len(blockList.Items))
45 return context.PutBlockList(container, filename, blockList)
46 }
47+
48+// ListAllBlobs requests from the API a list of blobs in a container.
49+func (context *StorageContext) ListAllBlobs(request *ListBlobsRequest) (*BlobEnumerationResults, error) {
50+ blobs := make([]Blob, 0)
51+ var batch *BlobEnumerationResults
52+
53+ // Request the initial result, using the empty marker. Then, for as long
54+ // as the result has a nonempty NextMarker, request the next batch using
55+ // that marker.
56+ // This loop is very similar to the one in ListContainers().
57+ for marker, nextMarker := "", "x"; nextMarker != ""; marker = nextMarker {
58+ var err error
59+ // Don't use := here or you'll shadow variables from the function's
60+ // outer scopes.
61+ request.Marker = marker
62+ batch, err = context.ListBlobs(request)
63+ if err != nil {
64+ return nil, err
65+ }
66+ // The response may contain a NextMarker field, to let us request a
67+ // subsequent batch of results. The XML parser won't trim whitespace out
68+ // of the marker tag, so we do that here.
69+ nextMarker = strings.TrimSpace(batch.NextMarker)
70+ blobs = append(blobs, batch.Blobs...)
71+ }
72+
73+ // There's more in a BlobsEnumerationResults than just the blobs.
74+ // Return the latest batch, but give it the full cumulative blobs list
75+ // instead of just the last batch.
76+ // To the caller, this will look like they made one call to Azure's
77+ // List Blobs method, but batch size was unlimited.
78+ batch.Blobs = blobs
79+ return batch, nil
80+}
81
82=== modified file 'storage_base.go'
83--- storage_base.go 2013-05-01 05:16:08 +0000
84+++ storage_base.go 2013-06-10 15:57:40 +0000
85@@ -383,18 +383,27 @@
86 return batch, nil
87 }
88
89-// getListBlobsBatch calls the "List Blobs" operation on the storage API, and
90-// returns a single batch of results.
91-// The marker argument should be empty for a new List Blobs request. For
92-// subsequent calls to get additional batches of the same result, pass the
93+type ListBlobsRequest struct {
94+ Container string
95+ Marker string
96+ Prefix string
97+}
98+
99+// ListBlobs calls the "List Blobs" operation on the storage API, and returns
100+// a single batch of results.
101+// The request.Marker argument should be empty for a new List Blobs request.
102+// For subsequent calls to get additional batches of the same result, pass the
103 // NextMarker from the previous call's result.
104-func (context *StorageContext) getListBlobsBatch(container, marker string) (*BlobEnumerationResults, error) {
105+func (context *StorageContext) ListBlobs(request *ListBlobsRequest) (*BlobEnumerationResults, error) {
106 uri := addURLQueryParams(
107- context.getContainerURL(container),
108+ context.getContainerURL(request.Container),
109 "restype", "container",
110 "comp", "list")
111- if marker != "" {
112- uri = addURLQueryParams(uri, "marker", marker)
113+ if request.Marker != "" {
114+ uri = addURLQueryParams(uri, "marker", request.Marker)
115+ }
116+ if request.Prefix != "" {
117+ uri = addURLQueryParams(uri, "prefix", request.Prefix)
118 }
119 blobs := BlobEnumerationResults{}
120 _, err := context.performRequest(requestParams{
121@@ -410,39 +419,6 @@
122 return &blobs, err
123 }
124
125-// ListBlobs requests from the API a list of blobs in a container.
126-func (context *StorageContext) ListBlobs(container string) (*BlobEnumerationResults, error) {
127- blobs := make([]Blob, 0)
128- var batch *BlobEnumerationResults
129-
130- // Request the initial result, using the empty marker. Then, for as long
131- // as the result has a nonempty NextMarker, request the next batch using
132- // that marker.
133- // This loop is very similar to the one in ListContainers().
134- for marker, nextMarker := "", "x"; nextMarker != ""; marker = nextMarker {
135- var err error
136- // Don't use := here or you'll shadow variables from the function's
137- // outer scopes.
138- batch, err = context.getListBlobsBatch(container, marker)
139- if err != nil {
140- return nil, err
141- }
142- // The response may contain a NextMarker field, to let us request a
143- // subsequent batch of results. The XML parser won't trim whitespace out
144- // of the marker tag, so we do that here.
145- nextMarker = strings.TrimSpace(batch.NextMarker)
146- blobs = append(blobs, batch.Blobs...)
147- }
148-
149- // There's more in a BlobsEnumerationResults than just the blobs.
150- // Return the latest batch, but give it the full cumulative blobs list
151- // instead of just the last batch.
152- // To the caller, this will look like they made one call to Azure's
153- // List Blobs method, but batch size was unlimited.
154- batch.Blobs = blobs
155- return batch, nil
156-}
157-
158 // Send a request to the storage service to create a new container. If the
159 // request fails, error is non-nil.
160 func (context *StorageContext) CreateContainer(container string) error {
161
162=== modified file 'storage_base_test.go'
163--- storage_base_test.go 2013-04-30 14:48:05 +0000
164+++ storage_base_test.go 2013-06-10 15:57:40 +0000
165@@ -533,7 +533,8 @@
166 }
167 transport := &TestTransport{Response: response}
168 context.client = &http.Client{Transport: transport}
169- results, err := context.ListBlobs("container")
170+ request := &ListBlobsRequest{Container: "container"}
171+ results, err := context.ListBlobs(request)
172 c.Assert(err, IsNil)
173 c.Check(transport.Request.URL.String(), Matches, context.getContainerURL("container")+"?.*")
174 c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{
175@@ -550,7 +551,8 @@
176 transport := &TestTransport{Error: error}
177 context := makeStorageContext()
178 context.client = &http.Client{Transport: transport}
179- _, err := context.ListBlobs("container")
180+ request := &ListBlobsRequest{Container: "container"}
181+ _, err := context.ListBlobs(request)
182 c.Assert(err, NotNil)
183 }
184
185@@ -563,63 +565,21 @@
186 transport := &TestTransport{Response: response}
187 context := makeStorageContext()
188 context.client = &http.Client{Transport: transport}
189- _, err := context.ListBlobs("container")
190+ request := &ListBlobsRequest{Container: "container"}
191+ _, err := context.ListBlobs(request)
192 c.Assert(err, NotNil)
193 }
194
195-// ListBlobs combines multiple batches of output.
196-func (suite *TestListBlobs) TestBatchedResult(c *C) {
197- firstBlob := "blob1"
198- lastBlob := "blob2"
199- marker := "moreplease"
200- firstBatch := http.Response{
201- StatusCode: http.StatusOK,
202- Body: makeResponseBody(fmt.Sprintf(`
203- <EnumerationResults>
204- <Blobs>
205- <Blob>
206- <Name>%s</Name>
207- </Blob>
208- </Blobs>
209- <NextMarker>%s</NextMarker>
210- </EnumerationResults>
211- `, firstBlob, marker)),
212- }
213- lastBatch := http.Response{
214- StatusCode: http.StatusOK,
215- Body: makeResponseBody(fmt.Sprintf(`
216- <EnumerationResults>
217- <Blobs>
218- <Blob>
219- <Name>%s</Name>
220- </Blob>
221- </Blobs>
222- </EnumerationResults>
223- `, lastBlob)),
224- }
225- transport := TestTransport2{}
226- transport.AddExchange(&firstBatch, nil)
227- transport.AddExchange(&lastBatch, nil)
228- context := makeStorageContext()
229- context.client = &http.Client{Transport: &transport}
230-
231- blobs, err := context.ListBlobs("mycontainer")
232- c.Assert(err, IsNil)
233-
234- c.Check(len(blobs.Blobs), Equals, 2)
235- c.Check(blobs.Blobs[0].Name, Equals, firstBlob)
236- c.Check(blobs.Blobs[1].Name, Equals, lastBlob)
237-}
238-
239-func (suite *TestListBlobs) TestGetListBlobsBatchPassesMarker(c *C) {
240+func (suite *TestListBlobs) TestListBlobsPassesMarker(c *C) {
241 transport := TestTransport2{}
242 transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil)
243 context := makeStorageContext()
244 context.client = &http.Client{Transport: &transport}
245
246- // Call getListBlobsBatch. This will fail because of the empty
247+ // Call ListBlobs. This will fail because of the empty
248 // response, but no matter. We only care about the request.
249- _, err := context.getListBlobsBatch("mycontainer", "thismarkerhere")
250+ request := &ListBlobsRequest{Container: "mycontainer", Marker: "thismarkerhere"}
251+ _, err := context.ListBlobs(request)
252 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
253 c.Assert(transport.ExchangeCount, Equals, 1)
254
255@@ -629,14 +589,15 @@
256 c.Check(values["marker"], DeepEquals, []string{"thismarkerhere"})
257 }
258
259-func (suite *TestListBlobs) TestGetListBlobsBatchDoesNotPassEmptyMarker(c *C) {
260+func (suite *TestListBlobs) TestListBlobsDoesNotPassEmptyMarker(c *C) {
261 transport := TestTransport2{}
262 transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil)
263 context := makeStorageContext()
264 context.client = &http.Client{Transport: &transport}
265
266 // The error is OK. We only care about the request.
267- _, err := context.getListBlobsBatch("mycontainer", "")
268+ request := &ListBlobsRequest{Container: "mycontainer"}
269+ _, err := context.ListBlobs(request)
270 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
271 c.Assert(transport.ExchangeCount, Equals, 1)
272
273@@ -648,6 +609,45 @@
274 c.Check(marker, DeepEquals, []string(nil))
275 }
276
277+func (suite *TestListBlobs) TestListBlobsPassesPrefix(c *C) {
278+ transport := TestTransport2{}
279+ transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil)
280+ context := makeStorageContext()
281+ context.client = &http.Client{Transport: &transport}
282+
283+ // Call ListBlobs. This will fail because of the empty
284+ // response, but no matter. We only care about the request.
285+ request := &ListBlobsRequest{Container: "mycontainer", Prefix: "thisprefixhere"}
286+ _, err := context.ListBlobs(request)
287+ c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
288+ c.Assert(transport.ExchangeCount, Equals, 1)
289+
290+ query := transport.Exchanges[0].Request.URL.RawQuery
291+ values, err := url.ParseQuery(query)
292+ c.Assert(err, IsNil)
293+ c.Check(values["prefix"], DeepEquals, []string{"thisprefixhere"})
294+}
295+
296+func (suite *TestListBlobs) TestListBlobsDoesNotPassEmptyPrefix(c *C) {
297+ transport := TestTransport2{}
298+ transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil)
299+ context := makeStorageContext()
300+ context.client = &http.Client{Transport: &transport}
301+
302+ // The error is OK. We only care about the request.
303+ request := &ListBlobsRequest{Container: "mycontainer"}
304+ _, err := context.ListBlobs(request)
305+ c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
306+ c.Assert(transport.ExchangeCount, Equals, 1)
307+
308+ query := transport.Exchanges[0].Request.URL.RawQuery
309+ values, err := url.ParseQuery(query)
310+ c.Assert(err, IsNil)
311+ prefix, present := values["prefix"]
312+ c.Check(present, Equals, false)
313+ c.Check(prefix, DeepEquals, []string(nil))
314+}
315+
316 type TestCreateContainer struct{}
317
318 var _ = Suite(&TestCreateContainer{})
319
320=== modified file 'storage_test.go'
321--- storage_test.go 2013-04-29 11:52:04 +0000
322+++ storage_test.go 2013-06-10 15:57:40 +0000
323@@ -91,3 +91,144 @@
324 expected += "</BlockList>"
325 c.Check(string(body), Equals, expected)
326 }
327+
328+type testListAllBlobs struct{}
329+
330+var _ = Suite(&testListAllBlobs{})
331+
332+// The ListAllBlobs Storage API call returns a BlobEnumerationResults struct
333+// on success.
334+func (suite *testListAllBlobs) Test(c *C) {
335+ response_body := `
336+ <?xml version="1.0" encoding="utf-8"?>
337+ <EnumerationResults ContainerName="http://myaccount.blob.core.windows.net/mycontainer">
338+ <Prefix>prefix</Prefix>
339+ <Marker>marker</Marker>
340+ <MaxResults>maxresults</MaxResults>
341+ <Delimiter>delimiter</Delimiter>
342+ <Blobs>
343+ <Blob>
344+ <Name>blob-name</Name>
345+ <Snapshot>snapshot-date-time</Snapshot>
346+ <Url>blob-address</Url>
347+ <Properties>
348+ <Last-Modified>last-modified</Last-Modified>
349+ <Etag>etag</Etag>
350+ <Content-Length>size-in-bytes</Content-Length>
351+ <Content-Type>blob-content-type</Content-Type>
352+ <Content-Encoding />
353+ <Content-Language />
354+ <Content-MD5 />
355+ <Cache-Control />
356+ <x-ms-blob-sequence-number>sequence-number</x-ms-blob-sequence-number>
357+ <BlobType>blobtype</BlobType>
358+ <LeaseStatus>leasestatus</LeaseStatus>
359+ <LeaseState>leasestate</LeaseState>
360+ <LeaseDuration>leasesduration</LeaseDuration>
361+ <CopyId>id</CopyId>
362+ <CopyStatus>copystatus</CopyStatus>
363+ <CopySource>copysource</CopySource>
364+ <CopyProgress>copyprogress</CopyProgress>
365+ <CopyCompletionTime>copycompletiontime</CopyCompletionTime>
366+ <CopyStatusDescription>copydesc</CopyStatusDescription>
367+ </Properties>
368+ <Metadata>
369+ <MetaName1>metadataname1</MetaName1>
370+ <MetaName2>metadataname2</MetaName2>
371+ </Metadata>
372+ </Blob>
373+ <BlobPrefix>
374+ <Name>blob-prefix</Name>
375+ </BlobPrefix>
376+ </Blobs>
377+ <NextMarker />
378+ </EnumerationResults>`
379+ context := makeStorageContext()
380+ response := &http.Response{
381+ Status: fmt.Sprintf("%d", http.StatusOK),
382+ StatusCode: http.StatusOK,
383+ Body: makeResponseBody(response_body),
384+ }
385+ transport := &TestTransport{Response: response}
386+ context.client = &http.Client{Transport: transport}
387+ request := &ListBlobsRequest{Container: "container"}
388+ results, err := context.ListAllBlobs(request)
389+ c.Assert(err, IsNil)
390+ c.Check(transport.Request.URL.String(), Matches, context.getContainerURL("container")+"?.*")
391+ c.Check(transport.Request.URL.Query(), DeepEquals, url.Values{
392+ "restype": {"container"},
393+ "comp": {"list"},
394+ })
395+ c.Check(transport.Request.Header.Get("Authorization"), Not(Equals), "")
396+ c.Assert(results, NotNil)
397+}
398+
399+// Client-side errors from the HTTP client are propagated back to the caller.
400+func (suite *testListAllBlobs) TestError(c *C) {
401+ error := fmt.Errorf("canned-error")
402+ transport := &TestTransport{Error: error}
403+ context := makeStorageContext()
404+ context.client = &http.Client{Transport: transport}
405+ request := &ListBlobsRequest{Container: "container"}
406+ _, err := context.ListAllBlobs(request)
407+ c.Assert(err, NotNil)
408+}
409+
410+// Server-side errors are propagated back to the caller.
411+func (suite *testListAllBlobs) TestErrorResponse(c *C) {
412+ response := &http.Response{
413+ Status: fmt.Sprintf("%d", http.StatusNotFound),
414+ StatusCode: http.StatusNotFound,
415+ }
416+ transport := &TestTransport{Response: response}
417+ context := makeStorageContext()
418+ context.client = &http.Client{Transport: transport}
419+ request := &ListBlobsRequest{Container: "container"}
420+ _, err := context.ListAllBlobs(request)
421+ c.Assert(err, NotNil)
422+}
423+
424+// ListAllBlobs combines multiple batches of output.
425+func (suite *testListAllBlobs) TestBatchedResult(c *C) {
426+ firstBlob := "blob1"
427+ lastBlob := "blob2"
428+ marker := "moreplease"
429+ firstBatch := http.Response{
430+ StatusCode: http.StatusOK,
431+ Body: makeResponseBody(fmt.Sprintf(`
432+ <EnumerationResults>
433+ <Blobs>
434+ <Blob>
435+ <Name>%s</Name>
436+ </Blob>
437+ </Blobs>
438+ <NextMarker>%s</NextMarker>
439+ </EnumerationResults>
440+ `, firstBlob, marker)),
441+ }
442+ lastBatch := http.Response{
443+ StatusCode: http.StatusOK,
444+ Body: makeResponseBody(fmt.Sprintf(`
445+ <EnumerationResults>
446+ <Blobs>
447+ <Blob>
448+ <Name>%s</Name>
449+ </Blob>
450+ </Blobs>
451+ </EnumerationResults>
452+ `, lastBlob)),
453+ }
454+ transport := TestTransport2{}
455+ transport.AddExchange(&firstBatch, nil)
456+ transport.AddExchange(&lastBatch, nil)
457+ context := makeStorageContext()
458+ context.client = &http.Client{Transport: &transport}
459+
460+ request := &ListBlobsRequest{Container: "mycontainer"}
461+ blobs, err := context.ListAllBlobs(request)
462+ c.Assert(err, IsNil)
463+
464+ c.Check(len(blobs.Blobs), Equals, 2)
465+ c.Check(blobs.Blobs[0].Name, Equals, firstBlob)
466+ c.Check(blobs.Blobs[1].Name, Equals, lastBlob)
467+}

Subscribers

People subscribed via source and target branches

to all changes: