~gwacl-hackers/gwacl/trunk

14.2.1 by Julian Edwards
Add storage beginning
1
// Copyright 2013 Canonical Ltd.  This software is licensed under the
2
// GNU Lesser General Public License version 3 (see the file COPYING).
3
4
package gwacl
5
14.2.13 by Julian Edwards
rvb's review suggestions
6
// This file contains the operations necessary to work with the Azure
7
// file storage API.  For more details, see
8
// http://msdn.microsoft.com/en-us/library/windowsazure/dd179355.aspx
9
50.2.2 by Gavin Panella
TODO for fixing documentation.
10
// TODO Improve function documentation: the Go documentation convention is for
11
// function documentation to start out with the name of the function. This may
12
// have special significance for godoc.
13
14.2.3 by Julian Edwards
add TestComposeCanonicalizedResource
14
import (
17.1.2 by Julian Edwards
Add addMD5Header
15
    "bytes"
14.2.10 by Julian Edwards
Add composeAuthHeader
16
    "crypto/hmac"
17
    "crypto/sha256"
18
    "encoding/base64"
101.1.2 by Jeroen Vermeulen
Smooth out some redundant error-checking. Create performRequest method, with its Parameter Object struct. Use HTTPStatus rather than int for HTTP statuses.
19
    "errors"
14.2.5 by Julian Edwards
add toLowerKeys
20
    "fmt"
42.1.1 by Julian Edwards
Add PutBlock
21
    "io"
17.1.2 by Julian Edwards
Add addMD5Header
22
    "io/ioutil"
14.2.3 by Julian Edwards
add TestComposeCanonicalizedResource
23
    "net/http"
14.2.5 by Julian Edwards
add toLowerKeys
24
    "net/url"
25
    "sort"
14.2.3 by Julian Edwards
add TestComposeCanonicalizedResource
26
    "strings"
17.1.5 by Julian Edwards
Add Date header
27
    "time"
14.2.3 by Julian Edwards
add TestComposeCanonicalizedResource
28
)
14.2.1 by Julian Edwards
Add storage beginning
29
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
30
var headersToSign = []string{
14.2.1 by Julian Edwards
Add storage beginning
31
    "Content-Encoding",
32
    "Content-Language",
33
    "Content-Length",
34
    "Content-MD5",
35
    "Content-Type",
36
    "Date",
37
    "If-Modified-Since",
38
    "If-Match",
39
    "If-None-Match",
40
    "If-Unmodified-Since",
41
    "Range",
42
}
43
17.1.6 by Julian Edwards
allenap's review suggestions
44
// Calculate the value required for an Authorization header.
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
45
func composeAuthHeader(req *http.Request, accountName, accountKey string) string {
46
    signable := composeStringToSign(req, accountName)
14.2.12 by Julian Edwards
small cleanups
47
    // Allegedly, this is already UTF8 encoded.
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
48
    decodedKey, err := base64.StdEncoding.DecodeString(accountKey)
14.2.10 by Julian Edwards
Add composeAuthHeader
49
    if err != nil {
50
        panic(fmt.Errorf("invalid account key: %s", err))
51
    }
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
52
    hash := hmac.New(sha256.New, decodedKey)
14.2.10 by Julian Edwards
Add composeAuthHeader
53
    _, err = hash.Write([]byte(signable))
14.2.12 by Julian Edwards
small cleanups
54
    if err != nil {
55
        panic(fmt.Errorf("failed to write hash: %s", err))
56
    }
14.2.10 by Julian Edwards
Add composeAuthHeader
57
    var hashed []byte
58
    hashed = hash.Sum(hashed)
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
59
    b64Hashed := base64.StdEncoding.EncodeToString(hashed)
60
    return fmt.Sprintf("SharedKey %s:%s", accountName, b64Hashed)
14.2.10 by Julian Edwards
Add composeAuthHeader
61
}
62
17.1.6 by Julian Edwards
allenap's review suggestions
63
// Calculate the string that needs to be HMAC signed.  It is comprised of
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
64
// the headers in headersToSign, x-ms-* headers and the URI params.
65
func composeStringToSign(req *http.Request, accountName string) string {
17.1.6 by Julian Edwards
allenap's review suggestions
66
    // TODO: whitespace should be normalised in value strings.
14.2.9 by Julian Edwards
add composeStringToSign
67
    return fmt.Sprintf(
17.1.4 by Julian Edwards
Add newlines consistently in each processed section
68
        "%s\n%s%s%s", req.Method, composeHeaders(req),
17.1.3 by Julian Edwards
Add Canonicalized Headers computation
69
        composeCanonicalizedHeaders(req),
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
70
        composeCanonicalizedResource(req, accountName))
14.2.1 by Julian Edwards
Add storage beginning
71
}
72
14.2.5 by Julian Edwards
add toLowerKeys
73
// toLowerKeys lower cases all map keys. If two keys exist, that differ
74
// by the case of their keys, the values will be concatenated.
75
func toLowerKeys(values url.Values) map[string][]string {
76
    m := make(map[string][]string)
77
    for k, v := range values {
78
        k = strings.ToLower(k)
79
        m[k] = append(m[k], v...)
80
    }
81
    for _, v := range m {
82
        sort.Strings(v)
83
    }
84
    return m
85
}
86
17.1.6 by Julian Edwards
allenap's review suggestions
87
// Encode the URI params as required by the API.  They are lower-cased,
88
// sorted and formatted as param:value,value,...\nparam:value...
14.2.5 by Julian Edwards
add toLowerKeys
89
func encodeParams(values map[string][]string) string {
90
    var keys []string
91
    values = toLowerKeys(values)
92
    for k := range values {
93
        keys = append(keys, k)
94
    }
95
    sort.Strings(keys)
96
    var result []string
97
    for _, v := range keys {
98
        result = append(result, fmt.Sprintf("%v:%s", v, strings.Join(values[v], ",")))
99
    }
100
    return strings.Join(result, "\n")
101
}
102
17.1.6 by Julian Edwards
allenap's review suggestions
103
// Calculate the headers required in the string to sign.
14.2.1 by Julian Edwards
Add storage beginning
104
func composeHeaders(req *http.Request) string {
14.2.9 by Julian Edwards
add composeStringToSign
105
    var result []string
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
106
    for _, headerName := range headersToSign {
107
        result = append(result, req.Header.Get(headerName)+"\n")
14.2.1 by Julian Edwards
Add storage beginning
108
    }
17.1.4 by Julian Edwards
Add newlines consistently in each processed section
109
    return strings.Join(result, "")
14.2.1 by Julian Edwards
Add storage beginning
110
}
111
17.1.6 by Julian Edwards
allenap's review suggestions
112
// Calculate the x-ms-* headers, encode as for encodeParams.
17.1.3 by Julian Edwards
Add Canonicalized Headers computation
113
func composeCanonicalizedHeaders(req *http.Request) string {
114
    var results []string
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
115
    for headerName, values := range req.Header {
116
        headerName = strings.ToLower(headerName)
117
        if strings.HasPrefix(headerName, "x-ms-") {
118
            results = append(results, fmt.Sprintf("%v:%s\n", headerName, strings.Join(values, ",")))
17.1.3 by Julian Edwards
Add Canonicalized Headers computation
119
        }
120
    }
121
    sort.Strings(results)
122
    return strings.Join(results, "")
123
}
124
17.1.6 by Julian Edwards
allenap's review suggestions
125
// Calculate the URI params and encode them in the string.
126
// See http://msdn.microsoft.com/en-us/library/windowsazure/dd179428.aspx
127
// for details of this encoding.
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
128
func composeCanonicalizedResource(req *http.Request, accountName string) string {
14.2.3 by Julian Edwards
add TestComposeCanonicalizedResource
129
    path := req.URL.Path
130
    if !strings.HasPrefix(path, "/") {
131
        path = "/" + path
132
    }
14.2.5 by Julian Edwards
add toLowerKeys
133
14.2.8 by Julian Edwards
final tests
134
    values := req.URL.Query()
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
135
    valuesLower := toLowerKeys(values)
136
    paramString := encodeParams(valuesLower)
14.2.8 by Julian Edwards
final tests
137
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
138
    result := "/" + accountName + path
139
    if paramString != "" {
140
        result += "\n" + paramString
14.2.8 by Julian Edwards
final tests
141
    }
142
143
    return result
14.2.1 by Julian Edwards
Add storage beginning
144
}
17.1.1 by Julian Edwards
Add addVersionHeader
145
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
146
// Take the passed msVersion string and add it to the request headers.
147
func addVersionHeader(req *http.Request, msVersion string) {
148
    req.Header.Set("x-ms-version", msVersion)
17.1.1 by Julian Edwards
Add addVersionHeader
149
}
17.1.2 by Julian Edwards
Add addMD5Header
150
43.2.1 by Julian Edwards
Add content-length to outgoing requests
151
// Calculate the mD5sum and content length for the request payload and add
152
// as the Content-MD5 header and Content-Length header respectively.
153
func addContentHeaders(req *http.Request) {
154
    if req.Body == nil {
45.1.3 by Julian Edwards
changes to make PutBlock work - prevent Go from chunking the data
155
        if req.Method == "PUT" || req.Method == "POST" {
45.1.5 by Julian Edwards
Tweaks as per allenap
156
            // This cannot be set for a GET, likewise it *must* be set for
157
            // PUT and POST.
45.1.3 by Julian Edwards
changes to make PutBlock work - prevent Go from chunking the data
158
            req.Header.Set("Content-Length", "0")
159
        }
43.2.1 by Julian Edwards
Add content-length to outgoing requests
160
        return
161
    }
17.1.2 by Julian Edwards
Add addMD5Header
162
    reqdata, err := ioutil.ReadAll(req.Body)
163
    if err != nil {
164
        panic(fmt.Errorf("Unable to read request body: %s", err))
165
    }
166
    // Replace the request's data because we just destroyed it by reading it.
167
    req.Body = ioutil.NopCloser(bytes.NewReader(reqdata))
43.2.1 by Julian Edwards
Add content-length to outgoing requests
168
    req.Header.Set("Content-Length", fmt.Sprintf("%d", len(reqdata)))
45.1.3 by Julian Edwards
changes to make PutBlock work - prevent Go from chunking the data
169
    // Stop Go's http lib from chunking the data because Azure will return
170
    // an authorization error if it's chunked.
171
    req.ContentLength = int64(len(reqdata))
17.1.2 by Julian Edwards
Add addMD5Header
172
}
17.1.5 by Julian Edwards
Add Date header
173
17.1.6 by Julian Edwards
allenap's review suggestions
174
// Add a Date: header in RFC1123 format.
17.1.5 by Julian Edwards
Add Date header
175
func addDateHeader(req *http.Request) {
176
    now := time.Now().UTC().Format(time.RFC1123)
17.1.6 by Julian Edwards
allenap's review suggestions
177
    // The Azure API requires "GMT" and not "UTC".
17.1.5 by Julian Edwards
Add Date header
178
    now = strings.Replace(now, "UTC", "GMT", 1)
45.1.5 by Julian Edwards
Tweaks as per allenap
179
    req.Header.Set("Date", now)
17.1.5 by Julian Edwards
Add Date header
180
}
181
101.1.1 by Jeroen Vermeulen
Extract signRequest from addStandardHeaders, and make it a method.
182
// signRequest adds the Authorization: header to a Request.
183
// Don't make any further changes to the request before sending it, or the
184
// signature will not be valid.
185
func (context *StorageContext) signRequest(req *http.Request) {
127.2.1 by Raphael Badin
If key is empty: anon access.
186
    // Only sign the request if the key is not empty.
187
    if context.Key != "" {
188
        header := composeAuthHeader(req, context.Account, context.Key)
189
        req.Header.Set("Authorization", header)
190
    }
17.1.5 by Julian Edwards
Add Date header
191
}
192
30.1.1 by Julian Edwards
Create StorageContext.Send() and add a ListBlobs() that uses it.
193
// StorageContext keeps track of the mandatory parameters required to send a
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
194
// request to the storage services API.  It also has an HTTP Client to allow
195
// overriding for custom behaviour, during testing for example.
26.1.1 by Gavin Panella
Add StorageContext.
196
type StorageContext struct {
197
    Account string
127.2.2 by Raphael Badin
Fix comment.
198
    // Access key: access will be anonymous if the key is the empty string.
127.2.1 by Raphael Badin
If key is empty: anon access.
199
    Key    string
200
    client *http.Client
26.1.1 by Gavin Panella
Add StorageContext.
201
}
202
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
203
// getClient is used when sending a request. If a custom client is specified
204
// in context.client it is returned, otherwise net.http.DefaultClient is
205
// returned.
26.1.1 by Gavin Panella
Add StorageContext.
206
func (context *StorageContext) getClient() *http.Client {
36.1.3 by Gavin Panella
Make StorageContext.Client private.
207
    if context.client == nil {
26.1.1 by Gavin Panella
Add StorageContext.
208
        return http.DefaultClient
209
    }
36.1.3 by Gavin Panella
Make StorageContext.Client private.
210
    return context.client
26.1.1 by Gavin Panella
Add StorageContext.
211
}
212
30.1.1 by Julian Edwards
Create StorageContext.Send() and add a ListBlobs() that uses it.
213
// Any object that deserializes XML must meet this interface.
214
type Deserializer interface {
215
    Deserialize([]byte) error
216
}
217
101.1.2 by Jeroen Vermeulen
Smooth out some redundant error-checking. Create performRequest method, with its Parameter Object struct. Use HTTPStatus rather than int for HTTP statuses.
218
// requestParams is a Parameter Object for performRequest().
219
type requestParams struct {
220
    Method         string       // HTTP method, e.g. "GET" or "PUT".
221
    URL            string       // Resource locator, e.g. "http://example.com/my/resource".
222
    Body           io.Reader    // Optional request body.
223
    APIVersion     string       // Expected Azure API version, e.g. "2012-02-12".
108.3.2 by Julian Edwards
Add x-ms-blob-type header to PutBlob request
224
    ExtraHeaders   http.Header  // Optional extra request headers.
101.1.2 by Jeroen Vermeulen
Smooth out some redundant error-checking. Create performRequest method, with its Parameter Object struct. Use HTTPStatus rather than int for HTTP statuses.
225
    Result         Deserializer // Optional object to parse API response into.
226
    ExpectedStatus HTTPStatus   // Expected response status, e.g. http.StatusOK.
227
}
228
229
// Check performs a basic sanity check on the request.  This will only catch
230
// a few superficial problems that you can spot at compile time, to save a
231
// debugging cycle for the most basic mistakes.
232
func (params *requestParams) Check() {
233
    const panicPrefix = "invalid request: "
234
    if params.Method == "" {
235
        panic(errors.New(panicPrefix + "HTTP method not specified"))
236
    }
237
    if params.URL == "" {
238
        panic(errors.New(panicPrefix + "URL not specified"))
239
    }
240
    if params.APIVersion == "" {
241
        panic(errors.New(panicPrefix + "API version not specified"))
242
    }
243
    if params.ExpectedStatus == 0 {
244
        panic(errors.New(panicPrefix + "expected HTTP status not specified"))
245
    }
246
    methods := map[string]bool{"GET": true, "PUT": true, "POST": true, "DELETE": true}
247
    if _, ok := methods[params.Method]; !ok {
248
        panic(fmt.Errorf(panicPrefix+"unsupported HTTP method '%s'", params.Method))
249
    }
250
}
251
252
// performRequest issues an HTTP request to Azure.
253
func (context *StorageContext) performRequest(params requestParams) (*http.Response, error) {
101.1.10 by Jeroen Vermeulen
Review change: forgot to call requestParams.Check().
254
    params.Check()
101.1.2 by Jeroen Vermeulen
Smooth out some redundant error-checking. Create performRequest method, with its Parameter Object struct. Use HTTPStatus rather than int for HTTP statuses.
255
    req, err := http.NewRequest(params.Method, params.URL, params.Body)
256
    if err != nil {
257
        return nil, err
258
    }
108.3.2 by Julian Edwards
Add x-ms-blob-type header to PutBlob request
259
    // net/http has no way of adding headers en-masse, hence this abomination.
260
    for header, values := range params.ExtraHeaders {
261
        for _, value := range values {
262
            req.Header.Add(header, value)
263
        }
264
    }
101.1.7 by Jeroen Vermeulen
No longer need addStandardHeaders.
265
    addVersionHeader(req, params.APIVersion)
266
    addDateHeader(req)
267
    addContentHeaders(req)
101.1.2 by Jeroen Vermeulen
Smooth out some redundant error-checking. Create performRequest method, with its Parameter Object struct. Use HTTPStatus rather than int for HTTP statuses.
268
    context.signRequest(req)
269
    return context.send(req, params.Result, params.ExpectedStatus)
270
}
271
30.1.1 by Julian Edwards
Create StorageContext.Send() and add a ListBlobs() that uses it.
272
// Send a request to the storage service and process the response.
273
// The "res" parameter is typically an XML struct that will deserialize the
59.1.1 by Julian Edwards
Add error handling to the storage context send()
274
// raw XML into the struct data.  The http Response object is returned.
65.1.1 by Jeroen Vermeulen
Unify Error and ServerError as HTTPError, with different implementations based on what information is available at runtime.
275
//
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
276
// If the response's HTTP status code is not the same as "expectedStatus"
65.1.1 by Jeroen Vermeulen
Unify Error and ServerError as HTTPError, with different implementations based on what information is available at runtime.
277
// then an HTTPError will be returned as the error.  When the returned error
278
// is an HTTPError, the request response is also returned.  In other error
279
// cases, the returned response may be the one received from the server or
280
// it may be nil.
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
281
func (context *StorageContext) send(req *http.Request, res Deserializer, expectedStatus HTTPStatus) (*http.Response, error) {
30.1.1 by Julian Edwards
Create StorageContext.Send() and add a ListBlobs() that uses it.
282
    client := context.getClient()
283
    resp, err := client.Do(req)
284
    if err != nil {
65.1.1 by Jeroen Vermeulen
Unify Error and ServerError as HTTPError, with different implementations based on what information is available at runtime.
285
        return nil, err
59.1.1 by Julian Edwards
Add error handling to the storage context send()
286
    }
287
288
    var data []byte
289
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
290
    if resp.StatusCode != int(expectedStatus) {
59.1.1 by Julian Edwards
Add error handling to the storage context send()
291
        if resp.Body != nil {
292
            data, err = ioutil.ReadAll(resp.Body)
293
            if err != nil {
65.1.1 by Jeroen Vermeulen
Unify Error and ServerError as HTTPError, with different implementations based on what information is available at runtime.
294
                return resp, err
59.1.1 by Julian Edwards
Add error handling to the storage context send()
295
            }
296
        }
65.1.1 by Jeroen Vermeulen
Unify Error and ServerError as HTTPError, with different implementations based on what information is available at runtime.
297
        msg := newHTTPError(resp.StatusCode, data, "Azure request failed")
298
        return resp, msg
59.1.1 by Julian Edwards
Add error handling to the storage context send()
299
    }
300
301
    // If the caller didn't supply an object to deserialize the message into
302
    // then just return.
36.1.1 by Gavin Panella
Return the HTTP response from Send.
303
    if res == nil {
304
        return resp, nil
30.1.1 by Julian Edwards
Create StorageContext.Send() and add a ListBlobs() that uses it.
305
    }
306
101.1.8 by Jeroen Vermeulen
Fold base URL getter calls into addition of query params.
307
    // TODO: Also deserialize response headers into the "res" object.
59.1.1 by Julian Edwards
Add error handling to the storage context send()
308
    data, err = ioutil.ReadAll(resp.Body)
30.1.1 by Julian Edwards
Create StorageContext.Send() and add a ListBlobs() that uses it.
309
    if err != nil {
65.1.1 by Jeroen Vermeulen
Unify Error and ServerError as HTTPError, with different implementations based on what information is available at runtime.
310
        msg := fmt.Errorf("failed to read response data: %s", err)
311
        return resp, msg
30.1.1 by Julian Edwards
Create StorageContext.Send() and add a ListBlobs() that uses it.
312
    }
313
    err = res.Deserialize(data)
314
    if err != nil {
65.1.1 by Jeroen Vermeulen
Unify Error and ServerError as HTTPError, with different implementations based on what information is available at runtime.
315
        msg := fmt.Errorf("Failed to deserialize data: %s", err)
316
        return resp, msg
30.1.1 by Julian Edwards
Create StorageContext.Send() and add a ListBlobs() that uses it.
317
    }
318
36.1.1 by Gavin Panella
Return the HTTP response from Send.
319
    return resp, nil
30.1.1 by Julian Edwards
Create StorageContext.Send() and add a ListBlobs() that uses it.
320
}
321
98.3.1 by Jeroen Vermeulen
Test, and create, helpers to compute the basic storage URLs.
322
// getAccountURL returns the base URL for the context's storage account.
323
// (The result ends in a slash.)
324
func (context *StorageContext) getAccountURL() string {
98.3.7 by Jeroen Vermeulen
No longer need interpolateURL. If we ever need it again, it's in revision history.
325
    escapedAccount := url.QueryEscape(context.Account)
326
    return fmt.Sprintf("http://%s.blob.core.windows.net/", escapedAccount)
98.3.1 by Jeroen Vermeulen
Test, and create, helpers to compute the basic storage URLs.
327
}
328
329
// getContainerURL returns the URL for a given storage container.
330
// (The result does not end in a slash.)
331
func (context *StorageContext) getContainerURL(container string) string {
332
    return context.getAccountURL() + url.QueryEscape(container)
333
}
334
114.2.1 by Raphael Badin
Export getFileURL.
335
// GetFileURL returns the URL for a given file in the given container.
98.3.1 by Jeroen Vermeulen
Test, and create, helpers to compute the basic storage URLs.
336
// (The result does not end in a slash.)
114.2.1 by Raphael Badin
Export getFileURL.
337
func (context *StorageContext) GetFileURL(container, filename string) string {
98.3.1 by Jeroen Vermeulen
Test, and create, helpers to compute the basic storage URLs.
338
    return context.getContainerURL(container) + "/" + url.QueryEscape(filename)
339
}
340
106.1.3 by Gavin Panella
New struct ListContainersRequest, similar to ListBlobsRequest.
341
type ListContainersRequest struct {
342
    Marker string
343
}
344
106.1.2 by Gavin Panella
Rename ListContainers to ListAllContainers, and getListContainersBatch to ListContainers.
345
// ListContainers calls the "List Containers" operation on the storage
98.1.3 by Jeroen Vermeulen
Support batching in ListBlobs(). Doesn't really test stripping and escaping of the marker yet; eliminate duplication first.
346
// API, and returns a single batch of results.
93.3.7 by Jeroen Vermeulen
Satisfy test.
347
// The marker argument should be empty for a new List Containers request.  for
348
// subsequent calls to get additional batches of the same result, pass the
98.1.3 by Jeroen Vermeulen
Support batching in ListBlobs(). Doesn't really test stripping and escaping of the marker yet; eliminate duplication first.
349
// NextMarker from the previous call's result.
106.1.3 by Gavin Panella
New struct ListContainersRequest, similar to ListBlobsRequest.
350
func (context *StorageContext) ListContainers(request *ListContainersRequest) (*ContainerEnumerationResults, error) {
101.1.8 by Jeroen Vermeulen
Fold base URL getter calls into addition of query params.
351
    uri := addURLQueryParams(context.getAccountURL(), "comp", "list")
106.1.3 by Gavin Panella
New struct ListContainersRequest, similar to ListBlobsRequest.
352
    if request.Marker != "" {
353
        uri = addURLQueryParams(uri, "marker", request.Marker)
96.2.2 by Jeroen Vermeulen
Satisfy test.
354
    }
93.3.7 by Jeroen Vermeulen
Satisfy test.
355
    containers := ContainerEnumerationResults{}
101.1.4 by Jeroen Vermeulen
Merge trunk.
356
    _, err := context.performRequest(requestParams{
101.1.3 by Jeroen Vermeulen
Convert one call site to performRequest, and add error context.
357
        Method:         "GET",
358
        URL:            uri,
359
        APIVersion:     "2012-02-12",
360
        Result:         &containers,
361
        ExpectedStatus: http.StatusOK,
362
    })
93.3.8 by Jeroen Vermeulen
Review suggestion: don't try to relay unreliable results in the error case.
363
    if err != nil {
122.2.1 by Raphael Badin
Propagate Azure errors, add IsNotFoundError() method.
364
        msg := "request for containers list failed: "
365
        return nil, extendError(err, msg)
93.3.8 by Jeroen Vermeulen
Review suggestion: don't try to relay unreliable results in the error case.
366
    }
98.1.3 by Jeroen Vermeulen
Support batching in ListBlobs(). Doesn't really test stripping and escaping of the marker yet; eliminate duplication first.
367
    return &containers, nil
17.1.6 by Julian Edwards
allenap's review suggestions
368
}
30.1.1 by Julian Edwards
Create StorageContext.Send() and add a ListBlobs() that uses it.
369
105.1.1 by Gavin Panella
New struct, ListBlobsRequest, for passing complex arguments.
370
type ListBlobsRequest struct {
371
    Container string
372
    Marker    string
105.1.5 by Gavin Panella
Pass a prefix in the call if one is specified.
373
    Prefix    string
105.1.1 by Gavin Panella
New struct, ListBlobsRequest, for passing complex arguments.
374
}
375
105.1.2 by Gavin Panella
Move ListBlobs to storage.go, rename it to ListAllBlobs, and rename getListBlobBatch to ListBlobs.
376
// ListBlobs calls the "List Blobs" operation on the storage API, and returns
377
// a single batch of results.
378
// The request.Marker argument should be empty for a new List Blobs request.
379
// For subsequent calls to get additional batches of the same result, pass the
98.1.3 by Jeroen Vermeulen
Support batching in ListBlobs(). Doesn't really test stripping and escaping of the marker yet; eliminate duplication first.
380
// NextMarker from the previous call's result.
105.1.2 by Gavin Panella
Move ListBlobs to storage.go, rename it to ListAllBlobs, and rename getListBlobBatch to ListBlobs.
381
func (context *StorageContext) ListBlobs(request *ListBlobsRequest) (*BlobEnumerationResults, error) {
101.1.8 by Jeroen Vermeulen
Fold base URL getter calls into addition of query params.
382
    uri := addURLQueryParams(
105.1.1 by Gavin Panella
New struct, ListBlobsRequest, for passing complex arguments.
383
        context.getContainerURL(request.Container),
98.3.5 by Jeroen Vermeulen
Review suggestions: use variadic parameters for addURLQueryParams(), scratch the singular version, and just panic when it's passed a malformed URL so there's no need to return an error.
384
        "restype", "container",
385
        "comp", "list")
105.1.1 by Gavin Panella
New struct, ListBlobsRequest, for passing complex arguments.
386
    if request.Marker != "" {
387
        uri = addURLQueryParams(uri, "marker", request.Marker)
98.1.3 by Jeroen Vermeulen
Support batching in ListBlobs(). Doesn't really test stripping and escaping of the marker yet; eliminate duplication first.
388
    }
105.1.5 by Gavin Panella
Pass a prefix in the call if one is specified.
389
    if request.Prefix != "" {
390
        uri = addURLQueryParams(uri, "prefix", request.Prefix)
391
    }
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
392
    blobs := BlobEnumerationResults{}
393
    _, err := context.performRequest(requestParams{
394
        Method:         "GET",
395
        URL:            uri,
396
        APIVersion:     "2012-02-12",
397
        Result:         &blobs,
398
        ExpectedStatus: http.StatusOK,
399
    })
30.1.2 by Julian Edwards
check an error
400
    if err != nil {
122.2.1 by Raphael Badin
Propagate Azure errors, add IsNotFoundError() method.
401
        msg := "request for blobs list failed: "
402
        return nil, extendError(err, msg)
30.1.2 by Julian Edwards
check an error
403
    }
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
404
    return &blobs, err
98.1.1 by Jeroen Vermeulen
Copy tests from earlier, oversized branch. Split out getListBlobsBatch(), but don't add batching yet. Tests: 1 panic, 1 failure.
405
}
406
59.1.1 by Julian Edwards
Add error handling to the storage context send()
407
// Send a request to the storage service to create a new container.  If the
408
// request fails, error is non-nil.
37.2.1 by Gavin Panella
New CreateContainer Storage API method.
409
func (context *StorageContext) CreateContainer(container string) error {
101.1.8 by Jeroen Vermeulen
Fold base URL getter calls into addition of query params.
410
    uri := addURLQueryParams(
411
        context.getContainerURL(container),
412
        "restype", "container")
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
413
    _, err := context.performRequest(requestParams{
414
        Method:         "PUT",
415
        URL:            uri,
416
        APIVersion:     "2012-02-12",
417
        ExpectedStatus: http.StatusCreated,
418
    })
37.2.1 by Gavin Panella
New CreateContainer Storage API method.
419
    if err != nil {
122.2.1 by Raphael Badin
Propagate Azure errors, add IsNotFoundError() method.
420
        msg := fmt.Sprintf("failed to create container %s: ", container)
421
        return extendError(err, msg)
37.2.1 by Gavin Panella
New CreateContainer Storage API method.
422
    }
37.2.2 by Gavin Panella
Sigh.
423
    return nil
37.2.1 by Gavin Panella
New CreateContainer Storage API method.
424
}
37.2.4 by Gavin Panella
Merge trunk.
425
108.3.1 by Julian Edwards
Use struct as request params for PutBlob and add a blob_type
426
type PutBlobRequest struct {
108.3.6 by Julian Edwards
go fmt
427
    Container string // Container name in the storage account
428
    BlobType  string // Pass "page" or "block"
429
    Filename  string // Filename for the new blob
115.1.2 by Julian Edwards
Take a size param for page putblobs
430
    Size      int    // Size for the new blob. Only required for page blobs.
108.3.1 by Julian Edwards
Use struct as request params for PutBlob and add a blob_type
431
}
432
37.1.1 by Julian Edwards
make PutBlobk
433
// Send a request to create a space to upload a blob.  Note that this does not
434
// do the uploading, it just makes an empty file.
108.3.1 by Julian Edwards
Use struct as request params for PutBlob and add a blob_type
435
func (context *StorageContext) PutBlob(req *PutBlobRequest) error {
108.3.4 by Julian Edwards
jtv's review comments addressed
436
    var blobType string
437
    switch req.BlobType {
108.3.2 by Julian Edwards
Add x-ms-blob-type header to PutBlob request
438
    case "page":
108.3.4 by Julian Edwards
jtv's review comments addressed
439
        blobType = "PageBlob"
115.1.2 by Julian Edwards
Take a size param for page putblobs
440
        if req.Size == 0 {
441
            return fmt.Errorf("Must supply a size for a page blob")
442
        }
134.1.1 by Raphael Badin
Add GetDeployment method. Add Deployment.GetFQDN() method.
443
        if req.Size%512 != 0 {
127.3.5 by Julian Edwards
gavin's review comments
444
            return fmt.Errorf("Size must be a multiple of 512 bytes")
127.3.4 by Julian Edwards
PutBlob checks for size being multiple of 512 bytes
445
        }
108.3.2 by Julian Edwards
Add x-ms-blob-type header to PutBlob request
446
    case "block":
108.3.4 by Julian Edwards
jtv's review comments addressed
447
        blobType = "BlockBlob"
108.3.2 by Julian Edwards
Add x-ms-blob-type header to PutBlob request
448
    default:
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
449
        panic("blockType must be 'page' or 'block'")
108.3.1 by Julian Edwards
Use struct as request params for PutBlob and add a blob_type
450
    }
451
108.3.4 by Julian Edwards
jtv's review comments addressed
452
    extraHeaders := http.Header{}
453
    extraHeaders.Add("x-ms-blob-type", blobType)
115.1.2 by Julian Edwards
Take a size param for page putblobs
454
    if req.BlobType == "page" {
455
        size := fmt.Sprintf("%d", req.Size)
456
        extraHeaders.Add("x-ms-blob-content-length", size)
457
    }
108.3.2 by Julian Edwards
Add x-ms-blob-type header to PutBlob request
458
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
459
    _, err := context.performRequest(requestParams{
108.3.6 by Julian Edwards
go fmt
460
        Method:         "PUT",
114.2.1 by Raphael Badin
Export getFileURL.
461
        URL:            context.GetFileURL(req.Container, req.Filename),
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
462
        APIVersion:     "2012-02-12",
108.3.4 by Julian Edwards
jtv's review comments addressed
463
        ExtraHeaders:   extraHeaders,
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
464
        ExpectedStatus: http.StatusCreated,
465
    })
37.1.1 by Julian Edwards
make PutBlobk
466
    if err != nil {
122.2.1 by Raphael Badin
Propagate Azure errors, add IsNotFoundError() method.
467
        msg := fmt.Sprintf("failed to create blob %s: ", req.Filename)
468
        return extendError(err, msg)
37.1.2 by Julian Edwards
Extra status code check
469
    }
470
    return nil
37.1.1 by Julian Edwards
make PutBlobk
471
}
42.1.1 by Julian Edwards
Add PutBlock
472
113.1.2 by Julian Edwards
Add first cut of putpage
473
type PutPageRequest struct {
474
    Container  string    // Container name in the storage account
475
    Filename   string    // The blob's file name
476
    StartRange int       // Must be modulo 512, or an error is returned.
477
    EndRange   int       // Must be (modulo 512)-1, or an error is returned.
478
    Data       io.Reader // The data to upload to the page.
479
}
480
481
// Send a request to add a range of data into a page blob.
113.1.5 by Julian Edwards
Add reference to API page on web
482
// See http://msdn.microsoft.com/en-us/library/windowsazure/ee691975.aspx
113.1.2 by Julian Edwards
Add first cut of putpage
483
func (context *StorageContext) PutPage(req *PutPageRequest) error {
127.3.5 by Julian Edwards
gavin's review comments
484
    validStart := (req.StartRange % 512) == 0
485
    validEnd := (req.EndRange % 512) == 511
127.3.2 by Julian Edwards
Make PutPage reject invalid ranges
486
    if !(validStart && validEnd) {
487
        return fmt.Errorf(
127.3.5 by Julian Edwards
gavin's review comments
488
            "StartRange must be a multiple of 512, EndRange must be one less than a multiple of 512")
127.3.2 by Julian Edwards
Make PutPage reject invalid ranges
489
    }
113.1.2 by Julian Edwards
Add first cut of putpage
490
    uri := addURLQueryParams(
114.2.1 by Raphael Badin
Export getFileURL.
491
        context.GetFileURL(req.Container, req.Filename),
113.1.2 by Julian Edwards
Add first cut of putpage
492
        "comp", "page")
493
494
    extraHeaders := http.Header{}
495
496
    rangeData := fmt.Sprintf("bytes=%d-%d", req.StartRange, req.EndRange)
497
    extraHeaders.Add("x-ms-range", rangeData)
498
    extraHeaders.Add("x-ms-page-write", "update")
499
500
    _, err := context.performRequest(requestParams{
501
        Method:         "PUT",
502
        URL:            uri,
503
        Body:           req.Data,
504
        APIVersion:     "2012-02-12",
505
        ExtraHeaders:   extraHeaders,
506
        ExpectedStatus: http.StatusCreated,
507
    })
508
    if err != nil {
122.2.1 by Raphael Badin
Propagate Azure errors, add IsNotFoundError() method.
509
        msg := fmt.Sprintf("failed to put page for file %s: ", req.Filename)
510
        return extendError(err, msg)
113.1.2 by Julian Edwards
Add first cut of putpage
511
    }
512
    return nil
513
}
514
51.1.2 by Julian Edwards
add GetBlockList
515
// Send a request to fetch the list of blocks that have been uploaded as part
516
// of a block blob.
517
func (context *StorageContext) GetBlockList(container, filename string) (*GetBlockList, error) {
101.1.8 by Jeroen Vermeulen
Fold base URL getter calls into addition of query params.
518
    uri := addURLQueryParams(
114.2.1 by Raphael Badin
Export getFileURL.
519
        context.GetFileURL(container, filename),
98.3.5 by Jeroen Vermeulen
Review suggestions: use variadic parameters for addURLQueryParams(), scratch the singular version, and just panic when it's passed a malformed URL so there's no need to return an error.
520
        "comp", "blocklist",
521
        "blocklisttype", "all")
99.1.1 by Jeroen Vermeulen
Use deserialization built into send(). It's identical, including error messages.
522
    bl := GetBlockList{}
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
523
    _, err := context.performRequest(requestParams{
524
        Method:         "GET",
525
        URL:            uri,
526
        APIVersion:     "2012-02-12",
527
        Result:         &bl,
528
        ExpectedStatus: http.StatusOK,
529
    })
530
    if err != nil {
122.2.1 by Raphael Badin
Propagate Azure errors, add IsNotFoundError() method.
531
        msg := fmt.Sprintf("request for block list in file %s failed: ", filename)
532
        return nil, extendError(err, msg)
51.1.2 by Julian Edwards
add GetBlockList
533
    }
99.1.1 by Jeroen Vermeulen
Use deserialization built into send(). It's identical, including error messages.
534
    return &bl, nil
51.1.2 by Julian Edwards
add GetBlockList
535
}
536
42.1.1 by Julian Edwards
Add PutBlock
537
// Send a request to create a new block.  The request payload contains the
538
// data block to upload.
539
func (context *StorageContext) PutBlock(container, filename, id string, data io.Reader) error {
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
540
    base64ID := base64.StdEncoding.EncodeToString([]byte(id))
101.1.8 by Jeroen Vermeulen
Fold base URL getter calls into addition of query params.
541
    uri := addURLQueryParams(
114.2.1 by Raphael Badin
Export getFileURL.
542
        context.GetFileURL(container, filename),
98.3.5 by Jeroen Vermeulen
Review suggestions: use variadic parameters for addURLQueryParams(), scratch the singular version, and just panic when it's passed a malformed URL so there's no need to return an error.
543
        "comp", "block",
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
544
        "blockid", base64ID)
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
545
    _, err := context.performRequest(requestParams{
546
        Method:         "PUT",
547
        URL:            uri,
548
        Body:           data,
549
        APIVersion:     "2012-02-12",
550
        ExpectedStatus: http.StatusCreated,
551
    })
552
    if err != nil {
122.2.1 by Raphael Badin
Propagate Azure errors, add IsNotFoundError() method.
553
        msg := fmt.Sprintf("failed to put block %s for file %s: ", id, filename)
554
        return extendError(err, msg)
42.1.1 by Julian Edwards
Add PutBlock
555
    }
556
    return nil
557
}
45.1.1 by Julian Edwards
Add a PutBlockList call
558
559
// Send a request to piece together blocks into a list that specifies a blob.
560
func (context *StorageContext) PutBlockList(container, filename string, blocklist *BlockList) error {
101.1.8 by Jeroen Vermeulen
Fold base URL getter calls into addition of query params.
561
    uri := addURLQueryParams(
114.2.1 by Raphael Badin
Export getFileURL.
562
        context.GetFileURL(container, filename),
101.1.8 by Jeroen Vermeulen
Fold base URL getter calls into addition of query params.
563
        "comp", "blocklist")
45.1.1 by Julian Edwards
Add a PutBlockList call
564
    data, err := blocklist.Serialize()
565
    if err != nil {
566
        return err
567
    }
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
568
    dataReader := bytes.NewReader(data)
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
569
570
    _, err = context.performRequest(requestParams{
571
        Method:         "PUT",
572
        URL:            uri,
139.1.1 by Gavin Panella
Camel-case everything, clean-up some comments.
573
        Body:           dataReader,
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
574
        APIVersion:     "2012-02-12",
575
        ExpectedStatus: http.StatusCreated,
576
    })
577
    if err != nil {
122.2.1 by Raphael Badin
Propagate Azure errors, add IsNotFoundError() method.
578
        msg := fmt.Sprintf("failed to put blocklist for file %s: ", filename)
579
        return extendError(err, msg)
45.1.1 by Julian Edwards
Add a PutBlockList call
580
    }
581
    return nil
582
}
50.2.1 by Gavin Panella
New Storage API operation, DeleteBlob().
583
124.1.1 by Raphael Badin
Deleting a non-existant blob does not return an error.
584
// Delete the specified blob from the given container.  Deleting a non-existant
585
// blob will return without an error.
50.2.1 by Gavin Panella
New Storage API operation, DeleteBlob().
586
func (context *StorageContext) DeleteBlob(container, filename string) error {
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
587
    _, err := context.performRequest(requestParams{
588
        Method:         "DELETE",
114.2.1 by Raphael Badin
Export getFileURL.
589
        URL:            context.GetFileURL(container, filename),
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
590
        APIVersion:     "2012-02-12",
591
        ExpectedStatus: http.StatusAccepted,
592
    })
50.2.1 by Gavin Panella
New Storage API operation, DeleteBlob().
593
    if err != nil {
124.1.1 by Raphael Badin
Deleting a non-existant blob does not return an error.
594
        // If the error is an Azure 404 error, return silently: the blob does
595
        // not exist.
596
        if IsNotFoundError(err) {
597
            return nil
598
        }
122.2.1 by Raphael Badin
Propagate Azure errors, add IsNotFoundError() method.
599
        msg := fmt.Sprintf("failed to delete blob %s: ", filename)
600
        return extendError(err, msg)
50.2.1 by Gavin Panella
New Storage API operation, DeleteBlob().
601
    }
602
    return nil
603
}
54.1.1 by Gavin Panella
New Storage API operation, GetBlob().
604
605
// Get the specified blob from the given container.
606
func (context *StorageContext) GetBlob(container, filename string) (io.ReadCloser, error) {
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
607
    response, err := context.performRequest(requestParams{
608
        Method:         "GET",
114.2.1 by Raphael Badin
Export getFileURL.
609
        URL:            context.GetFileURL(container, filename),
101.1.5 by Jeroen Vermeulen
Convert remaining callsites.
610
        APIVersion:     "2012-02-12",
611
        ExpectedStatus: http.StatusOK,
612
    })
613
    if err != nil {
122.2.1 by Raphael Badin
Propagate Azure errors, add IsNotFoundError() method.
614
        msg := fmt.Sprintf("failed to get blob %s: ", filename)
615
        return nil, extendError(err, msg)
101.1.2 by Jeroen Vermeulen
Smooth out some redundant error-checking. Create performRequest method, with its Parameter Object struct. Use HTTPStatus rather than int for HTTP statuses.
616
    }
617
    return response.Body, nil
54.1.1 by Gavin Panella
New Storage API operation, GetBlob().
618
}
127.4.2 by Julian Edwards
first stab at SetContainerACL
619
620
type SetContainerACLRequest struct {
621
    Container string // Container name in the storage account
622
    Access    string // "container", "blob", or "private"
623
}
624
625
// SetContainerACL sets the specified container's access rights.
626
// See http://msdn.microsoft.com/en-us/library/windowsazure/dd179391.aspx
627
func (context *StorageContext) SetContainerACL(req *SetContainerACLRequest) error {
628
    uri := addURLQueryParams(
629
        context.getContainerURL(req.Container),
630
        "restype", "container",
631
        "comp", "acl")
632
633
    extraHeaders := http.Header{}
634
    switch req.Access {
635
    case "container", "blob":
636
        extraHeaders.Add("x-ms-blob-public-access", req.Access)
637
    case "private":
638
        // Don't add a header, Azure resets to private if it's omitted.
639
    default:
640
        panic("Access must be one of 'container', 'blob' or 'private'")
641
    }
642
643
    _, err := context.performRequest(requestParams{
127.4.8 by Julian Edwards
format
644
        Method:         "PUT",
645
        URL:            uri,
646
        APIVersion:     "2009-09-19",
647
        ExtraHeaders:   extraHeaders,
127.4.2 by Julian Edwards
first stab at SetContainerACL
648
        ExpectedStatus: http.StatusOK,
649
    })
650
651
    if err != nil {
652
        msg := fmt.Sprintf("failed to set ACL for container %s: ", req.Container)
653
        return extendError(err, msg)
654
    }
655
    return nil
656
}