Skip to content

Commit 70a613f

Browse files
[AzDatalake] Fix Path Renames with SAS (Azure#21617)
* Fixed dir and file renames with sas * added changelog entry * handled errors
1 parent 526c154 commit 70a613f

File tree

5 files changed

+168
-32
lines changed

5 files changed

+168
-32
lines changed

sdk/storage/azdatalake/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Bugs Fixed
1010
* Fixed an issue where customers could not capture the raw HTTP response of directory and file GetProperties operations.
11+
* Fixed an issue where file/directory renames with source/destination SAS tokens fail with authorization failures.
1112

1213
### Other Changes
1314

sdk/storage/azdatalake/directory/client.go

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package directory
88

99
import (
1010
"context"
11+
"errors"
1112
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
1213
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
1314
"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
@@ -254,24 +255,48 @@ func (d *Client) GetProperties(ctx context.Context, options *GetPropertiesOption
254255
return newResp, err
255256
}
256257

257-
func (d *Client) renamePathInURL(newName string) (string, string, string) {
258-
endpoint := d.DFSURL()
259-
separator := "/"
260-
// Find the index of the last occurrence of the separator
261-
lastIndex := strings.LastIndex(endpoint, separator)
262-
// Split the string based on the last occurrence of the separator
263-
firstPart := endpoint[:lastIndex] // From the beginning of the string to the last occurrence of the separator
264-
newBlobURL, newPathURL := shared.GetURLs(runtime.JoinPaths(firstPart, newName))
265-
oldURL, _ := url.Parse(d.DFSURL())
266-
return oldURL.Path, newPathURL, newBlobURL
267-
}
268-
269258
// Rename renames a directory. The original directory will no longer exist and the client will be stale.
270-
func (d *Client) Rename(ctx context.Context, newName string, options *RenameOptions) (RenameResponse, error) {
271-
oldURL, newPathURL, newBlobURL := d.renamePathInURL(newName)
272-
lac, mac, smac, createOpts := path.FormatRenameOptions(options, oldURL)
259+
func (d *Client) Rename(ctx context.Context, destinationPath string, options *RenameOptions) (RenameResponse, error) {
273260
var newBlobClient *blockblob.Client
274-
var err error
261+
destinationPath = strings.Trim(strings.TrimSpace(destinationPath), "/")
262+
if len(destinationPath) == 0 {
263+
return RenameResponse{}, errors.New("destination path must not be empty")
264+
}
265+
urlParts, err := sas.ParseURL(d.DFSURL())
266+
if err != nil {
267+
return RenameResponse{}, err
268+
}
269+
270+
oldPath, err := url.Parse(d.DFSURL())
271+
if err != nil {
272+
return RenameResponse{}, err
273+
}
274+
srcParts := strings.Split(d.DFSURL(), "?")
275+
newSrcPath := oldPath.Path
276+
newSrcQuery := ""
277+
if len(srcParts) == 2 {
278+
newSrcQuery = srcParts[1]
279+
}
280+
if len(newSrcQuery) > 0 {
281+
newSrcPath = newSrcPath + "?" + newSrcQuery
282+
}
283+
284+
destParts := strings.Split(destinationPath, "?")
285+
newDestPath := destParts[0]
286+
newDestQuery := ""
287+
if len(destParts) == 2 {
288+
newDestQuery = destParts[1]
289+
}
290+
291+
urlParts.PathName = newDestPath
292+
newPathURL := urlParts.String()
293+
// replace the query part if it is present in destination path
294+
if len(newDestQuery) > 0 {
295+
newPathURL = strings.Split(newPathURL, "?")[0] + "?" + newDestQuery
296+
}
297+
newBlobURL, _ := shared.GetURLs(newPathURL)
298+
lac, mac, smac, createOpts := path.FormatRenameOptions(options, newSrcPath)
299+
275300
if d.identityCredential() != nil {
276301
newBlobClient, err = blockblob.NewClient(newBlobURL, *d.identityCredential(), nil)
277302
} else if d.sharedKey() != nil {
@@ -280,6 +305,7 @@ func (d *Client) Rename(ctx context.Context, newName string, options *RenameOpti
280305
} else {
281306
newBlobClient, err = blockblob.NewClientWithNoCredential(newBlobURL, nil)
282307
}
308+
283309
if err != nil {
284310
return RenameResponse{}, exported.ConvertToDFSError(err)
285311
}

sdk/storage/azdatalake/directory/client_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2166,6 +2166,47 @@ func (s *RecordedTestSuite) TestDirSetHTTPHeadersIfETagMatchFalse() {
21662166
testcommon.ValidateErrorCode(_require, err, datalakeerror.ConditionNotMet)
21672167
}
21682168

2169+
func (s *UnrecordedTestSuite) TestDirectoryRenameUsingSAS() {
2170+
_require := require.New(s.T())
2171+
testName := s.T().Name()
2172+
2173+
cred, err := testcommon.GetGenericSharedKeyCredential(testcommon.TestAccountDatalake)
2174+
_require.NoError(err)
2175+
2176+
filesystemName := testcommon.GenerateFileSystemName(testName)
2177+
fsClient, err := testcommon.GetFileSystemClient(filesystemName, s.T(), testcommon.TestAccountDatalake, nil)
2178+
_require.NoError(err)
2179+
defer testcommon.DeleteFileSystem(context.Background(), _require, fsClient)
2180+
2181+
_, err = fsClient.Create(context.Background(), nil)
2182+
_require.NoError(err)
2183+
2184+
perms := sas.DirectoryPermissions{Read: true, Create: true, Write: true, Move: true, Delete: true, List: true}
2185+
sasQueryParams, err := sas.DatalakeSignatureValues{
2186+
Protocol: sas.ProtocolHTTPS, // Users MUST use HTTPS (not HTTP)
2187+
ExpiryTime: time.Now().UTC().Add(48 * time.Hour), // 48-hours before expiration
2188+
FileSystemName: filesystemName,
2189+
Permissions: perms.String(),
2190+
}.SignWithSharedKey(cred)
2191+
_require.NoError(err)
2192+
2193+
sasToken := sasQueryParams.Encode()
2194+
2195+
srcDirClient, err := directory.NewClientWithNoCredential(fsClient.DFSURL()+"/dir1?"+sasToken, nil)
2196+
_require.NoError(err)
2197+
2198+
_, err = srcDirClient.Create(context.Background(), nil)
2199+
_require.NoError(err)
2200+
2201+
destPathWithSAS := "dir2?" + sasToken
2202+
_, err = srcDirClient.Rename(context.Background(), destPathWithSAS, nil)
2203+
_require.NoError(err)
2204+
2205+
_, err = srcDirClient.GetProperties(context.Background(), nil)
2206+
_require.Error(err)
2207+
testcommon.ValidateErrorCode(_require, err, datalakeerror.PathNotFound)
2208+
}
2209+
21692210
func (s *RecordedTestSuite) TestDirRenameNoOptions() {
21702211
_require := require.New(s.T())
21712212
testName := s.T().Name()

sdk/storage/azdatalake/file/client.go

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -238,24 +238,48 @@ func (f *Client) GetProperties(ctx context.Context, options *GetPropertiesOption
238238
return newResp, err
239239
}
240240

241-
func (f *Client) renamePathInURL(newName string) (string, string, string) {
242-
endpoint := f.DFSURL()
243-
separator := "/"
244-
// Find the index of the last occurrence of the separator
245-
lastIndex := strings.LastIndex(endpoint, separator)
246-
// Split the string based on the last occurrence of the separator
247-
firstPart := endpoint[:lastIndex] // From the beginning of the string to the last occurrence of the separator
248-
newBlobURL, newPathURL := shared.GetURLs(runtime.JoinPaths(firstPart, newName))
249-
oldURL, _ := url.Parse(f.DFSURL())
250-
return oldURL.Path, newPathURL, newBlobURL
251-
}
252-
253241
// Rename renames a file. The original file will no longer exist and the client will be stale.
254-
func (f *Client) Rename(ctx context.Context, newName string, options *RenameOptions) (RenameResponse, error) {
255-
oldPathWithoutURL, newPathURL, newBlobURL := f.renamePathInURL(newName)
256-
lac, mac, smac, createOpts := path.FormatRenameOptions(options, oldPathWithoutURL)
242+
func (f *Client) Rename(ctx context.Context, destinationPath string, options *RenameOptions) (RenameResponse, error) {
257243
var newBlobClient *blockblob.Client
258-
var err error
244+
destinationPath = strings.Trim(strings.TrimSpace(destinationPath), "/")
245+
if len(destinationPath) == 0 {
246+
return RenameResponse{}, errors.New("destination path must not be empty")
247+
}
248+
urlParts, err := sas.ParseURL(f.DFSURL())
249+
if err != nil {
250+
return RenameResponse{}, err
251+
}
252+
253+
oldPath, err := url.Parse(f.DFSURL())
254+
if err != nil {
255+
return RenameResponse{}, err
256+
}
257+
srcParts := strings.Split(f.DFSURL(), "?")
258+
newSrcPath := oldPath.Path
259+
newSrcQuery := ""
260+
if len(srcParts) == 2 {
261+
newSrcQuery = srcParts[1]
262+
}
263+
if len(newSrcQuery) > 0 {
264+
newSrcPath = newSrcPath + "?" + newSrcQuery
265+
}
266+
267+
destParts := strings.Split(destinationPath, "?")
268+
newDestPath := destParts[0]
269+
newDestQuery := ""
270+
if len(destParts) == 2 {
271+
newDestQuery = destParts[1]
272+
}
273+
274+
urlParts.PathName = newDestPath
275+
newPathURL := urlParts.String()
276+
// replace the query part if it is present in destination path
277+
if len(newDestQuery) > 0 {
278+
newPathURL = strings.Split(newPathURL, "?")[0] + "?" + newDestQuery
279+
}
280+
newBlobURL, _ := shared.GetURLs(newPathURL)
281+
lac, mac, smac, createOpts := path.FormatRenameOptions(options, newSrcPath)
282+
259283
if f.identityCredential() != nil {
260284
newBlobClient, err = blockblob.NewClient(newBlobURL, *f.identityCredential(), nil)
261285
} else if f.sharedKey() != nil {
@@ -264,15 +288,18 @@ func (f *Client) Rename(ctx context.Context, newName string, options *RenameOpti
264288
} else {
265289
newBlobClient, err = blockblob.NewClientWithNoCredential(newBlobURL, nil)
266290
}
291+
267292
if err != nil {
268293
return RenameResponse{}, exported.ConvertToDFSError(err)
269294
}
270295
newFileClient := (*Client)(base.NewPathClient(newPathURL, newBlobURL, newBlobClient, f.generatedFileClientWithDFS().InternalClient().WithClientName(shared.FileClient), f.sharedKey(), f.identityCredential(), f.getClientOptions()))
271296
resp, err := newFileClient.generatedFileClientWithDFS().Create(ctx, createOpts, nil, lac, mac, smac, nil)
297+
272298
//return RenameResponse{
273299
// Response: resp,
274300
// NewFileClient: newFileClient,
275301
//}, exported.ConvertToDFSError(err)
302+
276303
return path.FormatRenameResponse(&resp), exported.ConvertToDFSError(err)
277304
}
278305

sdk/storage/azdatalake/file/client_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1973,6 +1973,47 @@ func (s *RecordedTestSuite) TestFileSetHTTPHeadersIfETagMatchFalse() {
19731973
testcommon.ValidateErrorCode(_require, err, datalakeerror.ConditionNotMet)
19741974
}
19751975

1976+
func (s *UnrecordedTestSuite) TestFileRenameUsingSAS() {
1977+
_require := require.New(s.T())
1978+
testName := s.T().Name()
1979+
1980+
cred, err := testcommon.GetGenericSharedKeyCredential(testcommon.TestAccountDatalake)
1981+
_require.NoError(err)
1982+
1983+
filesystemName := testcommon.GenerateFileSystemName(testName)
1984+
fsClient, err := testcommon.GetFileSystemClient(filesystemName, s.T(), testcommon.TestAccountDatalake, nil)
1985+
_require.NoError(err)
1986+
defer testcommon.DeleteFileSystem(context.Background(), _require, fsClient)
1987+
1988+
_, err = fsClient.Create(context.Background(), nil)
1989+
_require.NoError(err)
1990+
1991+
perms := sas.FilePermissions{Read: true, Create: true, Write: true, Move: true, Delete: true, List: true}
1992+
sasQueryParams, err := sas.DatalakeSignatureValues{
1993+
Protocol: sas.ProtocolHTTPS, // Users MUST use HTTPS (not HTTP)
1994+
ExpiryTime: time.Now().UTC().Add(48 * time.Hour), // 48-hours before expiration
1995+
FileSystemName: filesystemName,
1996+
Permissions: perms.String(),
1997+
}.SignWithSharedKey(cred)
1998+
_require.NoError(err)
1999+
2000+
sasToken := sasQueryParams.Encode()
2001+
2002+
srcFileClient, err := file.NewClientWithNoCredential(fsClient.DFSURL()+"/file1?"+sasToken, nil)
2003+
_require.NoError(err)
2004+
2005+
_, err = srcFileClient.Create(context.Background(), nil)
2006+
_require.NoError(err)
2007+
2008+
destPathWithSAS := "file2?" + sasToken
2009+
_, err = srcFileClient.Rename(context.Background(), destPathWithSAS, nil)
2010+
_require.NoError(err)
2011+
2012+
_, err = srcFileClient.GetProperties(context.Background(), nil)
2013+
_require.Error(err)
2014+
testcommon.ValidateErrorCode(_require, err, datalakeerror.PathNotFound)
2015+
}
2016+
19762017
func (s *RecordedTestSuite) TestRenameNoOptions() {
19772018
_require := require.New(s.T())
19782019
testName := s.T().Name()

0 commit comments

Comments
 (0)