-
Notifications
You must be signed in to change notification settings - Fork 863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Azure table implementation #217
Changes from all commits
778e592
3a2ca1f
fa48f7a
bcbb55e
cff76ea
c1fc8cc
7790a43
6066ff9
ea24748
5ffabc4
5f2c805
7c25a4d
d810da5
1fbcae9
e14c38d
b5c2e22
069d5b1
8e926a4
7504f79
4e5c254
001e984
2323b79
2c7826a
5be7f82
8ce1ad0
b4339d3
de7a7c9
3e0094f
401b9d3
8bd86be
61b55bc
cba0d70
334d9e3
3367625
b6f6c50
fa2d011
27554ba
833cf1e
2d9dbdc
1d80355
fd3421a
515af2f
3db94ff
f17c583
5f16d10
3a56505
d013164
de36cad
64dec93
467f573
4aec3f7
a136306
beef942
7ed2aca
bf2e3e6
3bf9667
ca4300d
d7326d1
3cc45a3
4a2c867
f6c65e8
9a2c286
d38280f
66ee7e0
ae417ef
927b5a7
322889e
dbd2e47
5a04aa0
045caf2
e44729a
474030a
430d128
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ package storage | |
import ( | ||
"bytes" | ||
"encoding/base64" | ||
"encoding/json" | ||
"encoding/xml" | ||
"errors" | ||
"fmt" | ||
|
@@ -54,6 +55,11 @@ type storageResponse struct { | |
body io.ReadCloser | ||
} | ||
|
||
type odataResponse struct { | ||
storageResponse | ||
odata odataErrorMessage | ||
} | ||
|
||
// AzureStorageServiceError contains fields of the error response from | ||
// Azure Storage Service REST API. See https://msdn.microsoft.com/en-us/library/azure/dd179382.aspx | ||
// Some fields might be specific to certain calls. | ||
|
@@ -68,6 +74,20 @@ type AzureStorageServiceError struct { | |
RequestID string | ||
} | ||
|
||
type odataErrorMessageMessage struct { | ||
Lang string `json:"lang"` | ||
Value string `json:"value"` | ||
} | ||
|
||
type odataErrorMessageInternal struct { | ||
Code string `json:"code"` | ||
Message odataErrorMessageMessage `json:"message"` | ||
} | ||
|
||
type odataErrorMessage struct { | ||
Err odataErrorMessageInternal `json:"odata.error"` | ||
} | ||
|
||
// UnexpectedStatusCodeError is returned when a storage service responds with neither an error | ||
// nor with an HTTP status code indicating success. | ||
type UnexpectedStatusCodeError struct { | ||
|
@@ -166,6 +186,12 @@ func (c Client) GetQueueService() QueueServiceClient { | |
return QueueServiceClient{c} | ||
} | ||
|
||
// GetTableService returns a TableServiceClient which can operate on the table | ||
// service of the storage account. | ||
func (c Client) GetTableService() TableServiceClient { | ||
return TableServiceClient{c} | ||
} | ||
|
||
// GetFileService returns a FileServiceClient which can operate on the file | ||
// service of the storage account. | ||
func (c Client) GetFileService() FileServiceClient { | ||
|
@@ -228,6 +254,22 @@ func (c Client) buildCanonicalizedHeader(headers map[string]string) string { | |
return ch | ||
} | ||
|
||
func (c Client) buildCanonicalizedResourceTable(uri string) (string, error) { | ||
errMsg := "buildCanonicalizedResourceTable error: %s" | ||
u, err := url.Parse(uri) | ||
if err != nil { | ||
return "", fmt.Errorf(errMsg, err.Error()) | ||
} | ||
|
||
cr := "/" + c.accountName | ||
|
||
if len(u.Path) > 0 { | ||
cr += u.Path | ||
} | ||
|
||
return cr, nil | ||
} | ||
|
||
func (c Client) buildCanonicalizedResource(uri string) (string, error) { | ||
errMsg := "buildCanonicalizedResource error: %s" | ||
u, err := url.Parse(uri) | ||
|
@@ -236,6 +278,7 @@ func (c Client) buildCanonicalizedResource(uri string) (string, error) { | |
} | ||
|
||
cr := "/" + c.accountName | ||
|
||
if len(u.Path) > 0 { | ||
cr += u.Path | ||
} | ||
|
@@ -266,6 +309,7 @@ func (c Client) buildCanonicalizedResource(uri string) (string, error) { | |
} | ||
} | ||
} | ||
|
||
return cr, nil | ||
} | ||
|
||
|
@@ -364,6 +408,70 @@ func (c Client) exec(verb, url string, headers map[string]string, body io.Reader | |
body: resp.Body}, nil | ||
} | ||
|
||
func (c Client) execInternalJSON(verb, url string, headers map[string]string, body io.Reader) (*odataResponse, error) { | ||
req, err := http.NewRequest(verb, url, body) | ||
for k, v := range headers { | ||
req.Header.Add(k, v) | ||
} | ||
|
||
httpClient := c.HTTPClient | ||
if httpClient == nil { | ||
httpClient = http.DefaultClient | ||
} | ||
|
||
resp, err := httpClient.Do(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
respToRet := &odataResponse{} | ||
respToRet.body = resp.Body | ||
respToRet.statusCode = resp.StatusCode | ||
respToRet.headers = resp.Header | ||
|
||
statusCode := resp.StatusCode | ||
if statusCode >= 400 && statusCode <= 505 { | ||
var respBody []byte | ||
respBody, err = readResponseBody(resp) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if len(respBody) == 0 { | ||
// no error in response body | ||
err = fmt.Errorf("storage: service returned without a response body (%d)", resp.StatusCode) | ||
return respToRet, err | ||
} | ||
// try unmarshal as odata.error json | ||
err = json.Unmarshal(respBody, &respToRet.odata) | ||
return respToRet, err | ||
} | ||
|
||
return respToRet, nil | ||
} | ||
|
||
func (c Client) createSharedKeyLite(url string, headers map[string]string) (string, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'm going to switch everything to SharedKeyLite after merging. 😄 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I'm going to switch everything to SharedKeyLite after merging. 😄 |
||
can, err := c.buildCanonicalizedResourceTable(url) | ||
|
||
if err != nil { | ||
return "", err | ||
} | ||
strToSign := headers["x-ms-date"] + "\n" + can | ||
|
||
hmac := c.computeHmac256(strToSign) | ||
return fmt.Sprintf("SharedKeyLite %s:%s", c.accountName, hmac), nil | ||
} | ||
|
||
func (c Client) execTable(verb, url string, headers map[string]string, body io.Reader) (*odataResponse, error) { | ||
var err error | ||
headers["Authorization"], err = c.createSharedKeyLite(url, headers) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return c.execInternalJSON(verb, url, headers, body) | ||
} | ||
|
||
func readResponseBody(resp *http.Response) ([]byte, error) { | ||
defer resp.Body.Close() | ||
out, err := ioutil.ReadAll(resp.Body) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package storage | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
) | ||
|
||
// TableServiceClient contains operations for Microsoft Azure Table Storage | ||
// Service. | ||
type TableServiceClient struct { | ||
client Client | ||
} | ||
|
||
// AzureTable is the typedef of the Azure Table name | ||
type AzureTable string | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. most probably you don't need this typedef. Just use string for table names. :) |
||
|
||
const ( | ||
tablesURIPath = "/Tables" | ||
) | ||
|
||
type createTableRequest struct { | ||
TableName string `json:"TableName"` | ||
} | ||
|
||
func pathForTable(table AzureTable) string { return fmt.Sprintf("%s", table) } | ||
|
||
func (c *TableServiceClient) getStandardHeaders() map[string]string { | ||
return map[string]string{ | ||
"x-ms-version": "2015-02-21", | ||
"x-ms-date": currentTimeRfc1123Formatted(), | ||
"Accept": "application/json;odata=nometadata", | ||
"Accept-Charset": "UTF-8", | ||
"Content-Type": "application/json", | ||
} | ||
} | ||
|
||
// QueryTables returns the tables created in the | ||
// *TableServiceClient storage account. | ||
func (c *TableServiceClient) QueryTables() ([]AzureTable, error) { | ||
uri := c.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{}) | ||
|
||
headers := c.getStandardHeaders() | ||
headers["Content-Length"] = "0" | ||
|
||
resp, err := c.client.execTable("GET", uri, headers, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
defer resp.body.Close() | ||
|
||
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { | ||
return nil, err | ||
} | ||
|
||
buf := new(bytes.Buffer) | ||
buf.ReadFrom(resp.body) | ||
|
||
var respArray queryTablesResponse | ||
if err := json.Unmarshal(buf.Bytes(), &respArray); err != nil { | ||
return nil, err | ||
} | ||
|
||
s := make([]AzureTable, len(respArray.TableName)) | ||
for i, elem := range respArray.TableName { | ||
s[i] = AzureTable(elem.TableName) | ||
} | ||
|
||
return s, nil | ||
} | ||
|
||
// CreateTable creates the table given the specific | ||
// name. This function fails if the name is not compliant | ||
// with the specification or the tables already exists. | ||
func (c *TableServiceClient) CreateTable(table AzureTable) error { | ||
uri := c.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{}) | ||
|
||
headers := c.getStandardHeaders() | ||
|
||
req := createTableRequest{TableName: string(table)} | ||
buf := new(bytes.Buffer) | ||
|
||
if err := json.NewEncoder(buf).Encode(req); err != nil { | ||
return err | ||
} | ||
|
||
headers["Content-Length"] = fmt.Sprintf("%d", buf.Len()) | ||
|
||
resp, err := c.client.execTable("POST", uri, headers, buf) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
defer resp.body.Close() | ||
|
||
if err := checkRespCode(resp.statusCode, []int{http.StatusCreated}); err != nil { | ||
return err | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// DeleteTable deletes the table given the specific | ||
// name. This function fails if the table is not present. | ||
// Be advised: DeleteTable deletes all the entries | ||
// that may be present. | ||
func (c *TableServiceClient) DeleteTable(table AzureTable) error { | ||
uri := c.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{}) | ||
uri += fmt.Sprintf("('%s')", string(table)) | ||
|
||
headers := c.getStandardHeaders() | ||
|
||
headers["Content-Length"] = "0" | ||
|
||
resp, err := c.client.execTable("DELETE", uri, headers, nil) | ||
|
||
if err != nil { | ||
return err | ||
} | ||
defer resp.body.Close() | ||
|
||
if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil { | ||
return err | ||
|
||
} | ||
return nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MindFlavor do you ever use these error message/code fields in generating a human-readable error? perhaps we should return a typed-error that people can cast into and read these fields?