Merge lp:~jtv/gwacl/only-batch-list-blobs into lp:gwacl

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 105
Merged at revision: 99
Proposed branch: lp:~jtv/gwacl/only-batch-list-blobs
Merge into: lp:gwacl
Diff against target: 362 lines (+208/-27)
4 files modified
storage_base.go (+79/-23)
storage_base_test.go (+127/-4)
xmlobjects.go (+1/-0)
xmlobjects_test.go (+1/-0)
To merge this branch: bzr merge lp:~jtv/gwacl/only-batch-list-blobs
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Review via email: mp+161375@code.launchpad.net

Commit message

Support batched responses from List Blobs.

Description of the change

This duplicates the logic from ListContainers(). It's basically my previous branch, but without the refactoring. The next step is to factor out the awful commonality.

Repeated jobs that I'd like to factor out include:
 * Interpolation of the URLs for storage accounts, containers, and files.
 * Adding of query parameters to a storage URL.
 * Stripping markers and adding them to URL queries.
 * Generation, signing, and send() of HTTP requests.

A bit harder to do without complicating things is the commonality between the batching loops in ListContainers() and ListBlobs().

I'd also like to remove the useless (and in this case, sort of misleading) Marker field from BlobsEnumerationResult.

Jeroen

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

s/for/For/ on line 102 of the diff, otherwise nothing controversial here, thanks.

review: Approve
lp:~jtv/gwacl/only-batch-list-blobs updated
105. By Jeroen T. Vermeulen

