Merge lp:~jtv/gwacl/only-batch-list-blobs into lp:gwacl
- only-batch-list-blobs
- Merge into trunk
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 |
Related bugs: |
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 BlobsEnumeratio
Jeroen
- 105. By Jeroen T. Vermeulen
-
Review change. Failed to upper-case a letter in a comment. Cheap keyboard.
Preview Diff
1 | === modified file 'storage_base.go' | |||
2 | --- storage_base.go 2013-04-26 03:53:37 +0000 | |||
3 | +++ storage_base.go 2013-04-30 05:28:25 +0000 | |||
4 | @@ -265,48 +265,53 @@ | |||
5 | 265 | return resp, nil | 265 | return resp, nil |
6 | 266 | } | 266 | } |
7 | 267 | 267 | ||
8 | 268 | // addURLQueryParam adds a query parameter to a URL (and escapes as needed). | ||
9 | 269 | func addURLQueryParam(originalURL, key, value string) (string, error) { | ||
10 | 270 | parsedURL, err := url.Parse(originalURL) | ||
11 | 271 | if err != nil { | ||
12 | 272 | // Not much we can add beyond that the URL doesn't parse. Leave it | ||
13 | 273 | // to the caller to provide context. | ||
14 | 274 | return "", err | ||
15 | 275 | } | ||
16 | 276 | query := parsedURL.Query() | ||
17 | 277 | query.Add(key, value) | ||
18 | 278 | parsedURL.RawQuery = query.Encode() | ||
19 | 279 | return parsedURL.String(), nil | ||
20 | 280 | } | ||
21 | 281 | |||
22 | 268 | // getListContainersBatch calls the "List Containers" operation on the storage | 282 | // getListContainersBatch calls the "List Containers" operation on the storage |
25 | 269 | // API, and returns a single batch of results; its "next marker" for batching, | 283 | // API, and returns a single batch of results. |
24 | 270 | // and an error code. | ||
26 | 271 | // The marker argument should be empty for a new List Containers request. for | 284 | // The marker argument should be empty for a new List Containers request. for |
27 | 272 | // subsequent calls to get additional batches of the same result, pass the | 285 | // subsequent calls to get additional batches of the same result, pass the |
31 | 273 | // "next marker" returned by the previous call. | 286 | // NextMarker from the previous call's result. |
32 | 274 | // The "next marker" will be empty on the last batch. | 287 | func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, error) { |
33 | 275 | func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, string, error) { | 288 | var err error |
34 | 276 | uri := interpolateURL("http://___.blob.core.windows.net/?comp=list", context.Account) | 289 | uri := interpolateURL("http://___.blob.core.windows.net/?comp=list", context.Account) |
35 | 277 | if marker != "" { | 290 | if marker != "" { |
36 | 278 | // Add the marker argument. Do it after interpolation, in case the | 291 | // Add the marker argument. Do it after interpolation, in case the |
37 | 279 | // marker string might contain the interpolation's placeholder | 292 | // marker string might contain the interpolation's placeholder |
38 | 280 | // sequence. | 293 | // sequence. |
40 | 281 | parsedURL, err := url.Parse(uri) | 294 | uri, err = addURLQueryParam(uri, "marker", marker) |
41 | 282 | if err != nil { | 295 | if err != nil { |
43 | 283 | return nil, "", err | 296 | return nil, fmt.Errorf("malformed storage account URL '%s': %v", uri, err) |
44 | 284 | } | 297 | } |
45 | 285 | query := parsedURL.Query() | ||
46 | 286 | query.Set("marker", marker) | ||
47 | 287 | parsedURL.RawQuery = query.Encode() | ||
48 | 288 | uri = parsedURL.String() | ||
49 | 289 | } | 298 | } |
50 | 290 | req, err := http.NewRequest("GET", uri, nil) | 299 | req, err := http.NewRequest("GET", uri, nil) |
51 | 291 | if err != nil { | 300 | if err != nil { |
53 | 292 | return nil, "", err | 301 | return nil, err |
54 | 293 | } | 302 | } |
55 | 294 | addStandardHeaders(req, context.Account, context.Key, "2012-02-12") | 303 | addStandardHeaders(req, context.Account, context.Key, "2012-02-12") |
56 | 295 | containers := ContainerEnumerationResults{} | 304 | containers := ContainerEnumerationResults{} |
57 | 296 | _, err = context.send(req, &containers, http.StatusOK) | 305 | _, err = context.send(req, &containers, http.StatusOK) |
58 | 297 | if err != nil { | 306 | if err != nil { |
60 | 298 | return nil, "", err | 307 | return nil, err |
61 | 299 | } | 308 | } |
62 | 300 | 309 | ||
68 | 301 | // The response may contain a NextMarker field, to let us request a | 310 | return &containers, nil |
64 | 302 | // subsequent batch of results. The XML parser won't trim whitespace out | ||
65 | 303 | // of the marker tag, so we do that here. | ||
66 | 304 | nextMarker := strings.TrimSpace(containers.NextMarker) | ||
67 | 305 | return &containers, nextMarker, nil | ||
69 | 306 | } | 311 | } |
70 | 307 | 312 | ||
73 | 308 | // ListContainers sends a request to the storage service to list the containers | 313 | // ListContainers requests from the storage service a list of containers |
74 | 309 | // in the storage account. error is non-nil if an error occurred. | 314 | // in the storage account. |
75 | 310 | func (context *StorageContext) ListContainers() (*ContainerEnumerationResults, error) { | 315 | func (context *StorageContext) ListContainers() (*ContainerEnumerationResults, error) { |
76 | 311 | containers := make([]Container, 0) | 316 | containers := make([]Container, 0) |
77 | 312 | var batch *ContainerEnumerationResults | 317 | var batch *ContainerEnumerationResults |
78 | @@ -318,10 +323,14 @@ | |||
79 | 318 | var err error | 323 | var err error |
80 | 319 | // Don't use := here or you'll shadow variables from the function's | 324 | // Don't use := here or you'll shadow variables from the function's |
81 | 320 | // outer scopes. | 325 | // outer scopes. |
83 | 321 | batch, nextMarker, err = context.getListContainersBatch(marker) | 326 | batch, err = context.getListContainersBatch(marker) |
84 | 322 | if err != nil { | 327 | if err != nil { |
85 | 323 | return nil, err | 328 | return nil, err |
86 | 324 | } | 329 | } |
87 | 330 | // The response may contain a NextMarker field, to let us request a | ||
88 | 331 | // subsequent batch of results. The XML parser won't trim whitespace out | ||
89 | 332 | // of the marker tag, so we do that here. | ||
90 | 333 | nextMarker = strings.TrimSpace(batch.NextMarker) | ||
91 | 325 | containers = append(containers, batch.Containers...) | 334 | containers = append(containers, batch.Containers...) |
92 | 326 | } | 335 | } |
93 | 327 | 336 | ||
94 | @@ -334,11 +343,25 @@ | |||
95 | 334 | return batch, nil | 343 | return batch, nil |
96 | 335 | } | 344 | } |
97 | 336 | 345 | ||
100 | 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 |
101 | 338 | func (context *StorageContext) ListBlobs(container string) (*BlobEnumerationResults, error) { | 347 | // returns a single batch of results. |
102 | 348 | // The marker argument should be empty for a new List Blobs request. For | ||
103 | 349 | // subsequent calls to get additional batches of the same result, pass the | ||
104 | 350 | // NextMarker from the previous call's result. | ||
105 | 351 | func (context *StorageContext) getListBlobsBatch(container, marker string) (*BlobEnumerationResults, error) { | ||
106 | 352 | var err error | ||
107 | 339 | uri := interpolateURL( | 353 | uri := interpolateURL( |
108 | 340 | "http://___.blob.core.windows.net/___?restype=container&comp=list", | 354 | "http://___.blob.core.windows.net/___?restype=container&comp=list", |
109 | 341 | context.Account, container) | 355 | context.Account, container) |
110 | 356 | if marker != "" { | ||
111 | 357 | // Add the marker argument. Do it after interpolation, in case the | ||
112 | 358 | // marker string might contain the interpolation's placeholder | ||
113 | 359 | // sequence. | ||
114 | 360 | uri, err = addURLQueryParam(uri, "marker", marker) | ||
115 | 361 | if err != nil { | ||
116 | 362 | return nil, fmt.Errorf("malformed storage account URL '%s': %v", uri, err) | ||
117 | 363 | } | ||
118 | 364 | } | ||
119 | 342 | req, err := http.NewRequest("GET", uri, nil) | 365 | req, err := http.NewRequest("GET", uri, nil) |
120 | 343 | if err != nil { | 366 | if err != nil { |
121 | 344 | return nil, err | 367 | return nil, err |
122 | @@ -349,6 +372,39 @@ | |||
123 | 349 | return blob, err | 372 | return blob, err |
124 | 350 | } | 373 | } |
125 | 351 | 374 | ||
126 | 375 | // ListBlobs requests from the API a list of blobs in a container. | ||
127 | 376 | func (context *StorageContext) ListBlobs(container string) (*BlobEnumerationResults, error) { | ||
128 | 377 | blobs := make([]Blob, 0) | ||
129 | 378 | var batch *BlobEnumerationResults | ||
130 | 379 | |||
131 | 380 | // Request the initial result, using the empty marker. Then, for as long | ||
132 | 381 | // as the result has a nonempty NextMarker, request the next batch using | ||
133 | 382 | // that marker. | ||
134 | 383 | // This loop is very similar to the one in ListContainers(). | ||
135 | 384 | for marker, nextMarker := "", "x"; nextMarker != ""; marker = nextMarker { | ||
136 | 385 | var err error | ||
137 | 386 | // Don't use := here or you'll shadow variables from the function's | ||
138 | 387 | // outer scopes. | ||
139 | 388 | batch, err = context.getListBlobsBatch(container, marker) | ||
140 | 389 | if err != nil { | ||
141 | 390 | return nil, err | ||
142 | 391 | } | ||
143 | 392 | // The response may contain a NextMarker field, to let us request a | ||
144 | 393 | // subsequent batch of results. The XML parser won't trim whitespace out | ||
145 | 394 | // of the marker tag, so we do that here. | ||
146 | 395 | nextMarker = strings.TrimSpace(batch.NextMarker) | ||
147 | 396 | blobs = append(blobs, batch.Blobs...) | ||
148 | 397 | } | ||
149 | 398 | |||
150 | 399 | // There's more in a BlobsEnumerationResults than just the blobs. | ||
151 | 400 | // Return the latest batch, but give it the full cumulative blobs list | ||
152 | 401 | // instead of just the last batch. | ||
153 | 402 | // To the caller, this will look like they made one call to Azure's | ||
154 | 403 | // List Blobs method, but batch size was unlimited. | ||
155 | 404 | batch.Blobs = blobs | ||
156 | 405 | return batch, nil | ||
157 | 406 | } | ||
158 | 407 | |||
159 | 352 | // Send a request to the storage service to create a new container. If the | 408 | // Send a request to the storage service to create a new container. If the |
160 | 353 | // request fails, error is non-nil. | 409 | // request fails, error is non-nil. |
161 | 354 | func (context *StorageContext) CreateContainer(container string) error { | 410 | func (context *StorageContext) CreateContainer(container string) error { |
162 | 355 | 411 | ||
163 | === modified file 'storage_base_test.go' | |||
164 | --- storage_base_test.go 2013-04-26 03:58:24 +0000 | |||
165 | +++ storage_base_test.go 2013-04-30 05:28:25 +0000 | |||
166 | @@ -266,6 +266,48 @@ | |||
167 | 266 | } | 266 | } |
168 | 267 | } | 267 | } |
169 | 268 | 268 | ||
170 | 269 | type TestAddURLQueryParam struct{} | ||
171 | 270 | |||
172 | 271 | var _ = Suite(&TestAddURLQueryParam{}) | ||
173 | 272 | |||
174 | 273 | func (*TestAddURLQueryParam) TestAddURLQueryParamUsesBaseURL(c *C) { | ||
175 | 274 | baseURL := "http://example.com" | ||
176 | 275 | |||
177 | 276 | extendedURL, err := addURLQueryParam(baseURL, "key", "value") | ||
178 | 277 | c.Assert(err, IsNil) | ||
179 | 278 | |||
180 | 279 | parsedURL, err := url.Parse(extendedURL) | ||
181 | 280 | c.Assert(err, IsNil) | ||
182 | 281 | c.Check(parsedURL.Scheme, Equals, "http") | ||
183 | 282 | c.Check(parsedURL.Host, Equals, "example.com") | ||
184 | 283 | } | ||
185 | 284 | |||
186 | 285 | func (suite *TestAddURLQueryParam) TestEscapesParam(c *C) { | ||
187 | 286 | key := "key&key" | ||
188 | 287 | value := "value%value" | ||
189 | 288 | |||
190 | 289 | uri, err := addURLQueryParam("http://example.com", key, value) | ||
191 | 290 | c.Assert(err, IsNil) | ||
192 | 291 | |||
193 | 292 | parsedURL, err := url.Parse(uri) | ||
194 | 293 | c.Assert(err, IsNil) | ||
195 | 294 | c.Check(parsedURL.Query()[key], DeepEquals, []string{value}) | ||
196 | 295 | } | ||
197 | 296 | |||
198 | 297 | // The parameter may safely contain the placeholder that we happen to use | ||
199 | 298 | // for URL interpolation. | ||
200 | 299 | func (suite *TestAddURLQueryParam) TestDoesNotInterpolateParam(c *C) { | ||
201 | 300 | key := "key" + interpolationPlaceholder + "key" | ||
202 | 301 | value := "value" + interpolationPlaceholder + "value" | ||
203 | 302 | |||
204 | 303 | uri, err := addURLQueryParam("http://example.com", key, value) | ||
205 | 304 | c.Assert(err, IsNil) | ||
206 | 305 | |||
207 | 306 | parsedURL, err := url.Parse(uri) | ||
208 | 307 | c.Assert(err, IsNil) | ||
209 | 308 | c.Check(parsedURL.Query()[key], DeepEquals, []string{value}) | ||
210 | 309 | } | ||
211 | 310 | |||
212 | 269 | type TestListContainers struct{} | 311 | type TestListContainers struct{} |
213 | 270 | 312 | ||
214 | 271 | var _ = Suite(&TestListContainers{}) | 313 | var _ = Suite(&TestListContainers{}) |
215 | @@ -391,7 +433,7 @@ | |||
216 | 391 | 433 | ||
217 | 392 | // Call getListContainersBatch. This will fail because of the empty | 434 | // Call getListContainersBatch. This will fail because of the empty |
218 | 393 | // response, but no matter. We only care about the request. | 435 | // response, but no matter. We only care about the request. |
220 | 394 | _, _, err := context.getListContainersBatch("thismarkerhere") | 436 | _, err := context.getListContainersBatch("thismarkerhere") |
221 | 395 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") | 437 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") |
222 | 396 | c.Assert(transport.ExchangeCount, Equals, 1) | 438 | c.Assert(transport.ExchangeCount, Equals, 1) |
223 | 397 | 439 | ||
224 | @@ -408,7 +450,7 @@ | |||
225 | 408 | context.client = &http.Client{Transport: &transport} | 450 | context.client = &http.Client{Transport: &transport} |
226 | 409 | 451 | ||
227 | 410 | // The error is OK. We only care about the request. | 452 | // The error is OK. We only care about the request. |
229 | 411 | _, _, err := context.getListContainersBatch("") | 453 | _, err := context.getListContainersBatch("") |
230 | 412 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") | 454 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") |
231 | 413 | c.Assert(transport.ExchangeCount, Equals, 1) | 455 | c.Assert(transport.ExchangeCount, Equals, 1) |
232 | 414 | 456 | ||
233 | @@ -427,7 +469,7 @@ | |||
234 | 427 | context.client = &http.Client{Transport: &transport} | 469 | context.client = &http.Client{Transport: &transport} |
235 | 428 | 470 | ||
236 | 429 | // The error is OK. We only care about the request. | 471 | // The error is OK. We only care about the request. |
238 | 430 | _, _, err := context.getListContainersBatch("x&y") | 472 | _, err := context.getListContainersBatch("x&y") |
239 | 431 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") | 473 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") |
240 | 432 | c.Assert(transport.ExchangeCount, Equals, 1) | 474 | c.Assert(transport.ExchangeCount, Equals, 1) |
241 | 433 | 475 | ||
242 | @@ -448,7 +490,7 @@ | |||
243 | 448 | 490 | ||
244 | 449 | // An error about the http response is fine. What matters is that (1) we | 491 | // An error about the http response is fine. What matters is that (1) we |
245 | 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. |
247 | 451 | _, _, err := context.getListContainersBatch(marker) | 493 | _, err := context.getListContainersBatch(marker) |
248 | 452 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") | 494 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") |
249 | 453 | c.Assert(transport.ExchangeCount, Equals, 1) | 495 | c.Assert(transport.ExchangeCount, Equals, 1) |
250 | 454 | 496 | ||
251 | @@ -549,6 +591,87 @@ | |||
252 | 549 | c.Assert(err, NotNil) | 591 | c.Assert(err, NotNil) |
253 | 550 | } | 592 | } |
254 | 551 | 593 | ||
255 | 594 | // ListBlobs combines multiple batches of output. | ||
256 | 595 | func (suite *TestListBlobs) TestBatchedResult(c *C) { | ||
257 | 596 | firstBlob := "blob1" | ||
258 | 597 | lastBlob := "blob2" | ||
259 | 598 | marker := "moreplease" | ||
260 | 599 | firstBatch := http.Response{ | ||
261 | 600 | StatusCode: http.StatusOK, | ||
262 | 601 | Body: makeResponseBody(fmt.Sprintf(` | ||
263 | 602 | <EnumerationResults> | ||
264 | 603 | <Blobs> | ||
265 | 604 | <Blob> | ||
266 | 605 | <Name>%s</Name> | ||
267 | 606 | </Blob> | ||
268 | 607 | </Blobs> | ||
269 | 608 | <NextMarker>%s</NextMarker> | ||
270 | 609 | </EnumerationResults> | ||
271 | 610 | `, firstBlob, marker)), | ||
272 | 611 | } | ||
273 | 612 | lastBatch := http.Response{ | ||
274 | 613 | StatusCode: http.StatusOK, | ||
275 | 614 | Body: makeResponseBody(fmt.Sprintf(` | ||
276 | 615 | <EnumerationResults> | ||
277 | 616 | <Blobs> | ||
278 | 617 | <Blob> | ||
279 | 618 | <Name>%s</Name> | ||
280 | 619 | </Blob> | ||
281 | 620 | </Blobs> | ||
282 | 621 | </EnumerationResults> | ||
283 | 622 | `, lastBlob)), | ||
284 | 623 | } | ||
285 | 624 | transport := TestTransport2{} | ||
286 | 625 | transport.AddExchange(&firstBatch, nil) | ||
287 | 626 | transport.AddExchange(&lastBatch, nil) | ||
288 | 627 | context := makeStorageContext() | ||
289 | 628 | context.client = &http.Client{Transport: &transport} | ||
290 | 629 | |||
291 | 630 | blobs, err := context.ListBlobs("mycontainer") | ||
292 | 631 | c.Assert(err, IsNil) | ||
293 | 632 | |||
294 | 633 | c.Check(len(blobs.Blobs), Equals, 2) | ||
295 | 634 | c.Check(blobs.Blobs[0].Name, Equals, firstBlob) | ||
296 | 635 | c.Check(blobs.Blobs[1].Name, Equals, lastBlob) | ||
297 | 636 | } | ||
298 | 637 | |||
299 | 638 | func (suite *TestListBlobs) TestGetListBlobsBatchPassesMarker(c *C) { | ||
300 | 639 | transport := TestTransport2{} | ||
301 | 640 | transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil) | ||
302 | 641 | context := makeStorageContext() | ||
303 | 642 | context.client = &http.Client{Transport: &transport} | ||
304 | 643 | |||
305 | 644 | // Call getListBlobsBatch. This will fail because of the empty | ||
306 | 645 | // response, but no matter. We only care about the request. | ||
307 | 646 | _, err := context.getListBlobsBatch("mycontainer", "thismarkerhere") | ||
308 | 647 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") | ||
309 | 648 | c.Assert(transport.ExchangeCount, Equals, 1) | ||
310 | 649 | |||
311 | 650 | query := transport.Exchanges[0].Request.URL.RawQuery | ||
312 | 651 | values, err := url.ParseQuery(query) | ||
313 | 652 | c.Assert(err, IsNil) | ||
314 | 653 | c.Check(values["marker"], DeepEquals, []string{"thismarkerhere"}) | ||
315 | 654 | } | ||
316 | 655 | |||
317 | 656 | func (suite *TestListBlobs) TestGetListBlobsBatchDoesNotPassEmptyMarker(c *C) { | ||
318 | 657 | transport := TestTransport2{} | ||
319 | 658 | transport.AddExchange(&http.Response{StatusCode: http.StatusOK, Body: Empty}, nil) | ||
320 | 659 | context := makeStorageContext() | ||
321 | 660 | context.client = &http.Client{Transport: &transport} | ||
322 | 661 | |||
323 | 662 | // The error is OK. We only care about the request. | ||
324 | 663 | _, err := context.getListBlobsBatch("mycontainer", "") | ||
325 | 664 | c.Assert(err, ErrorMatches, ".*Failed to deserialize data.*") | ||
326 | 665 | c.Assert(transport.ExchangeCount, Equals, 1) | ||
327 | 666 | |||
328 | 667 | query := transport.Exchanges[0].Request.URL.RawQuery | ||
329 | 668 | values, err := url.ParseQuery(query) | ||
330 | 669 | c.Assert(err, IsNil) | ||
331 | 670 | marker, present := values["marker"] | ||
332 | 671 | c.Check(present, Equals, false) | ||
333 | 672 | c.Check(marker, DeepEquals, []string(nil)) | ||
334 | 673 | } | ||
335 | 674 | |||
336 | 552 | type TestCreateContainer struct{} | 675 | type TestCreateContainer struct{} |
337 | 553 | 676 | ||
338 | 554 | var _ = Suite(&TestCreateContainer{}) | 677 | var _ = Suite(&TestCreateContainer{}) |
339 | 555 | 678 | ||
340 | === modified file 'xmlobjects.go' | |||
341 | --- xmlobjects.go 2013-04-02 08:51:18 +0000 | |||
342 | +++ xmlobjects.go 2013-04-30 05:28:25 +0000 | |||
343 | @@ -447,6 +447,7 @@ | |||
344 | 447 | Delimiter string `xml:"Delimiter"` | 447 | Delimiter string `xml:"Delimiter"` |
345 | 448 | Blobs []Blob `xml:"Blobs>Blob"` | 448 | Blobs []Blob `xml:"Blobs>Blob"` |
346 | 449 | BlobPrefixes []string `xml:"Blobs>BlobPrefix>Name"` | 449 | BlobPrefixes []string `xml:"Blobs>BlobPrefix>Name"` |
347 | 450 | NextMarker string `xml:"NextMarker"` | ||
348 | 450 | } | 451 | } |
349 | 451 | 452 | ||
350 | 452 | func (b *BlobEnumerationResults) Deserialize(data []byte) error { | 453 | func (b *BlobEnumerationResults) Deserialize(data []byte) error { |
351 | 453 | 454 | ||
352 | === modified file 'xmlobjects_test.go' | |||
353 | --- xmlobjects_test.go 2013-04-01 07:27:12 +0000 | |||
354 | +++ xmlobjects_test.go 2013-04-30 05:28:25 +0000 | |||
355 | @@ -383,6 +383,7 @@ | |||
356 | 383 | c.Check(r.Prefix, Equals, "prefix") | 383 | c.Check(r.Prefix, Equals, "prefix") |
357 | 384 | c.Check(r.Marker, Equals, "marker") | 384 | c.Check(r.Marker, Equals, "marker") |
358 | 385 | c.Check(r.Delimiter, Equals, "delimiter") | 385 | c.Check(r.Delimiter, Equals, "delimiter") |
359 | 386 | c.Check(r.NextMarker, Equals, "") | ||
360 | 386 | b := r.Blobs[0] | 387 | b := r.Blobs[0] |
361 | 387 | c.Check(b.Name, Equals, "blob-name") | 388 | c.Check(b.Name, Equals, "blob-name") |
362 | 388 | c.Check(b.Snapshot, Equals, "snapshot-date-time") | 389 | c.Check(b.Snapshot, Equals, "snapshot-date-time") |
s/for/For/ on line 102 of the diff, otherwise nothing controversial here, thanks.