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/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/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.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/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/assets.json b/sdk/storage/Azure.Storage.DataMovement.Files.Shares/assets.json
index da9cd7249259..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_90fc7c3256"
+ "AssetsRepo": "Azure/azure-sdk-assets",
+ "AssetsRepoPrefixPath": "net",
+ "TagPrefix": "net/storage/Azure.Storage.DataMovement.Files.Shares",
+ "Tag": "net/storage/Azure.Storage.DataMovement.Files.Shares_b7d8da3dcc"
}
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)
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();