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 }