sdk/storage/azblob/container/client.go (304 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 container import ( "bytes" "context" "errors" "fmt" "net/http" "net/url" "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/appendblob" "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/blockblob" "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/pageblob" "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 Storage container allowing you to manipulate its blobs. type Client base.Client[generated.ContainerClient] // NewClient creates an instance of Client with the specified values. // - containerURL - the URL of the container e.g. https://<account>.blob.core.windows.net/container // - cred - an Azure AD credential, typically obtained via the azidentity module // - options - client options; pass nil to accept the default values func NewClient(containerURL 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.NewContainerClient(containerURL, azClient, &cred, (*base.ClientOptions)(conOptions))), nil } // NewClientWithNoCredential creates an instance of Client with the specified values. // This is used to anonymously access a container or with a shared access signature (SAS) token. // - containerURL - the URL of the container e.g. https://<account>.blob.core.windows.net/container?<sas token> // - options - client options; pass nil to accept the default values func NewClientWithNoCredential(containerURL 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.NewContainerClient(containerURL, azClient, nil, (*base.ClientOptions)(conOptions))), nil } // NewClientWithSharedKeyCredential creates an instance of Client with the specified values. // - containerURL - the URL of the container e.g. https://<account>.blob.core.windows.net/container // - cred - a SharedKeyCredential created with the matching container's storage account and access key // - options - client options; pass nil to accept the default values func NewClientWithSharedKeyCredential(containerURL 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.NewContainerClient(containerURL, 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 // - containerName - the name of the container within the storage account // - options - client options; pass nil to accept the default values func NewClientFromConnectionString(connectionString string, containerName string, options *ClientOptions) (*Client, error) { parsed, err := shared.ParseConnectionString(connectionString) if err != nil { return nil, err } parsed.ServiceURL = runtime.JoinPaths(parsed.ServiceURL, containerName) 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) } func (c *Client) generated() *generated.ContainerClient { return base.InnerClient((*base.Client[generated.ContainerClient])(c)) } func (c *Client) sharedKey() *SharedKeyCredential { return base.SharedKey((*base.Client[generated.ContainerClient])(c)) } func (c *Client) credential() any { return base.Credential((*base.Client[generated.ContainerClient])(c)) } // 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 (c *Client) getClientOptions() *base.ClientOptions { return base.GetClientOptions((*base.Client[generated.ContainerClient])(c)) } // URL returns the URL endpoint used by the Client object. func (c *Client) URL() string { return c.generated().Endpoint() } // NewBlobClient creates a new blob.Client object by concatenating blobName to the end of // Client's URL. The blob name will be URL-encoded. // The new blob.Client uses the same request policy pipeline as this Client. func (c *Client) NewBlobClient(blobName string) *blob.Client { blobName = url.PathEscape(blobName) blobURL := runtime.JoinPaths(c.URL(), blobName) return (*blob.Client)(base.NewBlobClient(blobURL, c.generated().InternalClient().WithClientName(exported.ModuleName), c.credential(), c.getClientOptions())) } // NewAppendBlobClient creates a new appendblob.Client object by concatenating blobName to the end of // this Client's URL. The blob name will be URL-encoded. // The new appendblob.Client uses the same request policy pipeline as this Client. func (c *Client) NewAppendBlobClient(blobName string) *appendblob.Client { blobName = url.PathEscape(blobName) blobURL := runtime.JoinPaths(c.URL(), blobName) return (*appendblob.Client)(base.NewAppendBlobClient(blobURL, c.generated().InternalClient().WithClientName(exported.ModuleName), c.sharedKey())) } // NewBlockBlobClient creates a new blockblob.Client object by concatenating blobName to the end of // this Client's URL. The blob name will be URL-encoded. // The new blockblob.Client uses the same request policy pipeline as this Client. func (c *Client) NewBlockBlobClient(blobName string) *blockblob.Client { blobName = url.PathEscape(blobName) blobURL := runtime.JoinPaths(c.URL(), blobName) return (*blockblob.Client)(base.NewBlockBlobClient(blobURL, c.generated().InternalClient().WithClientName(exported.ModuleName), c.sharedKey())) } // NewPageBlobClient creates a new pageblob.Client object by concatenating blobName to the end of // this Client's URL. The blob name will be URL-encoded. // The new pageblob.Client uses the same request policy pipeline as this Client. func (c *Client) NewPageBlobClient(blobName string) *pageblob.Client { blobName = url.PathEscape(blobName) blobURL := runtime.JoinPaths(c.URL(), blobName) return (*pageblob.Client)(base.NewPageBlobClient(blobURL, c.generated().InternalClient().WithClientName(exported.ModuleName), c.sharedKey())) } // Create creates a new container within a storage account. If a container with the same name already exists, the operation fails. // For more information, see https://docs.microsoft.com/rest/api/storageservices/create-container. func (c *Client) Create(ctx context.Context, options *CreateOptions) (CreateResponse, error) { var opts *generated.ContainerClientCreateOptions var cpkScopes *generated.ContainerCPKScopeInfo if options != nil { opts = &generated.ContainerClientCreateOptions{ Access: options.Access, Metadata: options.Metadata, } cpkScopes = options.CPKScopeInfo } resp, err := c.generated().Create(ctx, opts, cpkScopes) return resp, err } // Delete marks the specified container for deletion. The container and any blobs contained within it are later deleted during garbage collection. // For more information, see https://docs.microsoft.com/rest/api/storageservices/delete-container. func (c *Client) Delete(ctx context.Context, options *DeleteOptions) (DeleteResponse, error) { opts, leaseAccessConditions, modifiedAccessConditions := options.format() resp, err := c.generated().Delete(ctx, opts, leaseAccessConditions, modifiedAccessConditions) return resp, err } // Restore operation restore the contents and properties of a soft deleted container to a specified container. // For more information, see https://docs.microsoft.com/en-us/rest/api/storageservices/restore-container. func (c *Client) Restore(ctx context.Context, deletedContainerVersion string, options *RestoreOptions) (RestoreResponse, error) { urlParts, err := blob.ParseURL(c.URL()) if err != nil { return RestoreResponse{}, err } opts := &generated.ContainerClientRestoreOptions{ DeletedContainerName: &urlParts.ContainerName, DeletedContainerVersion: &deletedContainerVersion, } resp, err := c.generated().Restore(ctx, opts) return resp, err } // GetProperties returns the container's properties. // For more information, see https://docs.microsoft.com/rest/api/storageservices/get-container-metadata. func (c *Client) GetProperties(ctx context.Context, o *GetPropertiesOptions) (GetPropertiesResponse, error) { // NOTE: GetMetadata actually calls GetProperties internally because GetProperties returns the metadata AND the properties. // This allows us to not expose a GetMetadata method at all simplifying the API. // The optionals are nil, like they were in track 1.5 opts, leaseAccessConditions := o.format() resp, err := c.generated().GetProperties(ctx, opts, leaseAccessConditions) return resp, err } // SetMetadata sets the container's metadata. // For more information, see https://docs.microsoft.com/rest/api/storageservices/set-container-metadata. func (c *Client) SetMetadata(ctx context.Context, o *SetMetadataOptions) (SetMetadataResponse, error) { metadataOptions, lac, mac := o.format() resp, err := c.generated().SetMetadata(ctx, metadataOptions, lac, mac) return resp, err } // GetAccessPolicy returns the container's access policy. The access policy indicates whether container's blobs may be accessed publicly. // For more information, see https://docs.microsoft.com/rest/api/storageservices/get-container-acl. func (c *Client) GetAccessPolicy(ctx context.Context, o *GetAccessPolicyOptions) (GetAccessPolicyResponse, error) { options, ac := o.format() resp, err := c.generated().GetAccessPolicy(ctx, options, ac) return resp, err } // SetAccessPolicy sets the container's permissions. The access policy indicates whether blobs in a container may be accessed publicly. // For more information, see https://docs.microsoft.com/rest/api/storageservices/set-container-acl. func (c *Client) SetAccessPolicy(ctx context.Context, o *SetAccessPolicyOptions) (SetAccessPolicyResponse, error) { accessPolicy, mac, lac, acl, err := o.format() if err != nil { return SetAccessPolicyResponse{}, err } resp, err := c.generated().SetAccessPolicy(ctx, acl, accessPolicy, mac, lac) return resp, 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 (c *Client) GetAccountInfo(ctx context.Context, o *GetAccountInfoOptions) (GetAccountInfoResponse, error) { getAccountInfoOptions := o.format() resp, err := c.generated().GetAccountInfo(ctx, getAccountInfoOptions) return resp, err } // NewListBlobsFlatPager returns a pager for blobs starting from the specified Marker. Use an empty // Marker to start enumeration from the beginning. Blob names are returned in lexicographic order. // For more information, see https://docs.microsoft.com/rest/api/storageservices/list-blobs. func (c *Client) NewListBlobsFlatPager(o *ListBlobsFlatOptions) *runtime.Pager[ListBlobsFlatResponse] { listOptions := generated.ContainerClientListBlobFlatSegmentOptions{} if o != nil { listOptions.Include = o.Include.format() listOptions.Marker = o.Marker listOptions.Maxresults = o.MaxResults listOptions.Prefix = o.Prefix } return runtime.NewPager(runtime.PagingHandler[ListBlobsFlatResponse]{ More: func(page ListBlobsFlatResponse) bool { return page.NextMarker != nil && len(*page.NextMarker) > 0 }, Fetcher: func(ctx context.Context, page *ListBlobsFlatResponse) (ListBlobsFlatResponse, error) { var req *policy.Request var err error if page == nil { req, err = c.generated().ListBlobFlatSegmentCreateRequest(ctx, &listOptions) } else { listOptions.Marker = page.NextMarker req, err = c.generated().ListBlobFlatSegmentCreateRequest(ctx, &listOptions) } if err != nil { return ListBlobsFlatResponse{}, err } resp, err := c.generated().InternalClient().Pipeline().Do(req) if err != nil { return ListBlobsFlatResponse{}, err } if !runtime.HasStatusCode(resp, http.StatusOK) { // TOOD: storage error? return ListBlobsFlatResponse{}, runtime.NewResponseError(resp) } return c.generated().ListBlobFlatSegmentHandleResponse(resp) }, }) } // NewListBlobsHierarchyPager returns a channel of blobs starting from the specified Marker. Use an empty // Marker to start enumeration from the beginning. Blob names are returned in lexicographic order. // After getting a segment, process it, and then call ListBlobsHierarchicalSegment again (passing the // previously-returned Marker) to get the next segment. // For more information, see https://docs.microsoft.com/rest/api/storageservices/list-blobs. func (c *Client) NewListBlobsHierarchyPager(delimiter string, o *ListBlobsHierarchyOptions) *runtime.Pager[ListBlobsHierarchyResponse] { listOptions := o.format() return runtime.NewPager(runtime.PagingHandler[ListBlobsHierarchyResponse]{ More: func(page ListBlobsHierarchyResponse) bool { return page.NextMarker != nil && len(*page.NextMarker) > 0 }, Fetcher: func(ctx context.Context, page *ListBlobsHierarchyResponse) (ListBlobsHierarchyResponse, error) { var req *policy.Request var err error if page == nil { req, err = c.generated().ListBlobHierarchySegmentCreateRequest(ctx, delimiter, &listOptions) } else { listOptions.Marker = page.NextMarker req, err = c.generated().ListBlobHierarchySegmentCreateRequest(ctx, delimiter, &listOptions) } if err != nil { return ListBlobsHierarchyResponse{}, err } resp, err := c.generated().InternalClient().Pipeline().Do(req) if err != nil { return ListBlobsHierarchyResponse{}, err } if !runtime.HasStatusCode(resp, http.StatusOK) { return ListBlobsHierarchyResponse{}, runtime.NewResponseError(resp) } return c.generated().ListBlobHierarchySegmentHandleResponse(resp) }, }) } // GetSASURL is a convenience method for generating a SAS token for the currently pointed at container. // It can only be used if the credential supplied during creation was a SharedKeyCredential. func (c *Client) GetSASURL(permissions sas.ContainerPermissions, expiry time.Time, o *GetSASURLOptions) (string, error) { if c.sharedKey() == nil { return "", bloberror.MissingSharedKeyCredential } st := o.format() urlParts, err := blob.ParseURL(c.URL()) if err != nil { return "", err } // Containers do not have snapshots, nor versions. qps, err := sas.BlobSignatureValues{ Version: sas.Version, ContainerName: urlParts.ContainerName, Permissions: permissions.String(), StartTime: st, ExpiryTime: expiry.UTC(), }.SignWithSharedKey(c.sharedKey()) if err != nil { return "", err } endpoint := c.URL() + "?" + qps.Encode() return endpoint, nil } // 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. func (c *Client) NewBatchBuilder() (*BatchBuilder, error) { var authPolicy policy.Policy switch cred := c.credential().(type) { case *azcore.TokenCredential: conOptions := c.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: c.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 (c *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 := c.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 } // FilterBlobs operation finds all blobs in the container whose tags match a given search expression. // https://docs.microsoft.com/en-us/rest/api/storageservices/find-blobs-by-tags-container // eg. "dog='germanshepherd' and penguin='emperorpenguin'" func (c *Client) FilterBlobs(ctx context.Context, where string, o *FilterBlobsOptions) (FilterBlobsResponse, error) { containerClientFilterBlobsOptions := o.format() resp, err := c.generated().FilterBlobs(ctx, where, containerClientFilterBlobsOptions) return resp, err }