Skip to content

Commit

Permalink
[Storage] [DataMovement] Service Copy File Share (Azure#39530)
Browse files Browse the repository at this point in the history
* WIP

* WIP

* WIP

* WIP

* Enabled directory creation during enumeration; Tests; Some download directory tests don't work

* Export API

* Cleanup

* Cleanup and removing unnecessary comments

* Cleanup

* Changed LocalDirectory Create to no op

* Remove unnecessary tests
  • Loading branch information
amnguye authored and DevArjun23 committed Nov 14, 2023
1 parent 3136359 commit 4611b04
Show file tree
Hide file tree
Showing 21 changed files with 1,445 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -150,5 +150,19 @@ private string ApplyOptionalPrefix(string path)
=> IsDirectory
? string.Join("/", DirectoryPrefix, path)
: path;

// We will require containers to be created before the transfer starts
// Since blobs is a flat namespace, we do not need to create directories (as they are virtual).
protected override Task CreateIfNotExistsAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;

protected override StorageResourceContainer GetChildStorageResourceContainer(string path)
{
BlobStorageResourceContainerOptions options = _options.DeepCopy();
options.BlobDirectoryPrefix = string.Join("/", DirectoryPrefix, path);
return new BlobStorageResourceContainer(
BlobContainerClient,
options);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -471,5 +471,23 @@ internal static BlobStorageResourceContainerOptions GetBlobContainerOptions(
BlobOptions = baseOptions,
};
}

internal static BlobStorageResourceContainerOptions DeepCopy(this BlobStorageResourceContainerOptions options)
=> new BlobStorageResourceContainerOptions()
{
BlobType = options?.BlobType ?? BlobType.Block,
BlobDirectoryPrefix = options?.BlobDirectoryPrefix,
BlobOptions = new BlobStorageResourceOptions()
{
Metadata = options?.BlobOptions?.Metadata,
Tags = options?.BlobOptions?.Tags,
HttpHeaders = options?.BlobOptions?.HttpHeaders,
AccessTier = options?.BlobOptions?.AccessTier,
DestinationImmutabilityPolicy = options?.BlobOptions?.DestinationImmutabilityPolicy,
LegalHold = options?.BlobOptions?.LegalHold,
UploadTransferValidationOptions = options?.BlobOptions?.UploadTransferValidationOptions,
DownloadTransferValidationOptions = options?.BlobOptions?.DownloadTransferValidationOptions,
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,12 @@ internal static ShareFileUploadRangeOptions ToShareFileUploadRangeOptions(
};

internal static ShareFileUploadRangeFromUriOptions ToShareFileUploadRangeFromUriOptions(
this ShareFileStorageResourceOptions options)
this ShareFileStorageResourceOptions options,
HttpAuthorization sourceAuthorization)
=> new()
{
Conditions = options?.DestinationConditions,
SourceAuthentication = sourceAuthorization
};

internal static StorageResourceProperties ToStorageResourceProperties(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,17 @@ protected override StorageResourceItem GetStorageResourceReference(string path)
protected override async IAsyncEnumerable<StorageResource> GetStorageResourcesAsync(
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (ShareFileClient client in PathScanner.ScanFilesAsync(
await foreach ((ShareDirectoryClient dir, ShareFileClient file) in PathScanner.ScanAsync(
ShareDirectoryClient, cancellationToken).ConfigureAwait(false))
{
yield return new ShareFileStorageResource(client, ResourceOptions);
if (file != default)
{
yield return new ShareFileStorageResource(file, ResourceOptions);
}
else
{
yield return new ShareDirectoryStorageResourceContainer(dir, ResourceOptions);
}
}
}

Expand All @@ -59,5 +66,17 @@ protected override StorageResourceCheckpointData GetDestinationCheckpointData()
{
return new ShareFileDestinationCheckpointData(null, null);
}

protected override async Task CreateIfNotExistsAsync(CancellationToken cancellationToken = default)
{
await ShareDirectoryClient.CreateIfNotExistsAsync(
metadata: default,
smbProperties: default,
filePermission: default,
cancellationToken: cancellationToken).ConfigureAwait(false);
}

protected override StorageResourceContainer GetChildStorageResourceContainer(string path)
=> new ShareDirectoryStorageResourceContainer(ShareDirectoryClient.GetSubdirectoryClient(path), ResourceOptions);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ await ShareFileClient.UploadRangeFromUriAsync(
sourceUri: sourceResource.Uri,
range: range,
sourceRange: range,
options: _options.ToShareFileUploadRangeFromUriOptions(),
options: _options.ToShareFileUploadRangeFromUriOptions(options?.SourceAuthentication),
cancellationToken: cancellationToken).ConfigureAwait(false);
}

Expand Down Expand Up @@ -165,7 +165,7 @@ await ShareFileClient.UploadRangeFromUriAsync(
sourceUri: sourceResource.Uri,
range: new HttpRange(0, completeLength),
sourceRange: new HttpRange(0, completeLength),
options: _options.ToShareFileUploadRangeFromUriOptions(),
options: _options.ToShareFileUploadRangeFromUriOptions(options?.SourceAuthentication),
cancellationToken: cancellationToken).ConfigureAwait(false);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<Compile Include="$(AzureStorageDataMovementTestSharedSources)TestEventsRaised.cs" LinkBase="Shared\DataMovement" />
<Compile Include="$(AzureStorageDataMovementTestSharedSources)DisposingLocalDirectory.cs" LinkBase="Shared\DataMovement" />
<Compile Include="$(AzureStorageDataMovementTestSharedSources)StartTransferUploadTestBase.cs" LinkBase="Shared\DataMovement" />
<Compile Include="$(AzureStorageDataMovementTestSharedSources)StartTransferDirectoryCopyTestBase.cs" LinkBase="Shared\DataMovement" />
<Compile Include="$(AzureStorageDataMovementTestSharedSources)StartTransferCopyTestBase.cs" LinkBase="Shared\DataMovement" />
<Compile Include="$(AzureStorageDataMovementTestSharedSources)StartTransferDownloadTestBase.cs" LinkBase="Shared\DataMovement" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
using SharesClientBuilder = Azure.Storage.Test.Shared.ClientBuilder<
Azure.Storage.Files.Shares.ShareServiceClient,
Azure.Storage.Files.Shares.ShareClientOptions>;
using Azure.Storage.Files.Shares.Models;
using System.Threading;
using Azure.Core;

namespace Azure.Storage.DataMovement.Files.Shares.Tests
{
Expand Down Expand Up @@ -43,24 +44,15 @@ public static SharesClientBuilder GetNewShareClientBuilder(TenantConfigurationBu
(uri, azureSasCredential, clientOptions) => new ShareServiceClient(uri, azureSasCredential, clientOptions),
() => new ShareClientOptions(serviceVersion));

public static ShareServiceClient GetServiceClient_OAuthAccount_SharedKey(this SharesClientBuilder clientBuilder) =>
clientBuilder.GetServiceClientFromSharedKeyConfig(clientBuilder.Tenants.TestConfigOAuth);

public static ShareServiceClient GetServiceClient_OAuth(
this SharesClientBuilder clientBuilder, ShareClientOptions options = default)
{
options ??= clientBuilder.GetOptions();
options.ShareTokenIntent = ShareTokenIntent.Backup;
return clientBuilder.GetServiceClientFromOauthConfig(clientBuilder.Tenants.TestConfigOAuth, options);
}

public static async Task<DisposingShare> GetTestShareAsync(
this SharesClientBuilder clientBuilder,
ShareServiceClient service = default,
string shareName = default,
IDictionary<string, string> metadata = default,
ShareClientOptions options = default)
ShareClientOptions options = default,
CancellationToken cancellationToken = default)
{
CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
service ??= clientBuilder.GetServiceClientFromSharedKeyConfig(clientBuilder.Tenants.TestConfigDefault, options);
metadata ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
shareName ??= clientBuilder.GetNewShareName();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Azure.Core.TestFramework;
using Azure.Storage.DataMovement.Tests;
using Azure.Storage.Files.Shares;
using Azure.Storage.Files.Shares.Models;
using Azure.Storage.Test.Shared;
using Azure.Storage.Files.Shares.Tests;
using NUnit.Framework;
using System.Security.AccessControl;
using Microsoft.Extensions.Options;
using System.Threading;
using Azure.Core;

namespace Azure.Storage.DataMovement.Files.Shares.Tests
{
[ShareClientTestFixture]
public class ShareDirectoryStartTransferCopyTests : StartTransferDirectoryCopyTestBase<
ShareServiceClient,
ShareClient,
ShareClientOptions,
ShareServiceClient,
ShareClient,
ShareClientOptions,
StorageTestEnvironment>
{
private const string _fileResourcePrefix = "test-file-";
private const string _expectedOverwriteExceptionMessage = "Cannot overwrite file.";

public ShareDirectoryStartTransferCopyTests(bool async, ShareClientOptions.ServiceVersion serviceVersion)
: base(async, _expectedOverwriteExceptionMessage, _fileResourcePrefix, null /* RecordedTestMode.Record /* to re-record */)
{
SourceClientBuilder = ClientBuilderExtensions.GetNewShareClientBuilder(Tenants, serviceVersion);
DestinationClientBuilder = ClientBuilderExtensions.GetNewShareClientBuilder(Tenants, serviceVersion);
}

protected override async Task CreateObjectInSourceAsync(
ShareClient container,
long? objectLength = null,
string objectName = null,
Stream contents = default,
CancellationToken cancellationToken = default)
=> await CreateShareFileAsync(container, objectLength, objectName, contents, cancellationToken);

protected override async Task CreateObjectInDestinationAsync(
ShareClient container,
long? objectLength = null,
string objectName = null,
Stream contents = null,
CancellationToken cancellationToken = default)
=> await CreateShareFileAsync(container, objectLength, objectName, contents, cancellationToken);

protected override async Task<IDisposingContainer<ShareClient>> GetDestinationDisposingContainerAsync(
ShareServiceClient service = null,
string containerName = null,
CancellationToken cancellationToken = default)
=> await DestinationClientBuilder.GetTestShareAsync(service, containerName, cancellationToken: cancellationToken);

protected override StorageResourceContainer GetDestinationStorageResourceContainer(ShareClient containerClient, string prefix)
=> new ShareDirectoryStorageResourceContainer(containerClient.GetDirectoryClient(prefix), default);

protected override ShareClient GetOAuthSourceContainerClient(string containerName)
{
ShareClientOptions options = SourceClientBuilder.GetOptions();
options.ShareTokenIntent = ShareTokenIntent.Backup;
ShareServiceClient oauthService = SourceClientBuilder.GetServiceClientFromOauthConfig(Tenants.TestConfigOAuth, options);
return oauthService.GetShareClient(containerName);
}

protected override ShareClient GetOAuthDestinationContainerClient(string containerName)
{
ShareClientOptions options = DestinationClientBuilder.GetOptions();
options.ShareTokenIntent = ShareTokenIntent.Backup;
ShareServiceClient oauthService = DestinationClientBuilder.GetServiceClientFromOauthConfig(Tenants.TestConfigOAuth, options);
return oauthService.GetShareClient(containerName);
}

protected override async Task<IDisposingContainer<ShareClient>> GetSourceDisposingContainerAsync(ShareServiceClient service = null, string containerName = null, CancellationToken cancellationToken = default)
{
service ??= SourceClientBuilder.GetServiceClientFromSharedKeyConfig(SourceClientBuilder.Tenants.TestConfigDefault, SourceClientBuilder.GetOptions());
ShareServiceClient sasService = new ShareServiceClient(service.GenerateAccountSasUri(
Sas.AccountSasPermissions.All,
SourceClientBuilder.Recording.UtcNow.AddDays(1),
Sas.AccountSasResourceTypes.All),
SourceClientBuilder.GetOptions());
return await SourceClientBuilder.GetTestShareAsync(sasService, containerName, cancellationToken: cancellationToken);
}

protected override StorageResourceContainer GetSourceStorageResourceContainer(ShareClient containerClient, string prefix = null)
=> new ShareDirectoryStorageResourceContainer(containerClient.GetDirectoryClient(prefix), default);

protected override async Task CreateDirectoryInSourceAsync(ShareClient sourceContainer, string directoryPath, CancellationToken cancellationToken = default)
=> await CreateDirectoryTreeAsync(sourceContainer, directoryPath, cancellationToken);

protected override async Task CreateDirectoryInDestinationAsync(ShareClient destinationContainer, string directoryPath, CancellationToken cancellationToken = default)
=> await CreateDirectoryTreeAsync(destinationContainer, directoryPath, cancellationToken);

protected override async Task VerifyEmptyDestinationContainerAsync(
ShareClient destinationContainer,
string destinationPrefix,
CancellationToken cancellationToken = default)
{
CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
ShareDirectoryClient destinationDirectory = string.IsNullOrEmpty(destinationPrefix) ?
destinationContainer.GetRootDirectoryClient() :
destinationContainer.GetDirectoryClient(destinationPrefix);
IList<ShareFileItem> items = await destinationDirectory.GetFilesAndDirectoriesAsync(cancellationToken: cancellationToken).ToListAsync();
Assert.IsEmpty(items);
}

protected override async Task VerifyResultsAsync(
ShareClient sourceContainer,
string sourcePrefix,
ShareClient destinationContainer,
string destinationPrefix,
CancellationToken cancellationToken = default)
{
CancellationHelper.ThrowIfCancellationRequested(cancellationToken);

// List all files in source blob folder path
List<string> sourceFileNames = new List<string>();
List<string> sourceDirectoryNames = new List<string>();

// Get source directory client and list the paths
ShareDirectoryClient sourceDirectory = string.IsNullOrEmpty(sourcePrefix) ?
sourceContainer.GetRootDirectoryClient() :
sourceContainer.GetDirectoryClient(sourcePrefix);
await foreach (Page<ShareFileItem> page in sourceDirectory.GetFilesAndDirectoriesAsync().AsPages())
{
sourceFileNames.AddRange(page.Values.Where((ShareFileItem item) => !item.IsDirectory).Select((ShareFileItem item) => item.Name));
sourceDirectoryNames.AddRange(page.Values.Where((ShareFileItem item) => item.IsDirectory).Select((ShareFileItem item) => item.Name));
}

// List all files in the destination blob folder path
List<string> destinationFileNames = new List<string>();
List<string> destinationDirectoryNames = new List<string>();

ShareDirectoryClient destinationDirectory = string.IsNullOrEmpty(destinationPrefix) ?
destinationContainer.GetRootDirectoryClient() :
destinationContainer.GetDirectoryClient(destinationPrefix);
await foreach (Page<ShareFileItem> page in destinationDirectory.GetFilesAndDirectoriesAsync().AsPages())
{
destinationFileNames.AddRange(page.Values.Where((ShareFileItem item) => !item.IsDirectory).Select((ShareFileItem item) => item.Name));
destinationDirectoryNames.AddRange(page.Values.Where((ShareFileItem item) => item.IsDirectory).Select((ShareFileItem item) => item.Name));
}

// Assert subdirectories
Assert.AreEqual(sourceDirectoryNames.Count, destinationDirectoryNames.Count);
Assert.AreEqual(sourceDirectoryNames, destinationDirectoryNames);

// Assert file and file contents
Assert.AreEqual(sourceFileNames.Count, destinationFileNames.Count);
for (int i = 0; i < sourceFileNames.Count; i++)
{
Assert.AreEqual(
sourceFileNames[i],
destinationFileNames[i]);

// Verify Download
string sourceFileName = Path.Combine(sourcePrefix, sourceFileNames[i]);
using Stream sourceStream = await sourceDirectory.GetFileClient(sourceFileNames[i]).OpenReadAsync(cancellationToken: cancellationToken);
using Stream destinationStream = await destinationDirectory.GetFileClient(destinationFileNames[i]).OpenReadAsync(cancellationToken: cancellationToken);
Assert.AreEqual(sourceStream, destinationStream);
}
}

private async Task CreateShareFileAsync(
ShareClient container,
long? objectLength = null,
string objectName = null,
Stream contents = default,
CancellationToken cancellationToken = default)
{
CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
objectName ??= GetNewObjectName();
if (!objectLength.HasValue)
{
throw new InvalidOperationException($"Cannot create share file without size specified. Specify {nameof(objectLength)}.");
}
ShareFileClient fileClient = container.GetRootDirectoryClient().GetFileClient(objectName);
await fileClient.CreateAsync(objectLength.Value);

if (contents != default)
{
await fileClient.UploadAsync(contents, cancellationToken: cancellationToken);
}
}

private async Task CreateDirectoryTreeAsync(ShareClient container, string directoryPath, CancellationToken cancellationToken = default)
{
CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
ShareDirectoryClient directory = container.GetRootDirectoryClient().GetSubdirectoryClient(directoryPath);
await directory.CreateIfNotExistsAsync(cancellationToken: cancellationToken);
}
}
}
Loading

0 comments on commit 4611b04

Please sign in to comment.