github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/blob/azureblob/azureblob.go (about)

     1  // Copyright 2018 The Go Cloud Development Kit Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package azureblob provides a blob implementation that uses Azure Storage’s
    16  // BlockBlob. Use OpenBucket to construct a *blob.Bucket.
    17  //
    18  // NOTE: SignedURLs for PUT created with this package are not fully portable;
    19  // they will not work unless the PUT request includes a "x-ms-blob-type" header
    20  // set to "BlockBlob".
    21  // See https://stackoverflow.com/questions/37824136/put-on-sas-blob-url-without-specifying-x-ms-blob-type-header.
    22  //
    23  // # URLs
    24  //
    25  // For blob.OpenBucket, azureblob registers for the scheme "azblob".
    26  //
    27  // The default URL opener will use environment variables to generate
    28  // credentials and a service URL; see
    29  // https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
    30  // for a more complete descriptions of each approach.
    31  //   - AZURE_STORAGE_ACCOUNT: The service account name. Required if used along with AZURE_STORAGE KEY, because it defines
    32  //     authentication mechanism to be azblob.NewSharedKeyCredential, which creates immutable shared key credentials.
    33  //     Otherwise, "storage_account" in the URL query string parameter can be used.
    34  //   - AZURE_STORAGE_KEY: To use a shared key credential. The service account
    35  //     name and key are passed to NewSharedKeyCredential and then the
    36  //     resulting credential is passed to NewServiceClientWithSharedKey.
    37  //   - AZURE_STORAGE_CONNECTION_STRING: To use a connection string, passed to
    38  //     NewServiceClientFromConnectionString.
    39  //   - AZURE_STORAGE_SAS_TOKEN: To use a SAS token. The SAS token is added
    40  //     as a URL parameter to the service URL, and passed to
    41  //     NewServiceClientWithNoCredential.
    42  //   - If none of the above are provided, azureblob defaults to
    43  //     azidentity.NewDefaultAzureCredential:
    44  //     https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#NewDefaultAzureCredential.
    45  //     See the documentation there for the credential types it supports, including
    46  //     CLI creds, environment variables like AZURE_CLIENT_ID, AZURE_TENANT_ID, etc.
    47  //
    48  // In addition, the environment variables AZURE_STORAGE_ACCOUNT, AZURE_STORAGE_DOMAIN,
    49  // AZURE_STORAGE_PROTOCOL, AZURE_STORAGE_IS_CDN, and AZURE_STORAGE_IS_LOCAL_EMULATOR
    50  // can be used to configure how the default URLOpener generates the Azure
    51  // Service URL via ServiceURLOptions. These can all be configured via URL
    52  // parameters as well. See ServiceURLOptions and NewDefaultServiceURL
    53  // for more details.
    54  //
    55  // To customize the URL opener, or for more details on the URL format,
    56  // see URLOpener.
    57  //
    58  // See https://gocloud.dev/concepts/urls/ for background information.
    59  //
    60  // # Escaping
    61  //
    62  // Go CDK supports all UTF-8 strings; to make this work with services lacking
    63  // full UTF-8 support, strings must be escaped (during writes) and unescaped
    64  // (during reads). The following escapes are performed for azureblob:
    65  //   - Blob keys: ASCII characters 0-31, 92 ("\"), and 127 are escaped to
    66  //     "__0x<hex>__". Additionally, the "/" in "../" and a trailing "/" in a
    67  //     key (e.g., "foo/") are escaped in the same way.
    68  //   - Metadata keys: Per https://docs.microsoft.com/en-us/azure/storage/blobs/storage-properties-metadata,
    69  //     Azure only allows C# identifiers as metadata keys. Therefore, characters
    70  //     other than "[a-z][A-z][0-9]_" are escaped using "__0x<hex>__". In addition,
    71  //     characters "[0-9]" are escaped when they start the string.
    72  //     URL encoding would not work since "%" is not valid.
    73  //   - Metadata values: Escaped using URL encoding.
    74  //
    75  // # As
    76  //
    77  // azureblob exposes the following types for As:
    78  //   - Bucket: *azblob.ContainerClient
    79  //   - Error: *azcore.ReponseError, *azblob.InternalError, *azblob.StorageError
    80  //   - ListObject: azblob.BlobItemInternal for objects, azblob.BlobPrefix for "directories"
    81  //   - ListOptions.BeforeList: *azblob.ContainerListBlobsHierarchyOption
    82  //   - Reader: azblob.BlobDownloadResponse
    83  //   - Reader.BeforeRead: *azblob.BlockDownloadOptions
    84  //   - Attributes: azblob.BlobGetPropertiesResponse
    85  //   - CopyOptions.BeforeCopy: *azblob.BlobStartCopyOptions
    86  //   - WriterOptions.BeforeWrite: *azblob.UploadStreamOptions
    87  //   - SignedURLOptions.BeforeSign: *azblob.BlobSASPermissions
    88  package azureblob
    89  
    90  import (
    91  	"context"
    92  	"errors"
    93  	"fmt"
    94  	"io"
    95  	"log"
    96  	"net/http"
    97  	"net/url"
    98  	"os"
    99  	"sort"
   100  	"strconv"
   101  	"strings"
   102  	"sync"
   103  	"time"
   104  
   105  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
   106  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
   107  	"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
   108  	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
   109  	"github.com/Azure/go-autorest/autorest/to"
   110  	"github.com/google/wire"
   111  	"gocloud.dev/blob"
   112  	"gocloud.dev/blob/driver"
   113  	"gocloud.dev/gcerrors"
   114  
   115  	"gocloud.dev/internal/escape"
   116  	"gocloud.dev/internal/gcerr"
   117  	"gocloud.dev/internal/useragent"
   118  )
   119  
   120  const (
   121  	defaultMaxDownloadRetryRequests = 3               // download retry policy (Azure default is zero)
   122  	defaultPageSize                 = 1000            // default page size for ListPaged (Azure default is 5000)
   123  	defaultUploadBuffers            = 5               // configure the number of rotating buffers that are used when uploading (for degree of parallelism)
   124  	defaultUploadBlockSize          = 8 * 1024 * 1024 // configure the upload buffer size
   125  )
   126  
   127  func init() {
   128  	blob.DefaultURLMux().RegisterBucket(Scheme, new(lazyOpener))
   129  }
   130  
   131  // Set holds Wire providers for this package.
   132  var Set = wire.NewSet(
   133  	NewDefaultServiceURLOptions,
   134  	NewServiceURL,
   135  	NewDefaultServiceClient,
   136  )
   137  
   138  // Options sets options for constructing a *blob.Bucket backed by Azure Blob.
   139  type Options struct{}
   140  
   141  // ServiceURL represents an Azure service URL.
   142  type ServiceURL string
   143  
   144  // ServiceURLOptions sets options for constructing a service URL for Azure Blob.
   145  type ServiceURLOptions struct {
   146  	// AccountName is the account name the credentials are for.
   147  	AccountName string
   148  
   149  	// SASToken will be appended to the service URL.
   150  	// See https://docs.microsoft.com/en-us/azure/storage/common/storage-dotnet-shared-access-signature-part-1#shared-access-signature-parameters.
   151  	SASToken string
   152  
   153  	// StorageDomain can be provided to specify an Azure Cloud Environment
   154  	// domain to target for the blob storage account (i.e. public, government, china).
   155  	// Defaults to "blob.core.windows.net". Possible values will look similar
   156  	// to this but are different for each cloud (i.e. "blob.core.govcloudapi.net" for USGovernment).
   157  	// Check the Azure developer guide for the cloud environment where your bucket resides.
   158  	// See the docstring for NewServiceURL to see examples of how this is used
   159  	// along with the other Options fields.
   160  	StorageDomain string
   161  
   162  	// Protocol can be provided to specify protocol to access Azure Blob Storage.
   163  	// Protocols that can be specified are "http" for local emulator and "https" for general.
   164  	// Defaults to "https".
   165  	// See the docstring for NewServiceURL to see examples of how this is used
   166  	// along with the other Options fields.
   167  	Protocol string
   168  
   169  	// IsCDN can be set to true when using a CDN URL pointing to a blob storage account:
   170  	// https://docs.microsoft.com/en-us/azure/cdn/cdn-create-a-storage-account-with-cdn
   171  	// See the docstring for NewServiceURL to see examples of how this is used
   172  	// along with the other Options fields.
   173  	IsCDN bool
   174  
   175  	// IsLocalEmulator should be set to true when targeting Local Storage Emulator (Azurite).
   176  	// See the docstring for NewServiceURL to see examples of how this is used
   177  	// along with the other Options fields.
   178  	IsLocalEmulator bool
   179  }
   180  
   181  // NewDefaultServiceURLOptions generates a ServiceURLOptions based on environment variables.
   182  func NewDefaultServiceURLOptions() *ServiceURLOptions {
   183  	isCDN, _ := strconv.ParseBool(os.Getenv("AZURE_STORAGE_IS_CDN"))
   184  	isLocalEmulator, _ := strconv.ParseBool(os.Getenv("AZURE_STORAGE_IS_LOCAL_EMULATOR"))
   185  	return &ServiceURLOptions{
   186  		AccountName:     os.Getenv("AZURE_STORAGE_ACCOUNT"),
   187  		SASToken:        os.Getenv("AZURE_STORAGE_SAS_TOKEN"),
   188  		StorageDomain:   os.Getenv("AZURE_STORAGE_DOMAIN"),
   189  		Protocol:        os.Getenv("AZURE_STORAGE_PROTOCOL"),
   190  		IsCDN:           isCDN,
   191  		IsLocalEmulator: isLocalEmulator,
   192  	}
   193  }
   194  
   195  // withOverrides returns o with overrides from urlValues.
   196  // See URLOpener for supported overrides.
   197  func (o *ServiceURLOptions) withOverrides(urlValues url.Values) (*ServiceURLOptions, error) {
   198  	retval := *o
   199  	for param, values := range urlValues {
   200  		if len(values) > 1 {
   201  			return nil, fmt.Errorf("multiple values of %v not allowed", param)
   202  		}
   203  		value := values[0]
   204  		switch param {
   205  		case "domain":
   206  			retval.StorageDomain = value
   207  		case "protocol":
   208  			retval.Protocol = value
   209  		case "cdn":
   210  			isCDN, err := strconv.ParseBool(value)
   211  			if err != nil {
   212  				return nil, err
   213  			}
   214  			retval.IsCDN = isCDN
   215  		case "localemu":
   216  			isLocalEmulator, err := strconv.ParseBool(value)
   217  			if err != nil {
   218  				return nil, err
   219  			}
   220  			retval.IsLocalEmulator = isLocalEmulator
   221  		case "storage_account":
   222  			retval.AccountName = value
   223  		default:
   224  			return nil, fmt.Errorf("unknown query parameter %q", param)
   225  		}
   226  	}
   227  	return &retval, nil
   228  }
   229  
   230  // NewServiceURL generates a URL for addressing an Azure Blob service
   231  // account. It uses several parameters, each of which can be specified
   232  // via ServiceURLOptions.
   233  //
   234  // The generated URL is "<protocol>://<account name>.<domain>"
   235  // with the following caveats:
   236  //   - If opts.SASToken is provided, it is appended to the URL as a query
   237  //     parameter.
   238  //   - If opts.IsCDN is true, the <account name> part is dropped.
   239  //   - If opts.IsLocalEmulator is true, or the domain starts with "localhost"
   240  //     or "127.0.0.1", the account name and domain are flipped, e.g.:
   241  //     http://127.0.0.1:10000/myaccount
   242  func NewServiceURL(opts *ServiceURLOptions) (ServiceURL, error) {
   243  	if opts == nil {
   244  		opts = &ServiceURLOptions{}
   245  	}
   246  	accountName := opts.AccountName
   247  	if accountName == "" {
   248  		return "", errors.New("azureblob: Options.AccountName is required")
   249  	}
   250  	domain := opts.StorageDomain
   251  	if domain == "" {
   252  		domain = "blob.core.windows.net"
   253  	}
   254  	protocol := opts.Protocol
   255  	if protocol == "" {
   256  		protocol = "https"
   257  	} else if protocol != "http" && protocol != "https" {
   258  		return "", fmt.Errorf("invalid protocol %q", protocol)
   259  	}
   260  	var svcURL string
   261  	if strings.HasPrefix(domain, "127.0.0.1") || strings.HasPrefix(domain, "localhost") || opts.IsLocalEmulator {
   262  		svcURL = fmt.Sprintf("%s://%s/%s", protocol, domain, accountName)
   263  	} else if opts.IsCDN {
   264  		svcURL = fmt.Sprintf("%s://%s", protocol, domain)
   265  	} else {
   266  		svcURL = fmt.Sprintf("%s://%s.%s", protocol, accountName, domain)
   267  	}
   268  	if opts.SASToken != "" {
   269  		svcURL += "?" + opts.SASToken
   270  	}
   271  	log.Printf("azureblob: constructed service URL: %s\n", svcURL)
   272  	return ServiceURL(svcURL), nil
   273  }
   274  
   275  // lazyOpener obtains credentials and creates a client on the first call to OpenBucketURL.
   276  type lazyOpener struct {
   277  	init   sync.Once
   278  	opener *URLOpener
   279  }
   280  
   281  func (o *lazyOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) {
   282  	o.init.Do(func() {
   283  		credInfo := newCredInfoFromEnv()
   284  		opts := NewDefaultServiceURLOptions()
   285  		o.opener = &URLOpener{
   286  			MakeClient:        credInfo.NewServiceClient,
   287  			ServiceURLOptions: *opts,
   288  		}
   289  	})
   290  	return o.opener.OpenBucketURL(ctx, u)
   291  }
   292  
   293  type credTypeEnumT int
   294  
   295  const (
   296  	credTypeDefault credTypeEnumT = iota
   297  	credTypeSharedKey
   298  	credTypeSASViaNone
   299  	credTypeConnectionString
   300  )
   301  
   302  type credInfoT struct {
   303  	CredType credTypeEnumT
   304  
   305  	// For credTypeSharedKey.
   306  	AccountName string
   307  	AccountKey  string
   308  
   309  	// For credTypeSASViaNone.
   310  	//SASToken string
   311  
   312  	// For credTypeConnectionString
   313  	ConnectionString string
   314  }
   315  
   316  func newCredInfoFromEnv() *credInfoT {
   317  	accountName := os.Getenv("AZURE_STORAGE_ACCOUNT")
   318  	accountKey := os.Getenv("AZURE_STORAGE_KEY")
   319  	sasToken := os.Getenv("AZURE_STORAGE_SAS_TOKEN")
   320  	connectionString := os.Getenv("AZURE_STORAGE_CONNECTION_STRING")
   321  	credInfo := &credInfoT{
   322  		AccountName: accountName,
   323  	}
   324  	if accountName != "" && accountKey != "" {
   325  		credInfo.CredType = credTypeSharedKey
   326  		credInfo.AccountKey = accountKey
   327  	} else if sasToken != "" {
   328  		credInfo.CredType = credTypeSASViaNone
   329  		//credInfo.SASToken = sasToken
   330  	} else if connectionString != "" {
   331  		credInfo.CredType = credTypeConnectionString
   332  		credInfo.ConnectionString = connectionString
   333  	} else {
   334  		credInfo.CredType = credTypeDefault
   335  	}
   336  	return credInfo
   337  }
   338  
   339  func (i *credInfoT) NewServiceClient(svcURL ServiceURL) (*azblob.ServiceClient, error) {
   340  	// Set the ApplicationID.
   341  	azClientOpts := &azblob.ClientOptions{
   342  		Telemetry: policy.TelemetryOptions{
   343  			ApplicationID: useragent.AzureUserAgentPrefix("blob"),
   344  		},
   345  	}
   346  
   347  	switch i.CredType {
   348  	case credTypeDefault:
   349  		log.Println("azureblob.URLOpener: using NewDefaultAzureCredential")
   350  		cred, err := azidentity.NewDefaultAzureCredential(nil)
   351  		if err != nil {
   352  			return nil, fmt.Errorf("failed azidentity.NewDefaultAzureCredential: %v", err)
   353  		}
   354  		return azblob.NewServiceClient(string(svcURL), cred, azClientOpts)
   355  	case credTypeSharedKey:
   356  		log.Println("azureblob.URLOpener: using shared key credentials")
   357  		sharedKeyCred, err := azblob.NewSharedKeyCredential(i.AccountName, i.AccountKey)
   358  		if err != nil {
   359  			return nil, fmt.Errorf("failed azblob.NewSharedKeyCredential: %v", err)
   360  		}
   361  		return azblob.NewServiceClientWithSharedKey(string(svcURL), sharedKeyCred, azClientOpts)
   362  	case credTypeSASViaNone:
   363  		log.Println("azureblob.URLOpener: using SAS token and no other credentials")
   364  		return azblob.NewServiceClientWithNoCredential(string(svcURL), azClientOpts)
   365  	case credTypeConnectionString:
   366  		log.Println("azureblob.URLOpener: using connection string")
   367  		return azblob.NewServiceClientFromConnectionString(i.ConnectionString, azClientOpts)
   368  	default:
   369  		return nil, errors.New("internal error, unknown cred type")
   370  	}
   371  }
   372  
   373  // Scheme is the URL scheme gcsblob registers its URLOpener under on
   374  // blob.DefaultMux.
   375  const Scheme = "azblob"
   376  
   377  // URLOpener opens Azure URLs like "azblob://mybucket".
   378  //
   379  // The URL host is used as the bucket name.
   380  //
   381  // The following query options are supported:
   382  //   - domain: Overrides Options.StorageDomain.
   383  //   - protocol: Overrides Options.Protocol.
   384  //   - cdn: Overrides Options.IsCDN.
   385  //   - localemu: Overrides Options.IsLocalEmulator.
   386  type URLOpener struct {
   387  	// MakeClient must be set to a non-nil value.
   388  	MakeClient func(svcURL ServiceURL) (*azblob.ServiceClient, error)
   389  
   390  	// ServiceURLOptions specifies default options for generating the service URL.
   391  	// Some options can be overridden in the URL as described above.
   392  	ServiceURLOptions ServiceURLOptions
   393  
   394  	// Options specifies the options to pass to OpenBucket.
   395  	Options Options
   396  }
   397  
   398  // OpenBucketURL opens a blob.Bucket based on u.
   399  func (o *URLOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) {
   400  	opts, err := o.ServiceURLOptions.withOverrides(u.Query())
   401  	if err != nil {
   402  		return nil, err
   403  	}
   404  	svcURL, err := NewServiceURL(opts)
   405  	if err != nil {
   406  		return nil, err
   407  	}
   408  	svcClient, err := o.MakeClient(svcURL)
   409  	if err != nil {
   410  		return nil, err
   411  	}
   412  	return OpenBucket(ctx, svcClient, u.Host, &o.Options)
   413  }
   414  
   415  // bucket represents a Azure Storage Account Container, which handles read,
   416  // write and delete operations on objects within it.
   417  // See https://docs.microsoft.com/en-us/azure/storage/blobs/storage-blobs-introduction.
   418  type bucket struct {
   419  	client *azblob.ContainerClient
   420  	opts   *Options
   421  }
   422  
   423  // NewDefaultServiceClient returns an Azure Blob service client
   424  // with credentials from the environment as described in the package
   425  // docstring.
   426  func NewDefaultServiceClient(svcURL ServiceURL) (*azblob.ServiceClient, error) {
   427  	return newCredInfoFromEnv().NewServiceClient(svcURL)
   428  }
   429  
   430  // OpenBucket returns a *blob.Bucket backed by Azure Storage Account. See the package
   431  // documentation for an example and
   432  // https://godoc.org/github.com/Azure/azure-storage-blob-go/azblob
   433  // for more details.
   434  func OpenBucket(ctx context.Context, svcClient *azblob.ServiceClient, containerName string, opts *Options) (*blob.Bucket, error) {
   435  	b, err := openBucket(ctx, svcClient, containerName, opts)
   436  	if err != nil {
   437  		return nil, err
   438  	}
   439  	return blob.NewBucket(b), nil
   440  }
   441  
   442  func openBucket(ctx context.Context, svcClient *azblob.ServiceClient, containerName string, opts *Options) (*bucket, error) {
   443  	if svcClient == nil {
   444  		return nil, errors.New("azureblob.OpenBucket: client is required")
   445  	}
   446  	if containerName == "" {
   447  		return nil, errors.New("azureblob.OpenBucket: containerName is required")
   448  	}
   449  	containerClient, err := svcClient.NewContainerClient(containerName)
   450  	if err != nil {
   451  		return nil, err
   452  	}
   453  	if opts == nil {
   454  		opts = &Options{}
   455  	}
   456  	return &bucket{
   457  		client: containerClient,
   458  		opts:   opts,
   459  	}, nil
   460  }
   461  
   462  // Close implements driver.Close.
   463  func (b *bucket) Close() error {
   464  	return nil
   465  }
   466  
   467  // Copy implements driver.Copy.
   468  func (b *bucket) Copy(ctx context.Context, dstKey, srcKey string, opts *driver.CopyOptions) error {
   469  	dstKey = escapeKey(dstKey, false)
   470  	dstBlobClient, err := b.client.NewBlobClient(dstKey)
   471  	if err != nil {
   472  		return err
   473  	}
   474  	srcKey = escapeKey(srcKey, false)
   475  	srcBlobClient, err := b.client.NewBlobClient(srcKey)
   476  	if err != nil {
   477  		return err
   478  	}
   479  	copyOptions := &azblob.BlobStartCopyOptions{}
   480  	if opts.BeforeCopy != nil {
   481  		asFunc := func(i interface{}) bool {
   482  			switch v := i.(type) {
   483  			case **azblob.BlobStartCopyOptions:
   484  				*v = copyOptions
   485  				return true
   486  			}
   487  			return false
   488  		}
   489  		if err := opts.BeforeCopy(asFunc); err != nil {
   490  			return err
   491  		}
   492  	}
   493  	resp, err := dstBlobClient.StartCopyFromURL(ctx, srcBlobClient.URL(), copyOptions)
   494  	if err != nil {
   495  		return err
   496  	}
   497  	nErrors := 0
   498  	copyStatus := *resp.CopyStatus
   499  	for copyStatus == azblob.CopyStatusTypePending {
   500  		// Poll until the copy is complete.
   501  		time.Sleep(500 * time.Millisecond)
   502  		propertiesResp, err := dstBlobClient.GetProperties(ctx, nil)
   503  		if err != nil {
   504  			// A GetProperties failure may be transient, so allow a couple
   505  			// of them before giving up.
   506  			nErrors++
   507  			if ctx.Err() != nil || nErrors == 3 {
   508  				return err
   509  			}
   510  		}
   511  		copyStatus = *propertiesResp.CopyStatus
   512  	}
   513  	if copyStatus != azblob.CopyStatusTypeSuccess {
   514  		return fmt.Errorf("Copy failed with status: %s", copyStatus)
   515  	}
   516  	return nil
   517  }
   518  
   519  // Delete implements driver.Delete.
   520  func (b *bucket) Delete(ctx context.Context, key string) error {
   521  	key = escapeKey(key, false)
   522  	blobClient, err := b.client.NewBlobClient(key)
   523  	if err != nil {
   524  		return err
   525  	}
   526  	_, err = blobClient.Delete(ctx, nil)
   527  	return err
   528  }
   529  
   530  // reader reads an azblob. It implements io.ReadCloser.
   531  type reader struct {
   532  	body  io.ReadCloser
   533  	attrs driver.ReaderAttributes
   534  	raw   *azblob.BlobDownloadResponse
   535  }
   536  
   537  func (r *reader) Read(p []byte) (int, error) {
   538  	return r.body.Read(p)
   539  }
   540  func (r *reader) Close() error {
   541  	return r.body.Close()
   542  }
   543  func (r *reader) Attributes() *driver.ReaderAttributes {
   544  	return &r.attrs
   545  }
   546  func (r *reader) As(i interface{}) bool {
   547  	p, ok := i.(*azblob.BlobDownloadResponse)
   548  	if !ok {
   549  		return false
   550  	}
   551  	*p = *r.raw
   552  	return true
   553  }
   554  
   555  // NewRangeReader implements driver.NewRangeReader.
   556  func (b *bucket) NewRangeReader(ctx context.Context, key string, offset, length int64, opts *driver.ReaderOptions) (driver.Reader, error) {
   557  	key = escapeKey(key, false)
   558  	blobClient, err := b.client.NewBlobClient(key)
   559  
   560  	downloadOpts := azblob.BlobDownloadOptions{Offset: &offset}
   561  	if length >= 0 {
   562  		downloadOpts.Count = &length
   563  	}
   564  	if opts.BeforeRead != nil {
   565  		asFunc := func(i interface{}) bool {
   566  			if p, ok := i.(**azblob.BlobDownloadOptions); ok {
   567  				*p = &downloadOpts
   568  				return true
   569  			}
   570  			return false
   571  		}
   572  		if err := opts.BeforeRead(asFunc); err != nil {
   573  			return nil, err
   574  		}
   575  	}
   576  	blobDownloadResponse, err := blobClient.Download(ctx, &downloadOpts)
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  	attrs := driver.ReaderAttributes{
   581  		ContentType: to.String(blobDownloadResponse.ContentType),
   582  		Size:        getSize(*blobDownloadResponse.ContentLength, to.String(blobDownloadResponse.ContentRange)),
   583  		ModTime:     *blobDownloadResponse.LastModified,
   584  	}
   585  	var body io.ReadCloser
   586  	if length == 0 {
   587  		body = http.NoBody
   588  	} else {
   589  		body = blobDownloadResponse.Body(&azblob.RetryReaderOptions{MaxRetryRequests: defaultMaxDownloadRetryRequests})
   590  	}
   591  	return &reader{
   592  		body:  body,
   593  		attrs: attrs,
   594  		raw:   &blobDownloadResponse,
   595  	}, nil
   596  }
   597  
   598  func getSize(contentLength int64, contentRange string) int64 {
   599  	// Default size to ContentLength, but that's incorrect for partial-length reads,
   600  	// where ContentLength refers to the size of the returned Body, not the entire
   601  	// size of the blob. ContentRange has the full size.
   602  	size := contentLength
   603  	if contentRange != "" {
   604  		// Sample: bytes 10-14/27 (where 27 is the full size).
   605  		parts := strings.Split(contentRange, "/")
   606  		if len(parts) == 2 {
   607  			if i, err := strconv.ParseInt(parts[1], 10, 64); err == nil {
   608  				size = i
   609  			}
   610  		}
   611  	}
   612  	return size
   613  }
   614  
   615  // As implements driver.As.
   616  func (b *bucket) As(i interface{}) bool {
   617  	p, ok := i.(**azblob.ContainerClient)
   618  	if !ok {
   619  		return false
   620  	}
   621  	*p = b.client
   622  	return true
   623  }
   624  
   625  // As implements driver.ErrorAs.
   626  func (b *bucket) ErrorAs(err error, i interface{}) bool {
   627  	switch v := err.(type) {
   628  	case *azcore.ResponseError:
   629  		if p, ok := i.(**azcore.ResponseError); ok {
   630  			*p = v
   631  			return true
   632  		}
   633  	case *azblob.StorageError:
   634  		if p, ok := i.(**azblob.StorageError); ok {
   635  			*p = v
   636  			return true
   637  		}
   638  	case *azblob.InternalError:
   639  		if p, ok := i.(**azblob.InternalError); ok {
   640  			*p = v
   641  			return true
   642  		}
   643  	}
   644  	return false
   645  }
   646  
   647  func (b *bucket) ErrorCode(err error) gcerrors.ErrorCode {
   648  	var errorCode azblob.StorageErrorCode
   649  	var statusCode int
   650  	var sErr *azblob.StorageError
   651  	var rErr *azcore.ResponseError
   652  	if errors.As(err, &sErr) {
   653  		errorCode = sErr.ErrorCode
   654  		statusCode = sErr.StatusCode()
   655  	} else if errors.As(err, &rErr) {
   656  		errorCode = azblob.StorageErrorCode(rErr.ErrorCode)
   657  		statusCode = rErr.StatusCode
   658  	} else if strings.Contains(err.Error(), "no such host") {
   659  		// This happens with an invalid storage account name; the host
   660  		// is something like invalidstorageaccount.blob.core.windows.net.
   661  		return gcerrors.NotFound
   662  	} else {
   663  		return gcerrors.Unknown
   664  	}
   665  	if errorCode == azblob.StorageErrorCodeBlobNotFound || statusCode == 404 {
   666  		return gcerrors.NotFound
   667  	}
   668  	if errorCode == azblob.StorageErrorCodeAuthenticationFailed {
   669  		return gcerrors.PermissionDenied
   670  	}
   671  	return gcerrors.Unknown
   672  }
   673  
   674  // Attributes implements driver.Attributes.
   675  func (b *bucket) Attributes(ctx context.Context, key string) (*driver.Attributes, error) {
   676  	key = escapeKey(key, false)
   677  	blobClient, err := b.client.NewBlobClient(key)
   678  	if err != nil {
   679  		return nil, err
   680  	}
   681  	blobPropertiesResponse, err := blobClient.GetProperties(ctx, nil)
   682  	if err != nil {
   683  		return nil, err
   684  	}
   685  
   686  	md := make(map[string]string, len(blobPropertiesResponse.Metadata))
   687  	for k, v := range blobPropertiesResponse.Metadata {
   688  		// See the package comments for more details on escaping of metadata
   689  		// keys & values.
   690  		md[escape.HexUnescape(k)] = escape.URLUnescape(v)
   691  	}
   692  	return &driver.Attributes{
   693  		CacheControl:       to.String(blobPropertiesResponse.CacheControl),
   694  		ContentDisposition: to.String(blobPropertiesResponse.ContentDisposition),
   695  		ContentEncoding:    to.String(blobPropertiesResponse.ContentEncoding),
   696  		ContentLanguage:    to.String(blobPropertiesResponse.ContentLanguage),
   697  		ContentType:        to.String(blobPropertiesResponse.ContentType),
   698  		Size:               to.Int64(blobPropertiesResponse.ContentLength),
   699  		CreateTime:         *blobPropertiesResponse.CreationTime,
   700  		ModTime:            *blobPropertiesResponse.LastModified,
   701  		MD5:                blobPropertiesResponse.ContentMD5,
   702  		ETag:               to.String(blobPropertiesResponse.ETag),
   703  		Metadata:           md,
   704  		AsFunc: func(i interface{}) bool {
   705  			p, ok := i.(*azblob.BlobGetPropertiesResponse)
   706  			if !ok {
   707  				return false
   708  			}
   709  			*p = blobPropertiesResponse
   710  			return true
   711  		},
   712  	}, nil
   713  }
   714  
   715  // ListPaged implements driver.ListPaged.
   716  func (b *bucket) ListPaged(ctx context.Context, opts *driver.ListOptions) (*driver.ListPage, error) {
   717  	pageSize := opts.PageSize
   718  	if pageSize == 0 {
   719  		pageSize = defaultPageSize
   720  	}
   721  
   722  	var marker *string
   723  	if len(opts.PageToken) > 0 {
   724  		pt := string(opts.PageToken)
   725  		marker = &pt
   726  	}
   727  
   728  	pageSize32 := int32(pageSize)
   729  	prefix := escapeKey(opts.Prefix, true)
   730  	azOpts := azblob.ContainerListBlobsHierarchyOptions{
   731  		MaxResults: &pageSize32,
   732  		Prefix:     &prefix,
   733  		Marker:     marker,
   734  	}
   735  	if opts.BeforeList != nil {
   736  		asFunc := func(i interface{}) bool {
   737  			p, ok := i.(**azblob.ContainerListBlobsHierarchyOptions)
   738  			if !ok {
   739  				return false
   740  			}
   741  			*p = &azOpts
   742  			return true
   743  		}
   744  		if err := opts.BeforeList(asFunc); err != nil {
   745  			return nil, err
   746  		}
   747  	}
   748  	azPager := b.client.ListBlobsHierarchy(escapeKey(opts.Delimiter, true), &azOpts)
   749  	azPager.NextPage(ctx)
   750  	if err := azPager.Err(); err != nil {
   751  		return nil, err
   752  	}
   753  	resp := azPager.PageResponse()
   754  	page := &driver.ListPage{}
   755  	page.Objects = []*driver.ListObject{}
   756  	segment := resp.ListBlobsHierarchySegmentResponse.Segment
   757  	for _, blobPrefix := range segment.BlobPrefixes {
   758  		blobPrefix := blobPrefix // capture loop variable for use in AsFunc
   759  		page.Objects = append(page.Objects, &driver.ListObject{
   760  			Key:   unescapeKey(to.String(blobPrefix.Name)),
   761  			Size:  0,
   762  			IsDir: true,
   763  			AsFunc: func(i interface{}) bool {
   764  				p, ok := i.(*azblob.BlobPrefix)
   765  				if !ok {
   766  					return false
   767  				}
   768  				*p = *blobPrefix
   769  				return true
   770  			}})
   771  	}
   772  	for _, blobInfo := range segment.BlobItems {
   773  		blobInfo := blobInfo // capture loop variable for use in AsFunc
   774  		page.Objects = append(page.Objects, &driver.ListObject{
   775  			Key:     unescapeKey(to.String(blobInfo.Name)),
   776  			ModTime: *blobInfo.Properties.LastModified,
   777  			Size:    *blobInfo.Properties.ContentLength,
   778  			MD5:     blobInfo.Properties.ContentMD5,
   779  			IsDir:   false,
   780  			AsFunc: func(i interface{}) bool {
   781  				p, ok := i.(*azblob.BlobItemInternal)
   782  				if !ok {
   783  					return false
   784  				}
   785  				*p = *blobInfo
   786  				return true
   787  			},
   788  		})
   789  	}
   790  	if resp.NextMarker != nil {
   791  		page.NextPageToken = []byte(*resp.NextMarker)
   792  	}
   793  	if len(segment.BlobPrefixes) > 0 && len(segment.BlobItems) > 0 {
   794  		sort.Slice(page.Objects, func(i, j int) bool {
   795  			return page.Objects[i].Key < page.Objects[j].Key
   796  		})
   797  	}
   798  	return page, nil
   799  }
   800  
   801  // SignedURL implements driver.SignedURL.
   802  func (b *bucket) SignedURL(ctx context.Context, key string, opts *driver.SignedURLOptions) (string, error) {
   803  	if opts.ContentType != "" || opts.EnforceAbsentContentType {
   804  		return "", gcerr.New(gcerr.Unimplemented, nil, 1, "azureblob: does not enforce Content-Type on PUT")
   805  	}
   806  
   807  	key = escapeKey(key, false)
   808  	blobClient, err := b.client.NewBlobClient(key)
   809  	if err != nil {
   810  		return "", err
   811  	}
   812  
   813  	perms := azblob.BlobSASPermissions{}
   814  	switch opts.Method {
   815  	case http.MethodGet:
   816  		perms.Read = true
   817  	case http.MethodPut:
   818  		perms.Create = true
   819  		perms.Write = true
   820  	case http.MethodDelete:
   821  		perms.Delete = true
   822  	default:
   823  		return "", fmt.Errorf("unsupported Method %s", opts.Method)
   824  	}
   825  
   826  	if opts.BeforeSign != nil {
   827  		asFunc := func(i interface{}) bool {
   828  			v, ok := i.(**azblob.BlobSASPermissions)
   829  			if ok {
   830  				*v = &perms
   831  			}
   832  			return ok
   833  		}
   834  		if err := opts.BeforeSign(asFunc); err != nil {
   835  			return "", err
   836  		}
   837  	}
   838  	start := time.Now().UTC()
   839  	expiry := start.Add(opts.Expiry)
   840  	sasQueryParams, err := blobClient.GetSASToken(perms, start, expiry)
   841  	sasURL := fmt.Sprintf("%s?%s", blobClient.URL(), sasQueryParams.Encode())
   842  	return sasURL, nil
   843  }
   844  
   845  type writer struct {
   846  	ctx        context.Context
   847  	client     *azblob.BlockBlobClient
   848  	uploadOpts *azblob.UploadStreamOptions
   849  
   850  	w     *io.PipeWriter
   851  	donec chan struct{}
   852  	err   error
   853  }
   854  
   855  // escapeKey does all required escaping for UTF-8 strings to work with Azure.
   856  // isPrefix indicates whether the  key is a full key, or a prefix/delimiter.
   857  func escapeKey(key string, isPrefix bool) string {
   858  	return escape.HexEscape(key, func(r []rune, i int) bool {
   859  		c := r[i]
   860  		switch {
   861  		// Azure does not work well with backslashes in blob names.
   862  		case c == '\\':
   863  			return true
   864  		// Azure doesn't handle these characters (determined via experimentation).
   865  		case c < 32 || c == 127:
   866  			return true
   867  			// Escape trailing "/" for full keys, otherwise Azure can't address them
   868  			// consistently.
   869  		case !isPrefix && i == len(key)-1 && c == '/':
   870  			return true
   871  		// For "../", escape the trailing slash.
   872  		case i > 1 && r[i] == '/' && r[i-1] == '.' && r[i-2] == '.':
   873  			return true
   874  		}
   875  		return false
   876  	})
   877  }
   878  
   879  // unescapeKey reverses escapeKey.
   880  func unescapeKey(key string) string {
   881  	return escape.HexUnescape(key)
   882  }
   883  
   884  // NewTypedWriter implements driver.NewTypedWriter.
   885  func (b *bucket) NewTypedWriter(ctx context.Context, key string, contentType string, opts *driver.WriterOptions) (driver.Writer, error) {
   886  	key = escapeKey(key, false)
   887  	blobClient, err := b.client.NewBlockBlobClient(key)
   888  	if err != nil {
   889  		return nil, err
   890  	}
   891  	if opts.BufferSize == 0 {
   892  		opts.BufferSize = defaultUploadBlockSize
   893  	}
   894  	if opts.MaxConcurrency == 0 {
   895  		opts.MaxConcurrency = defaultUploadBuffers
   896  	}
   897  
   898  	md := make(map[string]string, len(opts.Metadata))
   899  	for k, v := range opts.Metadata {
   900  		// See the package comments for more details on escaping of metadata
   901  		// keys & values.
   902  		e := escape.HexEscape(k, func(runes []rune, i int) bool {
   903  			c := runes[i]
   904  			switch {
   905  			case i == 0 && c >= '0' && c <= '9':
   906  				return true
   907  			case escape.IsASCIIAlphanumeric(c):
   908  				return false
   909  			case c == '_':
   910  				return false
   911  			}
   912  			return true
   913  		})
   914  		if _, ok := md[e]; ok {
   915  			return nil, fmt.Errorf("duplicate keys after escaping: %q => %q", k, e)
   916  		}
   917  		md[e] = escape.URLEscape(v)
   918  	}
   919  	uploadOpts := &azblob.UploadStreamOptions{
   920  		BufferSize: opts.BufferSize,
   921  		MaxBuffers: opts.MaxConcurrency,
   922  		Metadata:   md,
   923  		HTTPHeaders: &azblob.BlobHTTPHeaders{
   924  			BlobCacheControl:       &opts.CacheControl,
   925  			BlobContentDisposition: &opts.ContentDisposition,
   926  			BlobContentEncoding:    &opts.ContentEncoding,
   927  			BlobContentLanguage:    &opts.ContentLanguage,
   928  			BlobContentMD5:         opts.ContentMD5,
   929  			BlobContentType:        &contentType,
   930  		},
   931  	}
   932  	if opts.BeforeWrite != nil {
   933  		asFunc := func(i interface{}) bool {
   934  			p, ok := i.(**azblob.UploadStreamOptions)
   935  			if !ok {
   936  				return false
   937  			}
   938  			*p = uploadOpts
   939  			return true
   940  		}
   941  		if err := opts.BeforeWrite(asFunc); err != nil {
   942  			return nil, err
   943  		}
   944  	}
   945  	return &writer{
   946  		ctx:        ctx,
   947  		client:     blobClient,
   948  		uploadOpts: uploadOpts,
   949  		donec:      make(chan struct{}),
   950  	}, nil
   951  }
   952  
   953  // Write appends p to w. User must call Close to close the w after done writing.
   954  func (w *writer) Write(p []byte) (int, error) {
   955  	if len(p) == 0 {
   956  		return 0, nil
   957  	}
   958  	if w.w == nil {
   959  		pr, pw := io.Pipe()
   960  		w.w = pw
   961  		if err := w.open(pr); err != nil {
   962  			return 0, err
   963  		}
   964  	}
   965  	return w.w.Write(p)
   966  }
   967  
   968  func (w *writer) open(pr *io.PipeReader) error {
   969  	go func() {
   970  		defer close(w.donec)
   971  
   972  		var body io.Reader
   973  		if pr == nil {
   974  			body = http.NoBody
   975  		} else {
   976  			body = pr
   977  		}
   978  		_, w.err = w.client.UploadStream(w.ctx, body, *w.uploadOpts)
   979  		if w.err != nil {
   980  			if pr != nil {
   981  				pr.CloseWithError(w.err)
   982  			}
   983  			return
   984  		}
   985  	}()
   986  	return nil
   987  }
   988  
   989  // Close completes the writer and closes it. Any error occurring during write will
   990  // be returned. If a writer is closed before any Write is called, Close will
   991  // create an empty file at the given key.
   992  func (w *writer) Close() error {
   993  	if w.w == nil {
   994  		w.open(nil)
   995  	} else if err := w.w.Close(); err != nil {
   996  		return err
   997  	}
   998  	<-w.donec
   999  	return w.err
  1000  }