// Package storage provides clients for Microsoft Azure Storage Services. package storage // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. import ( "bufio" "encoding/base64" "encoding/json" "encoding/xml" "errors" "fmt" "io" "io/ioutil" "mime" "mime/multipart" "net/http" "net/url" "regexp" "runtime" "strconv" "strings" "time" "github.com/Azure/azure-sdk-for-go/version" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure" ) const ( // DefaultBaseURL is the domain name used for storage requests in the // public cloud when a default client is created. DefaultBaseURL = "core.windows.net" // DefaultAPIVersion is the Azure Storage API version string used when a // basic client is created. DefaultAPIVersion = "2018-03-28" defaultUseHTTPS = true defaultRetryAttempts = 5 defaultRetryDuration = time.Second * 5 // StorageEmulatorAccountName is the fixed storage account used by Azure Storage Emulator StorageEmulatorAccountName = "devstoreaccount1" // StorageEmulatorAccountKey is the the fixed storage account used by Azure Storage Emulator StorageEmulatorAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" blobServiceName = "blob" tableServiceName = "table" queueServiceName = "queue" fileServiceName = "file" storageEmulatorBlob = "127.0.0.1:10000" storageEmulatorTable = "127.0.0.1:10002" storageEmulatorQueue = "127.0.0.1:10001" userAgentHeader = "User-Agent" userDefinedMetadataHeaderPrefix = "x-ms-meta-" connectionStringAccountName = "accountname" connectionStringAccountKey = "accountkey" connectionStringEndpointSuffix = "endpointsuffix" connectionStringEndpointProtocol = "defaultendpointsprotocol" connectionStringBlobEndpoint = "blobendpoint" connectionStringFileEndpoint = "fileendpoint" connectionStringQueueEndpoint = "queueendpoint" connectionStringTableEndpoint = "tableendpoint" connectionStringSAS = "sharedaccesssignature" ) var ( validStorageAccount = regexp.MustCompile("^[0-9a-z]{3,24}$") validCosmosAccount = regexp.MustCompile("^[0-9a-z-]{3,44}$") defaultValidStatusCodes = []int{ http.StatusRequestTimeout, // 408 http.StatusInternalServerError, // 500 http.StatusBadGateway, // 502 http.StatusServiceUnavailable, // 503 http.StatusGatewayTimeout, // 504 } ) // Sender sends a request type Sender interface { Send(*Client, *http.Request) (*http.Response, error) } // DefaultSender is the default sender for the client. It implements // an automatic retry strategy. type DefaultSender struct { RetryAttempts int RetryDuration time.Duration ValidStatusCodes []int attempts int // used for testing } // Send is the default retry strategy in the client func (ds *DefaultSender) Send(c *Client, req *http.Request) (resp *http.Response, err error) { rr := autorest.NewRetriableRequest(req) for attempts := 0; attempts < ds.RetryAttempts; attempts++ { err = rr.Prepare() if err != nil { return resp, err } resp, err = c.HTTPClient.Do(rr.Request()) if err == nil && !autorest.ResponseHasStatusCode(resp, ds.ValidStatusCodes...) { return resp, err } drainRespBody(resp) autorest.DelayForBackoff(ds.RetryDuration, attempts, req.Cancel) ds.attempts = attempts } ds.attempts++ return resp, err } // Client is the object that needs to be constructed to perform // operations on the storage account. type Client struct { // HTTPClient is the http.Client used to initiate API // requests. http.DefaultClient is used when creating a // client. HTTPClient *http.Client // Sender is an interface that sends the request. Clients are // created with a DefaultSender. The DefaultSender has an // automatic retry strategy built in. The Sender can be customized. Sender Sender accountName string accountKey []byte useHTTPS bool UseSharedKeyLite bool baseURL string apiVersion string userAgent string sasClient bool accountSASToken url.Values additionalHeaders map[string]string } type odataResponse struct { resp *http.Response odata odataErrorWrapper } // 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. type AzureStorageServiceError struct { Code string `xml:"Code"` Message string `xml:"Message"` AuthenticationErrorDetail string `xml:"AuthenticationErrorDetail"` QueryParameterName string `xml:"QueryParameterName"` QueryParameterValue string `xml:"QueryParameterValue"` Reason string `xml:"Reason"` Lang string StatusCode int RequestID string Date string APIVersion string } // AzureTablesServiceError contains fields of the error response from // Azure Table Storage Service REST API in Atom format. // See https://msdn.microsoft.com/en-us/library/azure/dd179382.aspx type AzureTablesServiceError struct { Code string `xml:"code"` Message string `xml:"message"` StatusCode int RequestID string Date string APIVersion string } func (e AzureTablesServiceError) Error() string { return fmt.Sprintf("storage: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=%s, RequestInitiated=%s, RequestId=%s, API Version=%s", e.StatusCode, e.Code, e.Message, e.Date, e.RequestID, e.APIVersion) } type odataErrorMessage struct { Lang string `json:"lang"` Value string `json:"value"` } type odataError struct { Code string `json:"code"` Message odataErrorMessage `json:"message"` } type odataErrorWrapper struct { Err odataError `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 { allowed []int got int inner error } func (e UnexpectedStatusCodeError) Error() string { s := func(i int) string { return fmt.Sprintf("%d %s", i, http.StatusText(i)) } got := s(e.got) expected := []string{} for _, v := range e.allowed { expected = append(expected, s(v)) } return fmt.Sprintf("storage: status code from service response is %s; was expecting %s. Inner error: %+v", got, strings.Join(expected, " or "), e.inner) } // Got is the actual status code returned by Azure. func (e UnexpectedStatusCodeError) Got() int { return e.got } // Inner returns any inner error info. func (e UnexpectedStatusCodeError) Inner() error { return e.inner } // NewClientFromConnectionString creates a Client from the connection string. func NewClientFromConnectionString(input string) (Client, error) { // build a map of connection string key/value pairs parts := map[string]string{} for _, pair := range strings.Split(input, ";") { if pair == "" { continue } equalDex := strings.IndexByte(pair, '=') if equalDex <= 0 { return Client{}, fmt.Errorf("Invalid connection segment %q", pair) } value := strings.TrimSpace(pair[equalDex+1:]) key := strings.TrimSpace(strings.ToLower(pair[:equalDex])) parts[key] = value } // TODO: validate parameter sets? if parts[connectionStringAccountName] == StorageEmulatorAccountName { return NewEmulatorClient() } if parts[connectionStringSAS] != "" { endpoint := "" if parts[connectionStringBlobEndpoint] != "" { endpoint = parts[connectionStringBlobEndpoint] } else if parts[connectionStringFileEndpoint] != "" { endpoint = parts[connectionStringFileEndpoint] } else if parts[connectionStringQueueEndpoint] != "" { endpoint = parts[connectionStringQueueEndpoint] } else { endpoint = parts[connectionStringTableEndpoint] } return NewAccountSASClientFromEndpointToken(endpoint, parts[connectionStringSAS]) } useHTTPS := defaultUseHTTPS if parts[connectionStringEndpointProtocol] != "" { useHTTPS = parts[connectionStringEndpointProtocol] == "https" } return NewClient(parts[connectionStringAccountName], parts[connectionStringAccountKey], parts[connectionStringEndpointSuffix], DefaultAPIVersion, useHTTPS) } // NewBasicClient constructs a Client with given storage service name and // key. func NewBasicClient(accountName, accountKey string) (Client, error) { if accountName == StorageEmulatorAccountName { return NewEmulatorClient() } return NewClient(accountName, accountKey, DefaultBaseURL, DefaultAPIVersion, defaultUseHTTPS) } // NewBasicClientOnSovereignCloud constructs a Client with given storage service name and // key in the referenced cloud. func NewBasicClientOnSovereignCloud(accountName, accountKey string, env azure.Environment) (Client, error) { if accountName == StorageEmulatorAccountName { return NewEmulatorClient() } return NewClient(accountName, accountKey, env.StorageEndpointSuffix, DefaultAPIVersion, defaultUseHTTPS) } //NewEmulatorClient contructs a Client intended to only work with Azure //Storage Emulator func NewEmulatorClient() (Client, error) { return NewClient(StorageEmulatorAccountName, StorageEmulatorAccountKey, DefaultBaseURL, DefaultAPIVersion, false) } // NewClient constructs a Client. This should be used if the caller wants // to specify whether to use HTTPS, a specific REST API version or a custom // storage endpoint than Azure Public Cloud. func NewClient(accountName, accountKey, serviceBaseURL, apiVersion string, useHTTPS bool) (Client, error) { var c Client if !IsValidStorageAccount(accountName) { return c, fmt.Errorf("azure: account name is not valid: it must be between 3 and 24 characters, and only may contain numbers and lowercase letters: %v", accountName) } else if accountKey == "" { return c, fmt.Errorf("azure: account key required") } else if serviceBaseURL == "" { return c, fmt.Errorf("azure: base storage service url required") } key, err := base64.StdEncoding.DecodeString(accountKey) if err != nil { return c, fmt.Errorf("azure: malformed storage account key: %v", err) } return newClient(accountName, key, serviceBaseURL, apiVersion, useHTTPS) } // NewCosmosClient constructs a Client for Azure CosmosDB. This should be used if the caller wants // to specify whether to use HTTPS, a specific REST API version or a custom // cosmos endpoint than Azure Public Cloud. func NewCosmosClient(accountName, accountKey, serviceBaseURL, apiVersion string, useHTTPS bool) (Client, error) { var c Client if !IsValidCosmosAccount(accountName) { return c, fmt.Errorf("azure: account name is not valid: The name can contain only lowercase letters, numbers and the '-' character, and must be between 3 and 44 characters: %v", accountName) } else if accountKey == "" { return c, fmt.Errorf("azure: account key required") } else if serviceBaseURL == "" { return c, fmt.Errorf("azure: base storage service url required") } key, err := base64.StdEncoding.DecodeString(accountKey) if err != nil { return c, fmt.Errorf("azure: malformed cosmos account key: %v", err) } return newClient(accountName, key, serviceBaseURL, apiVersion, useHTTPS) } // newClient constructs a Client with given parameters. func newClient(accountName string, accountKey []byte, serviceBaseURL, apiVersion string, useHTTPS bool) (Client, error) { c := Client{ HTTPClient: http.DefaultClient, accountName: accountName, accountKey: accountKey, useHTTPS: useHTTPS, baseURL: serviceBaseURL, apiVersion: apiVersion, sasClient: false, UseSharedKeyLite: false, Sender: &DefaultSender{ RetryAttempts: defaultRetryAttempts, ValidStatusCodes: defaultValidStatusCodes, RetryDuration: defaultRetryDuration, }, } c.userAgent = c.getDefaultUserAgent() return c, nil } // IsValidStorageAccount checks if the storage account name is valid. // See https://docs.microsoft.com/en-us/azure/storage/storage-create-storage-account func IsValidStorageAccount(account string) bool { return validStorageAccount.MatchString(account) } // IsValidCosmosAccount checks if the Cosmos account name is valid. // See https://docs.microsoft.com/en-us/azure/cosmos-db/how-to-manage-database-account func IsValidCosmosAccount(account string) bool { return validCosmosAccount.MatchString(account) } // NewAccountSASClient contructs a client that uses accountSAS authorization // for its operations. func NewAccountSASClient(account string, token url.Values, env azure.Environment) Client { return newSASClient(account, env.StorageEndpointSuffix, token) } // NewAccountSASClientFromEndpointToken constructs a client that uses accountSAS authorization // for its operations using the specified endpoint and SAS token. func NewAccountSASClientFromEndpointToken(endpoint string, sasToken string) (Client, error) { u, err := url.Parse(endpoint) if err != nil { return Client{}, err } _, err = url.ParseQuery(sasToken) if err != nil { return Client{}, err } u.RawQuery = sasToken return newSASClientFromURL(u) } func newSASClient(accountName, baseURL string, sasToken url.Values) Client { c := Client{ HTTPClient: http.DefaultClient, apiVersion: DefaultAPIVersion, sasClient: true, Sender: &DefaultSender{ RetryAttempts: defaultRetryAttempts, ValidStatusCodes: defaultValidStatusCodes, RetryDuration: defaultRetryDuration, }, accountName: accountName, baseURL: baseURL, accountSASToken: sasToken, useHTTPS: defaultUseHTTPS, } c.userAgent = c.getDefaultUserAgent() // Get API version and protocol from token c.apiVersion = sasToken.Get("sv") if spr := sasToken.Get("spr"); spr != "" { c.useHTTPS = spr == "https" } return c } func newSASClientFromURL(u *url.URL) (Client, error) { // the host name will look something like this // - foo.blob.core.windows.net // "foo" is the account name // "core.windows.net" is the baseURL // find the first dot to get account name i1 := strings.IndexByte(u.Host, '.') if i1 < 0 { return Client{}, fmt.Errorf("failed to find '.' in %s", u.Host) } // now find the second dot to get the base URL i2 := strings.IndexByte(u.Host[i1+1:], '.') if i2 < 0 { return Client{}, fmt.Errorf("failed to find '.' in %s", u.Host[i1+1:]) } sasToken := u.Query() c := newSASClient(u.Host[:i1], u.Host[i1+i2+2:], sasToken) if spr := sasToken.Get("spr"); spr == "" { // infer from URL if not in the query params set c.useHTTPS = u.Scheme == "https" } return c, nil } func (c Client) isServiceSASClient() bool { return c.sasClient && c.accountSASToken == nil } func (c Client) isAccountSASClient() bool { return c.sasClient && c.accountSASToken != nil } func (c Client) getDefaultUserAgent() string { return fmt.Sprintf("Go/%s (%s-%s) azure-storage-go/%s api-version/%s", runtime.Version(), runtime.GOARCH, runtime.GOOS, version.Number, c.apiVersion, ) } // AddToUserAgent adds an extension to the current user agent func (c *Client) AddToUserAgent(extension string) error { if extension != "" { c.userAgent = fmt.Sprintf("%s %s", c.userAgent, extension) return nil } return fmt.Errorf("Extension was empty, User Agent stayed as %s", c.userAgent) } // AddAdditionalHeaders adds additional standard headers func (c *Client) AddAdditionalHeaders(headers map[string]string) { if headers != nil { c.additionalHeaders = map[string]string{} for k, v := range headers { c.additionalHeaders[k] = v } } } // protectUserAgent is used in funcs that include extraheaders as a parameter. // It prevents the User-Agent header to be overwritten, instead if it happens to // be present, it gets added to the current User-Agent. Use it before getStandardHeaders func (c *Client) protectUserAgent(extraheaders map[string]string) map[string]string { if v, ok := extraheaders[userAgentHeader]; ok { c.AddToUserAgent(v) delete(extraheaders, userAgentHeader) } return extraheaders } func (c Client) getBaseURL(service string) *url.URL { scheme := "http" if c.useHTTPS { scheme = "https" } host := "" if c.accountName == StorageEmulatorAccountName { switch service { case blobServiceName: host = storageEmulatorBlob case tableServiceName: host = storageEmulatorTable case queueServiceName: host = storageEmulatorQueue } } else { host = fmt.Sprintf("%s.%s.%s", c.accountName, service, c.baseURL) } return &url.URL{ Scheme: scheme, Host: host, } } func (c Client) getEndpoint(service, path string, params url.Values) string { u := c.getBaseURL(service) // API doesn't accept path segments not starting with '/' if !strings.HasPrefix(path, "/") { path = fmt.Sprintf("/%v", path) } if c.accountName == StorageEmulatorAccountName { path = fmt.Sprintf("/%v%v", StorageEmulatorAccountName, path) } u.Path = path u.RawQuery = params.Encode() return u.String() } // AccountSASTokenOptions includes options for constructing // an account SAS token. // https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-an-account-sas type AccountSASTokenOptions struct { APIVersion string Services Services ResourceTypes ResourceTypes Permissions Permissions Start time.Time Expiry time.Time IP string UseHTTPS bool } // Services specify services accessible with an account SAS. type Services struct { Blob bool Queue bool Table bool File bool } // ResourceTypes specify the resources accesible with an // account SAS. type ResourceTypes struct { Service bool Container bool Object bool } // Permissions specifies permissions for an accountSAS. type Permissions struct { Read bool Write bool Delete bool List bool Add bool Create bool Update bool Process bool } // GetAccountSASToken creates an account SAS token // See https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-an-account-sas func (c Client) GetAccountSASToken(options AccountSASTokenOptions) (url.Values, error) { if options.APIVersion == "" { options.APIVersion = c.apiVersion } if options.APIVersion < "2015-04-05" { return url.Values{}, fmt.Errorf("account SAS does not support API versions prior to 2015-04-05. API version : %s", options.APIVersion) } // build services string services := "" if options.Services.Blob { services += "b" } if options.Services.Queue { services += "q" } if options.Services.Table { services += "t" } if options.Services.File { services += "f" } // build resources string resources := "" if options.ResourceTypes.Service { resources += "s" } if options.ResourceTypes.Container { resources += "c" } if options.ResourceTypes.Object { resources += "o" } // build permissions string permissions := "" if options.Permissions.Read { permissions += "r" } if options.Permissions.Write { permissions += "w" } if options.Permissions.Delete { permissions += "d" } if options.Permissions.List { permissions += "l" } if options.Permissions.Add { permissions += "a" } if options.Permissions.Create { permissions += "c" } if options.Permissions.Update { permissions += "u" } if options.Permissions.Process { permissions += "p" } // build start time, if exists start := "" if options.Start != (time.Time{}) { start = options.Start.UTC().Format(time.RFC3339) } // build expiry time expiry := options.Expiry.UTC().Format(time.RFC3339) protocol := "https,http" if options.UseHTTPS { protocol = "https" } stringToSign := strings.Join([]string{ c.accountName, permissions, services, resources, start, expiry, options.IP, protocol, options.APIVersion, "", }, "\n") signature := c.computeHmac256(stringToSign) sasParams := url.Values{ "sv": {options.APIVersion}, "ss": {services}, "srt": {resources}, "sp": {permissions}, "se": {expiry}, "spr": {protocol}, "sig": {signature}, } if start != "" { sasParams.Add("st", start) } if options.IP != "" { sasParams.Add("sip", options.IP) } return sasParams, nil } // GetBlobService returns a BlobStorageClient which can operate on the blob // service of the storage account. func (c Client) GetBlobService() BlobStorageClient { b := BlobStorageClient{ client: c, } b.client.AddToUserAgent(blobServiceName) b.auth = sharedKey if c.UseSharedKeyLite { b.auth = sharedKeyLite } return b } // GetQueueService returns a QueueServiceClient which can operate on the queue // service of the storage account. func (c Client) GetQueueService() QueueServiceClient { q := QueueServiceClient{ client: c, } q.client.AddToUserAgent(queueServiceName) q.auth = sharedKey if c.UseSharedKeyLite { q.auth = sharedKeyLite } return q } // GetTableService returns a TableServiceClient which can operate on the table // service of the storage account. func (c Client) GetTableService() TableServiceClient { t := TableServiceClient{ client: c, } t.client.AddToUserAgent(tableServiceName) t.auth = sharedKeyForTable if c.UseSharedKeyLite { t.auth = sharedKeyLiteForTable } return t } // GetFileService returns a FileServiceClient which can operate on the file // service of the storage account. func (c Client) GetFileService() FileServiceClient { f := FileServiceClient{ client: c, } f.client.AddToUserAgent(fileServiceName) f.auth = sharedKey if c.UseSharedKeyLite { f.auth = sharedKeyLite } return f } func (c Client) getStandardHeaders() map[string]string { headers := map[string]string{} for k, v := range c.additionalHeaders { headers[k] = v } headers[userAgentHeader] = c.userAgent headers["x-ms-version"] = c.apiVersion headers["x-ms-date"] = currentTimeRfc1123Formatted() return headers } func (c Client) exec(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*http.Response, error) { headers, err := c.addAuthorizationHeader(verb, url, headers, auth) if err != nil { return nil, err } req, err := http.NewRequest(verb, url, body) if err != nil { return nil, errors.New("azure/storage: error creating request: " + err.Error()) } // http.NewRequest() will automatically set req.ContentLength for a handful of types // otherwise we will handle here. if req.ContentLength < 1 { if clstr, ok := headers["Content-Length"]; ok { if cl, err := strconv.ParseInt(clstr, 10, 64); err == nil { req.ContentLength = cl } } } for k, v := range headers { req.Header[k] = append(req.Header[k], v) // Must bypass case munging present in `Add` by using map functions directly. See https://github.com/Azure/azure-sdk-for-go/issues/645 } if c.isAccountSASClient() { // append the SAS token to the query params v := req.URL.Query() v = mergeParams(v, c.accountSASToken) req.URL.RawQuery = v.Encode() } resp, err := c.Sender.Send(&c, req) if err != nil { return nil, err } if resp.StatusCode >= 400 && resp.StatusCode <= 505 { return resp, getErrorFromResponse(resp) } return resp, nil } func (c Client) execInternalJSONCommon(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, *http.Request, *http.Response, error) { headers, err := c.addAuthorizationHeader(verb, url, headers, auth) if err != nil { return nil, nil, nil, err } req, err := http.NewRequest(verb, url, body) for k, v := range headers { req.Header.Add(k, v) } resp, err := c.Sender.Send(&c, req) if err != nil { return nil, nil, nil, err } respToRet := &odataResponse{resp: resp} statusCode := resp.StatusCode if statusCode >= 400 && statusCode <= 505 { var respBody []byte respBody, err = readAndCloseBody(resp.Body) if err != nil { return nil, nil, nil, err } requestID, date, version := getDebugHeaders(resp.Header) if len(respBody) == 0 { // no error in response body, might happen in HEAD requests err = serviceErrFromStatusCode(resp.StatusCode, resp.Status, requestID, date, version) return respToRet, req, resp, err } // response contains storage service error object, unmarshal if resp.Header.Get("Content-Type") == "application/xml" { storageErr := AzureTablesServiceError{ StatusCode: resp.StatusCode, RequestID: requestID, Date: date, APIVersion: version, } if err := xml.Unmarshal(respBody, &storageErr); err != nil { storageErr.Message = fmt.Sprintf("Response body could no be unmarshaled: %v. Body: %v.", err, string(respBody)) } err = storageErr } else { err = json.Unmarshal(respBody, &respToRet.odata) } } return respToRet, req, resp, err } func (c Client) execInternalJSON(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, error) { respToRet, _, _, err := c.execInternalJSONCommon(verb, url, headers, body, auth) return respToRet, err } func (c Client) execBatchOperationJSON(verb, url string, headers map[string]string, body io.Reader, auth authentication) (*odataResponse, error) { // execute common query, get back generated request, response etc... for more processing. respToRet, req, resp, err := c.execInternalJSONCommon(verb, url, headers, body, auth) if err != nil { return nil, err } // return the OData in the case of executing batch commands. // In this case we need to read the outer batch boundary and contents. // Then we read the changeset information within the batch var respBody []byte respBody, err = readAndCloseBody(resp.Body) if err != nil { return nil, err } // outer multipart body _, batchHeader, err := mime.ParseMediaType(resp.Header["Content-Type"][0]) if err != nil { return nil, err } // batch details. batchBoundary := batchHeader["boundary"] batchPartBuf, changesetBoundary, err := genBatchReader(batchBoundary, respBody) if err != nil { return nil, err } // changeset details. err = genChangesetReader(req, respToRet, batchPartBuf, changesetBoundary) if err != nil { return nil, err } return respToRet, nil } func genChangesetReader(req *http.Request, respToRet *odataResponse, batchPartBuf io.Reader, changesetBoundary string) error { changesetMultiReader := multipart.NewReader(batchPartBuf, changesetBoundary) changesetPart, err := changesetMultiReader.NextPart() if err != nil { return err } changesetPartBufioReader := bufio.NewReader(changesetPart) changesetResp, err := http.ReadResponse(changesetPartBufioReader, req) if err != nil { return err } if changesetResp.StatusCode != http.StatusNoContent { changesetBody, err := readAndCloseBody(changesetResp.Body) err = json.Unmarshal(changesetBody, &respToRet.odata) if err != nil { return err } respToRet.resp = changesetResp } return nil } func genBatchReader(batchBoundary string, respBody []byte) (io.Reader, string, error) { respBodyString := string(respBody) respBodyReader := strings.NewReader(respBodyString) // reading batchresponse batchMultiReader := multipart.NewReader(respBodyReader, batchBoundary) batchPart, err := batchMultiReader.NextPart() if err != nil { return nil, "", err } batchPartBufioReader := bufio.NewReader(batchPart) _, changesetHeader, err := mime.ParseMediaType(batchPart.Header.Get("Content-Type")) if err != nil { return nil, "", err } changesetBoundary := changesetHeader["boundary"] return batchPartBufioReader, changesetBoundary, nil } func readAndCloseBody(body io.ReadCloser) ([]byte, error) { defer body.Close() out, err := ioutil.ReadAll(body) if err == io.EOF { err = nil } return out, err } // reads the response body then closes it func drainRespBody(resp *http.Response) { if resp != nil { io.Copy(ioutil.Discard, resp.Body) resp.Body.Close() } } func serviceErrFromXML(body []byte, storageErr *AzureStorageServiceError) error { if err := xml.Unmarshal(body, storageErr); err != nil { storageErr.Message = fmt.Sprintf("Response body could no be unmarshaled: %v. Body: %v.", err, string(body)) return err } return nil } func serviceErrFromJSON(body []byte, storageErr *AzureStorageServiceError) error { odataError := odataErrorWrapper{} if err := json.Unmarshal(body, &odataError); err != nil { storageErr.Message = fmt.Sprintf("Response body could no be unmarshaled: %v. Body: %v.", err, string(body)) return err } storageErr.Code = odataError.Err.Code storageErr.Message = odataError.Err.Message.Value storageErr.Lang = odataError.Err.Message.Lang return nil } func serviceErrFromStatusCode(code int, status string, requestID, date, version string) AzureStorageServiceError { return AzureStorageServiceError{ StatusCode: code, Code: status, RequestID: requestID, Date: date, APIVersion: version, Message: "no response body was available for error status code", } } func (e AzureStorageServiceError) Error() string { return fmt.Sprintf("storage: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=%s, RequestInitiated=%s, RequestId=%s, API Version=%s, QueryParameterName=%s, QueryParameterValue=%s", e.StatusCode, e.Code, e.Message, e.Date, e.RequestID, e.APIVersion, e.QueryParameterName, e.QueryParameterValue) } // checkRespCode returns UnexpectedStatusError if the given response code is not // one of the allowed status codes; otherwise nil. func checkRespCode(resp *http.Response, allowed []int) error { for _, v := range allowed { if resp.StatusCode == v { return nil } } err := getErrorFromResponse(resp) return UnexpectedStatusCodeError{ allowed: allowed, got: resp.StatusCode, inner: err, } } func (c Client) addMetadataToHeaders(h map[string]string, metadata map[string]string) map[string]string { metadata = c.protectUserAgent(metadata) for k, v := range metadata { h[userDefinedMetadataHeaderPrefix+k] = v } return h } func getDebugHeaders(h http.Header) (requestID, date, version string) { requestID = h.Get("x-ms-request-id") version = h.Get("x-ms-version") date = h.Get("Date") return } func getErrorFromResponse(resp *http.Response) error { respBody, err := readAndCloseBody(resp.Body) if err != nil { return err } requestID, date, version := getDebugHeaders(resp.Header) if len(respBody) == 0 { // no error in response body, might happen in HEAD requests err = serviceErrFromStatusCode(resp.StatusCode, resp.Status, requestID, date, version) } else { storageErr := AzureStorageServiceError{ StatusCode: resp.StatusCode, RequestID: requestID, Date: date, APIVersion: version, } // response contains storage service error object, unmarshal if resp.Header.Get("Content-Type") == "application/xml" { errIn := serviceErrFromXML(respBody, &storageErr) if err != nil { // error unmarshaling the error response err = errIn } } else { errIn := serviceErrFromJSON(respBody, &storageErr) if err != nil { // error unmarshaling the error response err = errIn } } err = storageErr } return err }