Skip to content

Commit

Permalink
storage: Azure Table implementation (#217)
Browse files Browse the repository at this point in the history
Initial implementation for Azure Table Storage
  • Loading branch information
MindFlavor authored and ahmetb committed May 3, 2016
1 parent b4dd1ab commit d4e45a6
Show file tree
Hide file tree
Showing 4 changed files with 875 additions and 0 deletions.
108 changes: 108 additions & 0 deletions storage/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package storage
import (
"bytes"
"encoding/base64"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
Expand Down Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -236,6 +278,7 @@ func (c Client) buildCanonicalizedResource(uri string) (string, error) {
}

cr := "/" + c.accountName

if len(u.Path) > 0 {
cr += u.Path
}
Expand Down Expand Up @@ -266,6 +309,7 @@ func (c Client) buildCanonicalizedResource(uri string) (string, error) {
}
}
}

return cr, nil
}

Expand Down Expand Up @@ -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) {
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)
Expand Down
129 changes: 129 additions & 0 deletions storage/table.go
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

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
}
Loading

0 comments on commit d4e45a6

Please sign in to comment.