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 }