Review change. Failed to upper-case a letter in a comment. Cheap keyboard.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'storage_base.go'
--- storage_base.go 2013-04-26 03:53:37 +0000
+++ storage_base.go 2013-04-30 05:28:25 +0000
@@ -265,48 +265,53 @@
265 return resp, nil265 return resp, nil
266}266}
267267
268// addURLQueryParam adds a query parameter to a URL (and escapes as needed).
269func addURLQueryParam(originalURL, key, value string) (string, error) {
270 parsedURL, err := url.Parse(originalURL)
271 if err != nil {
272 // Not much we can add beyond that the URL doesn't parse. Leave it
273 // to the caller to provide context.
274 return "", err
275 }
276 query := parsedURL.Query()
277 query.Add(key, value)
278 parsedURL.RawQuery = query.Encode()
279 return parsedURL.String(), nil
280}
281
268// getListContainersBatch calls the "List Containers" operation on the storage282// getListContainersBatch calls the "List Containers" operation on the storage
269// API, and returns a single batch of results; its "next marker" for batching,283// API, and returns a single batch of results.
270// and an error code.
271// The marker argument should be empty for a new List Containers request. for284// The marker argument should be empty for a new List Containers request. for
272// subsequent calls to get additional batches of the same result, pass the285// subsequent calls to get additional batches of the same result, pass the
273// "next marker" returned by the previous call.286// NextMarker from the previous call's result.
274// The "next marker" will be empty on the last batch.287func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, error) {
275func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, string, error) {288 var err error
276 uri := interpolateURL("http://___.blob.core.windows.net/?comp=list", context.Account)289 uri := interpolateURL("http://___.blob.core.windows.net/?comp=list", context.Account)
277 if marker != "" {290 if marker != "" {
278 // Add the marker argument. Do it after interpolation, in case the291 // Add the marker argument. Do it after interpolation, in case the
279 // marker string might contain the interpolation's placeholder292 // marker string might contain the interpolation's placeholder
280 // sequence.293 // sequence.
281 parsedURL, err := url.Parse(uri)294 uri, err = addURLQueryParam(uri, "marker", marker)
282 if err != nil {295 if err != nil {
283 return nil, "", err296 return nil, fmt.Errorf("malformed storage account URL '%s': %v", uri, err)
284 }297 }
285 query := parsedURL.Query()
286 query.Set("marker", marker)
287 parsedURL.RawQuery = query.Encode()
288 uri = parsedURL.String()
289 }298 }
290 req, err := http.NewRequest("GET", uri, nil)299 req, err := http.NewRequest("GET", uri, nil)
291 if err != nil {300 if err != nil {
292 return nil, "", err301 return nil, err
293 }302 }
294 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")303 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
295 containers := ContainerEnumerationResults{}304 containers := ContainerEnumerationResults{}
296 _, err = context.send(req, &containers, http.StatusOK)305 _, err = context.send(req, &containers, http.StatusOK)
297 if err != nil {306 if err != nil {
298 return nil, "", err307 return nil, err
299 }308 }
300309
301 // The response may contain a NextMarker field, to let us request a310 return &containers, nil
302 // subsequent batch of results. The XML parser won't trim whitespace out
303 // of the marker tag, so we do that here.
304 nextMarker := strings.TrimSpace(containers.NextMarker)
305 return &containers, nextMarker, nil
306}311}
307312
308// ListContainers sends a request to the storage service to list the containers313// ListContainers requests from the storage service a list of containers
309// in the storage account. error is non-nil if an error occurred.314// in the storage account.
310func (context *StorageContext) ListContainers() (*ContainerEnumerationResults, error) {315func (context *StorageContext) ListContainers() (*ContainerEnumerationResults, error) {
311 containers := make([]Container, 0)316 containers := make([]Container, 0)
312 var batch *ContainerEnumerationResults317 var batch *ContainerEnumerationResults
@@ -318,10 +323,14 @@
318 var err error323 var err error
319 // Don't use := here or you'll shadow variables from the function's324 // Don't use := here or you'll shadow variables from the function's
320 // outer scopes.325 // outer scopes.
321 batch, nextMarker, err = context.getListContainersBatch(marker)326 batch, err = context.getListContainersBatch(marker)
322 if err != nil {327 if err != nil {
323 return nil, err328 return nil, err
324 }329 }
330 // The response may contain a NextMarker field, to let us request a
331 // subsequent batch of results. The XML parser won't trim whitespace out
332 // of the marker tag, so we do that here.
333 nextMarker = strings.TrimSpace(batch.NextMarker)
325 containers = append(containers, batch.Containers...)334 containers = append(containers, batch.Containers...)
326 }335 }
327336
@@ -334,11 +343,25 @@
334 return batch, nil343 return batch, nil
335}344}
336345
337// Send a request to the storage service to list the blobs in a container.346// getListBlobsBatch calls the "List Blobs" operation on the storage API, and
338func (context *StorageContext) ListBlobs(container string) (*BlobEnumerationResults, error) {347// returns a single batch of results.
348// The marker argument should be empty for a new List Blobs request. For
349// subsequent calls to get additional batches of the same result, pass the
350// NextMarker from the previous call's result.
351func (context *StorageContext) getListBlobsBatch(container, marker string) (*BlobEnumerationResults, error) {
352 var err error
339 uri := interpolateURL(353 uri := interpolateURL(
340 "http://___.blob.core.windows.net/___?restype=container&comp=list",354 "http://___.blob.core.windows.net/___?restype=container&comp=list",
341 context.Account, container)355 context.Account, container)
356 if marker != "" {
357 // Add the marker argument. Do it after interpolation, in case the
358 // marker string might contain the interpolation's placeholder
359 // sequence.
360 uri, err = addURLQueryParam(uri, "marker", marker)
361 if err != nil {
362 return nil, fmt.Errorf("malformed storage account URL '%s': %v", uri, err)
363 }
364 }
342 req, err := http.NewRequest("GET", uri, nil)365 req, err := http.NewRequest("GET", uri, nil)
343 if err != nil {366 if err != nil {
344 return nil, err367 return nil, err
@@ -349,6 +372,39 @@
349 return blob, err372 return blob, err
350}373}
351374
375// ListBlobs requests from the API a list of blobs in a container.
376func (context *StorageContext) ListBlobs(container string) (*BlobEnumerationResults, error) {
377 blobs := make([]Blob, 0)
378 var batch *BlobEnumerationResults
379
380 // Request the initial result, using the empty marker. Then, for as long
381 // as the result has a nonempty NextMarker, request the next batch using
382 // that marker.
383 // This loop is very similar to the one in ListContainers().
384 for marker, nextMarker := "", "x"; nextMarker != ""; marker = nextMarker {
385 var err error
386 // Don't use := here or you'll shadow variables from the function's
387 // outer scopes.
388 batch, err = context.getListBlobsBatch(container, marker)
389 if err != nil {
390 return nil, err
391 }
392 // The response may contain a NextMarker field, to let us request a
393 // subsequent batch of results. The XML parser won't trim whitespace out
394 // of the marker tag, so we do that here.
395 nextMarker = strings.TrimSpace(batch.NextMarker)
396 blobs = append(blobs, batch.Blobs...)
397 }
398
399 // There's more in a BlobsEnumerationResults than just the blobs.
400 // Return the latest batch, but give it the full cumulative blobs list
401 // instead of just the last batch.
402 // To the caller, this will look like they made one call to Azure's
403 // List Blobs method, but batch size was unlimited.
404 batch.Blobs = blobs
405 return batch, nil
406}
407
352// Send a request to the storage service to create a new container. If the408// Send a request to the storage service to create a new container. If the
353// request fails, error is non-nil.409// request fails, error is non-nil.
354func (context *StorageContext) CreateContainer(container string) error {410func (context *StorageContext) CreateContainer(container string) error {
355411
=== modified file 'storage_base_test.go'
--- storage_base_test.go 2013-04-26 03:58:24 +0000
+++ storage_base_test.go 2013-04-30 05:28:25 +0000
@@ -266,6 +266,48 @@
266 }266 }
267}267}
268268
269type TestAddURLQueryParam struct{}
270
271var _ = Suite(&TestAddURLQueryParam{})
272
273func (*TestAddURLQueryParam) TestAddURLQueryParamUsesBaseURL(c *C) {
274 baseURL := "http://example.com"
275
276 extendedURL, err := addURLQueryParam(baseURL, "key", "value")
277 c.Assert(err, IsNil)
278
279 parsedURL, err := url.Parse(extendedURL)
280 c.Assert(err, IsNil)
281 c.Check(parsedURL.Scheme, Equals, "http")
282 c.Check(parsedURL.Host, Equals, "example.com")
283}
284
285func (suite *TestAddURLQueryParam) TestEscapesParam(c *C) {
286 key := "key&key"
287 value := "value%value"
288
289 uri, err := addURLQueryParam("http://example.com", key, value)
290 c.Assert(err, IsNil)
291
292 parsedURL, err := url.Parse(uri)
293 c.Assert(err, IsNil)
294 c.Check(parsedURL.Query()[key], DeepEquals, []string{value})
295}
296
297// The parameter may safely contain the placeholder that we happen to use
298// for URL interpolation.
299func (suite *TestAddURLQueryParam) TestDoesNotInterpolateParam(c *C) {
300 key := "key" + interpolationPlaceholder + "key"
301 value := "value" + interpolationPlaceholder + "value"
302
303 uri, err := addURLQueryParam("http://example.com", key, value)
304 c.Assert(err, IsNil)
305
306 parsedURL, err := url.Parse(uri)
307 c.Assert(err, IsNil)
308 c.Check(parsedURL.Query()[key], DeepEquals, []string{value})
309}
310
269type TestListContainers struct{}311type TestListContainers struct{}
270312
271var _ = Suite(&TestListContainers{})313var _ = Suite(&TestListContainers{})
@@ -391,7 +433,7 @@
391433
392 // Call getListContainersBatch. This will fail because of the empty434 // Call getListContainersBatch. This will fail because of the empty
393 // response, but no matter. We only care about the request.435 // response, but no matter. We only care about the request.
394 _, _, err := context.getListContainersBatch("thismarkerhere")436 _, err := context.getListContainersBatch("thismarkerhere")
395 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")437 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
396 c.Assert(transport.ExchangeCount, Equals, 1)438 c.Assert(transport.ExchangeCount, Equals, 1)
397439
@@ -408,7 +450,7 @@
408 context.client = &http.Client{Transport: &transport}450 context.client = &http.Client{Transport: &transport}
409451
410 // The error is OK. We only care about the request.452 // The error is OK. We only care about the request.
411 _, _, err := context.getListContainersBatch("")453 _, err := context.getListContainersBatch("")
412 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")454 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
413 c.Assert(transport.ExchangeCount, Equals, 1)455 c.Assert(transport.ExchangeCount, Equals, 1)
414456
@@ -427,7 +469,7 @@
427 context.client = &http.Client{Transport: &transport}469 context.client = &http.Client{Transport: &transport}
428470
429 // The error is OK. We only care about the request.471 // The error is OK. We only care about the request.
430 _, _, err := context.getListContainersBatch("x&y")472 _, err := context.getListContainersBatch("x&y")
431 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")473 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
432 c.Assert(transport.ExchangeCount, Equals, 1)474 c.Assert(transport.ExchangeCount, Equals, 1)
433475
@@ -448,7 +490,7 @@
448490
449 // An error about the http response is fine. What matters is that (1) we491 // An error about the http response is fine. What matters is that (1) we
450 // don't fail while putting together the URL, and (2) we get the right URL.492 // don't fail while putting together the URL, and (2) we get the right URL.
451 _, _, err := context.getListContainersBatch(marker)493 _, err := context.getListContainersBatch(marker)
452 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")494 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
453 c.Assert(transport.ExchangeCount, Equals, 1)495 c.Assert(transport.ExchangeCount, Equals, 1)
454496
@@ -549,6 +591,87 @@
549 c.Assert(err, NotNil)591 c.Assert(err, NotNil)
550}592}
551593
594// ListBlobs combines multiple batches of output.
595func (suite *TestListBlobs) TestBatchedResult(c *C) {
596 firstBlob := "blob1"
597 lastBlob := "blob2"
598 marker := "moreplease"
599 firstBatch := http.Response{
600 StatusCode: http.StatusOK,
601 Body: makeResponseBody(fmt.Sprintf(`
602 <EnumerationResults>
603 <Blobs>
604 <Blob>
605 <Name>%s</Name>
606 </Blob>
607 </Blobs>
608 <NextMarker>%s</NextMarker>
609 </EnumerationResults>
610 `, firstBlob, marker)),
611 }
612 lastBatch := http.Response{
613 StatusCode: http.StatusOK,
614 Body: makeResponseBody(fmt.Sprintf(`
615 <EnumerationResults>
616 <Blobs>
617 <Blob>
618 <Name>%s</Name>
619 </Blob>
620 </Blobs>
621 </EnumerationResults>
622 `, lastBlob)),
623 }
624 transport := TestTransport2{}
625 transport.AddExchange(&firstBatch, nil)
626 transport.AddExchange(&lastBatch, nil)
627 context := makeStorageContext()
628 context.client = &http.Client{Transport: &transport}
629
630 blobs, err := context.ListBlobs("mycontainer")
631 c.Assert(err, IsNil)
632
633 c.Check(len(blobs.Blobs), Equals, 2)
634 c.Check(blobs.Blobs[0].Name, Equals, firstBlob)
635 c.Check(blobs.Blobs[1].Name, Equals, lastBlob)
636}
637
638func (suite *TestListBlobs) TestGetListBlobsBatchPassesMarker(c *C) {
639 transport := TestTransport2{}
640 transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil)
641 context := makeStorageContext()
642 context.client = &http.Client{Transport: &transport}
643
644 // Call getListBlobsBatch. This will fail because of the empty
645 // response, but no matter. We only care about the request.
646 _, err := context.getListBlobsBatch("mycontainer", "thismarkerhere")
647 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
648 c.Assert(transport.ExchangeCount, Equals, 1)
649
650 query := transport.Exchanges[0].Request.URL.RawQuery
651 values, err := url.ParseQuery(query)
652 c.Assert(err, IsNil)
653 c.Check(values["marker"], DeepEquals, []string{"thismarkerhere"})
654}
655
656func (suite *TestListBlobs) TestGetListBlobsBatchDoesNotPassEmptyMarker(c *C) {
657 transport := TestTransport2{}
658 transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil)
659 context := makeStorageContext()
660 context.client = &http.Client{Transport: &transport}
661
662 // The error is OK. We only care about the request.
663 _, err := context.getListBlobsBatch("mycontainer", "")
664 c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*")
665 c.Assert(transport.ExchangeCount, Equals, 1)
666
667 query := transport.Exchanges[0].Request.URL.RawQuery
668 values, err := url.ParseQuery(query)
669 c.Assert(err, IsNil)
670 marker, present := values["marker"]
671 c.Check(present, Equals, false)
672 c.Check(marker, DeepEquals, []string(nil))
673}
674
552type TestCreateContainer struct{}675type TestCreateContainer struct{}
553676
554var _ = Suite(&TestCreateContainer{})677var _ = Suite(&TestCreateContainer{})
555678
=== modified file 'xmlobjects.go'
--- xmlobjects.go 2013-04-02 08:51:18 +0000
+++ xmlobjects.go 2013-04-30 05:28:25 +0000
@@ -447,6 +447,7 @@
447 Delimiter string `xml:"Delimiter"`447 Delimiter string `xml:"Delimiter"`
448 Blobs []Blob `xml:"Blobs>Blob"`448 Blobs []Blob `xml:"Blobs>Blob"`
449 BlobPrefixes []string `xml:"Blobs>BlobPrefix>Name"`449 BlobPrefixes []string `xml:"Blobs>BlobPrefix>Name"`
450 NextMarker string `xml:"NextMarker"`
450}451}
451452
452func (b *BlobEnumerationResults) Deserialize(data []byte) error {453func (b *BlobEnumerationResults) Deserialize(data []byte) error {
453454
=== modified file 'xmlobjects_test.go'
--- xmlobjects_test.go 2013-04-01 07:27:12 +0000
+++ xmlobjects_test.go 2013-04-30 05:28:25 +0000
@@ -383,6 +383,7 @@
383 c.Check(r.Prefix, Equals, "prefix")383 c.Check(r.Prefix, Equals, "prefix")
384 c.Check(r.Marker, Equals, "marker")384 c.Check(r.Marker, Equals, "marker")
385 c.Check(r.Delimiter, Equals, "delimiter")385 c.Check(r.Delimiter, Equals, "delimiter")
386 c.Check(r.NextMarker, Equals, "")
386 b := r.Blobs[0]387 b := r.Blobs[0]
387 c.Check(b.Name, Equals, "blob-name")388 c.Check(b.Name, Equals, "blob-name")
388 c.Check(b.Snapshot, Equals, "snapshot-date-time")389 c.Check(b.Snapshot, Equals, "snapshot-date-time")

Subscribers

People subscribed via source and target branches