Skip to content

Commit 170a31c

Browse files
[Storage][Datamovement] Fix copying of empty directories under prefixes (Azure#51233)
1 parent 2f33878 commit 170a31c

File tree

4 files changed

+37
-25
lines changed

4 files changed

+37
-25
lines changed

sdk/storage/Azure.Storage.DataMovement.Blobs/assets.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
"AssetsRepo": "Azure/azure-sdk-assets",
33
"AssetsRepoPrefixPath": "net",
44
"TagPrefix": "net/storage/Azure.Storage.DataMovement.Blobs",
5-
"Tag": "net/storage/Azure.Storage.DataMovement.Blobs_1ebb672122"
5+
"Tag": "net/storage/Azure.Storage.DataMovement.Blobs_f1a4120258"
66
}

sdk/storage/Azure.Storage.DataMovement.Blobs/src/BlobStorageResourceContainer.cs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -156,21 +156,21 @@ protected override async IAsyncEnumerable<StorageResource> GetStorageResourcesAs
156156
{
157157
// Suffix the slash when searching if there's a prefix specified,
158158
// to only list blobs in the specified virtual directory.
159-
string fullPrefix = string.IsNullOrEmpty(DirectoryPrefix) ?
159+
string sourcePrefix = string.IsNullOrEmpty(DirectoryPrefix) ?
160160
"" :
161161
string.Concat(DirectoryPrefix, Constants.PathBackSlashDelimiter);
162162

163-
Queue<string> prefixes = new();
164-
prefixes.Enqueue(fullPrefix); // Start with the initial prefix
163+
Queue<string> paths = new();
164+
paths.Enqueue(sourcePrefix); // Start with the initial prefix
165165

166-
while (prefixes.Count > 0)
166+
while (paths.Count > 0)
167167
{
168-
string currentPrefix = prefixes.Dequeue();
168+
string currentPath = paths.Dequeue();
169169

170170
int childCount = 0;
171171
await foreach (BlobHierarchyItem blobHierarchyItem in BlobContainerClient.GetBlobsByHierarchyAsync(
172172
traits: BlobTraits.Metadata,
173-
prefix: currentPrefix,
173+
prefix: currentPath,
174174
delimiter: Constants.PathBackSlashDelimiter,
175175
cancellationToken: cancellationToken).ConfigureAwait(false))
176176
{
@@ -186,9 +186,9 @@ protected override async IAsyncEnumerable<StorageResource> GetStorageResourcesAs
186186
else if (blobHierarchyItem.IsPrefix)
187187
{
188188
// Return the blob virtual directory as a StorageResourceContainer
189-
yield return GetChildStorageResourceContainer(blobHierarchyItem.Prefix.Substring(fullPrefix.Length));
189+
yield return GetChildStorageResourceContainer(blobHierarchyItem.Prefix.Substring(sourcePrefix.Length));
190190
// Enqueue the prefix for further traversal
191-
prefixes.Enqueue(blobHierarchyItem.Prefix);
191+
paths.Enqueue(blobHierarchyItem.Prefix);
192192
}
193193
}
194194

@@ -200,10 +200,13 @@ protected override async IAsyncEnumerable<StorageResource> GetStorageResourcesAs
200200
// with the folder metadata set which represents a directory stub on HNS accounts. No other
201201
// properties will be copied from the source. We only do this for empty directories because non-empty
202202
// directories are created automatically.
203-
if (childCount == 0 && destinationContainer is BlobStorageResourceContainer destBlobContainer)
203+
if (childCount == 0 &&
204+
currentPath != sourcePrefix && // If doing an empty copy
205+
destinationContainer is BlobStorageResourceContainer destBlobContainer)
204206
{
207+
// Remove source prefix and add destination prefix
205208
BlockBlobStorageResource destinationDirectoryResource = destBlobContainer.GetBlobAsStorageResource(
206-
currentPrefix,
209+
destBlobContainer.ApplyOptionalPrefix(currentPath.Substring(sourcePrefix.Length)),
207210
BlobType.Block) as BlockBlobStorageResource;
208211
await destinationDirectoryResource.CreateEmptyDirectoryStubAsync(cancellationToken).ConfigureAwait(false);
209212
}
@@ -219,7 +222,7 @@ protected override StorageResourceCheckpointDetails GetSourceCheckpointDetails()
219222
protected override StorageResourceCheckpointDetails GetDestinationCheckpointDetails()
220223
=> new BlobDestinationCheckpointDetails(_options);
221224

222-
private string ApplyOptionalPrefix(string path)
225+
internal string ApplyOptionalPrefix(string path)
223226
=> IsDirectory
224227
? string.Join("/", DirectoryPrefix, path)
225228
: path;

sdk/storage/Azure.Storage.DataMovement.Blobs/tests/BlockBlobDirectoryToDirectoryTests.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ protected internal override BlockBlobClient GetSourceBlob(BlobContainerClient co
8787

8888
private async Task PopulateVirtualDirectoryContainer(
8989
BlobContainerClient containerClient,
90-
long objectLength)
90+
long objectLength,
91+
string prefix = default)
9192
{
9293
byte[] data = GetRandomBuffer(objectLength);
9394
Metadata folderMetadata = new Dictionary<string, string>()
@@ -110,16 +111,18 @@ private async Task PopulateVirtualDirectoryContainer(
110111
];
111112
foreach (string file in files)
112113
{
114+
string fileName = prefix != default ? $"{prefix}/{file}" : file;
113115
using MemoryStream stream = new(data);
114-
BlobClient blobClient = containerClient.GetBlobClient(file);
116+
BlobClient blobClient = containerClient.GetBlobClient(fileName);
115117
await blobClient.UploadAsync(stream);
116118
}
117119

118120
// List of empty directories to be created
119121
string[] emptyDirs = ["emptyDir", "recursiveDir/emptySubDir"];
120122
foreach (string dir in emptyDirs)
121123
{
122-
BlobClient blobClient = containerClient.GetBlobClient(dir);
124+
string dirName = prefix != default ? $"{prefix}/{dir}" : dir;
125+
BlobClient blobClient = containerClient.GetBlobClient(dirName);
123126
await blobClient.UploadAsync(Stream.Null, new BlobUploadOptions()
124127
{
125128
Metadata = folderMetadata
@@ -128,7 +131,9 @@ private async Task PopulateVirtualDirectoryContainer(
128131
}
129132

130133
[RecordedTest]
131-
public async Task DirectoryCopyWithVirtualDirectories([Values(true, false)] bool hns)
134+
public async Task DirectoryCopyWithVirtualDirectories(
135+
[Values(true, false)] bool hns,
136+
[Values(true, false)] bool usePrefix)
132137
{
133138
BlobServiceClient serviceClient = SourceClientBuilder.GetServiceClientFromOauthConfig(
134139
hns ? Tenants.TestConfigHierarchicalNamespace : Tenants.TestConfigDefault,
@@ -137,7 +142,9 @@ public async Task DirectoryCopyWithVirtualDirectories([Values(true, false)] bool
137142
await using DisposingBlobContainer sourceContainer = await SourceClientBuilder.GetTestContainerAsync(serviceClient);
138143
await using DisposingBlobContainer destinationContainer = await DestinationClientBuilder.GetTestContainerAsync(serviceClient);
139144

140-
await PopulateVirtualDirectoryContainer(sourceContainer.Container, 1024);
145+
string sourcePrefix = usePrefix ? "source" : default;
146+
string destinationPrefix = usePrefix ? "destination" : default;
147+
await PopulateVirtualDirectoryContainer(sourceContainer.Container, 1024, prefix: sourcePrefix);
141148

142149
TransferManager transferManager = new();
143150
BlobsStorageResourceProvider blobProvider = new(TestEnvironment.Credential);
@@ -146,8 +153,8 @@ public async Task DirectoryCopyWithVirtualDirectories([Values(true, false)] bool
146153
TestEventsRaised testEventsRaised = new(transferOptions);
147154

148155
TransferOperation transfer = await transferManager.StartTransferAsync(
149-
GetSourceStorageResourceContainer(sourceContainer.Container, default),
150-
GetSourceStorageResourceContainer(destinationContainer.Container, default),
156+
GetSourceStorageResourceContainer(sourceContainer.Container, sourcePrefix),
157+
GetSourceStorageResourceContainer(destinationContainer.Container, destinationPrefix),
151158
transferOptions);
152159

153160
CancellationTokenSource tokenSource = new(TimeSpan.FromSeconds(30));
@@ -158,8 +165,8 @@ await TestTransferWithTimeout.WaitForCompletionAsync(
158165

159166
testEventsRaised.AssertUnexpectedFailureCheck();
160167
await VerifyResultsAsync(
161-
sourceContainer.Container, string.Empty,
162-
destinationContainer.Container, string.Empty);
168+
sourceContainer.Container, sourcePrefix,
169+
destinationContainer.Container, destinationPrefix);
163170
}
164171
}
165172
}

sdk/storage/Azure.Storage.DataMovement.Blobs/tests/StartTransferBlobDirectoryCopyTestBase.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,17 +285,19 @@ protected override async Task VerifyResultsAsync(
285285

286286
// List all files in source blob folder path
287287
List<string> sourceFileNames = new List<string>();
288-
289-
// Get source directory client and list the paths
290-
await foreach (Page<BlobItem> page in sourceContainer.GetBlobsAsync(prefix: sourcePrefix, cancellationToken: cancellationToken).AsPages())
288+
await foreach (Page<BlobItem> page in sourceContainer.GetBlobsAsync(
289+
prefix: !string.IsNullOrEmpty(sourcePrefix) ? sourcePrefix + '/' : sourcePrefix,
290+
cancellationToken: cancellationToken).AsPages())
291291
{
292292
sourceFileNames.AddRange(page.Values.Select(
293293
(BlobItem item) => !string.IsNullOrEmpty(sourcePrefix) ? item.Name.Substring(sourcePrefix.Length + 1) : item.Name));
294294
}
295295

296296
// List all files in the destination blob folder path
297297
List<string> destinationFileNames = new List<string>();
298-
await foreach (Page<BlobItem> page in destinationContainer.GetBlobsAsync(prefix: destinationPrefix, cancellationToken: cancellationToken).AsPages())
298+
await foreach (Page<BlobItem> page in destinationContainer.GetBlobsAsync(
299+
prefix: !string.IsNullOrEmpty(destinationPrefix) ? destinationPrefix + '/' : destinationPrefix,
300+
cancellationToken: cancellationToken).AsPages())
299301
{
300302
destinationFileNames.AddRange(page.Values.Select(
301303
(BlobItem item) => !string.IsNullOrEmpty(destinationPrefix) ? item.Name.Substring(destinationPrefix.Length + 1) : item.Name));

0 commit comments

Comments
 (0)