Merge lp:~jtv/gwacl/factor-storage-requests into lp:gwacl

Proposed by Jeroen T. Vermeulen
Status: Merged
Approved by: Jeroen T. Vermeulen
Approved revision: 111
Merged at revision: 103
Proposed branch: lp:~jtv/gwacl/factor-storage-requests
Merge into: lp:gwacl
Diff against target: 416 lines (+149/-117)
2 files modified
storage_base.go (+147/-116)
storage_base_test.go (+2/-1)
To merge this branch: bzr merge lp:~jtv/gwacl/factor-storage-requests
Reviewer Review Type Date Requested Status
Julian Edwards (community) Approve
Gavin Panella Approve
Review via email: mp+161617@code.launchpad.net

Commit message

Factor out "perform HTTP request" method in StorageContext.

Description of the change

This combines a few tightly-related refactorings in the storage-API code (mostly in separate commits):

 * Move signRequest() into StorageAPI.

 * Move the signRequest() call out of addStandardheaders().

 * Fix up a few bits of dead code where call sites tried to check for unexpected HTTP statuses — after send() would already have returned an error.

 * Factor out a performRequest() method, with a parameter-object struct, to encapsulate the repeated code to perform HTTP requests.

The parameter-object initializer makes the calls to performRequest() rather verbose, but I think it's better than passing lots of positional arguments, especially when several are of the same type and easily confused. Personally I find named arguments preferable to positional ones once you have more than four of them. It would be possible to move some of the required fields into the call as positional parameters, but it would become either an arbitrary selection, or if we move all required fields, we'd be back to the long parameter list. In which case the parameter object would just make things more irregular so we'd want to get rid of it, and we'd have truly ridiculously long argument lists.

Some small changes in addition to strict refactoring:

 * Document that you can't change a request further after signing — I lost some time to that trap in Brisbane, since signing was hidden inside addStandardHeaders().

 * Added some more detail to error messages.

To help keep future development smooth I also added some sanity-checking to the parameter object. No, it's not technically needed, but it helped make sure that I didn't make any of the obvious mistakes during refactoring — without having to test the same things again for every call. We can take it out now if we want, but I figure it's an easy way to find out about mistakes sooner.

Jeroen

To post a comment you must log in.
110. By Jeroen T. Vermeulen

Removed a bit of Java-like silliness.

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

Looks good.

[1]

