From 23f4ddf76d1e91e2820320128959c5c3729f7274 Mon Sep 17 00:00:00 2001 From: Jacob Lauzon Date: Wed, 10 Sep 2025 11:34:15 -0700 Subject: [PATCH 1/3] Setup branch for hotfix --- sdk/storage/Azure.Storage.DataMovement.Blobs/CHANGELOG.md | 5 +++++ .../src/Azure.Storage.DataMovement.Blobs.csproj | 2 +- .../Azure.Storage.DataMovement.Files.Shares/CHANGELOG.md | 5 +++++ .../src/Azure.Storage.DataMovement.Files.Shares.csproj | 2 +- sdk/storage/Azure.Storage.DataMovement/CHANGELOG.md | 5 +++++ .../src/Azure.Storage.DataMovement.csproj | 2 +- 6 files changed, 18 insertions(+), 3 deletions(-) diff --git a/sdk/storage/Azure.Storage.DataMovement.Blobs/CHANGELOG.md b/sdk/storage/Azure.Storage.DataMovement.Blobs/CHANGELOG.md index 1608c9970214..0e548d4b40f4 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Blobs/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.DataMovement.Blobs/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 12.2.2 (2025-09-10) + +### Bugs Fixed +- Fixed an issue on upload transfers where file/directory names on the destination may be incorrect. The issue could occur if the path passed to `LocalFilesStorageResourceProvider.FromDirectory` contained a trailing slash. + ## 12.2.1 (2025-08-06) ### Bugs Fixed diff --git a/sdk/storage/Azure.Storage.DataMovement.Blobs/src/Azure.Storage.DataMovement.Blobs.csproj b/sdk/storage/Azure.Storage.DataMovement.Blobs/src/Azure.Storage.DataMovement.Blobs.csproj index eb78b9de052c..7c2e2a6c85ff 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Blobs/src/Azure.Storage.DataMovement.Blobs.csproj +++ b/sdk/storage/Azure.Storage.DataMovement.Blobs/src/Azure.Storage.DataMovement.Blobs.csproj @@ -5,7 +5,7 @@ Microsoft Azure.Storage.DataMovement.Blobs client library - 12.2.1 + 12.2.2 12.2.0 BlobDataMovementSDK;$(DefineConstants) diff --git a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/CHANGELOG.md b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/CHANGELOG.md index e9b42edceb84..add3f7054c93 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 12.2.2 (2025-09-10) + +### Bugs Fixed +- Fixed an issue on upload transfers where file/directory names on the destination may be incorrect. The issue could occur if the path passed to `LocalFilesStorageResourceProvider.FromDirectory` contained a trailing slash. + ## 12.2.1 (2025-08-06) ### Bugs Fixed diff --git a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/src/Azure.Storage.DataMovement.Files.Shares.csproj b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/src/Azure.Storage.DataMovement.Files.Shares.csproj index e72b93277c0a..fe9f5a3cd808 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/src/Azure.Storage.DataMovement.Files.Shares.csproj +++ b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/src/Azure.Storage.DataMovement.Files.Shares.csproj @@ -6,7 +6,7 @@ Microsoft Azure.Storage.DataMovement.Files.Shares client library - 12.2.1 + 12.2.2 12.2.0 ShareDataMovementSDK;$(DefineConstants) diff --git a/sdk/storage/Azure.Storage.DataMovement/CHANGELOG.md b/sdk/storage/Azure.Storage.DataMovement/CHANGELOG.md index 125a8e39be8d..ee125b711693 100644 --- a/sdk/storage/Azure.Storage.DataMovement/CHANGELOG.md +++ b/sdk/storage/Azure.Storage.DataMovement/CHANGELOG.md @@ -1,5 +1,10 @@ # Release History +## 12.2.2 (2025-09-10) + +### Bugs Fixed +- Fixed an issue on upload transfers where file/directory names on the destination may be incorrect. The issue could occur if the path passed to `LocalFilesStorageResourceProvider.FromDirectory` contained a trailing slash. + ## 12.2.1 (2025-08-06) ### Bugs Fixed diff --git a/sdk/storage/Azure.Storage.DataMovement/src/Azure.Storage.DataMovement.csproj b/sdk/storage/Azure.Storage.DataMovement/src/Azure.Storage.DataMovement.csproj index 7e3668170a85..325ec1f3bcd2 100644 --- a/sdk/storage/Azure.Storage.DataMovement/src/Azure.Storage.DataMovement.csproj +++ b/sdk/storage/Azure.Storage.DataMovement/src/Azure.Storage.DataMovement.csproj @@ -5,7 +5,7 @@ Microsoft Azure.Storage.DataMovement client library - 12.2.1 + 12.2.2 12.2.0 DataMovementSDK;$(DefineConstants) From 3eb0c4e18ab3f361c2b65e803f774474f48de59b Mon Sep 17 00:00:00 2001 From: Jacob Lauzon <96087589+jalauzon-msft@users.noreply.github.com> Date: Tue, 9 Sep 2025 17:15:16 -0700 Subject: [PATCH 2/3] [Storage][DataMovement] Fixed potential incorrect resource names for upload transfers (#52491) --- .../assets.json | 2 +- .../BlobContainerClientExtensionsTests.cs | 4 +-- .../assets.json | 2 +- .../LocalDirectoryStorageResourceContainer.cs | 1 + .../src/TransferJobInternal.cs | 24 +++++++++-------- .../LocalDirectoryStorageResourceTests.cs | 24 +++++++++-------- .../StartTransferDirectoryDownloadTestBase.cs | 27 +++++++++++++++---- .../StartTransferUploadDirectoryTestBase.cs | 23 ++++++++++++++++ .../tests/Shared/TransferValidator.Local.cs | 1 + 9 files changed, 77 insertions(+), 31 deletions(-) diff --git a/sdk/storage/Azure.Storage.DataMovement.Blobs/assets.json b/sdk/storage/Azure.Storage.DataMovement.Blobs/assets.json index 2bee441be7b6..fa5d9300e453 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Blobs/assets.json +++ b/sdk/storage/Azure.Storage.DataMovement.Blobs/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/storage/Azure.Storage.DataMovement.Blobs", - "Tag": "net/storage/Azure.Storage.DataMovement.Blobs_f1a4120258" + "Tag": "net/storage/Azure.Storage.DataMovement.Blobs_c2f1f2fc3f" } diff --git a/sdk/storage/Azure.Storage.DataMovement.Blobs/tests/BlobContainerClientExtensionsTests.cs b/sdk/storage/Azure.Storage.DataMovement.Blobs/tests/BlobContainerClientExtensionsTests.cs index f68308b24ed1..922eca28cb7a 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Blobs/tests/BlobContainerClientExtensionsTests.cs +++ b/sdk/storage/Azure.Storage.DataMovement.Blobs/tests/BlobContainerClientExtensionsTests.cs @@ -46,7 +46,7 @@ public async Task VerifyStartUploadDirectoryAsync([Values] bool addBlobDirectory var blobUri = new Uri(accountUrl + (addBlobDirectoryPath ? containerName + "/" + blobDirectoryPrefix : containerName)); - var directoryPath = Path.GetTempPath(); + var directoryPath = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar); var options = addTransferOptions ? new TransferOptions() : (TransferOptions)null; @@ -91,7 +91,7 @@ public async Task VerifyStartDownloadToDirectoryAsync([Values] bool addBlobDirec var blobUri = new Uri(accountUrl + (addBlobDirectoryPath ? containerName + "/" + blobDirectoryPrefix : containerName)); - var directoryPath = Path.GetTempPath(); + var directoryPath = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar); var options = addTransferOptions ? new TransferOptions() : (TransferOptions)null; diff --git a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/assets.json b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/assets.json index da9cd7249259..66d91f836dda 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/assets.json +++ b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "net", "TagPrefix": "net/storage/Azure.Storage.DataMovement.Files.Shares", - "Tag": "net/storage/Azure.Storage.DataMovement.Files.Shares_90fc7c3256" + "Tag": "net/storage/Azure.Storage.DataMovement.Files.Shares_c847746677" } diff --git a/sdk/storage/Azure.Storage.DataMovement/src/LocalDirectoryStorageResourceContainer.cs b/sdk/storage/Azure.Storage.DataMovement/src/LocalDirectoryStorageResourceContainer.cs index 0d8cb6d3311e..fe9b5e3181a4 100644 --- a/sdk/storage/Azure.Storage.DataMovement/src/LocalDirectoryStorageResourceContainer.cs +++ b/sdk/storage/Azure.Storage.DataMovement/src/LocalDirectoryStorageResourceContainer.cs @@ -29,6 +29,7 @@ internal class LocalDirectoryStorageResourceContainer : StorageResourceContainer public LocalDirectoryStorageResourceContainer(string path) { Argument.AssertNotNullOrWhiteSpace(path, nameof(path)); + path = path.TrimEnd(Path.DirectorySeparatorChar); _uri = PathScanner.GetEncodedUriFromPath(path); } diff --git a/sdk/storage/Azure.Storage.DataMovement/src/TransferJobInternal.cs b/sdk/storage/Azure.Storage.DataMovement/src/TransferJobInternal.cs index f1033c94aee2..ebe5c248216e 100644 --- a/sdk/storage/Azure.Storage.DataMovement/src/TransferJobInternal.cs +++ b/sdk/storage/Azure.Storage.DataMovement/src/TransferJobInternal.cs @@ -329,13 +329,7 @@ private async IAsyncEnumerable EnumerateAndCreateJobPartsAsync( if (current.IsContainer) { - // Create sub-container - string containerUriPath = _sourceResourceContainer.Uri.GetPath(); - string subContainerPath = string.IsNullOrEmpty(containerUriPath) - ? current.Uri.GetPath() - : current.Uri.GetPath().Substring(containerUriPath.Length + 1); - // Decode the container name as it was pulled from encoded Uri and will be re-encoded on destination. - subContainerPath = Uri.UnescapeDataString(subContainerPath); + string subContainerPath = GetChildResourcePath(_sourceResourceContainer, current); StorageResourceContainer subContainer = _destinationResourceContainer.GetChildStorageResourceContainer(subContainerPath); @@ -369,10 +363,7 @@ private async IAsyncEnumerable EnumerateAndCreateJobPartsAsync( // Real container trasnfer else { - string containerUriPath = _sourceResourceContainer.Uri.GetPath(); - sourceName = current.Uri.GetPath().Substring(containerUriPath.Length + 1); - // Decode the resource name as it was pulled from encoded Uri and will be re-encoded on destination. - sourceName = Uri.UnescapeDataString(sourceName); + sourceName = GetChildResourcePath(_sourceResourceContainer, current); } StorageResourceItem sourceItem = (StorageResourceItem)current; @@ -653,5 +644,16 @@ internal async ValueTask IncrementJobParts() { await _progressTracker.IncrementQueuedFilesAsync(_cancellationToken).ConfigureAwait(false); } + + private static string GetChildResourcePath(StorageResourceContainer parent, StorageResource child) + { + string parentPath = parent.Uri.GetPath(); + string childPath = child.Uri.GetPath().Substring(parentPath.Length); + // If container path does not contain a '/' (normal case), then childPath will have one after substring. + // Safe to use / here as we are using AbsolutePath which normalizes to /. + childPath = childPath.TrimStart('/'); + // Decode the resource name as it was pulled from encoded Uri and will be re-encoded on destination. + return Uri.UnescapeDataString(childPath); + } } } diff --git a/sdk/storage/Azure.Storage.DataMovement/tests/LocalDirectoryStorageResourceTests.cs b/sdk/storage/Azure.Storage.DataMovement/tests/LocalDirectoryStorageResourceTests.cs index 8b5e2b6c6cc6..49b80da0405b 100644 --- a/sdk/storage/Azure.Storage.DataMovement/tests/LocalDirectoryStorageResourceTests.cs +++ b/sdk/storage/Azure.Storage.DataMovement/tests/LocalDirectoryStorageResourceTests.cs @@ -17,16 +17,14 @@ public LocalDirectoryStorageResourceTests(bool async) : base(async, null /* TestMode.Record /* to re-record */) { } - private string[] fileNames => new[] - { - "C:\\Users\\user1\\Documents\\directory", - "C:\\Users\\user1\\Documents\\directory1\\", - "/user1/Documents/directory", - }; - [Test] public void Ctor_string() { + string[] fileNames = + { + "C:\\Users\\user1\\Documents\\directory", + "/user1/Documents/directory", + }; foreach (string path in fileNames) { // Arrange @@ -43,12 +41,14 @@ public void Ctor_string() [TestCase("C:\\test\\path=true@&#%", "C:/test/path%3Dtrue%40%26%23%25")] [TestCase("C:\\test\\path%3Dtest%26", "C:/test/path%253Dtest%2526")] [TestCase("C:\\test\\folder with spaces", "C:/test/folder%20with%20spaces")] + [TestCase("X:\\testing\\test\\", "X:/testing/test")] + [TestCase("X:\\testing\\test\\\\", "X:/testing/test")] public void Ctor_String_Encoding_Windows(string path, string absolutePath) { LocalDirectoryStorageResourceContainer storageResource = new(path); Assert.That(storageResource.Uri.AbsolutePath, Is.EqualTo(absolutePath)); - // LocalPath should equal original path - Assert.That(storageResource.Uri.LocalPath, Is.EqualTo(path)); + // LocalPath should equal original path (trimmed) + Assert.That(storageResource.Uri.LocalPath, Is.EqualTo(path.TrimEnd('\\'))); } [Test] @@ -56,12 +56,14 @@ public void Ctor_String_Encoding_Windows(string path, string absolutePath) [TestCase("/test/path=true@&#%", "/test/path%3Dtrue%40%26%23%25")] [TestCase("/test/path%3Dtest%26", "/test/path%253Dtest%2526")] [TestCase("/test/folder with spaces", "/test/folder%20with%20spaces")] + [TestCase("/testing/test/", "/testing/test")] + [TestCase("/testing/test//", "/testing/test")] public void Ctor_String_Encoding_Unix(string path, string absolutePath) { LocalDirectoryStorageResourceContainer storageResource = new(path); Assert.That(storageResource.Uri.AbsolutePath, Is.EqualTo(absolutePath)); - // LocalPath should equal original path - Assert.That(storageResource.Uri.LocalPath, Is.EqualTo(path)); + // LocalPath should equal original path (trimmed) + Assert.That(storageResource.Uri.LocalPath, Is.EqualTo(path.TrimEnd('/'))); } [Test] diff --git a/sdk/storage/Azure.Storage.DataMovement/tests/Shared/StartTransferDirectoryDownloadTestBase.cs b/sdk/storage/Azure.Storage.DataMovement/tests/Shared/StartTransferDirectoryDownloadTestBase.cs index 92b7926a8d10..ecab1a16ca2a 100644 --- a/sdk/storage/Azure.Storage.DataMovement/tests/Shared/StartTransferDirectoryDownloadTestBase.cs +++ b/sdk/storage/Azure.Storage.DataMovement/tests/Shared/StartTransferDirectoryDownloadTestBase.cs @@ -10,6 +10,7 @@ using Azure.Core; using Azure.Core.TestFramework; using Azure.Storage.Common; +using Azure.Storage.Test; using Azure.Storage.Test.Shared; using NUnit.Framework; @@ -141,7 +142,8 @@ private async Task DownloadDirectoryAndVerifyAsync( string directoryName = default, TransferManagerOptions transferManagerOptions = default, TransferOptions options = default, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, + bool trailingSlash = false) { await SetupSourceDirectoryAsync(sourceContainer, sourcePrefix, itemSizes, cancellationToken); @@ -157,7 +159,8 @@ private async Task DownloadDirectoryAndVerifyAsync( }; StorageResourceContainer sourceResource = GetStorageResourceContainer(sourceContainer, sourcePrefix); - StorageResourceContainer destinationResource = LocalFilesStorageResourceProvider.FromDirectory(disposingLocalDirectory.DirectoryPath); + StorageResourceContainer destinationResource = LocalFilesStorageResourceProvider.FromDirectory( + disposingLocalDirectory.DirectoryPath + (trailingSlash ? Path.DirectorySeparatorChar : string.Empty)); await new TransferValidator().TransferAndVerifyAsync( sourceResource, @@ -407,14 +410,28 @@ public async Task DownloadDirectoryAsync_SpecialChars(string prefix) string.Join("/", prefix, "space folder", "space file"), ]; - CancellationTokenSource cts = new(); - cts.CancelAfter(TimeSpan.FromSeconds(30)); + CancellationToken cancellationToken = TestHelper.GetTimeoutToken(30); await DownloadDirectoryAndVerifyAsync( test.Container, prefix, itemNames.Select(name => (name, Constants.KB)).ToList(), directoryName: directoryName, - cancellationToken: cts.Token).ConfigureAwait(false); + cancellationToken: cancellationToken); + } + + [Test] + public async Task DownloadDirectoryAsync_TrailingSlash() + { + await using IDisposingContainer test = await GetDisposingContainerAsync(); + + string[] items = { "file1", "file2", "dir1/file1" }; + + CancellationToken cancellationToken = TestHelper.GetTimeoutToken(30); + await DownloadDirectoryAndVerifyAsync( + test.Container, + string.Empty, + items.Select(name => (name, Constants.KB)).ToList(), + cancellationToken: cancellationToken); } #endregion DirectoryDownloadTests diff --git a/sdk/storage/Azure.Storage.DataMovement/tests/Shared/StartTransferUploadDirectoryTestBase.cs b/sdk/storage/Azure.Storage.DataMovement/tests/Shared/StartTransferUploadDirectoryTestBase.cs index 3de233227cc8..7f6c89b41fba 100644 --- a/sdk/storage/Azure.Storage.DataMovement/tests/Shared/StartTransferUploadDirectoryTestBase.cs +++ b/sdk/storage/Azure.Storage.DataMovement/tests/Shared/StartTransferUploadDirectoryTestBase.cs @@ -523,5 +523,28 @@ await UploadDirectoryAndVerifyAsync( expectedTransfers: files.Count, cancellationToken: cancellationToken); } + + [RecordedTest] + public async Task Upload_TrailingSlash() + { + using DisposingLocalDirectory disposingLocalDirectory = DisposingLocalDirectory.GetTestDirectory(); + await using IDisposingContainer test = await GetDisposingContainerAsync(); + + List files = [ "file1", "file2", "dir1/file1" ]; + + CancellationToken cancellationToken = TestHelper.GetTimeoutToken(30); + await SetupDirectoryAsync( + disposingLocalDirectory.DirectoryPath, + files.Select(path => (path, (long)Constants.KB)).ToList(), + cancellationToken); + + // Intentionally append trailing slash + string sourcePath = disposingLocalDirectory.DirectoryPath + Path.DirectorySeparatorChar; + await UploadDirectoryAndVerifyAsync( + sourcePath, + test.Container, + expectedTransfers: files.Count, + cancellationToken: cancellationToken); + } } } diff --git a/sdk/storage/Azure.Storage.DataMovement/tests/Shared/TransferValidator.Local.cs b/sdk/storage/Azure.Storage.DataMovement/tests/Shared/TransferValidator.Local.cs index 457b7f65431d..c1898f2ca279 100644 --- a/sdk/storage/Azure.Storage.DataMovement/tests/Shared/TransferValidator.Local.cs +++ b/sdk/storage/Azure.Storage.DataMovement/tests/Shared/TransferValidator.Local.cs @@ -30,6 +30,7 @@ public Task OpenReadAsync(CancellationToken cancellationToken) public static ListFilesAsync GetLocalFileLister(string directoryPath) { + directoryPath = directoryPath.TrimEnd(Path.DirectorySeparatorChar); Task> ListFiles(CancellationToken cancellationToken) { List result = new(); From 1276bd57599ac0506b9c78d07995bef9f2034095 Mon Sep 17 00:00:00 2001 From: Jacob Lauzon Date: Wed, 10 Sep 2025 12:32:22 -0700 Subject: [PATCH 3/3] Recordings --- .../Azure.Storage.DataMovement.Files.Shares/assets.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/assets.json b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/assets.json index 66d91f836dda..93d03fe9856f 100644 --- a/sdk/storage/Azure.Storage.DataMovement.Files.Shares/assets.json +++ b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/assets.json @@ -1,6 +1,6 @@ { - "AssetsRepo": "Azure/azure-sdk-assets", - "AssetsRepoPrefixPath": "net", - "TagPrefix": "net/storage/Azure.Storage.DataMovement.Files.Shares", - "Tag": "net/storage/Azure.Storage.DataMovement.Files.Shares_c847746677" + "AssetsRepo": "Azure/azure-sdk-assets", + "AssetsRepoPrefixPath": "net", + "TagPrefix": "net/storage/Azure.Storage.DataMovement.Files.Shares", + "Tag": "net/storage/Azure.Storage.DataMovement.Files.Shares_b7d8da3dcc" }