forked from cerc-io/plugeth
c76ad94492
This commit adds a build step to travis to auto-delete unstable archives older than 14 days (our regular release schedule) from Azure via ci.go purge. The commit also pulls in the latest Azure storage code, also switching over from the old import path (github.com/Azure/azure-sdk-for-go) to the new split one (github.com/Azure/azure-storage-go).
1131 lines
39 KiB
Go
1131 lines
39 KiB
Go
package storage
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// A Blob is an entry in BlobListResponse.
|
|
type Blob struct {
|
|
Name string `xml:"Name"`
|
|
Properties BlobProperties `xml:"Properties"`
|
|
Metadata BlobMetadata `xml:"Metadata"`
|
|
}
|
|
|
|
// BlobMetadata is a set of custom name/value pairs.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179404.aspx
|
|
type BlobMetadata map[string]string
|
|
|
|
type blobMetadataEntries struct {
|
|
Entries []blobMetadataEntry `xml:",any"`
|
|
}
|
|
type blobMetadataEntry struct {
|
|
XMLName xml.Name
|
|
Value string `xml:",chardata"`
|
|
}
|
|
|
|
// UnmarshalXML converts the xml:Metadata into Metadata map
|
|
func (bm *BlobMetadata) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
|
var entries blobMetadataEntries
|
|
if err := d.DecodeElement(&entries, &start); err != nil {
|
|
return err
|
|
}
|
|
for _, entry := range entries.Entries {
|
|
if *bm == nil {
|
|
*bm = make(BlobMetadata)
|
|
}
|
|
(*bm)[strings.ToLower(entry.XMLName.Local)] = entry.Value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarshalXML implements the xml.Marshaler interface. It encodes
|
|
// metadata name/value pairs as they would appear in an Azure
|
|
// ListBlobs response.
|
|
func (bm BlobMetadata) MarshalXML(enc *xml.Encoder, start xml.StartElement) error {
|
|
entries := make([]blobMetadataEntry, 0, len(bm))
|
|
for k, v := range bm {
|
|
entries = append(entries, blobMetadataEntry{
|
|
XMLName: xml.Name{Local: http.CanonicalHeaderKey(k)},
|
|
Value: v,
|
|
})
|
|
}
|
|
return enc.EncodeElement(blobMetadataEntries{
|
|
Entries: entries,
|
|
}, start)
|
|
}
|
|
|
|
// BlobProperties contains various properties of a blob
|
|
// returned in various endpoints like ListBlobs or GetBlobProperties.
|
|
type BlobProperties struct {
|
|
LastModified string `xml:"Last-Modified"`
|
|
Etag string `xml:"Etag"`
|
|
ContentMD5 string `xml:"Content-MD5"`
|
|
ContentLength int64 `xml:"Content-Length"`
|
|
ContentType string `xml:"Content-Type"`
|
|
ContentEncoding string `xml:"Content-Encoding"`
|
|
CacheControl string `xml:"Cache-Control"`
|
|
ContentLanguage string `xml:"Cache-Language"`
|
|
BlobType BlobType `xml:"x-ms-blob-blob-type"`
|
|
SequenceNumber int64 `xml:"x-ms-blob-sequence-number"`
|
|
CopyID string `xml:"CopyId"`
|
|
CopyStatus string `xml:"CopyStatus"`
|
|
CopySource string `xml:"CopySource"`
|
|
CopyProgress string `xml:"CopyProgress"`
|
|
CopyCompletionTime string `xml:"CopyCompletionTime"`
|
|
CopyStatusDescription string `xml:"CopyStatusDescription"`
|
|
LeaseStatus string `xml:"LeaseStatus"`
|
|
LeaseState string `xml:"LeaseState"`
|
|
}
|
|
|
|
// BlobHeaders contains various properties of a blob and is an entry
|
|
// in SetBlobProperties
|
|
type BlobHeaders struct {
|
|
ContentMD5 string `header:"x-ms-blob-content-md5"`
|
|
ContentLanguage string `header:"x-ms-blob-content-language"`
|
|
ContentEncoding string `header:"x-ms-blob-content-encoding"`
|
|
ContentType string `header:"x-ms-blob-content-type"`
|
|
CacheControl string `header:"x-ms-blob-cache-control"`
|
|
}
|
|
|
|
// BlobType defines the type of the Azure Blob.
|
|
type BlobType string
|
|
|
|
// Types of page blobs
|
|
const (
|
|
BlobTypeBlock BlobType = "BlockBlob"
|
|
BlobTypePage BlobType = "PageBlob"
|
|
BlobTypeAppend BlobType = "AppendBlob"
|
|
)
|
|
|
|
// PageWriteType defines the type updates that are going to be
|
|
// done on the page blob.
|
|
type PageWriteType string
|
|
|
|
// Types of operations on page blobs
|
|
const (
|
|
PageWriteTypeUpdate PageWriteType = "update"
|
|
PageWriteTypeClear PageWriteType = "clear"
|
|
)
|
|
|
|
const (
|
|
blobCopyStatusPending = "pending"
|
|
blobCopyStatusSuccess = "success"
|
|
blobCopyStatusAborted = "aborted"
|
|
blobCopyStatusFailed = "failed"
|
|
)
|
|
|
|
// lease constants.
|
|
const (
|
|
leaseHeaderPrefix = "x-ms-lease-"
|
|
headerLeaseID = "x-ms-lease-id"
|
|
leaseAction = "x-ms-lease-action"
|
|
leaseBreakPeriod = "x-ms-lease-break-period"
|
|
leaseDuration = "x-ms-lease-duration"
|
|
leaseProposedID = "x-ms-proposed-lease-id"
|
|
leaseTime = "x-ms-lease-time"
|
|
|
|
acquireLease = "acquire"
|
|
renewLease = "renew"
|
|
changeLease = "change"
|
|
releaseLease = "release"
|
|
breakLease = "break"
|
|
)
|
|
|
|
// BlockListType is used to filter out types of blocks in a Get Blocks List call
|
|
// for a block blob.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179400.aspx for all
|
|
// block types.
|
|
type BlockListType string
|
|
|
|
// Filters for listing blocks in block blobs
|
|
const (
|
|
BlockListTypeAll BlockListType = "all"
|
|
BlockListTypeCommitted BlockListType = "committed"
|
|
BlockListTypeUncommitted BlockListType = "uncommitted"
|
|
)
|
|
|
|
// Maximum sizes (per REST API) for various concepts
|
|
const (
|
|
MaxBlobBlockSize = 100 * 1024 * 1024
|
|
MaxBlobPageSize = 4 * 1024 * 1024
|
|
)
|
|
|
|
// BlockStatus defines states a block for a block blob can
|
|
// be in.
|
|
type BlockStatus string
|
|
|
|
// List of statuses that can be used to refer to a block in a block list
|
|
const (
|
|
BlockStatusUncommitted BlockStatus = "Uncommitted"
|
|
BlockStatusCommitted BlockStatus = "Committed"
|
|
BlockStatusLatest BlockStatus = "Latest"
|
|
)
|
|
|
|
// Block is used to create Block entities for Put Block List
|
|
// call.
|
|
type Block struct {
|
|
ID string
|
|
Status BlockStatus
|
|
}
|
|
|
|
// BlockListResponse contains the response fields from Get Block List call.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179400.aspx
|
|
type BlockListResponse struct {
|
|
XMLName xml.Name `xml:"BlockList"`
|
|
CommittedBlocks []BlockResponse `xml:"CommittedBlocks>Block"`
|
|
UncommittedBlocks []BlockResponse `xml:"UncommittedBlocks>Block"`
|
|
}
|
|
|
|
// BlockResponse contains the block information returned
|
|
// in the GetBlockListCall.
|
|
type BlockResponse struct {
|
|
Name string `xml:"Name"`
|
|
Size int64 `xml:"Size"`
|
|
}
|
|
|
|
// GetPageRangesResponse contains the response fields from
|
|
// Get Page Ranges call.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/ee691973.aspx
|
|
type GetPageRangesResponse struct {
|
|
XMLName xml.Name `xml:"PageList"`
|
|
PageList []PageRange `xml:"PageRange"`
|
|
}
|
|
|
|
// PageRange contains information about a page of a page blob from
|
|
// Get Pages Range call.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/ee691973.aspx
|
|
type PageRange struct {
|
|
Start int64 `xml:"Start"`
|
|
End int64 `xml:"End"`
|
|
}
|
|
|
|
var (
|
|
errBlobCopyAborted = errors.New("storage: blob copy is aborted")
|
|
errBlobCopyIDMismatch = errors.New("storage: blob copy id is a mismatch")
|
|
)
|
|
|
|
// BlobExists returns true if a blob with given name exists on the specified
|
|
// container of the storage account.
|
|
func (b BlobStorageClient) BlobExists(container, name string) (bool, error) {
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{})
|
|
headers := b.client.getStandardHeaders()
|
|
resp, err := b.client.exec(http.MethodHead, uri, headers, nil, b.auth)
|
|
if resp != nil {
|
|
defer readAndCloseBody(resp.body)
|
|
if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound {
|
|
return resp.statusCode == http.StatusOK, nil
|
|
}
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
// GetBlobURL gets the canonical URL to the blob with the specified name in the
|
|
// specified container. If name is not specified, the canonical URL for the entire
|
|
// container is obtained.
|
|
// This method does not create a publicly accessible URL if the blob or container
|
|
// is private and this method does not check if the blob exists.
|
|
func (b BlobStorageClient) GetBlobURL(container, name string) string {
|
|
if container == "" {
|
|
container = "$root"
|
|
}
|
|
return b.client.getEndpoint(blobServiceName, pathForResource(container, name), url.Values{})
|
|
}
|
|
|
|
// GetBlob returns a stream to read the blob. Caller must call Close() the
|
|
// reader to close on the underlying connection.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179440.aspx
|
|
func (b BlobStorageClient) GetBlob(container, name string) (io.ReadCloser, error) {
|
|
resp, err := b.getBlobRange(container, name, "", nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.body, nil
|
|
}
|
|
|
|
// GetBlobRange reads the specified range of a blob to a stream. The bytesRange
|
|
// string must be in a format like "0-", "10-100" as defined in HTTP 1.1 spec.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179440.aspx
|
|
func (b BlobStorageClient) GetBlobRange(container, name, bytesRange string, extraHeaders map[string]string) (io.ReadCloser, error) {
|
|
resp, err := b.getBlobRange(container, name, bytesRange, extraHeaders)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := checkRespCode(resp.statusCode, []int{http.StatusPartialContent}); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp.body, nil
|
|
}
|
|
|
|
func (b BlobStorageClient) getBlobRange(container, name, bytesRange string, extraHeaders map[string]string) (*storageResponse, error) {
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{})
|
|
|
|
extraHeaders = b.client.protectUserAgent(extraHeaders)
|
|
headers := b.client.getStandardHeaders()
|
|
if bytesRange != "" {
|
|
headers["Range"] = fmt.Sprintf("bytes=%s", bytesRange)
|
|
}
|
|
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
resp, err := b.client.exec(http.MethodGet, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return resp, err
|
|
}
|
|
|
|
// leasePut is common PUT code for the various acquire/release/break etc functions.
|
|
func (b BlobStorageClient) leaseCommonPut(container string, name string, headers map[string]string, expectedStatus int) (http.Header, error) {
|
|
params := url.Values{"comp": {"lease"}}
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params)
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
if err := checkRespCode(resp.statusCode, []int{expectedStatus}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return resp.headers, nil
|
|
}
|
|
|
|
// SnapshotBlob creates a snapshot for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691971.aspx
|
|
func (b BlobStorageClient) SnapshotBlob(container string, name string, timeout int, extraHeaders map[string]string) (snapshotTimestamp *time.Time, err error) {
|
|
extraHeaders = b.client.protectUserAgent(extraHeaders)
|
|
headers := b.client.getStandardHeaders()
|
|
params := url.Values{"comp": {"snapshot"}}
|
|
|
|
if timeout > 0 {
|
|
params.Add("timeout", strconv.Itoa(timeout))
|
|
}
|
|
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params)
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, nil, b.auth)
|
|
if err != nil || resp == nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
if err := checkRespCode(resp.statusCode, []int{http.StatusCreated}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
snapshotResponse := resp.headers.Get(http.CanonicalHeaderKey("x-ms-snapshot"))
|
|
if snapshotResponse != "" {
|
|
snapshotTimestamp, err := time.Parse(time.RFC3339, snapshotResponse)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &snapshotTimestamp, nil
|
|
}
|
|
|
|
return nil, errors.New("Snapshot not created")
|
|
}
|
|
|
|
// AcquireLease creates a lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx
|
|
// returns leaseID acquired
|
|
// In API Versions starting on 2012-02-12, the minimum leaseTimeInSeconds is 15, the maximum
|
|
// non-infinite leaseTimeInSeconds is 60. To specify an infinite lease, provide the value -1.
|
|
func (b BlobStorageClient) AcquireLease(container string, name string, leaseTimeInSeconds int, proposedLeaseID string) (returnedLeaseID string, err error) {
|
|
headers := b.client.getStandardHeaders()
|
|
headers[leaseAction] = acquireLease
|
|
|
|
if leaseTimeInSeconds == -1 {
|
|
// Do nothing, but don't trigger the following clauses.
|
|
} else if leaseTimeInSeconds > 60 || b.client.apiVersion < "2012-02-12" {
|
|
leaseTimeInSeconds = 60
|
|
} else if leaseTimeInSeconds < 15 {
|
|
leaseTimeInSeconds = 15
|
|
}
|
|
|
|
headers[leaseDuration] = strconv.Itoa(leaseTimeInSeconds)
|
|
|
|
if proposedLeaseID != "" {
|
|
headers[leaseProposedID] = proposedLeaseID
|
|
}
|
|
|
|
respHeaders, err := b.leaseCommonPut(container, name, headers, http.StatusCreated)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
returnedLeaseID = respHeaders.Get(http.CanonicalHeaderKey(headerLeaseID))
|
|
|
|
if returnedLeaseID != "" {
|
|
return returnedLeaseID, nil
|
|
}
|
|
|
|
return "", errors.New("LeaseID not returned")
|
|
}
|
|
|
|
// BreakLease breaks the lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx
|
|
// Returns the timeout remaining in the lease in seconds
|
|
func (b BlobStorageClient) BreakLease(container string, name string) (breakTimeout int, err error) {
|
|
headers := b.client.getStandardHeaders()
|
|
headers[leaseAction] = breakLease
|
|
return b.breakLeaseCommon(container, name, headers)
|
|
}
|
|
|
|
// BreakLeaseWithBreakPeriod breaks the lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx
|
|
// breakPeriodInSeconds is used to determine how long until new lease can be created.
|
|
// Returns the timeout remaining in the lease in seconds
|
|
func (b BlobStorageClient) BreakLeaseWithBreakPeriod(container string, name string, breakPeriodInSeconds int) (breakTimeout int, err error) {
|
|
headers := b.client.getStandardHeaders()
|
|
headers[leaseAction] = breakLease
|
|
headers[leaseBreakPeriod] = strconv.Itoa(breakPeriodInSeconds)
|
|
return b.breakLeaseCommon(container, name, headers)
|
|
}
|
|
|
|
// breakLeaseCommon is common code for both version of BreakLease (with and without break period)
|
|
func (b BlobStorageClient) breakLeaseCommon(container string, name string, headers map[string]string) (breakTimeout int, err error) {
|
|
|
|
respHeaders, err := b.leaseCommonPut(container, name, headers, http.StatusAccepted)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
breakTimeoutStr := respHeaders.Get(http.CanonicalHeaderKey(leaseTime))
|
|
if breakTimeoutStr != "" {
|
|
breakTimeout, err = strconv.Atoi(breakTimeoutStr)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
return breakTimeout, nil
|
|
}
|
|
|
|
// ChangeLease changes a lease ID for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx
|
|
// Returns the new LeaseID acquired
|
|
func (b BlobStorageClient) ChangeLease(container string, name string, currentLeaseID string, proposedLeaseID string) (newLeaseID string, err error) {
|
|
headers := b.client.getStandardHeaders()
|
|
headers[leaseAction] = changeLease
|
|
headers[headerLeaseID] = currentLeaseID
|
|
headers[leaseProposedID] = proposedLeaseID
|
|
|
|
respHeaders, err := b.leaseCommonPut(container, name, headers, http.StatusOK)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
newLeaseID = respHeaders.Get(http.CanonicalHeaderKey(headerLeaseID))
|
|
if newLeaseID != "" {
|
|
return newLeaseID, nil
|
|
}
|
|
|
|
return "", errors.New("LeaseID not returned")
|
|
}
|
|
|
|
// ReleaseLease releases the lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx
|
|
func (b BlobStorageClient) ReleaseLease(container string, name string, currentLeaseID string) error {
|
|
headers := b.client.getStandardHeaders()
|
|
headers[leaseAction] = releaseLease
|
|
headers[headerLeaseID] = currentLeaseID
|
|
|
|
_, err := b.leaseCommonPut(container, name, headers, http.StatusOK)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RenewLease renews the lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx
|
|
func (b BlobStorageClient) RenewLease(container string, name string, currentLeaseID string) error {
|
|
headers := b.client.getStandardHeaders()
|
|
headers[leaseAction] = renewLease
|
|
headers[headerLeaseID] = currentLeaseID
|
|
|
|
_, err := b.leaseCommonPut(container, name, headers, http.StatusOK)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetBlobProperties provides various information about the specified
|
|
// blob. See https://msdn.microsoft.com/en-us/library/azure/dd179394.aspx
|
|
func (b BlobStorageClient) GetBlobProperties(container, name string) (*BlobProperties, error) {
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{})
|
|
|
|
headers := b.client.getStandardHeaders()
|
|
resp, err := b.client.exec(http.MethodHead, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var contentLength int64
|
|
contentLengthStr := resp.headers.Get("Content-Length")
|
|
if contentLengthStr != "" {
|
|
contentLength, err = strconv.ParseInt(contentLengthStr, 0, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var sequenceNum int64
|
|
sequenceNumStr := resp.headers.Get("x-ms-blob-sequence-number")
|
|
if sequenceNumStr != "" {
|
|
sequenceNum, err = strconv.ParseInt(sequenceNumStr, 0, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &BlobProperties{
|
|
LastModified: resp.headers.Get("Last-Modified"),
|
|
Etag: resp.headers.Get("Etag"),
|
|
ContentMD5: resp.headers.Get("Content-MD5"),
|
|
ContentLength: contentLength,
|
|
ContentEncoding: resp.headers.Get("Content-Encoding"),
|
|
ContentType: resp.headers.Get("Content-Type"),
|
|
CacheControl: resp.headers.Get("Cache-Control"),
|
|
ContentLanguage: resp.headers.Get("Content-Language"),
|
|
SequenceNumber: sequenceNum,
|
|
CopyCompletionTime: resp.headers.Get("x-ms-copy-completion-time"),
|
|
CopyStatusDescription: resp.headers.Get("x-ms-copy-status-description"),
|
|
CopyID: resp.headers.Get("x-ms-copy-id"),
|
|
CopyProgress: resp.headers.Get("x-ms-copy-progress"),
|
|
CopySource: resp.headers.Get("x-ms-copy-source"),
|
|
CopyStatus: resp.headers.Get("x-ms-copy-status"),
|
|
BlobType: BlobType(resp.headers.Get("x-ms-blob-type")),
|
|
LeaseStatus: resp.headers.Get("x-ms-lease-status"),
|
|
LeaseState: resp.headers.Get("x-ms-lease-state"),
|
|
}, nil
|
|
}
|
|
|
|
// SetBlobProperties replaces the BlobHeaders for the specified blob.
|
|
//
|
|
// Some keys may be converted to Camel-Case before sending. All keys
|
|
// are returned in lower case by GetBlobProperties. HTTP header names
|
|
// are case-insensitive so case munging should not matter to other
|
|
// applications either.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/ee691966.aspx
|
|
func (b BlobStorageClient) SetBlobProperties(container, name string, blobHeaders BlobHeaders) error {
|
|
params := url.Values{"comp": {"properties"}}
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params)
|
|
headers := b.client.getStandardHeaders()
|
|
|
|
extraHeaders := headersFromStruct(blobHeaders)
|
|
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
return checkRespCode(resp.statusCode, []int{http.StatusOK})
|
|
}
|
|
|
|
// SetBlobMetadata replaces the metadata for the specified blob.
|
|
//
|
|
// Some keys may be converted to Camel-Case before sending. All keys
|
|
// are returned in lower case by GetBlobMetadata. HTTP header names
|
|
// are case-insensitive so case munging should not matter to other
|
|
// applications either.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx
|
|
func (b BlobStorageClient) SetBlobMetadata(container, name string, metadata map[string]string, extraHeaders map[string]string) error {
|
|
params := url.Values{"comp": {"metadata"}}
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params)
|
|
metadata = b.client.protectUserAgent(metadata)
|
|
extraHeaders = b.client.protectUserAgent(extraHeaders)
|
|
headers := b.client.getStandardHeaders()
|
|
for k, v := range metadata {
|
|
headers[userDefinedMetadataHeaderPrefix+k] = v
|
|
}
|
|
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
return checkRespCode(resp.statusCode, []int{http.StatusOK})
|
|
}
|
|
|
|
// GetBlobMetadata returns all user-defined metadata for the specified blob.
|
|
//
|
|
// All metadata keys will be returned in lower case. (HTTP header
|
|
// names are case-insensitive.)
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx
|
|
func (b BlobStorageClient) GetBlobMetadata(container, name string) (map[string]string, error) {
|
|
params := url.Values{"comp": {"metadata"}}
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params)
|
|
headers := b.client.getStandardHeaders()
|
|
|
|
resp, err := b.client.exec(http.MethodGet, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
metadata := make(map[string]string)
|
|
for k, v := range resp.headers {
|
|
// Can't trust CanonicalHeaderKey() to munge case
|
|
// reliably. "_" is allowed in identifiers:
|
|
// https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx
|
|
// https://msdn.microsoft.com/library/aa664670(VS.71).aspx
|
|
// http://tools.ietf.org/html/rfc7230#section-3.2
|
|
// ...but "_" is considered invalid by
|
|
// CanonicalMIMEHeaderKey in
|
|
// https://golang.org/src/net/textproto/reader.go?s=14615:14659#L542
|
|
// so k can be "X-Ms-Meta-Foo" or "x-ms-meta-foo_bar".
|
|
k = strings.ToLower(k)
|
|
if len(v) == 0 || !strings.HasPrefix(k, strings.ToLower(userDefinedMetadataHeaderPrefix)) {
|
|
continue
|
|
}
|
|
// metadata["foo"] = content of the last X-Ms-Meta-Foo header
|
|
k = k[len(userDefinedMetadataHeaderPrefix):]
|
|
metadata[k] = v[len(v)-1]
|
|
}
|
|
return metadata, nil
|
|
}
|
|
|
|
// CreateBlockBlob initializes an empty block blob with no blocks.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179451.aspx
|
|
func (b BlobStorageClient) CreateBlockBlob(container, name string) error {
|
|
return b.CreateBlockBlobFromReader(container, name, 0, nil, nil)
|
|
}
|
|
|
|
// CreateBlockBlobFromReader initializes a block blob using data from
|
|
// reader. Size must be the number of bytes read from reader. To
|
|
// create an empty blob, use size==0 and reader==nil.
|
|
//
|
|
// The API rejects requests with size > 256 MiB (but this limit is not
|
|
// checked by the SDK). To write a larger blob, use CreateBlockBlob,
|
|
// PutBlock, and PutBlockList.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179451.aspx
|
|
func (b BlobStorageClient) CreateBlockBlobFromReader(container, name string, size uint64, blob io.Reader, extraHeaders map[string]string) error {
|
|
path := fmt.Sprintf("%s/%s", container, name)
|
|
uri := b.client.getEndpoint(blobServiceName, path, url.Values{})
|
|
extraHeaders = b.client.protectUserAgent(extraHeaders)
|
|
headers := b.client.getStandardHeaders()
|
|
headers["x-ms-blob-type"] = string(BlobTypeBlock)
|
|
headers["Content-Length"] = fmt.Sprintf("%d", size)
|
|
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, blob, b.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
}
|
|
|
|
// PutBlock saves the given data chunk to the specified block blob with
|
|
// given ID.
|
|
//
|
|
// The API rejects chunks larger than 100 MB (but this limit is not
|
|
// checked by the SDK).
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd135726.aspx
|
|
func (b BlobStorageClient) PutBlock(container, name, blockID string, chunk []byte) error {
|
|
return b.PutBlockWithLength(container, name, blockID, uint64(len(chunk)), bytes.NewReader(chunk), nil)
|
|
}
|
|
|
|
// PutBlockWithLength saves the given data stream of exactly specified size to
|
|
// the block blob with given ID. It is an alternative to PutBlocks where data
|
|
// comes as stream but the length is known in advance.
|
|
//
|
|
// The API rejects requests with size > 100 MB (but this limit is not
|
|
// checked by the SDK).
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd135726.aspx
|
|
func (b BlobStorageClient) PutBlockWithLength(container, name, blockID string, size uint64, blob io.Reader, extraHeaders map[string]string) error {
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{"comp": {"block"}, "blockid": {blockID}})
|
|
extraHeaders = b.client.protectUserAgent(extraHeaders)
|
|
headers := b.client.getStandardHeaders()
|
|
headers["x-ms-blob-type"] = string(BlobTypeBlock)
|
|
headers["Content-Length"] = fmt.Sprintf("%v", size)
|
|
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, blob, b.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer readAndCloseBody(resp.body)
|
|
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
}
|
|
|
|
// PutBlockList saves list of blocks to the specified block blob.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179467.aspx
|
|
func (b BlobStorageClient) PutBlockList(container, name string, blocks []Block) error {
|
|
blockListXML := prepareBlockListRequest(blocks)
|
|
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{"comp": {"blocklist"}})
|
|
headers := b.client.getStandardHeaders()
|
|
headers["Content-Length"] = fmt.Sprintf("%v", len(blockListXML))
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, strings.NewReader(blockListXML), b.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
}
|
|
|
|
// GetBlockList retrieves list of blocks in the specified block blob.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179400.aspx
|
|
func (b BlobStorageClient) GetBlockList(container, name string, blockType BlockListType) (BlockListResponse, error) {
|
|
params := url.Values{"comp": {"blocklist"}, "blocklisttype": {string(blockType)}}
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params)
|
|
headers := b.client.getStandardHeaders()
|
|
|
|
var out BlockListResponse
|
|
resp, err := b.client.exec(http.MethodGet, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
defer resp.body.Close()
|
|
|
|
err = xmlUnmarshal(resp.body, &out)
|
|
return out, err
|
|
}
|
|
|
|
// PutPageBlob initializes an empty page blob with specified name and maximum
|
|
// size in bytes (size must be aligned to a 512-byte boundary). A page blob must
|
|
// be created using this method before writing pages.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179451.aspx
|
|
func (b BlobStorageClient) PutPageBlob(container, name string, size int64, extraHeaders map[string]string) error {
|
|
path := fmt.Sprintf("%s/%s", container, name)
|
|
uri := b.client.getEndpoint(blobServiceName, path, url.Values{})
|
|
extraHeaders = b.client.protectUserAgent(extraHeaders)
|
|
headers := b.client.getStandardHeaders()
|
|
headers["x-ms-blob-type"] = string(BlobTypePage)
|
|
headers["x-ms-blob-content-length"] = fmt.Sprintf("%v", size)
|
|
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
}
|
|
|
|
// PutPage writes a range of pages to a page blob or clears the given range.
|
|
// In case of 'clear' writes, given chunk is discarded. Ranges must be aligned
|
|
// with 512-byte boundaries and chunk must be of size multiplies by 512.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/ee691975.aspx
|
|
func (b BlobStorageClient) PutPage(container, name string, startByte, endByte int64, writeType PageWriteType, chunk []byte, extraHeaders map[string]string) error {
|
|
path := fmt.Sprintf("%s/%s", container, name)
|
|
uri := b.client.getEndpoint(blobServiceName, path, url.Values{"comp": {"page"}})
|
|
extraHeaders = b.client.protectUserAgent(extraHeaders)
|
|
headers := b.client.getStandardHeaders()
|
|
headers["x-ms-blob-type"] = string(BlobTypePage)
|
|
headers["x-ms-page-write"] = string(writeType)
|
|
headers["x-ms-range"] = fmt.Sprintf("bytes=%v-%v", startByte, endByte)
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
var contentLength int64
|
|
var data io.Reader
|
|
if writeType == PageWriteTypeClear {
|
|
contentLength = 0
|
|
data = bytes.NewReader([]byte{})
|
|
} else {
|
|
contentLength = int64(len(chunk))
|
|
data = bytes.NewReader(chunk)
|
|
}
|
|
headers["Content-Length"] = fmt.Sprintf("%v", contentLength)
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, data, b.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
}
|
|
|
|
// GetPageRanges returns the list of valid page ranges for a page blob.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/ee691973.aspx
|
|
func (b BlobStorageClient) GetPageRanges(container, name string) (GetPageRangesResponse, error) {
|
|
path := fmt.Sprintf("%s/%s", container, name)
|
|
uri := b.client.getEndpoint(blobServiceName, path, url.Values{"comp": {"pagelist"}})
|
|
headers := b.client.getStandardHeaders()
|
|
|
|
var out GetPageRangesResponse
|
|
resp, err := b.client.exec(http.MethodGet, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
defer resp.body.Close()
|
|
|
|
if err = checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil {
|
|
return out, err
|
|
}
|
|
err = xmlUnmarshal(resp.body, &out)
|
|
return out, err
|
|
}
|
|
|
|
// PutAppendBlob initializes an empty append blob with specified name. An
|
|
// append blob must be created using this method before appending blocks.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179451.aspx
|
|
func (b BlobStorageClient) PutAppendBlob(container, name string, extraHeaders map[string]string) error {
|
|
path := fmt.Sprintf("%s/%s", container, name)
|
|
uri := b.client.getEndpoint(blobServiceName, path, url.Values{})
|
|
extraHeaders = b.client.protectUserAgent(extraHeaders)
|
|
headers := b.client.getStandardHeaders()
|
|
headers["x-ms-blob-type"] = string(BlobTypeAppend)
|
|
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
}
|
|
|
|
// AppendBlock appends a block to an append blob.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/mt427365.aspx
|
|
func (b BlobStorageClient) AppendBlock(container, name string, chunk []byte, extraHeaders map[string]string) error {
|
|
path := fmt.Sprintf("%s/%s", container, name)
|
|
uri := b.client.getEndpoint(blobServiceName, path, url.Values{"comp": {"appendblock"}})
|
|
extraHeaders = b.client.protectUserAgent(extraHeaders)
|
|
headers := b.client.getStandardHeaders()
|
|
headers["x-ms-blob-type"] = string(BlobTypeAppend)
|
|
headers["Content-Length"] = fmt.Sprintf("%v", len(chunk))
|
|
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, bytes.NewReader(chunk), b.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
return checkRespCode(resp.statusCode, []int{http.StatusCreated})
|
|
}
|
|
|
|
// CopyBlob starts a blob copy operation and waits for the operation to
|
|
// complete. sourceBlob parameter must be a canonical URL to the blob (can be
|
|
// obtained using GetBlobURL method.) There is no SLA on blob copy and therefore
|
|
// this helper method works faster on smaller files.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd894037.aspx
|
|
func (b BlobStorageClient) CopyBlob(container, name, sourceBlob string) error {
|
|
copyID, err := b.StartBlobCopy(container, name, sourceBlob)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.WaitForBlobCopy(container, name, copyID)
|
|
}
|
|
|
|
// StartBlobCopy starts a blob copy operation.
|
|
// sourceBlob parameter must be a canonical URL to the blob (can be
|
|
// obtained using GetBlobURL method.)
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd894037.aspx
|
|
func (b BlobStorageClient) StartBlobCopy(container, name, sourceBlob string) (string, error) {
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{})
|
|
|
|
headers := b.client.getStandardHeaders()
|
|
headers["x-ms-copy-source"] = sourceBlob
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
if err := checkRespCode(resp.statusCode, []int{http.StatusAccepted, http.StatusCreated}); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
copyID := resp.headers.Get("x-ms-copy-id")
|
|
if copyID == "" {
|
|
return "", errors.New("Got empty copy id header")
|
|
}
|
|
return copyID, nil
|
|
}
|
|
|
|
// AbortBlobCopy aborts a BlobCopy which has already been triggered by the StartBlobCopy function.
|
|
// copyID is generated from StartBlobCopy function.
|
|
// currentLeaseID is required IF the destination blob has an active lease on it.
|
|
// As defined in https://msdn.microsoft.com/en-us/library/azure/jj159098.aspx
|
|
func (b BlobStorageClient) AbortBlobCopy(container, name, copyID, currentLeaseID string, timeout int) error {
|
|
params := url.Values{"comp": {"copy"}, "copyid": {copyID}}
|
|
if timeout > 0 {
|
|
params.Add("timeout", strconv.Itoa(timeout))
|
|
}
|
|
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params)
|
|
headers := b.client.getStandardHeaders()
|
|
headers["x-ms-copy-action"] = "abort"
|
|
|
|
if currentLeaseID != "" {
|
|
headers[headerLeaseID] = currentLeaseID
|
|
}
|
|
|
|
resp, err := b.client.exec(http.MethodPut, uri, headers, nil, b.auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
|
|
if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// WaitForBlobCopy loops until a BlobCopy operation is completed (or fails with error)
|
|
func (b BlobStorageClient) WaitForBlobCopy(container, name, copyID string) error {
|
|
for {
|
|
props, err := b.GetBlobProperties(container, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if props.CopyID != copyID {
|
|
return errBlobCopyIDMismatch
|
|
}
|
|
|
|
switch props.CopyStatus {
|
|
case blobCopyStatusSuccess:
|
|
return nil
|
|
case blobCopyStatusPending:
|
|
continue
|
|
case blobCopyStatusAborted:
|
|
return errBlobCopyAborted
|
|
case blobCopyStatusFailed:
|
|
return fmt.Errorf("storage: blob copy failed. Id=%s Description=%s", props.CopyID, props.CopyStatusDescription)
|
|
default:
|
|
return fmt.Errorf("storage: unhandled blob copy status: '%s'", props.CopyStatus)
|
|
}
|
|
}
|
|
}
|
|
|
|
// DeleteBlob deletes the given blob from the specified container.
|
|
// If the blob does not exists at the time of the Delete Blob operation, it
|
|
// returns error. See https://msdn.microsoft.com/en-us/library/azure/dd179413.aspx
|
|
func (b BlobStorageClient) DeleteBlob(container, name string, extraHeaders map[string]string) error {
|
|
resp, err := b.deleteBlob(container, name, extraHeaders)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer readAndCloseBody(resp.body)
|
|
return checkRespCode(resp.statusCode, []int{http.StatusAccepted})
|
|
}
|
|
|
|
// DeleteBlobIfExists deletes the given blob from the specified container If the
|
|
// blob is deleted with this call, returns true. Otherwise returns false.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/dd179413.aspx
|
|
func (b BlobStorageClient) DeleteBlobIfExists(container, name string, extraHeaders map[string]string) (bool, error) {
|
|
resp, err := b.deleteBlob(container, name, extraHeaders)
|
|
if resp != nil {
|
|
defer readAndCloseBody(resp.body)
|
|
if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound {
|
|
return resp.statusCode == http.StatusAccepted, nil
|
|
}
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func (b BlobStorageClient) deleteBlob(container, name string, extraHeaders map[string]string) (*storageResponse, error) {
|
|
uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{})
|
|
extraHeaders = b.client.protectUserAgent(extraHeaders)
|
|
headers := b.client.getStandardHeaders()
|
|
for k, v := range extraHeaders {
|
|
headers[k] = v
|
|
}
|
|
|
|
return b.client.exec(http.MethodDelete, uri, headers, nil, b.auth)
|
|
}
|
|
|
|
// helper method to construct the path to a blob given its container and blob
|
|
// name
|
|
func pathForBlob(container, name string) string {
|
|
return fmt.Sprintf("/%s/%s", container, name)
|
|
}
|
|
|
|
// helper method to construct the path to either a blob or container
|
|
func pathForResource(container, name string) string {
|
|
if len(name) > 0 {
|
|
return fmt.Sprintf("/%s/%s", container, name)
|
|
}
|
|
return fmt.Sprintf("/%s", container)
|
|
}
|
|
|
|
// GetBlobSASURIWithSignedIPAndProtocol creates an URL to the specified blob which contains the Shared
|
|
// Access Signature with specified permissions and expiration time. Also includes signedIPRange and allowed protocols.
|
|
// If old API version is used but no signedIP is passed (ie empty string) then this should still work.
|
|
// We only populate the signedIP when it non-empty.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/ee395415.aspx
|
|
func (b BlobStorageClient) GetBlobSASURIWithSignedIPAndProtocol(container, name string, expiry time.Time, permissions string, signedIPRange string, HTTPSOnly bool) (string, error) {
|
|
var (
|
|
signedPermissions = permissions
|
|
blobURL = b.GetBlobURL(container, name)
|
|
)
|
|
canonicalizedResource, err := b.client.buildCanonicalizedResource(blobURL, b.auth)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// "The canonicalizedresouce portion of the string is a canonical path to the signed resource.
|
|
// It must include the service name (blob, table, queue or file) for version 2015-02-21 or
|
|
// later, the storage account name, and the resource name, and must be URL-decoded.
|
|
// -- https://msdn.microsoft.com/en-us/library/azure/dn140255.aspx
|
|
|
|
// We need to replace + with %2b first to avoid being treated as a space (which is correct for query strings, but not the path component).
|
|
canonicalizedResource = strings.Replace(canonicalizedResource, "+", "%2b", -1)
|
|
canonicalizedResource, err = url.QueryUnescape(canonicalizedResource)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
signedExpiry := expiry.UTC().Format(time.RFC3339)
|
|
|
|
//If blob name is missing, resource is a container
|
|
signedResource := "c"
|
|
if len(name) > 0 {
|
|
signedResource = "b"
|
|
}
|
|
|
|
protocols := "https,http"
|
|
if HTTPSOnly {
|
|
protocols = "https"
|
|
}
|
|
stringToSign, err := blobSASStringToSign(b.client.apiVersion, canonicalizedResource, signedExpiry, signedPermissions, signedIPRange, protocols)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
sig := b.client.computeHmac256(stringToSign)
|
|
sasParams := url.Values{
|
|
"sv": {b.client.apiVersion},
|
|
"se": {signedExpiry},
|
|
"sr": {signedResource},
|
|
"sp": {signedPermissions},
|
|
"sig": {sig},
|
|
}
|
|
|
|
if b.client.apiVersion >= "2015-04-05" {
|
|
sasParams.Add("spr", protocols)
|
|
if signedIPRange != "" {
|
|
sasParams.Add("sip", signedIPRange)
|
|
}
|
|
}
|
|
|
|
sasURL, err := url.Parse(blobURL)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
sasURL.RawQuery = sasParams.Encode()
|
|
return sasURL.String(), nil
|
|
}
|
|
|
|
// GetBlobSASURI creates an URL to the specified blob which contains the Shared
|
|
// Access Signature with specified permissions and expiration time.
|
|
//
|
|
// See https://msdn.microsoft.com/en-us/library/azure/ee395415.aspx
|
|
func (b BlobStorageClient) GetBlobSASURI(container, name string, expiry time.Time, permissions string) (string, error) {
|
|
url, err := b.GetBlobSASURIWithSignedIPAndProtocol(container, name, expiry, permissions, "", false)
|
|
return url, err
|
|
}
|
|
|
|
func blobSASStringToSign(signedVersion, canonicalizedResource, signedExpiry, signedPermissions string, signedIP string, protocols string) (string, error) {
|
|
var signedStart, signedIdentifier, rscc, rscd, rsce, rscl, rsct string
|
|
|
|
if signedVersion >= "2015-02-21" {
|
|
canonicalizedResource = "/blob" + canonicalizedResource
|
|
}
|
|
|
|
// https://msdn.microsoft.com/en-us/library/azure/dn140255.aspx#Anchor_12
|
|
if signedVersion >= "2015-04-05" {
|
|
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s", signedPermissions, signedStart, signedExpiry, canonicalizedResource, signedIdentifier, signedIP, protocols, signedVersion, rscc, rscd, rsce, rscl, rsct), nil
|
|
}
|
|
|
|
// reference: http://msdn.microsoft.com/en-us/library/azure/dn140255.aspx
|
|
if signedVersion >= "2013-08-15" {
|
|
return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s", signedPermissions, signedStart, signedExpiry, canonicalizedResource, signedIdentifier, signedVersion, rscc, rscd, rsce, rscl, rsct), nil
|
|
}
|
|
|
|
return "", errors.New("storage: not implemented SAS for versions earlier than 2013-08-15")
|
|
}
|