+func (params *requestParams) Check() {

This isn't used anywhere.

[2]

+func (context *StorageContext) performRequest(params requestParams) ...

So, does this mean that a copy of requestParams gets passed in? Does
that mean that copies of params' fields are made too? Body is an
io.Reader, so does that mean it's always a pointer/reference type, and
so only the pointer is copied? Same with Deserializer.

[3]

Is it worth keeping send() distinct from performRequest()?

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

Very nice change, thanks Jeroen.

75 + methods := map[string]bool{"GET": true, "PUT": true, "POST": true, "DELETE": true}
76 + if _, ok := methods[params.Method]; !ok {

This could possibly be the best 2 lines of Go I've ever seen :)

81 +// performRequest issues an HTTP request to Azure.
82 +func (context *StorageContext) performRequest(params requestParams) (*http.Response, error) {

Make sure you take a pointer for params here, we don't want to copy them. Been doing Python lately? :)
Also you forgot to call Check().

On another note, I wish there were a way of linking the doc for params into the doc for performRequest. Param descriptions are nowhere near the function :( This is a huge problem with godoc (I believe I contributed that bit to Gripes).

141 + _, err := context.performRequest(requestParams{
142 + Method: "GET",
143 + URL: uri,
144 + APIVersion: "2012-02-12",
145 + Result: &containers,
146 + ExpectedStatus: http.StatusOK,
147 + })

You're right - for long lists of params this style is hugely better than positional args. I really miss named parameters in Go :(

BTW I have an idea for error handling in the library. I will email the team about it so we don't cloud the MP here.

cheers

review: Approve
111. By Jeroen T. Vermeulen

Review change: forgot to call requestParams.Check().

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (4.0 KiB)

> [1]
>
> +func (params *requestParams) Check() {
>
> This isn't used anywhere.

Thanks for spotting that. I added the one call that was supposed to be there, right at the top of performRequest().

> [2]
>
> +func (context *StorageContext) performRequest(params requestParams) ...
>
> So, does this mean that a copy of requestParams gets passed in? Does
> that mean that copies of params' fields are made too? Body is an
> io.Reader, so does that mean it's always a pointer/reference type, and
> so only the pointer is copied? Same with Deserializer.

That's right. The io.Reader and Deserializer types are interfaces that wrap pointers, so we get to court disaster with Go's fragile type system; there isn't much we can do about that.

The requestParams object, on the other hand, is passed by value. I feel this is appropriate for an object that doesn't go through a series of stateful changes.

That may sound strange. You may think "but passing a pointer is more efficient, right?" But even if it's not particularly relevant when speaking http[s] across the internet, that's not necessarily the case, for fairly complicated reasons. I'll try to explain these not because they matter much here, but because they may put a new perspective on other situations you come across in the future.

To begin with, a smart compiler can detect that the function does not modify the parameter, and so pass it by reference if it that seems more efficient. Or it may pass parts of the object in registers, instead of writing to memory and possibly pushing data out of cache in the process. The trade-off for how to pass the parameter could shift depending on architecture, targeted CPU model, estimated cache pressure, prevalent optimization goals, changes in the compiler, profiling feedback... More than you'd care to tweak your code for every time.

The key thing is that the compiler has to know exactly what goes on with the object. If you pass an object by value, it's easy to see that no changes to the object will "flow back" to the caller. That may also allow the compiler to prove that the caller won't modify the object either, and this gives it the freedom to pass by reference in more situations. Similarly, it's hard to registerize (parts of) the object if there may be pointers to the object through which it may be modified, or through which it should be possible to observe changes. Whenever the compiler is not sure about these things (or when it can't be bothered to find out), it does the safe thing which is also the inefficient thing — with pass-by-reference, that means treating every potentially mutable item as location in memory instead of as a value that can be optimized in all sorts of ways.

Usually it's easy to see whether there may be other references to an object, but this "aliasing problem" as it's called is not generally computable.

So be kind to your compiler. Don't micro-manage it. Trust it, and give it the information it needs to do its job well. You do that by minimizing the scope of what "can" happen to an item. Copy by value where you can. In languages that have it, avoid pointer arithmetic. Try to avoid creating pointers to objects as ...

Read more...

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :

> 75 + methods := map[string]bool{"GET": true, "PUT": true, "POST":
> true, "DELETE": true}
> 76 + if _, ok := methods[params.Method]; !ok {
>
> This could possibly be the best 2 lines of Go I've ever seen :)

Thank you!

It means “if params.Method not in {"GET", "PUT", "POST", "DELETE"}:”

Really goes to show how Go was designed to put the fun back in programming. Expressing this complex problem reasonably well kept me amused for a while.

Oh, you wanted fun while *reading* the code? Too bad. Put on some music or something.

> 81 +// performRequest issues an HTTP request to Azure.
> 82 +func (context *StorageContext) performRequest(params requestParams)
> (*http.Response, error) {
>
> Make sure you take a pointer for params here, we don't want to copy them.
> Been doing Python lately? :)
> Also you forgot to call Check().

Yes, I forgot to call Check(), but too late! Gavin spotted that in his review 9—10 hours earlier. Sometimes you do need to refresh merge proposals in your browser. :)

The copying is deliberate, harmless, and beneficial. See my explanation to Gavin above.

.

> On another note, I wish there were a way of linking the doc for params into
> the doc for performRequest. Param descriptions are nowhere near the function
> :( This is a huge problem with godoc (I believe I contributed that bit to
> Gripes).

Actually, I don't think I have that particular point there. Might be worth having a separate section about godoc, if you care to provide the material. I haven't built up much experience with godoc myself, because I haven't seen any use for it yet. From my point of view it's just been a roundabout way of reading a function's comment without seeing its body.

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

FWIW I think you are giving Go's compiler far too much benefit. Why risk it
not getting optimised when passing a pointer will definitely not copy
anything. And who's to say it won't optimise that, too?

Revision history for this message
Jeroen T. Vermeulen (jtv) wrote :
Download full text (4.9 KiB)

> FWIW I think you are giving Go's compiler far too much benefit. Why risk it
> not getting optimised when passing a pointer will definitely not copy
> anything. And who's to say it won't optimise that, too?

How much experience do you have with compiler back-ends? Compilers improve over time, and absent a very direct immediate need you're better off letting them optimize your code better in the future, than trying to clamp down on every risk of something not getting optimized now — and interfering with both future maintenance and future optimization. This is no different from when some people kept using "goto" because loops in the new structured-programming style were slower. Soon the compilers were tiling, fusing, fissioning, strip-mining, unrolling, and parallellizing the structured loops to stupendous effect... but skipping the parts with goto in them because those were icky and potentially hard to figure out.

Also, if you study compiler optimization you will find that passing a pointer definitely can cause excessive copying, and other costs, because of the aliasing problem. It just doesn't fit into a naïve, 1970s mental model of how compiled code works. Go may be in many ways a 1970s language, but much of this is now standard fare even for a young compiler. Plus there's a gcc front-end for Go, and gcc definitely has mature optimizations.

Where do the costs come from? Seemingly everywhere. Every time the compiler wants to access a variable, it needs to re-read that variable from memory — unless it can rule out aliasing, in which case it can registerize. Reloading the variable on demand every time is a form of copying, without the efficiencies of a full object copy. And if the access changes the variable, the same goes for writing the new value back to memory afterwards. The register allocator gets less freedom to make optimal choices. Trivial operations can't be optimized away if they put an object through intermediate states that may be observed through aliasing. The scheduler is much more constrained, which also eats away at the benefits you get from inlining. Escape analysis is less successful, so more objects need to be allocated on the heap that could have been allocated on the stack and/or registerized. More objects on the heap means more work for the garbage collector, more lock contention in the memory allocator, less cache locality.

You ask who's to say that the compiler won't optimise away pass-by-reference. Nothing obscures the compiler's view of what happens to an object quite like passing its address around. That's what usually creates aliases. It's harder for a compiler (and as I said, technically incomputable) to prove that aliasing is harmless in a given case, than it is to optimize away an object copy in cases where there is no aliasing in the first place. The two are not symmetrical.

Maybe it's best if I give you an example. Imagine you're the compiler. Consider this pseudocode. (If it looks silly, imagine it being the outcome of earlier optimization stages such as inlining. Imagine the computations being more interesting. Imagine much of this happening in a performance-critical loop. Any...

Read more...

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

So what you're saying is that Go is missing const pointer args ;)

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

OK I'll stop trolling now!

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-30 09:51:53 +0000
+++ storage_base.go 2013-05-01 05:20:37 +0000
@@ -16,6 +16,7 @@
16 "crypto/hmac"16 "crypto/hmac"
17 "crypto/sha256"17 "crypto/sha256"
18 "encoding/base64"18 "encoding/base64"
19 "errors"
19 "fmt"20 "fmt"
20 "io"21 "io"
21 "io/ioutil"22 "io/ioutil"
@@ -40,12 +41,6 @@
40 "Range",41 "Range",
41}42}
4243
43// Add the Authorization: header to a Request.
44func signRequest(req *http.Request, account_name, account_key string) {
45 header := composeAuthHeader(req, account_name, account_key)
46 req.Header.Set("Authorization", header)
47}
48
49// Calculate the value required for an Authorization header.44// Calculate the value required for an Authorization header.
50func composeAuthHeader(req *http.Request, account_name, account_key string) string {45func composeAuthHeader(req *http.Request, account_name, account_key string) string {
51 signable := composeStringToSign(req, account_name)46 signable := composeStringToSign(req, account_name)
@@ -184,12 +179,12 @@
184 req.Header.Set("Date", now)179 req.Header.Set("Date", now)
185}180}
186181
187// Convenience function to add mandatory headers required in every request.182// signRequest adds the Authorization: header to a Request.
188func addStandardHeaders(req *http.Request, account, key, ms_version string) {183// Don't make any further changes to the request before sending it, or the
189 addVersionHeader(req, ms_version)184// signature will not be valid.
190 addDateHeader(req)185func (context *StorageContext) signRequest(req *http.Request) {
191 addContentHeaders(req)186 header := composeAuthHeader(req, context.Account, context.Key)
192 signRequest(req, account, key)187 req.Header.Set("Authorization", header)
193}188}
194189
195// StorageContext keeps track of the mandatory parameters required to send a190// StorageContext keeps track of the mandatory parameters required to send a
@@ -215,6 +210,53 @@
215 Deserialize([]byte) error210 Deserialize([]byte) error
216}211}
217212
213// requestParams is a Parameter Object for performRequest().
214type requestParams struct {
215 Method string // HTTP method, e.g. "GET" or "PUT".
216 URL string // Resource locator, e.g. "http://example.com/my/resource".
217 Body io.Reader // Optional request body.
218 APIVersion string // Expected Azure API version, e.g. "2012-02-12".
219 Result Deserializer // Optional object to parse API response into.
220 ExpectedStatus HTTPStatus // Expected response status, e.g. http.StatusOK.
221}
222
223// Check performs a basic sanity check on the request. This will only catch
224// a few superficial problems that you can spot at compile time, to save a
225// debugging cycle for the most basic mistakes.
226func (params *requestParams) Check() {
227 const panicPrefix = "invalid request: "
228 if params.Method == "" {
229 panic(errors.New(panicPrefix + "HTTP method not specified"))
230 }
231 if params.URL == "" {
232 panic(errors.New(panicPrefix + "URL not specified"))
233 }
234 if params.APIVersion == "" {
235 panic(errors.New(panicPrefix + "API version not specified"))
236 }
237 if params.ExpectedStatus == 0 {
238 panic(errors.New(panicPrefix + "expected HTTP status not specified"))
239 }
240 methods := map[string]bool{"GET": true, "PUT": true, "POST": true, "DELETE": true}
241 if _, ok := methods[params.Method]; !ok {
242 panic(fmt.Errorf(panicPrefix+"unsupported HTTP method '%s'", params.Method))
243 }
244}
245
246// performRequest issues an HTTP request to Azure.
247func (context *StorageContext) performRequest(params requestParams) (*http.Response, error) {
248 params.Check()
249 req, err := http.NewRequest(params.Method, params.URL, params.Body)
250 if err != nil {
251 return nil, err
252 }
253 addVersionHeader(req, params.APIVersion)
254 addDateHeader(req)
255 addContentHeaders(req)
256 context.signRequest(req)
257 return context.send(req, params.Result, params.ExpectedStatus)
258}
259
218// Send a request to the storage service and process the response.260// Send a request to the storage service and process the response.
219// The "res" parameter is typically an XML struct that will deserialize the261// The "res" parameter is typically an XML struct that will deserialize the
220// raw XML into the struct data. The http Response object is returned.262// raw XML into the struct data. The http Response object is returned.
@@ -224,7 +266,7 @@
224// is an HTTPError, the request response is also returned. In other error266// is an HTTPError, the request response is also returned. In other error
225// cases, the returned response may be the one received from the server or267// cases, the returned response may be the one received from the server or
226// it may be nil.268// it may be nil.
227func (context *StorageContext) send(req *http.Request, res Deserializer, expected_status int) (*http.Response, error) {269func (context *StorageContext) send(req *http.Request, res Deserializer, expected_status HTTPStatus) (*http.Response, error) {
228 client := context.getClient()270 client := context.getClient()
229 resp, err := client.Do(req)271 resp, err := client.Do(req)
230 if err != nil {272 if err != nil {
@@ -233,7 +275,7 @@
233275
234 var data []byte276 var data []byte
235277
236 if resp.StatusCode != expected_status {278 if resp.StatusCode != int(expected_status) {
237 if resp.Body != nil {279 if resp.Body != nil {
238 data, err = ioutil.ReadAll(resp.Body)280 data, err = ioutil.ReadAll(resp.Body)
239 if err != nil {281 if err != nil {
@@ -250,7 +292,7 @@
250 return resp, nil292 return resp, nil
251 }293 }
252294
253 // TODO: deserialize response headers into the "res" object.295 // TODO: Also deserialize response headers into the "res" object.
254 data, err = ioutil.ReadAll(resp.Body)296 data, err = ioutil.ReadAll(resp.Body)
255 if err != nil {297 if err != nil {
256 msg := fmt.Errorf("failed to read response data: %s", err)298 msg := fmt.Errorf("failed to read response data: %s", err)
@@ -290,22 +332,21 @@
290// subsequent calls to get additional batches of the same result, pass the332// subsequent calls to get additional batches of the same result, pass the
291// NextMarker from the previous call's result.333// NextMarker from the previous call's result.
292func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, error) {334func (context *StorageContext) getListContainersBatch(marker string) (*ContainerEnumerationResults, error) {
293 uri := context.getAccountURL()335 uri := addURLQueryParams(context.getAccountURL(), "comp", "list")
294 uri = addURLQueryParams(uri, "comp", "list")
295 if marker != "" {336 if marker != "" {
296 uri = addURLQueryParams(uri, "marker", marker)337 uri = addURLQueryParams(uri, "marker", marker)
297 }338 }
298 req, err := http.NewRequest("GET", uri, nil)
299 if err != nil {
300 return nil, err
301 }
302 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
303 containers := ContainerEnumerationResults{}339 containers := ContainerEnumerationResults{}
304 _, err = context.send(req, &containers, http.StatusOK)340 _, err := context.performRequest(requestParams{
341 Method: "GET",
342 URL: uri,
343 APIVersion: "2012-02-12",
344 Result: &containers,
345 ExpectedStatus: http.StatusOK,
346 })
305 if err != nil {347 if err != nil {
306 return nil, err348 return nil, fmt.Errorf("request for containers list failed: %v", err)
307 }349 }
308
309 return &containers, nil350 return &containers, nil
310}351}
311352
@@ -348,22 +389,25 @@
348// subsequent calls to get additional batches of the same result, pass the389// subsequent calls to get additional batches of the same result, pass the
349// NextMarker from the previous call's result.390// NextMarker from the previous call's result.
350func (context *StorageContext) getListBlobsBatch(container, marker string) (*BlobEnumerationResults, error) {391func (context *StorageContext) getListBlobsBatch(container, marker string) (*BlobEnumerationResults, error) {
351 uri := context.getContainerURL(container)392 uri := addURLQueryParams(
352 uri = addURLQueryParams(393 context.getContainerURL(container),
353 uri,
354 "restype", "container",394 "restype", "container",
355 "comp", "list")395 "comp", "list")
356 if marker != "" {396 if marker != "" {
357 uri = addURLQueryParams(uri, "marker", marker)397 uri = addURLQueryParams(uri, "marker", marker)
358 }398 }
359 req, err := http.NewRequest("GET", uri, nil)399 blobs := BlobEnumerationResults{}
400 _, err := context.performRequest(requestParams{
401 Method: "GET",
402 URL: uri,
403 APIVersion: "2012-02-12",
404 Result: &blobs,
405 ExpectedStatus: http.StatusOK,
406 })
360 if err != nil {407 if err != nil {
361 return nil, err408 return nil, fmt.Errorf("request for blobs list failed: %v", err)
362 }409 }
363 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")410 return &blobs, err
364 blob := &BlobEnumerationResults{}
365 _, err = context.send(req, blob, http.StatusOK)
366 return blob, err
367}411}
368412
369// ListBlobs requests from the API a list of blobs in a container.413// ListBlobs requests from the API a list of blobs in a container.
@@ -402,16 +446,17 @@
402// Send a request to the storage service to create a new container. If the446// Send a request to the storage service to create a new container. If the
403// request fails, error is non-nil.447// request fails, error is non-nil.
404func (context *StorageContext) CreateContainer(container string) error {448func (context *StorageContext) CreateContainer(container string) error {
405 uri := context.getContainerURL(container)449 uri := addURLQueryParams(
406 uri = addURLQueryParams(uri, "restype", "container")450 context.getContainerURL(container),
407 req, err := http.NewRequest("PUT", uri, nil)451 "restype", "container")
452 _, err := context.performRequest(requestParams{
453 Method: "PUT",
454 URL: uri,
455 APIVersion: "2012-02-12",
456 ExpectedStatus: http.StatusCreated,
457 })
408 if err != nil {458 if err != nil {
409 return err459 return fmt.Errorf("failed to create container %s: %v", container, err)
410 }
411 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
412 _, errmsg := context.send(req, nil, http.StatusCreated)
413 if errmsg != nil {
414 return errmsg
415 }460 }
416 return nil461 return nil
417}462}
@@ -419,16 +464,15 @@
419// Send a request to create a space to upload a blob. Note that this does not464// Send a request to create a space to upload a blob. Note that this does not
420// do the uploading, it just makes an empty file.465// do the uploading, it just makes an empty file.
421func (context *StorageContext) PutBlob(container, filename string) error {466func (context *StorageContext) PutBlob(container, filename string) error {
422 // TODO: Add blobtype header.467 _, err := context.performRequest(requestParams{
423 uri := context.getFileURL(container, filename)468 Method: "PUT",
424 req, err := http.NewRequest("PUT", uri, nil)469 // TODO: Add blobtype header.
470 URL: context.getFileURL(container, filename),
471 APIVersion: "2012-02-12",
472 ExpectedStatus: http.StatusCreated,
473 })
425 if err != nil {474 if err != nil {
426 return err475 return fmt.Errorf("failed to create blob %s: %v", filename, err)
427 }
428 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
429 _, errmsg := context.send(req, nil, http.StatusCreated)
430 if errmsg != nil {
431 return errmsg
432 }476 }
433 return nil477 return nil
434}478}
@@ -436,23 +480,21 @@
436// Send a request to fetch the list of blocks that have been uploaded as part480// Send a request to fetch the list of blocks that have been uploaded as part
437// of a block blob.481// of a block blob.
438func (context *StorageContext) GetBlockList(container, filename string) (*GetBlockList, error) {482func (context *StorageContext) GetBlockList(container, filename string) (*GetBlockList, error) {
439 uri := context.getFileURL(container, filename)483 uri := addURLQueryParams(
440 uri = addURLQueryParams(484 context.getFileURL(container, filename),
441 uri,
442 "comp", "blocklist",485 "comp", "blocklist",
443 "blocklisttype", "all")486 "blocklisttype", "all")
444
445 req, err := http.NewRequest("GET", uri, nil)
446 if err != nil {
447 return nil, err
448 }
449 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")
450 bl := GetBlockList{}487 bl := GetBlockList{}
451 _, errmsg := context.send(req, &bl, http.StatusOK)488 _, err := context.performRequest(requestParams{
452 if errmsg != nil {489 Method: "GET",
453 return nil, errmsg490 URL: uri,
491 APIVersion: "2012-02-12",
492 Result: &bl,
493 ExpectedStatus: http.StatusOK,
494 })
495 if err != nil {
496 return nil, fmt.Errorf("request for block list in file %s failed: %v", filename, err)
454 }497 }
455
456 return &bl, nil498 return &bl, nil
457}499}
458500
@@ -460,83 +502,72 @@
460// data block to upload.502// data block to upload.
461func (context *StorageContext) PutBlock(container, filename, id string, data io.Reader) error {503func (context *StorageContext) PutBlock(container, filename, id string, data io.Reader) error {
462 base64_id := base64.StdEncoding.EncodeToString([]byte(id))504 base64_id := base64.StdEncoding.EncodeToString([]byte(id))
463 uri := context.getFileURL(container, filename)505 uri := addURLQueryParams(
464 uri = addURLQueryParams(506 context.getFileURL(container, filename),
465 uri,
466 "comp", "block",507 "comp", "block",
467 "blockid", base64_id)508 "blockid", base64_id)
468 req, err := http.NewRequest("PUT", uri, data)509 _, err := context.performRequest(requestParams{
469 if err != nil {510 Method: "PUT",
470 return err511 URL: uri,
471 }512 Body: data,
472 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")513 APIVersion: "2012-02-12",
473 resp, err := context.send(req, nil, http.StatusCreated)514 ExpectedStatus: http.StatusCreated,
474 if err != nil {515 })
475 return err516 if err != nil {
476 }517 return fmt.Errorf("failed to put block %s for file %s: %v", id, filename, err)
477 if resp.StatusCode != http.StatusCreated {
478 return fmt.Errorf("failed to put block: %s", resp.Status)
479 }518 }
480 return nil519 return nil
481}520}
482521
483// Send a request to piece together blocks into a list that specifies a blob.522// Send a request to piece together blocks into a list that specifies a blob.
484func (context *StorageContext) PutBlockList(container, filename string, blocklist *BlockList) error {523func (context *StorageContext) PutBlockList(container, filename string, blocklist *BlockList) error {
485 uri := context.getFileURL(container, filename)524 uri := addURLQueryParams(
486 uri = addURLQueryParams(uri, "comp", "blocklist")525 context.getFileURL(container, filename),
526 "comp", "blocklist")
487 data, err := blocklist.Serialize()527 data, err := blocklist.Serialize()
488 if err != nil {528 if err != nil {
489 return err529 return err
490 }530 }
491 data_reader := bytes.NewReader(data)531 data_reader := bytes.NewReader(data)
492 req, err := http.NewRequest("PUT", uri, data_reader)532
493 if err != nil {533 _, err = context.performRequest(requestParams{
494 return err534 Method: "PUT",
495 }535 URL: uri,
496 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")536 Body: data_reader,
497 resp, err := context.send(req, nil, http.StatusCreated)537 APIVersion: "2012-02-12",
498 if err != nil {538 ExpectedStatus: http.StatusCreated,
499 return err539 })
500 }540 if err != nil {
501 if resp.StatusCode != http.StatusCreated {541 return fmt.Errorf("failed to put blocklist for file %s: %v", filename, err)
502 return fmt.Errorf("failed to put blocklist: %s", resp.Status)
503 }542 }
504 return nil543 return nil
505}544}
506545
507// Delete the specified blob from the given container.546// Delete the specified blob from the given container.
508func (context *StorageContext) DeleteBlob(container, filename string) error {547func (context *StorageContext) DeleteBlob(container, filename string) error {
509 uri := context.getFileURL(container, filename)548 _, err := context.performRequest(requestParams{
510 req, err := http.NewRequest("DELETE", uri, nil)549 Method: "DELETE",
511 if err != nil {550 URL: context.getFileURL(container, filename),
512 return err551 APIVersion: "2012-02-12",
513 }552 ExpectedStatus: http.StatusAccepted,
514 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")553 })
515 resp, err := context.send(req, nil, http.StatusAccepted)
516 // TODO Handle a 404 with an <Error>BlobNotFound response body silently.554 // TODO Handle a 404 with an <Error>BlobNotFound response body silently.
517 if err != nil {555 if err != nil {
518 return err556 return fmt.Errorf("failed to delete blob %s: %v", filename, err)
519 }
520 if resp.StatusCode != http.StatusAccepted {
521 return fmt.Errorf("failed to delete blob: %s", resp.Status)
522 }557 }
523 return nil558 return nil
524}559}
525560
526// Get the specified blob from the given container.561// Get the specified blob from the given container.
527func (context *StorageContext) GetBlob(container, filename string) (io.ReadCloser, error) {562func (context *StorageContext) GetBlob(container, filename string) (io.ReadCloser, error) {
528 uri := context.getFileURL(container, filename)563 response, err := context.performRequest(requestParams{
529 req, err := http.NewRequest("GET", uri, nil)564 Method: "GET",
565 URL: context.getFileURL(container, filename),
566 APIVersion: "2012-02-12",
567 ExpectedStatus: http.StatusOK,
568 })
530 if err != nil {569 if err != nil {
531 return nil, err570 return nil, fmt.Errorf("failed to get blob %s: %v", filename, err)
532 }571 }
533 addStandardHeaders(req, context.Account, context.Key, "2012-02-12")572 return response.Body, nil
534 resp, errmsg := context.send(req, nil, http.StatusOK)
535 if errmsg != nil {
536 return nil, errmsg
537 }
538 if resp.StatusCode != http.StatusOK {
539 return nil, fmt.Errorf("failed to get blob: %s", resp.Status)
540 }
541 return resp.Body, nil
542}573}
543574
=== modified file 'storage_base_test.go'
--- storage_base_test.go 2013-04-30 08:42:44 +0000
+++ storage_base_test.go 2013-05-01 05:20:37 +0000
@@ -180,7 +180,8 @@
180 c.Assert(req.Header.Get("Authorization"), Equals, "")180 c.Assert(req.Header.Get("Authorization"), Equals, "")
181181
182 key := base64.StdEncoding.EncodeToString([]byte("dummykey"))182 key := base64.StdEncoding.EncodeToString([]byte("dummykey"))
183 signRequest(req, "myname", key)183 context := StorageContext{client: nil, Account: "myname", Key: key}
184 context.signRequest(req)
184185
185 expected := "SharedKey myname:Xf9hWQ99mM0IyEOL6rNeAUdTQlixVqiYnt2TpLCCpY0="186 expected := "SharedKey myname:Xf9hWQ99mM0IyEOL6rNeAUdTQlixVqiYnt2TpLCCpY0="
186 c.Assert(req.Header.Get("Authorization"), Equals, expected)187 c.Assert(req.Header.Get("Authorization"), Equals, expected)

Subscribers

People subscribed via source and target branches