github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/block/azure/adapter.go (about)

     1  package azure
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"net/http"
     9  	"net/url"
    10  	"regexp"
    11  	"slices"
    12  	"strings"
    13  	"time"
    14  
    15  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
    16  	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob"
    17  	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob"
    18  	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror"
    19  	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blockblob"
    20  	"github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas"
    21  	"github.com/treeverse/lakefs/pkg/block"
    22  	"github.com/treeverse/lakefs/pkg/block/params"
    23  	"github.com/treeverse/lakefs/pkg/logging"
    24  )
    25  
    26  const (
    27  	sizeSuffix = "_size"
    28  	idSuffix   = "_id"
    29  	_1MiB      = 1024 * 1024
    30  	MaxBuffers = 1
    31  	// udcCacheSize - Arbitrary number: exceeding this number means that in the expiry timeframe we requested pre-signed urls from
    32  	// more the 5000 different accounts, which is highly unlikely
    33  	udcCacheSize = 5000
    34  
    35  	BlobEndpointDefaultDomain = "blob.core.windows.net"
    36  	BlobEndpointChinaDomain   = "blob.core.chinacloudapi.cn"
    37  	BlobEndpointUSGovDomain   = "blob.core.usgovcloudapi.net"
    38  	BlobEndpointTestDomain    = "azurite.test"
    39  )
    40  
    41  var (
    42  	ErrInvalidDomain = errors.New("invalid Azure Domain")
    43  
    44  	endpointRegex    = regexp.MustCompile(`https://(?P<account>[\w]+).(?P<domain>[\w.-]+)[/:][\w-/,]*$`)
    45  	supportedDomains = []string{
    46  		BlobEndpointDefaultDomain,
    47  		BlobEndpointChinaDomain,
    48  		BlobEndpointUSGovDomain,
    49  		BlobEndpointTestDomain,
    50  	}
    51  )
    52  
    53  type Adapter struct {
    54  	clientCache        *ClientCache
    55  	preSignedExpiry    time.Duration
    56  	disablePreSigned   bool
    57  	disablePreSignedUI bool
    58  }
    59  
    60  func NewAdapter(ctx context.Context, params params.Azure) (*Adapter, error) {
    61  	logging.FromContext(ctx).WithField("type", "azure").Info("initialized blockstore adapter")
    62  	preSignedExpiry := params.PreSignedExpiry
    63  	if preSignedExpiry == 0 {
    64  		preSignedExpiry = block.DefaultPreSignExpiryDuration
    65  	}
    66  
    67  	if params.Domain == "" {
    68  		params.Domain = BlobEndpointDefaultDomain
    69  	} else if !slices.Contains(supportedDomains, params.Domain) {
    70  		return nil, ErrInvalidDomain
    71  	}
    72  
    73  	cache, err := NewCache(params)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	return &Adapter{
    79  		clientCache:        cache,
    80  		preSignedExpiry:    preSignedExpiry,
    81  		disablePreSigned:   params.DisablePreSigned,
    82  		disablePreSignedUI: params.DisablePreSignedUI,
    83  	}, nil
    84  }
    85  
    86  type BlobURLInfo struct {
    87  	StorageAccountName string
    88  	ContainerURL       string
    89  	ContainerName      string
    90  	BlobURL            string
    91  	Host               string
    92  }
    93  
    94  type PrefixURLInfo struct {
    95  	StorageAccountName string
    96  	ContainerURL       string
    97  	ContainerName      string
    98  	Prefix             string
    99  }
   100  
   101  func ExtractStorageAccount(storageAccount *url.URL) (string, error) {
   102  	// In azure the subdomain is the storage account
   103  	const expectedHostParts = 2
   104  	hostParts := strings.SplitN(storageAccount.Host, ".", expectedHostParts)
   105  	if len(hostParts) != expectedHostParts {
   106  		return "", fmt.Errorf("wrong host parts(%d): %w", len(hostParts), block.ErrInvalidAddress)
   107  	}
   108  
   109  	return hostParts[0], nil
   110  }
   111  
   112  func ResolveBlobURLInfoFromURL(pathURL *url.URL) (BlobURLInfo, error) {
   113  	var qk BlobURLInfo
   114  	err := block.ValidateStorageType(pathURL, block.StorageTypeAzure)
   115  	if err != nil {
   116  		return qk, err
   117  	}
   118  
   119  	// In azure, the first part of the path is part of the storage namespace
   120  	trimmedPath := strings.Trim(pathURL.Path, "/")
   121  	pathParts := strings.Split(trimmedPath, "/")
   122  	if len(pathParts) == 0 {
   123  		return qk, fmt.Errorf("wrong path parts(%d): %w", len(pathParts), block.ErrInvalidAddress)
   124  	}
   125  
   126  	storageAccount, err := ExtractStorageAccount(pathURL)
   127  	if err != nil {
   128  		return qk, err
   129  	}
   130  
   131  	return BlobURLInfo{
   132  		StorageAccountName: storageAccount,
   133  		ContainerURL:       fmt.Sprintf("%s://%s/%s", pathURL.Scheme, pathURL.Host, pathParts[0]),
   134  		ContainerName:      pathParts[0],
   135  		BlobURL:            strings.Join(pathParts[1:], "/"),
   136  		Host:               pathURL.Host,
   137  	}, nil
   138  }
   139  
   140  func resolveBlobURLInfo(obj block.ObjectPointer) (BlobURLInfo, error) {
   141  	key := obj.Identifier
   142  	defaultNamespace := obj.StorageNamespace
   143  	var qk BlobURLInfo
   144  	// check if the key is fully qualified
   145  	parsedKey, err := url.ParseRequestURI(key)
   146  	if err != nil {
   147  		// is not fully qualified, treat as key only
   148  		// if we don't have a trailing slash for the namespace, add it.
   149  		parsedNamespace, err := url.ParseRequestURI(defaultNamespace)
   150  		if err != nil {
   151  			return qk, err
   152  		}
   153  		qp, err := ResolveBlobURLInfoFromURL(parsedNamespace)
   154  		if err != nil {
   155  			return qk, err
   156  		}
   157  		info := BlobURLInfo{
   158  			StorageAccountName: qp.StorageAccountName,
   159  			ContainerURL:       qp.ContainerURL,
   160  			ContainerName:      qp.ContainerName,
   161  			BlobURL:            qp.BlobURL + "/" + key,
   162  			Host:               parsedNamespace.Host,
   163  		}
   164  		if qp.BlobURL == "" {
   165  			info.BlobURL = key
   166  		}
   167  		return info, nil
   168  	}
   169  	return ResolveBlobURLInfoFromURL(parsedKey)
   170  }
   171  
   172  func (a *Adapter) translatePutOpts(ctx context.Context, opts block.PutOpts) azblob.UploadStreamOptions {
   173  	res := azblob.UploadStreamOptions{}
   174  	if opts.StorageClass == nil {
   175  		return res
   176  	}
   177  
   178  	for _, t := range blob.PossibleAccessTierValues() {
   179  		if strings.EqualFold(*opts.StorageClass, string(t)) {
   180  			accessTier := t
   181  			res.AccessTier = &accessTier
   182  			break
   183  		}
   184  	}
   185  
   186  	if res.AccessTier == nil {
   187  		a.log(ctx).WithField("tier_type", *opts.StorageClass).Warn("Unknown Azure tier type")
   188  	}
   189  
   190  	return res
   191  }
   192  
   193  func (a *Adapter) log(ctx context.Context) logging.Logger {
   194  	return logging.FromContext(ctx)
   195  }
   196  
   197  func (a *Adapter) Put(ctx context.Context, obj block.ObjectPointer, sizeBytes int64, reader io.Reader, opts block.PutOpts) error {
   198  	var err error
   199  	defer reportMetrics("Put", time.Now(), &sizeBytes, &err)
   200  	qualifiedKey, err := resolveBlobURLInfo(obj)
   201  	if err != nil {
   202  		return err
   203  	}
   204  	o := a.translatePutOpts(ctx, opts)
   205  	containerClient, err := a.clientCache.NewContainerClient(qualifiedKey.StorageAccountName, qualifiedKey.ContainerName)
   206  	if err != nil {
   207  		return err
   208  	}
   209  	_, err = containerClient.NewBlockBlobClient(qualifiedKey.BlobURL).UploadStream(ctx, reader, &o)
   210  	return err
   211  }
   212  
   213  func (a *Adapter) Get(ctx context.Context, obj block.ObjectPointer, _ int64) (io.ReadCloser, error) {
   214  	var err error
   215  	defer reportMetrics("Get", time.Now(), nil, &err)
   216  
   217  	return a.Download(ctx, obj, 0, blockblob.CountToEnd)
   218  }
   219  
   220  func (a *Adapter) GetWalker(uri *url.URL) (block.Walker, error) {
   221  	if err := block.ValidateStorageType(uri, block.StorageTypeAzure); err != nil {
   222  		return nil, err
   223  	}
   224  
   225  	storageAccount, domain, err := ParseURL(uri)
   226  	if err != nil {
   227  		return nil, err
   228  	}
   229  	if domain != a.clientCache.params.Domain {
   230  		return nil, fmt.Errorf("domain mismatch! expected: %s, got: %s. %w", a.clientCache.params.Domain, domain, ErrInvalidDomain)
   231  	}
   232  
   233  	client, err := a.clientCache.NewServiceClient(storageAccount)
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  
   238  	return NewAzureDataLakeWalker(client, false)
   239  }
   240  
   241  func (a *Adapter) GetPreSignedURL(ctx context.Context, obj block.ObjectPointer, mode block.PreSignMode) (string, time.Time, error) {
   242  	if a.disablePreSigned {
   243  		return "", time.Time{}, block.ErrOperationNotSupported
   244  	}
   245  
   246  	permissions := sas.BlobPermissions{Read: true}
   247  	if mode == block.PreSignModeWrite {
   248  		permissions = sas.BlobPermissions{
   249  			Read:  true,
   250  			Add:   true,
   251  			Write: true,
   252  		}
   253  	}
   254  	preSignedURL, err := a.getPreSignedURL(ctx, obj, permissions)
   255  	// TODO(#6347): Report expiry.
   256  	return preSignedURL, time.Time{}, err
   257  }
   258  
   259  func (a *Adapter) getPreSignedURL(ctx context.Context, obj block.ObjectPointer, permissions sas.BlobPermissions) (string, error) {
   260  	if a.disablePreSigned {
   261  		return "", block.ErrOperationNotSupported
   262  	}
   263  
   264  	qualifiedKey, err := resolveBlobURLInfo(obj)
   265  	if err != nil {
   266  		return "", err
   267  	}
   268  
   269  	// Use shared credential for clients initialized with storage access key
   270  	if qualifiedKey.StorageAccountName == a.clientCache.params.StorageAccount && a.clientCache.params.StorageAccessKey != "" {
   271  		container, err := a.clientCache.NewContainerClient(qualifiedKey.StorageAccountName, qualifiedKey.ContainerName)
   272  		if err != nil {
   273  			return "", err
   274  		}
   275  		client := container.NewBlobClient(qualifiedKey.BlobURL)
   276  		urlExpiry := a.newPreSignedTime()
   277  		return client.GetSASURL(permissions, urlExpiry, &blob.GetSASURLOptions{})
   278  	}
   279  
   280  	// Otherwise assume using role based credentials and build signed URL using user delegation credentials
   281  	urlExpiry := a.newPreSignedTime()
   282  	udc, err := a.clientCache.NewUDC(ctx, qualifiedKey.StorageAccountName, &urlExpiry)
   283  	if err != nil {
   284  		return "", err
   285  	}
   286  
   287  	// Create Blob Signature Values with desired permissions and sign with user delegation credential
   288  	blobSignatureValues := sas.BlobSignatureValues{
   289  		Protocol:      sas.ProtocolHTTPS,
   290  		ExpiryTime:    urlExpiry,
   291  		Permissions:   to.Ptr(permissions).String(),
   292  		ContainerName: qualifiedKey.ContainerName,
   293  		BlobName:      qualifiedKey.BlobURL,
   294  	}
   295  	sasQueryParams, err := blobSignatureValues.SignWithUserDelegation(udc)
   296  	if err != nil {
   297  		return "", err
   298  	}
   299  
   300  	var accountEndpoint string
   301  	// format blob URL with signed SAS query params
   302  	if a.clientCache.params.TestEndpointURL != "" {
   303  		accountEndpoint = a.clientCache.params.TestEndpointURL
   304  	} else {
   305  		accountEndpoint = buildAccountEndpoint(qualifiedKey.StorageAccountName, a.clientCache.params.Domain)
   306  	}
   307  
   308  	u, err := url.JoinPath(accountEndpoint, qualifiedKey.ContainerName, qualifiedKey.BlobURL)
   309  	if err != nil {
   310  		return "", err
   311  	}
   312  	u += "?" + sasQueryParams.Encode()
   313  	return u, nil
   314  }
   315  
   316  func (a *Adapter) GetRange(ctx context.Context, obj block.ObjectPointer, startPosition int64, endPosition int64) (io.ReadCloser, error) {
   317  	var err error
   318  	defer reportMetrics("GetRange", time.Now(), nil, &err)
   319  
   320  	return a.Download(ctx, obj, startPosition, endPosition-startPosition+1)
   321  }
   322  
   323  func (a *Adapter) Download(ctx context.Context, obj block.ObjectPointer, offset, count int64) (io.ReadCloser, error) {
   324  	qualifiedKey, err := resolveBlobURLInfo(obj)
   325  	if err != nil {
   326  		return nil, err
   327  	}
   328  	container, err := a.clientCache.NewContainerClient(qualifiedKey.StorageAccountName, qualifiedKey.ContainerName)
   329  	if err != nil {
   330  		return nil, err
   331  	}
   332  	blobURL := container.NewBlockBlobClient(qualifiedKey.BlobURL)
   333  
   334  	downloadResponse, err := blobURL.DownloadStream(ctx, &azblob.DownloadStreamOptions{
   335  		RangeGetContentMD5: nil,
   336  		Range: blob.HTTPRange{
   337  			Offset: offset,
   338  			Count:  count,
   339  		},
   340  	})
   341  	if bloberror.HasCode(err, bloberror.BlobNotFound) {
   342  		return nil, block.ErrDataNotFound
   343  	}
   344  	if err != nil {
   345  		a.log(ctx).WithError(err).Errorf("failed to get azure blob from container %s key %s", container, blobURL)
   346  		return nil, err
   347  	}
   348  	return downloadResponse.Body, nil
   349  }
   350  
   351  func (a *Adapter) Exists(ctx context.Context, obj block.ObjectPointer) (bool, error) {
   352  	var err error
   353  	defer reportMetrics("Exists", time.Now(), nil, &err)
   354  
   355  	qualifiedKey, err := resolveBlobURLInfo(obj)
   356  	if err != nil {
   357  		return false, err
   358  	}
   359  
   360  	containerClient, err := a.clientCache.NewContainerClient(qualifiedKey.StorageAccountName, qualifiedKey.ContainerName)
   361  	if err != nil {
   362  		return false, err
   363  	}
   364  	blobURL := containerClient.NewBlobClient(qualifiedKey.BlobURL)
   365  
   366  	_, err = blobURL.GetProperties(ctx, nil)
   367  
   368  	if bloberror.HasCode(err, bloberror.BlobNotFound) {
   369  		return false, nil
   370  	}
   371  	if err != nil {
   372  		return false, err
   373  	}
   374  	return true, nil
   375  }
   376  
   377  func (a *Adapter) GetProperties(ctx context.Context, obj block.ObjectPointer) (block.Properties, error) {
   378  	var err error
   379  	defer reportMetrics("GetProperties", time.Now(), nil, &err)
   380  
   381  	qualifiedKey, err := resolveBlobURLInfo(obj)
   382  	if err != nil {
   383  		return block.Properties{}, err
   384  	}
   385  
   386  	containerClient, err := a.clientCache.NewContainerClient(qualifiedKey.StorageAccountName, qualifiedKey.ContainerName)
   387  	if err != nil {
   388  		return block.Properties{}, err
   389  	}
   390  	blobURL := containerClient.NewBlobClient(qualifiedKey.BlobURL)
   391  
   392  	props, err := blobURL.GetProperties(ctx, nil)
   393  	if err != nil {
   394  		return block.Properties{}, err
   395  	}
   396  	return block.Properties{StorageClass: props.AccessTier}, nil
   397  }
   398  
   399  func (a *Adapter) Remove(ctx context.Context, obj block.ObjectPointer) error {
   400  	var err error
   401  	defer reportMetrics("Remove", time.Now(), nil, &err)
   402  	qualifiedKey, err := resolveBlobURLInfo(obj)
   403  	if err != nil {
   404  		return err
   405  	}
   406  	containerClient, err := a.clientCache.NewContainerClient(qualifiedKey.StorageAccountName, qualifiedKey.ContainerName)
   407  	if err != nil {
   408  		return err
   409  	}
   410  	blobURL := containerClient.NewBlobClient(qualifiedKey.BlobURL)
   411  
   412  	_, err = blobURL.Delete(ctx, nil)
   413  	return err
   414  }
   415  
   416  func (a *Adapter) Copy(ctx context.Context, sourceObj, destinationObj block.ObjectPointer) error {
   417  	var err error
   418  	defer reportMetrics("Copy", time.Now(), nil, &err)
   419  
   420  	qualifiedDestinationKey, err := resolveBlobURLInfo(destinationObj)
   421  	if err != nil {
   422  		return err
   423  	}
   424  
   425  	destContainerClient, err := a.clientCache.NewContainerClient(qualifiedDestinationKey.StorageAccountName, qualifiedDestinationKey.ContainerName)
   426  	if err != nil {
   427  		return err
   428  	}
   429  	destClient := destContainerClient.NewBlobClient(qualifiedDestinationKey.BlobURL)
   430  
   431  	sasKey, _, err := a.GetPreSignedURL(ctx, sourceObj, block.PreSignModeRead)
   432  	if err != nil {
   433  		return err
   434  	}
   435  
   436  	// Optimistic flow - try to copy synchronously
   437  	_, err = destClient.CopyFromURL(ctx, sasKey, nil)
   438  	if err == nil {
   439  		return nil
   440  	}
   441  	// Azure API (backend) returns ambiguous error code which requires us to parse the error message to understand what is the nature of the error
   442  	// See: https://github.com/Azure/azure-sdk-for-go/issues/19880
   443  	if !bloberror.HasCode(err, bloberror.CannotVerifyCopySource) ||
   444  		!strings.Contains(err.Error(), "The source request body for synchronous copy is too large and exceeds the maximum permissible limit") {
   445  		return err
   446  	}
   447  
   448  	// Blob too big for synchronous copy. Perform async copy
   449  	logger := a.log(ctx).WithFields(logging.Fields{
   450  		"sourceObj": sourceObj.Identifier,
   451  		"destObj":   destinationObj.Identifier,
   452  	})
   453  	logger.Debug("Perform async copy")
   454  	res, err := destClient.StartCopyFromURL(ctx, sasKey, nil)
   455  	if err != nil {
   456  		return err
   457  	}
   458  	copyStatus := res.CopyStatus
   459  	if copyStatus == nil {
   460  		return fmt.Errorf("%w: failed to get copy status", block.ErrAsyncCopyFailed)
   461  	}
   462  
   463  	progress := ""
   464  	const asyncPollInterval = 5 * time.Second
   465  	for {
   466  		select {
   467  		case <-ctx.Done():
   468  			logger.WithField("copy_progress", progress).Warn("context canceled, aborting copy")
   469  			// Context canceled - perform abort on copy use a different context for the abort
   470  			_, err := destClient.AbortCopyFromURL(context.Background(), *res.CopyID, nil)
   471  			if err != nil {
   472  				logger.WithError(err).Error("failed to abort copy")
   473  			}
   474  			return ctx.Err()
   475  
   476  		case <-time.After(asyncPollInterval):
   477  			p, err := destClient.GetProperties(ctx, nil)
   478  			if err != nil {
   479  				return err
   480  			}
   481  			copyStatus = p.CopyStatus
   482  			if copyStatus == nil {
   483  				return fmt.Errorf("%w: failed to get copy status", block.ErrAsyncCopyFailed)
   484  			}
   485  			progress = *p.CopyProgress
   486  			switch *copyStatus {
   487  			case blob.CopyStatusTypeSuccess:
   488  				logger.WithField("object_properties", p).Debug("Async copy successful")
   489  				return nil
   490  
   491  			case blob.CopyStatusTypeAborted:
   492  				return fmt.Errorf("%w: unexpected abort", block.ErrAsyncCopyFailed)
   493  
   494  			case blob.CopyStatusTypeFailed:
   495  				return fmt.Errorf("%w: copy status failed", block.ErrAsyncCopyFailed)
   496  
   497  			case blob.CopyStatusTypePending:
   498  				logger.WithField("copy_progress", progress).Debug("Copy pending")
   499  
   500  			default:
   501  				return fmt.Errorf("%w: invalid copy status: %s", block.ErrAsyncCopyFailed, *copyStatus)
   502  			}
   503  		}
   504  	}
   505  }
   506  
   507  func (a *Adapter) CreateMultiPartUpload(_ context.Context, obj block.ObjectPointer, _ *http.Request, _ block.CreateMultiPartUploadOpts) (*block.CreateMultiPartUploadResponse, error) {
   508  	// Azure has no create multipart upload
   509  	var err error
   510  	defer reportMetrics("CreateMultiPartUpload", time.Now(), nil, &err)
   511  
   512  	qualifiedKey, err := resolveBlobURLInfo(obj)
   513  	if err != nil {
   514  		return nil, err
   515  	}
   516  
   517  	return &block.CreateMultiPartUploadResponse{
   518  		UploadID: qualifiedKey.BlobURL,
   519  	}, nil
   520  }
   521  
   522  func (a *Adapter) UploadPart(ctx context.Context, obj block.ObjectPointer, _ int64, reader io.Reader, _ string, _ int) (*block.UploadPartResponse, error) {
   523  	var err error
   524  	defer reportMetrics("UploadPart", time.Now(), nil, &err)
   525  
   526  	qualifiedKey, err := resolveBlobURLInfo(obj)
   527  	if err != nil {
   528  		return nil, err
   529  	}
   530  
   531  	container, err := a.clientCache.NewContainerClient(qualifiedKey.StorageAccountName, qualifiedKey.ContainerName)
   532  	if err != nil {
   533  		return nil, err
   534  	}
   535  	hashReader := block.NewHashingReader(reader, block.HashFunctionMD5)
   536  
   537  	multipartBlockWriter := NewMultipartBlockWriter(hashReader, *container, qualifiedKey.BlobURL)
   538  	_, err = copyFromReader(ctx, hashReader, multipartBlockWriter, blockblob.UploadStreamOptions{
   539  		BlockSize:   _1MiB,
   540  		Concurrency: MaxBuffers,
   541  	})
   542  	if err != nil {
   543  		return nil, err
   544  	}
   545  	return &block.UploadPartResponse{
   546  		ETag: strings.Trim(multipartBlockWriter.etag, `"`),
   547  	}, nil
   548  }
   549  
   550  func (a *Adapter) UploadCopyPart(ctx context.Context, sourceObj, destinationObj block.ObjectPointer, _ string, _ int) (*block.UploadPartResponse, error) {
   551  	var err error
   552  	defer reportMetrics("UploadPart", time.Now(), nil, &err)
   553  
   554  	return a.copyPartRange(ctx, sourceObj, destinationObj, 0, blockblob.CountToEnd)
   555  }
   556  
   557  func (a *Adapter) UploadCopyPartRange(ctx context.Context, sourceObj, destinationObj block.ObjectPointer, _ string, _ int, startPosition, endPosition int64) (*block.UploadPartResponse, error) {
   558  	var err error
   559  	defer reportMetrics("UploadPart", time.Now(), nil, &err)
   560  	return a.copyPartRange(ctx, sourceObj, destinationObj, startPosition, endPosition-startPosition+1)
   561  }
   562  
   563  func (a *Adapter) copyPartRange(ctx context.Context, sourceObj, destinationObj block.ObjectPointer, startPosition, count int64) (*block.UploadPartResponse, error) {
   564  	qualifiedSourceKey, err := resolveBlobURLInfo(sourceObj)
   565  	if err != nil {
   566  		return nil, err
   567  	}
   568  
   569  	qualifiedDestinationKey, err := resolveBlobURLInfo(destinationObj)
   570  	if err != nil {
   571  		return nil, err
   572  	}
   573  
   574  	return copyPartRange(ctx, a.clientCache, qualifiedDestinationKey, qualifiedSourceKey, startPosition, count)
   575  }
   576  
   577  func (a *Adapter) AbortMultiPartUpload(_ context.Context, _ block.ObjectPointer, _ string) error {
   578  	// Azure has no abort. In case of commit, uncommitted parts are erased. Otherwise, staged data is erased after 7 days
   579  	return nil
   580  }
   581  
   582  func (a *Adapter) BlockstoreType() string {
   583  	return block.BlockstoreTypeAzure
   584  }
   585  
   586  func (a *Adapter) CompleteMultiPartUpload(ctx context.Context, obj block.ObjectPointer, _ string, multipartList *block.MultipartUploadCompletion) (*block.CompleteMultiPartUploadResponse, error) {
   587  	var err error
   588  	defer reportMetrics("CompleteMultiPartUpload", time.Now(), nil, &err)
   589  	qualifiedKey, err := resolveBlobURLInfo(obj)
   590  	if err != nil {
   591  		return nil, err
   592  	}
   593  	containerURL, err := a.clientCache.NewContainerClient(qualifiedKey.StorageAccountName, qualifiedKey.ContainerName)
   594  	if err != nil {
   595  		return nil, err
   596  	}
   597  
   598  	return completeMultipart(ctx, multipartList.Part, *containerURL, qualifiedKey.BlobURL)
   599  }
   600  
   601  func (a *Adapter) GetStorageNamespaceInfo() block.StorageNamespaceInfo {
   602  	info := block.DefaultStorageNamespaceInfo(block.BlockstoreTypeAzure)
   603  
   604  	info.ImportValidityRegex = fmt.Sprintf(`^https?://[a-z0-9_-]+\.%s`, a.clientCache.params.Domain)
   605  	info.ValidityRegex = fmt.Sprintf(`^https?://[a-z0-9_-]+\.%s`, a.clientCache.params.Domain)
   606  
   607  	info.Example = fmt.Sprintf("https://mystorageaccount.%s/mycontainer/", a.clientCache.params.Domain)
   608  	if a.disablePreSigned {
   609  		info.PreSignSupport = false
   610  	}
   611  	if !(a.disablePreSignedUI || a.disablePreSigned) {
   612  		info.PreSignSupportUI = true
   613  	}
   614  	return info
   615  }
   616  
   617  func (a *Adapter) ResolveNamespace(storageNamespace, key string, identifierType block.IdentifierType) (block.QualifiedKey, error) {
   618  	return block.DefaultResolveNamespace(storageNamespace, key, identifierType)
   619  }
   620  
   621  func (a *Adapter) RuntimeStats() map[string]string {
   622  	return nil
   623  }
   624  
   625  func (a *Adapter) newPreSignedTime() time.Time {
   626  	return time.Now().UTC().Add(a.preSignedExpiry)
   627  }
   628  
   629  func (a *Adapter) GetPresignUploadPartURL(_ context.Context, _ block.ObjectPointer, _ string, _ int) (string, error) {
   630  	return "", block.ErrOperationNotSupported
   631  }
   632  
   633  func (a *Adapter) ListParts(_ context.Context, _ block.ObjectPointer, _ string, _ block.ListPartsOpts) (*block.ListPartsResponse, error) {
   634  	return nil, block.ErrOperationNotSupported
   635  }
   636  
   637  // ParseURL - parses url and extracts account name and domain. If either are not found returns an error
   638  func ParseURL(uri *url.URL) (accountName string, domain string, err error) {
   639  	u, err := uri.Parse("")
   640  	if err != nil {
   641  		return "", "", err
   642  	}
   643  
   644  	u.RawQuery = ""
   645  	matches := endpointRegex.FindStringSubmatch(u.String())
   646  	if matches == nil {
   647  		return "", "", ErrAzureInvalidURL
   648  	}
   649  
   650  	domainIdx := endpointRegex.SubexpIndex("domain")
   651  	if domainIdx < 0 {
   652  		return "", "", fmt.Errorf("invalid domain: %w", ErrInvalidDomain)
   653  	}
   654  
   655  	accountIdx := endpointRegex.SubexpIndex("account")
   656  	if accountIdx < 0 {
   657  		return "", "", fmt.Errorf("missing storage account: %w", ErrAzureInvalidURL)
   658  	}
   659  
   660  	return matches[accountIdx], matches[domainIdx], nil
   661  }