diff --git a/sdk/storage/azdatalake/directory/client_test.go b/sdk/storage/azdatalake/directory/client_test.go index 782c0714a11f..43af8404ff06 100644 --- a/sdk/storage/azdatalake/directory/client_test.go +++ b/sdk/storage/azdatalake/directory/client_test.go @@ -2528,6 +2528,102 @@ func (s *RecordedTestSuite) TestDirRenameNoOptions() { _require.NoError(err) } +func (s *RecordedTestSuite) TestDirRenameWithSpecialCharacters() { + _require := require.New(s.T()) + testName := s.T().Name() + + filesystemName := testcommon.GenerateFileSystemName(testName) + fsClient, err := testcommon.GetFileSystemClient(filesystemName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + defer testcommon.DeleteFileSystem(context.Background(), _require, fsClient) + + _, err = fsClient.Create(context.Background(), nil) + _require.NoError(err) + + // Test with directory name containing spaces + dirName := "dir with spaces" + dirClient, err := testcommon.GetDirClient(filesystemName, dirName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + resp, err := dirClient.Create(context.Background(), nil) + _require.NoError(err) + _require.NotNil(resp) + + // Rename to a path with special characters + newName := "renamed dir+special@!&% chars" + _, err = dirClient.Rename(context.Background(), newName, nil) + _require.NoError(err) + + // Verify new directory exists by creating a client to it and checking properties + newClient, err := testcommon.GetDirClient(filesystemName, newName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + _, err = newClient.GetProperties(context.Background(), nil) + _require.NoError(err) + + // Test with Unicode characters + unicodeDirName := "lör mapp" + unicodeClient, err := testcommon.GetDirClient(filesystemName, unicodeDirName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + resp, err = unicodeClient.Create(context.Background(), nil) + _require.NoError(err) + _require.NotNil(resp) + + // Rename Unicode directory to another Unicode name + newUnicodeName := "ångström ümlaut 目录" + _, err = unicodeClient.Rename(context.Background(), newUnicodeName, nil) + _require.NoError(err) + + // Verify new directory exists + newUnicodeClient, err := testcommon.GetDirClient(filesystemName, newUnicodeName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + _, err = newUnicodeClient.GetProperties(context.Background(), nil) + _require.NoError(err) +} + +func (s *RecordedTestSuite) TestDirRenameWithQueryParameters() { + _require := require.New(s.T()) + testName := s.T().Name() + + filesystemName := testcommon.GenerateFileSystemName(testName) + fsClient, err := testcommon.GetFileSystemClient(filesystemName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + defer testcommon.DeleteFileSystem(context.Background(), _require, fsClient) + + _, err = fsClient.Create(context.Background(), nil) + _require.NoError(err) + + // Create a regular directory + dirName := "original-dir" + dirClient, err := testcommon.GetDirClient(filesystemName, dirName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + resp, err := dirClient.Create(context.Background(), nil) + _require.NoError(err) + _require.NotNil(resp) + + // Create a client with the path including query parameters + _, err = dirClient.Rename(context.Background(), "new-dir", nil) + _require.NoError(err) + + // Create a new directory to test the rename with query parameters + newDir := "query-test-dir" + queryClient, err := testcommon.GetDirClient(filesystemName, newDir, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + resp, err = queryClient.Create(context.Background(), nil) + _require.NoError(err) + _require.NotNil(resp) + + _require.NotNil(createOpts) + _require.NotNil(createOpts.RenameSource) + // Verify the source path was properly encoded + expected := "query-test-dir?param1=value1¶m2=value+with+spaces" + _require.Equal(expected, *createOpts.RenameSource) +} + func (s *RecordedTestSuite) TestDirRenameRequestWithCPK() { _require := require.New(s.T()) testName := s.T().Name() diff --git a/sdk/storage/azdatalake/file/client_test.go b/sdk/storage/azdatalake/file/client_test.go index 795302668217..86931da4ffd3 100644 --- a/sdk/storage/azdatalake/file/client_test.go +++ b/sdk/storage/azdatalake/file/client_test.go @@ -15,7 +15,6 @@ import ( "fmt" "github.com/Azure/azure-sdk-for-go/sdk/azcore/log" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/lease" - "github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake/internal/path" "hash/crc64" "io" "net/http" @@ -2404,6 +2403,107 @@ func (s *RecordedTestSuite) TestRenameNoOptions() { _require.NoError(err) } +func (s *RecordedTestSuite) TestRenameWithSpecialCharacters() { + _require := require.New(s.T()) + testName := s.T().Name() + + filesystemName := testcommon.GenerateFileSystemName(testName) + fsClient, err := testcommon.GetFileSystemClient(filesystemName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + defer testcommon.DeleteFileSystem(context.Background(), _require, fsClient) + + _, err = fsClient.Create(context.Background(), nil) + _require.NoError(err) + + // Test with file name containing spaces + fileName := "file with spaces.txt" + fClient, err := testcommon.GetFileClient(filesystemName, fileName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + resp, err := fClient.Create(context.Background(), nil) + _require.NoError(err) + _require.NotNil(resp) + + // Rename to a path with special characters + newName := "renamed file+special@!&% chars.txt" + _, err = fClient.Rename(context.Background(), newName, nil) + _require.NoError(err) + + // Verify new file exists by creating a client to it and checking properties + newClient, err := testcommon.GetFileClient(filesystemName, newName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + _, err = newClient.GetProperties(context.Background(), nil) + _require.NoError(err) + + // Test with Unicode characters + unicodeFileName := "lör 006.jpg" + unicodeClient, err := testcommon.GetFileClient(filesystemName, unicodeFileName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + resp, err = unicodeClient.Create(context.Background(), nil) + _require.NoError(err) + _require.NotNil(resp) + + // Rename Unicode file to another Unicode name + newUnicodeName := "ångström ümlaut 我的文件.jpg" + _, err = unicodeClient.Rename(context.Background(), newUnicodeName, nil) + _require.NoError(err) + + // Verify new file exists + newUnicodeClient, err := testcommon.GetFileClient(filesystemName, newUnicodeName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + _, err = newUnicodeClient.GetProperties(context.Background(), nil) + _require.NoError(err) +} + +func (s *RecordedTestSuite) TestRenameWithQueryParameters() { + _require := require.New(s.T()) + testName := s.T().Name() + + filesystemName := testcommon.GenerateFileSystemName(testName) + fsClient, err := testcommon.GetFileSystemClient(filesystemName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + defer testcommon.DeleteFileSystem(context.Background(), _require, fsClient) + + _, err = fsClient.Create(context.Background(), nil) + _require.NoError(err) + + // Create a regular file + fileName := "original-file.txt" + fClient, err := testcommon.GetFileClient(filesystemName, fileName, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + resp, err := fClient.Create(context.Background(), nil) + _require.NoError(err) + _require.NotNil(resp) + + // Create a source file with query parameters + srcClient, err := testcommon.GetFileClient(filesystemName, "source-file.txt", s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + resp, err = srcClient.Create(context.Background(), nil) + _require.NoError(err) + _require.NotNil(resp) + + // Create a destination path + destFile := "dest-file.txt" + + // Rename source file (with query params) to destination + // This is the operation that would fail without proper URL encoding + _, err = srcClient.Rename(context.Background(), destFile, nil) + _require.NoError(err) + + // Verify destination file exists + destClient, err := testcommon.GetFileClient(filesystemName, destFile, s.T(), testcommon.TestAccountDatalake, nil) + _require.NoError(err) + + destResp, err := destClient.GetProperties(context.Background(), nil) + _require.NoError(err) + _require.NotNil(destResp) +} + func (s *RecordedTestSuite) TestRenameFileWithCPK() { _require := require.New(s.T()) testName := s.T().Name() @@ -3932,8 +4032,8 @@ func (s *RecordedTestSuite) TestFileAppendWithFlushReleaseLease() { opts := &file.FlushDataOptions{ LeaseAction: &file.LeaseActionRelease, - AccessConditions: &path.AccessConditions{ - LeaseAccessConditions: &path.LeaseAccessConditions{LeaseID: proposedLeaseIDs[0]}, + AccessConditions: &file.AccessConditions{ + LeaseAccessConditions: &file.LeaseAccessConditions{LeaseID: proposedLeaseIDs[0]}, }, } diff --git a/sdk/storage/azdatalake/internal/path/models.go b/sdk/storage/azdatalake/internal/path/models.go index 96ab9f55def6..7af34f16f50c 100644 --- a/sdk/storage/azdatalake/internal/path/models.go +++ b/sdk/storage/azdatalake/internal/path/models.go @@ -8,6 +8,9 @@ package path import ( "errors" + "net/url" + "strings" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/blob" "github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake/datalakeerror" "github.com/Azure/azure-sdk-for-go/sdk/storage/azdatalake/internal/exported" @@ -47,9 +50,27 @@ type RenameOptions struct { func FormatRenameOptions(o *RenameOptions, path string) (*generated.LeaseAccessConditions, *generated.ModifiedAccessConditions, *generated.SourceModifiedAccessConditions, *generated.PathClientCreateOptions, *generated.CPKInfo) { // we don't need sourceModAccCond since this is not rename mode := generated.PathRenameModeLegacy + // URL encode the source path to handle special characters + pathPart, queryPart, found := strings.Cut(path, "?") + + // URL encode each path segment individually to preserve structure + segments := strings.Split(pathPart, "/") + for i, segment := range segments { + segments[i] = url.QueryEscape(segment) + } + encodedPath := strings.Join(segments, "/") + + // If there was a query part, encode and append it + if found && queryPart != "" { + encodedQueryURL, err := runtime.EncodeQueryParams("?" + queryPart) + if err == nil { + encodedPath += encodedQueryURL + } + } + createOpts := &generated.PathClientCreateOptions{ Mode: &mode, - RenameSource: &path, + RenameSource: &encodedPath, } if o == nil { return nil, nil, nil, createOpts, nil diff --git a/sdk/storage/azdatalake/internal/path/models_test.go b/sdk/storage/azdatalake/internal/path/models_test.go new file mode 100644 index 000000000000..3f11587eda50 --- /dev/null +++ b/sdk/storage/azdatalake/internal/path/models_test.go @@ -0,0 +1,61 @@ +//go:build go1.18 +// +build go1.18 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +package path + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFormatRenameOptions(t *testing.T) { + tests := []struct { + name string + path string + expectedSource string + }{ + { + name: "Simple path", + path: "dir1/file1.txt", + expectedSource: "dir1/file1.txt", + }, + { + name: "Path with spaces", + path: "dir1/file with spaces.txt", + expectedSource: "dir1/file+with+spaces.txt", + }, + { + name: "Path with unicode characters", + path: "dir1/lör 006.jpg", + expectedSource: "dir1/l%C3%B6r+006.jpg", + }, + { + name: "Path with special characters", + path: "dir1/file+name@!&%.txt", + expectedSource: "dir1/file%2Bname%40%21%26%25.txt", + }, + { + name: "Path with query parameters", + path: "dir1/file1.txt?param1=value1¶m2=value2", + expectedSource: "dir1/file1.txt?param1=value1¶m2=value2", + }, + { + name: "Path with special characters and query parameters", + path: "dir1/file name+special@!&%.txt?param=value with spaces", + expectedSource: "dir1/file+name%2Bspecial%40%21%26%25.txt?param=value+with+spaces", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, _, _, createOpts, _ := FormatRenameOptions(nil, test.path) + assert.NotNil(t, createOpts) + assert.NotNil(t, createOpts.RenameSource) + assert.Equal(t, test.expectedSource, *createOpts.RenameSource) + }) + } +} \ No newline at end of file