diff --git a/sdk/storage/azblob/CHANGELOG.md b/sdk/storage/azblob/CHANGELOG.md index 8cc0c432732d..b35689b66267 100644 --- a/sdk/storage/azblob/CHANGELOG.md +++ b/sdk/storage/azblob/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.0.1 (Unreleased) ### Features Added +* UploadBlobFromURL API. For more information view [here](https://learn.microsoft.com/rest/api/storageservices/put-blob-from-url). * Added [Blob Batch API](https://learn.microsoft.com/rest/api/storageservices/blob-batch). * Added support for bearer challenge for identity based managed disks. diff --git a/sdk/storage/azblob/assets.json b/sdk/storage/azblob/assets.json index 96a9c9aa5055..8e31e22ae3eb 100644 --- a/sdk/storage/azblob/assets.json +++ b/sdk/storage/azblob/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "go", "TagPrefix": "go/storage/azblob", - "Tag": "go/storage/azblob_5d20008f59" + "Tag": "go/storage/azblob_2d480c1412" } diff --git a/sdk/storage/azblob/blockblob/client.go b/sdk/storage/azblob/blockblob/client.go index 3119de9d6165..f3e0d5b2ca85 100644 --- a/sdk/storage/azblob/blockblob/client.go +++ b/sdk/storage/azblob/blockblob/client.go @@ -165,6 +165,20 @@ func (bb *Client) Upload(ctx context.Context, body io.ReadSeekCloser, options *U return resp, err } +// UploadBlobFromURL - The Put Blob from URL operation creates a new Block Blob where the contents of the blob are read from +// a given URL. Partial updates are not supported with Put Blob from URL; the content of an existing blob is overwritten +// with the content of the new blob. To perform partial updates to a block blob’s contents using a source URL, use the Put +// Block from URL API in conjunction with Put Block List. +// For more information, see https://learn.microsoft.com/rest/api/storageservices/put-blob-from-url +func (bb *Client) UploadBlobFromURL(ctx context.Context, copySource string, options *UploadBlobFromURLOptions) (UploadBlobFromURLResponse, error) { + opts, httpHeaders, leaseAccessConditions, cpkInfo, cpkSourceInfo, modifiedAccessConditions, sourceModifiedConditions := options.format() + + resp, err := bb.generated().PutBlobFromURL(ctx, int64(0), copySource, opts, httpHeaders, leaseAccessConditions, + cpkInfo, cpkSourceInfo, modifiedAccessConditions, sourceModifiedConditions) + + return resp, err +} + // StageBlock uploads the specified block to the block blob's "staging area" to be later committed by a call to CommitBlockList. // Note that the http client closes the body stream after the request is sent to the service. // For more information, see https://docs.microsoft.com/rest/api/storageservices/put-block. diff --git a/sdk/storage/azblob/blockblob/client_test.go b/sdk/storage/azblob/blockblob/client_test.go index 6f8d26717b7d..e57a430d1eb0 100644 --- a/sdk/storage/azblob/blockblob/client_test.go +++ b/sdk/storage/azblob/blockblob/client_test.go @@ -14,6 +14,7 @@ import ( "encoding/binary" "errors" "fmt" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "hash/crc64" "io" "math/rand" @@ -587,6 +588,602 @@ func (s *BlockBlobRecordedTestsSuite) TestUploadBlockWithImmutabilityPolicy() { _require.Nil(err) } +func setUpPutBlobFromURLTest(s *BlockBlobUnrecordedTestsSuite, testName string, _require *require.Assertions, svcClient *service.Client) (*container.Client, *blockblob.Client, *blockblob.Client, string) { + containerName := testcommon.GenerateContainerName(testName) + containerClient := testcommon.CreateNewContainer(context.Background(), _require, containerName, svcClient) + + srcBlob := testcommon.GenerateBlobName("src" + testName) + srcBBClient := testcommon.CreateNewBlockBlob(context.Background(), _require, srcBlob, containerClient) + + dest := testcommon.GenerateBlobName("dest" + testName) + destBBClient := testcommon.CreateNewBlockBlob(context.Background(), _require, dest, containerClient) + + content := make([]byte, 0) + body := bytes.NewReader(content) + + // create empty dest blob + _, err := destBBClient.Upload(context.Background(), streaming.NopCloser(body), nil) + _require.Nil(err) + + expiryTime, err := time.Parse(time.UnixDate, "Fri Jun 11 20:00:00 UTC 2049") + _require.Nil(err) + + credential, err := testcommon.GetGenericSharedKeyCredential(testcommon.TestAccountDefault) + if err != nil { + s.T().Fatal("Couldn't fetch credential because " + err.Error()) + } + + sasQueryParams, err := sas.AccountSignatureValues{ + Protocol: sas.ProtocolHTTPS, + ExpiryTime: expiryTime, + Permissions: to.Ptr(sas.AccountPermissions{Read: true, List: true}).String(), + ResourceTypes: to.Ptr(sas.AccountResourceTypes{Container: true, Object: true}).String(), + }.SignWithSharedKey(credential) + _require.Nil(err) + + srcBlobParts, _ := blob.ParseURL(srcBBClient.URL()) + srcBlobParts.SAS = sasQueryParams + srcBlobURLWithSAS := srcBlobParts.String() + + return containerClient, srcBBClient, destBBClient, srcBlobURLWithSAS +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromURL() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, _, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + pbResp, err := destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, nil) + _require.NotNil(pbResp) + _require.NoError(err) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromURLWithHeaders() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, _, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + options := blockblob.UploadBlobFromURLOptions{ + Tags: testcommon.BasicBlobTagsMap, + HTTPHeaders: &testcommon.BasicHeaders, + Metadata: testcommon.BasicMetadata, + Tier: &testcommon.CoolAccessTier, + } + + pbResp, err := destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.NotNil(pbResp) + _require.NoError(err) + + resp, err := destBlob.GetProperties(context.Background(), nil) + _require.NoError(err) + h := blob.ParseHTTPHeaders(resp) + h.BlobContentMD5 = nil // the service generates a MD5 value, omit before comparing + _require.EqualValues(h, testcommon.BasicHeaders) + _require.EqualValues(resp.AccessTier, &testcommon.CoolAccessTier) + tagcount := int64(len(testcommon.BasicBlobTagsMap)) + _require.EqualValues(resp.TagCount, &tagcount) + _require.EqualValues(resp.Metadata, testcommon.BasicMetadata) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlWithCPK() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, _, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + options := blockblob.UploadBlobFromURLOptions{ + CPKInfo: &testcommon.TestCPKByValue, + } + + pbResp, err := destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.NotNil(pbResp) + _require.NoError(err) + + getBlobPropertiesOptions := blob.GetPropertiesOptions{ + CPKInfo: &testcommon.TestCPKByValue, + } + + getResp, err := destBlob.GetProperties(context.Background(), &getBlobPropertiesOptions) + _require.Nil(err) + _require.EqualValues(getResp.EncryptionKeySHA256, testcommon.TestCPKByValue.EncryptionKeySHA256) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlCPKScope() { + _require := require.New(s.T()) + testName := s.T().Name() + encryptionScope := testcommon.GetCPKScopeInfo(s.T()) + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, _, destBlob, _ := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + bbName := testcommon.GenerateBlobName(testName) + srcBlob := testcommon.CreateNewBlockBlobWithCPK(context.Background(), _require, bbName, containerClient, nil, &encryptionScope) + expiryTime, err := time.Parse(time.UnixDate, "Fri Jun 11 20:00:00 UTC 2049") + _require.Nil(err) + + credential, err := testcommon.GetGenericSharedKeyCredential(testcommon.TestAccountDefault) + if err != nil { + s.T().Fatal("Couldn't fetch credential because " + err.Error()) + } + + sasQueryParams, err := sas.AccountSignatureValues{ + Protocol: sas.ProtocolHTTPS, + ExpiryTime: expiryTime, + Permissions: to.Ptr(sas.AccountPermissions{Read: true, List: true}).String(), + ResourceTypes: to.Ptr(sas.AccountResourceTypes{Container: true, Object: true}).String(), + }.SignWithSharedKey(credential) + _require.Nil(err) + + srcBlobParts, _ := blob.ParseURL(srcBlob.URL()) + srcBlobParts.SAS = sasQueryParams + srcBlobURLWithSAS := srcBlobParts.String() + + options := blockblob.UploadBlobFromURLOptions{ + CPKScopeInfo: &encryptionScope, + } + + pbResp, err := destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.NotNil(pbResp) + _require.NoError(err) + + getResp, err := destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) + _require.EqualValues(*getResp.EncryptionScope, *encryptionScope.EncryptionScope) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlSourceContentMD5() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + if err != nil { + s.Fail("Unable to fetch service client because " + err.Error()) + } + + contentSize := 1 * 1024 * 1024 // 1MB + r, sourceData := testcommon.GenerateData(contentSize) + sourceDataMD5Value := md5.Sum(sourceData) + + containerClient, srcBlob, destBlob, _ := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + credential, err := testcommon.GetGenericSharedKeyCredential(testcommon.TestAccountDefault) + if err != nil { + s.T().Fatal("Couldn't fetch credential because " + err.Error()) + } + + _, err = srcBlob.Upload(context.Background(), r, nil) + _require.Nil(err) + + expiryTime, err := time.Parse(time.UnixDate, "Fri Jun 11 20:00:00 UTC 2049") + _require.Nil(err) + // Get source blob url with SAS for UploadBlobFromURL. + sasQueryParams, err := sas.AccountSignatureValues{ + Protocol: sas.ProtocolHTTPS, + ExpiryTime: expiryTime, + Permissions: to.Ptr(sas.AccountPermissions{Read: true, List: true}).String(), + ResourceTypes: to.Ptr(sas.AccountResourceTypes{Container: true, Object: true}).String(), + }.SignWithSharedKey(credential) + _require.Nil(err) + + srcBlobParts, _ := blob.ParseURL(srcBlob.URL()) + srcBlobParts.SAS = sasQueryParams + srcBlobURLWithSAS := srcBlobParts.String() + + sourceContentMD5 := sourceDataMD5Value[:] + options := blockblob.UploadBlobFromURLOptions{ + SourceContentMD5: sourceContentMD5, + } + resp, err := destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.Nil(err) + _require.NotEqual(*resp.ETag, "") + _require.NotEqual(*resp.RequestID, "") + _require.NotEqual(*resp.Version, "") + _require.Equal((*resp.Date).IsZero(), false) + _require.EqualValues(resp.ContentMD5, sourceDataMD5Value[:]) + + // Try UploadBlobFromURL with bad MD5 + _, badMD5 := testcommon.GetRandomDataAndReader(16) + options2 := blockblob.UploadBlobFromURLOptions{ + SourceContentMD5: badMD5, + } + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options2) + _require.NotNil(err) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlSourceIfMatchTrue() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, srcBlob, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + resp, err := srcBlob.GetProperties(context.Background(), nil) + _require.Nil(err) + + options := blockblob.UploadBlobFromURLOptions{ + SourceModifiedAccessConditions: &blob.SourceModifiedAccessConditions{ + SourceIfMatch: resp.ETag, + }, + } + + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.Nil(err) + + _, err = destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlSourceIfMatchFalse() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, _, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + randomEtag := azcore.ETag("a") + accessConditions := blob.SourceModifiedAccessConditions{ + SourceIfMatch: &randomEtag, + } + options := blockblob.UploadBlobFromURLOptions{ + SourceModifiedAccessConditions: &accessConditions, + } + + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.NotNil(err) + testcommon.ValidateBlobErrorCode(_require, err, bloberror.SourceConditionNotMet) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlSourceIfNoneMatchTrue() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, srcBlob, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + _, err = srcBlob.GetProperties(context.Background(), nil) + _require.Nil(err) + + options := blockblob.UploadBlobFromURLOptions{ + SourceModifiedAccessConditions: &blob.SourceModifiedAccessConditions{ + SourceIfNoneMatch: to.Ptr(azcore.ETag("a")), + }, + } + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.Nil(err) + + _, err = destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlSourceIfNoneMatchFalse() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, srcBlob, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + resp, err := srcBlob.GetProperties(context.Background(), nil) + _require.Nil(err) + + options := blockblob.UploadBlobFromURLOptions{ + SourceModifiedAccessConditions: &blob.SourceModifiedAccessConditions{ + SourceIfNoneMatch: resp.ETag, + }, + } + + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.NotNil(err) + testcommon.ValidateBlobErrorCode(_require, err, bloberror.CannotVerifyCopySource) + _require.ErrorContains(err, "304") +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlDestIfModifiedSinceTrue() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, srcBlob, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + cResp, err := srcBlob.Upload(context.Background(), streaming.NopCloser(strings.NewReader(testcommon.BlockBlobDefaultData)), nil) + _require.Nil(err) + + currentTime := testcommon.GetRelativeTimeFromAnchor(cResp.Date, -10) + + options := blockblob.UploadBlobFromURLOptions{ + AccessConditions: &blob.AccessConditions{ + ModifiedAccessConditions: &blob.ModifiedAccessConditions{ + IfModifiedSince: ¤tTime, + }, + }, + } + + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.Nil(err) + + _, err = destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlDestIfModifiedSinceFalse() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, srcBlob, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + cResp, err := srcBlob.Upload(context.Background(), streaming.NopCloser(strings.NewReader(testcommon.BlockBlobDefaultData)), nil) + _require.Nil(err) + + currentTime := testcommon.GetRelativeTimeFromAnchor(cResp.Date, 10) + options := blockblob.UploadBlobFromURLOptions{ + AccessConditions: &blob.AccessConditions{ + ModifiedAccessConditions: &blob.ModifiedAccessConditions{ + IfModifiedSince: ¤tTime, + }, + }, + } + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + testcommon.ValidateBlobErrorCode(_require, err, bloberror.ConditionNotMet) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlDestIfUnmodifiedSinceTrue() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, srcBlob, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + cResp, err := srcBlob.Upload(context.Background(), streaming.NopCloser(strings.NewReader(testcommon.BlockBlobDefaultData)), nil) + _require.Nil(err) + + currentTime := testcommon.GetRelativeTimeFromAnchor(cResp.Date, 10) + + options := blockblob.UploadBlobFromURLOptions{ + AccessConditions: &blob.AccessConditions{ + ModifiedAccessConditions: &blob.ModifiedAccessConditions{ + IfUnmodifiedSince: ¤tTime, + }, + }, + } + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.Nil(err) + + _, err = destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlDestIfUnmodifiedSinceFalse() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, srcBlob, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + cResp, err := srcBlob.Upload(context.Background(), streaming.NopCloser(strings.NewReader(testcommon.BlockBlobDefaultData)), nil) + _require.Nil(err) + + currentTime := testcommon.GetRelativeTimeFromAnchor(cResp.Date, -10) + + options := blockblob.UploadBlobFromURLOptions{ + AccessConditions: &blob.AccessConditions{ + ModifiedAccessConditions: &blob.ModifiedAccessConditions{ + IfUnmodifiedSince: ¤tTime, + }, + }, + } + + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.NotNil(err) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestBlobPutBlobFromUrlDestIfMatchTrue() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, _, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + resp, err := destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) + + options := blockblob.UploadBlobFromURLOptions{ + AccessConditions: &blob.AccessConditions{ + ModifiedAccessConditions: &blob.ModifiedAccessConditions{ + IfMatch: resp.ETag, + }, + }, + } + + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.Nil(err) + + resp, err = destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlDestIfMatchFalse() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, _, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + resp, err := destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) + + options := blockblob.UploadBlobFromURLOptions{ + AccessConditions: &blob.AccessConditions{ + ModifiedAccessConditions: &blob.ModifiedAccessConditions{ + IfMatch: resp.ETag, + }, + }, + } + metadata := make(map[string]*string) + metadata["bla"] = to.Ptr("bla") + _, err = destBlob.SetMetadata(context.Background(), metadata, nil) + _require.Nil(err) + + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.NotNil(err) + testcommon.ValidateBlobErrorCode(_require, err, bloberror.ConditionNotMet) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlDestIfNoneMatchTrue() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, _, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + resp, err := destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) + + options := blockblob.UploadBlobFromURLOptions{ + AccessConditions: &blob.AccessConditions{ + ModifiedAccessConditions: &blob.ModifiedAccessConditions{ + IfNoneMatch: resp.ETag, + }, + }, + } + + _, err = destBlob.SetMetadata(context.Background(), nil, nil) // SetMetadata chances the blob's etag + _require.Nil(err) + + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.Nil(err) + + resp, err = destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromUrlDestIfNoneMatchFalse() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, _, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + resp, err := destBlob.GetProperties(context.Background(), nil) + _require.Nil(err) + + options := blockblob.UploadBlobFromURLOptions{ + AccessConditions: &blob.AccessConditions{ + ModifiedAccessConditions: &blob.ModifiedAccessConditions{ + IfNoneMatch: resp.ETag, + }, + }, + } + + _, err = destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.NotNil(err) + testcommon.ValidateBlobErrorCode(_require, err, bloberror.ConditionNotMet) +} + +func (s *BlockBlobUnrecordedTestsSuite) TestPutBlobFromURLCopySourceFalse() { + _require := require.New(s.T()) + testName := s.T().Name() + svcClient, err := testcommon.GetServiceClient(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + containerClient, srcBlob, destBlob, srcBlobURLWithSAS := setUpPutBlobFromURLTest(s, testName, _require, svcClient) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + _, err = srcBlob.SetTier(context.Background(), testcommon.CoolAccessTier, nil) + _require.Nil(err) + + resp, err := srcBlob.GetProperties(context.Background(), nil) + _require.Nil(err) + _require.Equal(resp.AccessTier, to.Ptr("Cool")) + + // CopySourceBlobProperties is true by default, trying false here + options := blockblob.UploadBlobFromURLOptions{ + CopySourceBlobProperties: to.Ptr(false), + } + + pbResp, err := destBlob.UploadBlobFromURL(context.Background(), srcBlobURLWithSAS, &options) + _require.NoError(err) + _require.NotNil(pbResp) + + resp, err = destBlob.GetProperties(context.Background(), nil) + _require.NoError(err) + _require.NotEqual(resp.AccessTier, to.Ptr("Cool")) +} + +func (s *BlockBlobRecordedTestsSuite) TestPutBlobFromURLCopySourceAuth() { + _require := require.New(s.T()) + testName := s.T().Name() + _, svcCred, err := testcommon.GetServiceClientCred(s.T(), testcommon.TestAccountDefault, nil) + _require.NoError(err) + + // Getting AAD Authentication + cred, err := azidentity.NewDefaultAzureCredential(nil) + _require.NoError(err) + + containerName := testcommon.GenerateContainerName(testName) + containerClient := testcommon.CreateContainerCred(context.Background(), _require, containerName, svcCred, cred) + defer testcommon.DeleteContainer(context.Background(), _require, containerClient) + + // Create source and destination blobs + srcBBClient := testcommon.CreateNewBlockBlob(context.Background(), _require, "src "+testName, containerClient) + destBBClient := testcommon.CreateNewBlockBlob(context.Background(), _require, "dest"+testName, containerClient) + + // create empty dest blob + content := make([]byte, 0) + body := bytes.NewReader(content) + _, err = destBBClient.Upload(context.Background(), streaming.NopCloser(body), nil) + _require.Nil(err) + + // Getting token + token, err := cred.GetToken(context.Background(), policy.TokenRequestOptions{Scopes: []string{"https://storage.azure.com/.default"}}) + _require.NoError(err) + + options := blockblob.UploadBlobFromURLOptions{ + CopySourceAuthorization: to.Ptr("Bearer " + token.Token), + } + + pbResp, err := destBBClient.UploadBlobFromURL(context.Background(), srcBBClient.URL(), &options) + _require.NoError(err) + _require.NotNil(pbResp) + +} + func (s *BlockBlobRecordedTestsSuite) TestPutBlockListWithImmutabilityPolicy() { _require := require.New(s.T()) testName := s.T().Name() diff --git a/sdk/storage/azblob/blockblob/models.go b/sdk/storage/azblob/blockblob/models.go index 3da15aab99df..ba1b9ee9f67b 100644 --- a/sdk/storage/azblob/blockblob/models.go +++ b/sdk/storage/azblob/blockblob/models.go @@ -70,6 +70,56 @@ func (o *UploadOptions) format() (*generated.BlockBlobClientUploadOptions, *gene // --------------------------------------------------------------------------------------------------------------------- +// UploadBlobFromURLOptions contains the optional parameters for the Client.UploadBlobFromURL method. +type UploadBlobFromURLOptions struct { + // Optional. Used to set blob tags in various blob operations. + Tags map[string]string + + // Only Bearer type is supported. Credentials should be a valid OAuth access token to copy source. + CopySourceAuthorization *string + + // Optional, default is true. Indicates if properties from the source blob should be copied. + CopySourceBlobProperties *bool + + // Optional. Specifies a user-defined name-value pair associated with the blob. + Metadata map[string]*string + + // Optional. Specifies the md5 calculated for the range of bytes that must be read from the copy source. + SourceContentMD5 []byte + + // Optional. Indicates the tier to be set on the blob. + Tier *blob.AccessTier + + // Additional optional headers + HTTPHeaders *blob.HTTPHeaders + AccessConditions *blob.AccessConditions + CPKInfo *blob.CPKInfo + CPKScopeInfo *blob.CPKScopeInfo + SourceModifiedAccessConditions *blob.SourceModifiedAccessConditions +} + +func (o *UploadBlobFromURLOptions) format() (*generated.BlockBlobClientPutBlobFromURLOptions, *generated.BlobHTTPHeaders, + *generated.LeaseAccessConditions, *generated.CPKInfo, *generated.CPKScopeInfo, *generated.ModifiedAccessConditions, + *generated.SourceModifiedAccessConditions) { + if o == nil { + return nil, nil, nil, nil, nil, nil, nil + } + + options := generated.BlockBlobClientPutBlobFromURLOptions{ + BlobTagsString: shared.SerializeBlobTagsToStrPtr(o.Tags), + CopySourceAuthorization: o.CopySourceAuthorization, + CopySourceBlobProperties: o.CopySourceBlobProperties, + Metadata: o.Metadata, + SourceContentMD5: o.SourceContentMD5, + Tier: o.Tier, + } + + leaseAccessConditions, modifiedAccessConditions := exported.FormatBlobAccessConditions(o.AccessConditions) + return &options, o.HTTPHeaders, leaseAccessConditions, o.CPKInfo, o.CPKScopeInfo, modifiedAccessConditions, o.SourceModifiedAccessConditions +} + +// --------------------------------------------------------------------------------------------------------------------- + // StageBlockOptions contains the optional parameters for the Client.StageBlock method. type StageBlockOptions struct { CPKInfo *blob.CPKInfo diff --git a/sdk/storage/azblob/blockblob/responses.go b/sdk/storage/azblob/blockblob/responses.go index 00093ec1a75c..917f71809779 100644 --- a/sdk/storage/azblob/blockblob/responses.go +++ b/sdk/storage/azblob/blockblob/responses.go @@ -16,6 +16,9 @@ import ( // UploadResponse contains the response from method Client.Upload. type UploadResponse = generated.BlockBlobClientUploadResponse +// UploadBlobFromURLResponse contains the response from the method Client.UploadBlobFromURL +type UploadBlobFromURLResponse = generated.BlockBlobClientPutBlobFromURLResponse + // StageBlockResponse contains the response from method Client.StageBlock. type StageBlockResponse = generated.BlockBlobClientStageBlockResponse diff --git a/sdk/storage/azblob/internal/testcommon/clients_auth.go b/sdk/storage/azblob/internal/testcommon/clients_auth.go index 5d9cc24c3575..9eabe7f39f9c 100644 --- a/sdk/storage/azblob/internal/testcommon/clients_auth.go +++ b/sdk/storage/azblob/internal/testcommon/clients_auth.go @@ -57,6 +57,12 @@ const ( FakeStorageURL = "https://fakestorage.blob.core.windows.net" ) +var ( + CoolAccessTier = blob.AccessTierCool + HotAccessTier = blob.AccessTierHot + ArchiveAccessTier = blob.AccessTierArchive +) + var ( BlobContentType = "my_type" BlobContentDisposition = "my_disposition" @@ -133,6 +139,24 @@ func GetServiceClient(t *testing.T, accountType TestAccountType, options *servic return serviceClient, err } +func GetServiceClientCred(t *testing.T, accountType TestAccountType, options *service.ClientOptions) (*service.Client, *azblob.SharedKeyCredential, error) { + if options == nil { + options = &service.ClientOptions{} + } + + SetClientOptions(t, &options.ClientOptions) + + cred, err := GetGenericSharedKeyCredential(accountType) + if err != nil { + return nil, nil, err + } + + serviceClient, err := service.NewClientWithSharedKeyCredential("https://"+cred.AccountName()+".blob.core.windows.net/", cred, options) + + // Returns SharedKeyCredential + return serviceClient, cred, err +} + func GetServiceClientNoCredential(t *testing.T, sasUrl string, options *service.ClientOptions) (*service.Client, error) { if options == nil { options = &service.ClientOptions{} @@ -201,7 +225,18 @@ func CreateNewContainer(ctx context.Context, _require *require.Assertions, conta _, err := containerClient.Create(ctx, nil) _require.Nil(err) - // _require.Equal(cResp.RawResponse.StatusCode, 201) + + return containerClient +} + +func CreateContainerCred(ctx context.Context, _require *require.Assertions, containerName string, svcCred *service.SharedKeyCredential, cred *azidentity.DefaultAzureCredential) *container.Client { + containerURL := fmt.Sprintf("https://%s.blob.core.windows.net/%s", svcCred.AccountName(), containerName) + + containerClient, err := container.NewClient(containerURL, cred, nil) + _require.Nil(err) + _, err = containerClient.Create(context.Background(), nil) + _require.Nil(err) + return containerClient }