sdk/storage/azblob/service/client.go (253 lines of code) (raw):

//go:build go1.18 // +build go1.18 // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. package service import ( "bytes" "context" "errors" "fmt" "net/http" "strings" "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/azcore/streaming" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/bloberror" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/container" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/internal/base" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/internal/exported" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/internal/generated" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/internal/shared" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas" ) // ClientOptions contains the optional parameters when creating a Client. type ClientOptions base.ClientOptions // Client represents a URL to the Azure Blob Storage service allowing you to manipulate blob containers. type Client base.Client[generated.ServiceClient] // NewClient creates an instance of Client with the specified values. // - serviceURL - the URL of the storage account e.g. https://<account>.blob.core.windows.net/ // - cred - an Azure AD credential, typically obtained via the azidentity module // - options - client options; pass nil to accept the default values func NewClient(serviceURL string, cred azcore.TokenCredential, options *ClientOptions) (*Client, error) { audience := base.GetAudience((*base.ClientOptions)(options)) conOptions := shared.GetClientOptions(options) authPolicy := shared.NewStorageChallengePolicy(cred, audience, conOptions.InsecureAllowCredentialWithHTTP) plOpts := runtime.PipelineOptions{PerRetry: []policy.Policy{authPolicy}} azClient, err := azcore.NewClient(exported.ModuleName, exported.ModuleVersion, plOpts, &conOptions.ClientOptions) if err != nil { return nil, err } return (*Client)(base.NewServiceClient(serviceURL, azClient, &cred, (*base.ClientOptions)(conOptions))), nil } // NewClientWithNoCredential creates an instance of Client with the specified values. // This is used to anonymously access a storage account or with a shared access signature (SAS) token. // - serviceURL - the URL of the storage account e.g. https://<account>.blob.core.windows.net/?<sas token> // - options - client options; pass nil to accept the default values func NewClientWithNoCredential(serviceURL string, options *ClientOptions) (*Client, error) { conOptions := shared.GetClientOptions(options) azClient, err := azcore.NewClient(exported.ModuleName, exported.ModuleVersion, runtime.PipelineOptions{}, &conOptions.ClientOptions) if err != nil { return nil, err } return (*Client)(base.NewServiceClient(serviceURL, azClient, nil, (*base.ClientOptions)(conOptions))), nil } // NewClientWithSharedKeyCredential creates an instance of Client with the specified values. // - serviceURL - the URL of the storage account e.g. https://<account>.blob.core.windows.net/ // - cred - a SharedKeyCredential created with the matching storage account and access key // - options - client options; pass nil to accept the default values func NewClientWithSharedKeyCredential(serviceURL string, cred *SharedKeyCredential, options *ClientOptions) (*Client, error) { authPolicy := exported.NewSharedKeyCredPolicy(cred) conOptions := shared.GetClientOptions(options) plOpts := runtime.PipelineOptions{PerRetry: []policy.Policy{authPolicy}} azClient, err := azcore.NewClient(exported.ModuleName, exported.ModuleVersion, plOpts, &conOptions.ClientOptions) if err != nil { return nil, err } return (*Client)(base.NewServiceClient(serviceURL, azClient, cred, (*base.ClientOptions)(conOptions))), nil } // NewClientFromConnectionString creates an instance of Client with the specified values. // - connectionString - a connection string for the desired storage account // - options - client options; pass nil to accept the default values func NewClientFromConnectionString(connectionString string, options *ClientOptions) (*Client, error) { parsed, err := shared.ParseConnectionString(connectionString) if err != nil { return nil, err } if parsed.AccountKey != "" && parsed.AccountName != "" { credential, err := exported.NewSharedKeyCredential(parsed.AccountName, parsed.AccountKey) if err != nil { return nil, err } return NewClientWithSharedKeyCredential(parsed.ServiceURL, credential, options) } return NewClientWithNoCredential(parsed.ServiceURL, options) } // GetUserDelegationCredential obtains a UserDelegationKey object using the base ServiceURL object. // OAuth is required for this call, as well as any role that can delegate access to the storage account. func (s *Client) GetUserDelegationCredential(ctx context.Context, info KeyInfo, o *GetUserDelegationCredentialOptions) (*UserDelegationCredential, error) { url, err := blob.ParseURL(s.URL()) if err != nil { return nil, err } getUserDelegationKeyOptions := o.format() udk, err := s.generated().GetUserDelegationKey(ctx, info, getUserDelegationKeyOptions) if err != nil { return nil, err } return exported.NewUserDelegationCredential(strings.Split(url.Host, ".")[0], udk.UserDelegationKey), nil } func (s *Client) generated() *generated.ServiceClient { return base.InnerClient((*base.Client[generated.ServiceClient])(s)) } func (s *Client) sharedKey() *SharedKeyCredential { return base.SharedKey((*base.Client[generated.ServiceClient])(s)) } func (s *Client) credential() any { return base.Credential((*base.Client[generated.ServiceClient])(s)) } // helper method to return the generated.BlobClient which is used for creating the sub-requests func getGeneratedBlobClient(b *blob.Client) *generated.BlobClient { return base.InnerClient((*base.Client[generated.BlobClient])(b)) } func (s *Client) getClientOptions() *base.ClientOptions { return base.GetClientOptions((*base.Client[generated.ServiceClient])(s)) } // URL returns the URL endpoint used by the Client object. func (s *Client) URL() string { return s.generated().Endpoint() } // NewContainerClient creates a new container.Client object by concatenating containerName to the end of // this Client's URL. The new container.Client uses the same request policy pipeline as the Client. func (s *Client) NewContainerClient(containerName string) *container.Client { containerURL := runtime.JoinPaths(s.generated().Endpoint(), containerName) return (*container.Client)(base.NewContainerClient(containerURL, s.generated().InternalClient().WithClientName(exported.ModuleName), s.credential(), s.getClientOptions())) } // CreateContainer is a lifecycle method to creates a new container under the specified account. // If the container with the same name already exists, a ResourceExistsError will be raised. // This method returns a client with which to interact with the newly created container. func (s *Client) CreateContainer(ctx context.Context, containerName string, options *CreateContainerOptions) (CreateContainerResponse, error) { containerClient := s.NewContainerClient(containerName) containerCreateResp, err := containerClient.Create(ctx, options) return containerCreateResp, err } // DeleteContainer is a lifecycle method that marks the specified container for deletion. // The container and any blobs contained within it are later deleted during garbage collection. // If the container is not found, a ResourceNotFoundError will be raised. func (s *Client) DeleteContainer(ctx context.Context, containerName string, options *DeleteContainerOptions) (DeleteContainerResponse, error) { containerClient := s.NewContainerClient(containerName) containerDeleteResp, err := containerClient.Delete(ctx, options) return containerDeleteResp, err } // RestoreContainer restores soft-deleted container // Operation will only be successful if used within the specified number of days set in the delete retention policy func (s *Client) RestoreContainer(ctx context.Context, deletedContainerName string, deletedContainerVersion string, options *RestoreContainerOptions) (RestoreContainerResponse, error) { containerClient := s.NewContainerClient(deletedContainerName) containerRestoreResp, err := containerClient.Restore(ctx, deletedContainerVersion, options) return containerRestoreResp, err } // GetAccountInfo provides account level information // For more information, see https://learn.microsoft.com/en-us/rest/api/storageservices/get-account-information?tabs=shared-access-signatures. func (s *Client) GetAccountInfo(ctx context.Context, o *GetAccountInfoOptions) (GetAccountInfoResponse, error) { getAccountInfoOptions := o.format() resp, err := s.generated().GetAccountInfo(ctx, getAccountInfoOptions) return resp, err } // NewListContainersPager operation returns a pager of the containers under the specified account. // Use an empty Marker to start enumeration from the beginning. Container names are returned in lexicographic order. // For more information, see https://docs.microsoft.com/rest/api/storageservices/list-containers2. func (s *Client) NewListContainersPager(o *ListContainersOptions) *runtime.Pager[ListContainersResponse] { listOptions := generated.ServiceClientListContainersSegmentOptions{} if o != nil { if o.Include.Deleted { listOptions.Include = append(listOptions.Include, generated.ListContainersIncludeTypeDeleted) } if o.Include.Metadata { listOptions.Include = append(listOptions.Include, generated.ListContainersIncludeTypeMetadata) } if o.Include.System { listOptions.Include = append(listOptions.Include, generated.ListContainersIncludeTypeSystem) } listOptions.Marker = o.Marker listOptions.Maxresults = o.MaxResults listOptions.Prefix = o.Prefix } return runtime.NewPager(runtime.PagingHandler[ListContainersResponse]{ More: func(page ListContainersResponse) bool { return page.NextMarker != nil && len(*page.NextMarker) > 0 }, Fetcher: func(ctx context.Context, page *ListContainersResponse) (ListContainersResponse, error) { var req *policy.Request var err error if page == nil { req, err = s.generated().ListContainersSegmentCreateRequest(ctx, &listOptions) } else { listOptions.Marker = page.NextMarker req, err = s.generated().ListContainersSegmentCreateRequest(ctx, &listOptions) } if err != nil { return ListContainersResponse{}, err } resp, err := s.generated().InternalClient().Pipeline().Do(req) if err != nil { return ListContainersResponse{}, err } if !runtime.HasStatusCode(resp, http.StatusOK) { return ListContainersResponse{}, runtime.NewResponseError(resp) } return s.generated().ListContainersSegmentHandleResponse(resp) }, }) } // GetProperties - gets the properties of a storage account's Blob service, including properties for Storage Analytics // and CORS (Cross-Origin Resource Sharing) rules. func (s *Client) GetProperties(ctx context.Context, o *GetPropertiesOptions) (GetPropertiesResponse, error) { getPropertiesOptions := o.format() resp, err := s.generated().GetProperties(ctx, getPropertiesOptions) return resp, err } // SetProperties Sets the properties of a storage account's Blob service, including Azure Storage Analytics. // If an element (e.g. analytics_logging) is left as None, the existing settings on the service for that functionality are preserved. func (s *Client) SetProperties(ctx context.Context, o *SetPropertiesOptions) (SetPropertiesResponse, error) { properties, setPropertiesOptions := o.format() resp, err := s.generated().SetProperties(ctx, properties, setPropertiesOptions) return resp, err } // GetStatistics Retrieves statistics related to replication for the Blob service. // It is only available when read-access geo-redundant replication is enabled for the storage account. // With geo-redundant replication, Azure Storage maintains your data durable // in two locations. In both locations, Azure Storage constantly maintains // multiple healthy replicas of your data. The location where you read, // create, update, or delete data is the primary storage account location. // The primary location exists in the region you choose at the time you // create an account via the Azure Management Azure classic portal, for // example, North Central US. The location to which your data is replicated // is the secondary location. The secondary location is automatically // determined based on the location of the primary; it is in a second data // center that resides in the same region as the primary location. Read-only // access is available from the secondary location, if read-access geo-redundant // replication is enabled for your storage account. func (s *Client) GetStatistics(ctx context.Context, o *GetStatisticsOptions) (GetStatisticsResponse, error) { getStatisticsOptions := o.format() resp, err := s.generated().GetStatistics(ctx, getStatisticsOptions) return resp, err } // GetSASURL is a convenience method for generating a SAS token for the currently pointed at account. // It can only be used if the credential supplied during creation was a SharedKeyCredential. func (s *Client) GetSASURL(resources sas.AccountResourceTypes, permissions sas.AccountPermissions, expiry time.Time, o *GetSASURLOptions) (string, error) { if s.sharedKey() == nil { return "", bloberror.MissingSharedKeyCredential } st := o.format() qps, err := sas.AccountSignatureValues{ Version: sas.Version, Permissions: permissions.String(), ResourceTypes: resources.String(), StartTime: st, ExpiryTime: expiry.UTC(), }.SignWithSharedKey(s.sharedKey()) if err != nil { return "", err } endpoint := s.URL() if !strings.HasSuffix(endpoint, "/") { // add a trailing slash to be consistent with the portal endpoint += "/" } endpoint += "?" + qps.Encode() return endpoint, nil } // FilterBlobs operation finds all blobs in the storage account whose tags match a given search expression. // Filter blobs searches across all containers within a storage account but can be scoped within the expression to a single container. // https://docs.microsoft.com/en-us/rest/api/storageservices/find-blobs-by-tags // eg. "dog='germanshepherd' and penguin='emperorpenguin'" // To specify a container, eg. "@container=’containerName’ and Name = ‘C’" func (s *Client) FilterBlobs(ctx context.Context, where string, o *FilterBlobsOptions) (FilterBlobsResponse, error) { serviceFilterBlobsOptions := o.format() resp, err := s.generated().FilterBlobs(ctx, where, serviceFilterBlobsOptions) return resp, err } // NewBatchBuilder creates an instance of BatchBuilder using the same auth policy as the client. // BatchBuilder is used to build the batch consisting of either delete or set tier sub-requests. // All sub-requests in the batch must be of the same type, either delete or set tier. // NOTE: Service level Blob Batch operation is supported only when the Client was created using SharedKeyCredential and Account SAS. func (s *Client) NewBatchBuilder() (*BatchBuilder, error) { var authPolicy policy.Policy switch cred := s.credential().(type) { case *azcore.TokenCredential: conOptions := s.getClientOptions() authPolicy = shared.NewStorageChallengePolicy(*cred, base.GetAudience(conOptions), conOptions.InsecureAllowCredentialWithHTTP) case *SharedKeyCredential: authPolicy = exported.NewSharedKeyCredPolicy(cred) case nil: // for authentication using SAS authPolicy = nil default: return nil, fmt.Errorf("unrecognised authentication type %T", cred) } return &BatchBuilder{ endpoint: s.URL(), authPolicy: authPolicy, }, nil } // SubmitBatch operation allows multiple API calls to be embedded into a single HTTP request. // It builds the request body using the BatchBuilder object passed. // BatchBuilder contains the list of operations to be submitted. It supports up to 256 sub-requests in a single batch. // For more information, see https://docs.microsoft.com/rest/api/storageservices/blob-batch. func (s *Client) SubmitBatch(ctx context.Context, bb *BatchBuilder, options *SubmitBatchOptions) (SubmitBatchResponse, error) { if bb == nil || len(bb.subRequests) == 0 { return SubmitBatchResponse{}, errors.New("batch builder is empty") } // create the request body batchReq, batchID, err := exported.CreateBatchRequest(&exported.BlobBatchBuilder{ AuthPolicy: bb.authPolicy, SubRequests: bb.subRequests, }) if err != nil { return SubmitBatchResponse{}, err } reader := bytes.NewReader(batchReq) rsc := streaming.NopCloser(reader) multipartContentType := "multipart/mixed; boundary=" + batchID resp, err := s.generated().SubmitBatch(ctx, int64(len(batchReq)), multipartContentType, rsc, options.format()) if err != nil { return SubmitBatchResponse{}, err } batchResponses, err := exported.ParseBlobBatchResponse(resp.Body, resp.ContentType, bb.subRequests) if err != nil { return SubmitBatchResponse{}, err } return SubmitBatchResponse{ Responses: batchResponses, ContentType: resp.ContentType, RequestID: resp.RequestID, Version: resp.Version, }, nil }