Skip to content

Commit 7f5ab05

Browse files
wonwuakpa-msftadreed-msftgapra-msft
authored
Added support for trailing dot transfers to unsafe locations (#2827)
* Add AllowToUnsafeDestination * implementation to stop msg sends on a closed channel * RWMutex added on channel closed flag * mutex synchronism mechanism added to channel sends and closures * replaced mutex panic handling implementation with recover * replace console msgs to log msg * modified panic handling * removed unnecessary print stmt * added unsafe destination trailing dot option and tests * testing for trailing dot * added unsafe destination trailing dot option and tests * testing for trailing dot * set AllowToUnsafeDestination on client + tests * test for third trailing dot option * widened the client options type * address Pr comments * Update e2etest/zt_newe2e_basic_functionality_test.go Co-authored-by: Gauri Lamunion <[email protected]> * small fixes * fix tests * cleaned tests * fixed tests * removed list test * trailing dot should be stripped in tests --------- Co-authored-by: Adele Reed <[email protected]> Co-authored-by: Gauri Lamunion <[email protected]>
1 parent 8ae772c commit 7f5ab05

19 files changed

+186
-66
lines changed

cmd/copy.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1568,7 +1568,7 @@ func (cca *CookedCopyCmdArgs) processCopyJobPartOrders() (err error) {
15681568
var azureFileSpecificOptions any
15691569
if cca.FromTo.From() == common.ELocation.File() {
15701570
azureFileSpecificOptions = &common.FileClientOptions{
1571-
AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable(),
1571+
AllowTrailingDot: cca.trailingDot.IsEnabled(),
15721572
}
15731573
}
15741574

@@ -1590,8 +1590,8 @@ func (cca *CookedCopyCmdArgs) processCopyJobPartOrders() (err error) {
15901590

15911591
if cca.FromTo.To() == common.ELocation.File() {
15921592
azureFileSpecificOptions = &common.FileClientOptions{
1593-
AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable(),
1594-
AllowSourceTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable() && cca.FromTo.From() == common.ELocation.File(),
1593+
AllowTrailingDot: cca.trailingDot.IsEnabled(),
1594+
AllowSourceTrailingDot: cca.trailingDot.IsEnabled() && cca.FromTo.From() == common.ELocation.File(),
15951595
}
15961596
}
15971597

@@ -2134,9 +2134,11 @@ func init() {
21342134
// so properties can be get in parallel, at same time no additional go routines are created for this specific job.
21352135
// The usage of this hidden flag is to provide fallback to traditional behavior, when service supports returning full properties during list.
21362136
cpCmd.PersistentFlags().BoolVar(&raw.s2sGetPropertiesInBackend, "s2s-get-properties-in-backend", true, "True by default. Gets S3 objects' or Azure files' properties in backend, if properties need to be accessed. Properties need to be accessed if s2s-preserve-properties is true, and in certain other cases where we need the properties for modification time checks or MD5 checks.")
2137-
cpCmd.PersistentFlags().StringVar(&raw.trailingDot, "trailing-dot", "", "'Enable' by default to treat file share related operations in a safe manner. Available options: "+strings.Join(common.ValidTrailingDotOptions(), ", ")+". "+
2138-
"Choose 'Disable' to go back to legacy (potentially unsafe) treatment of trailing dot files where the file service will trim any trailing dots in paths. This can result in potential data corruption if the transfer contains two paths that differ only by a trailing dot (ex: mypath and mypath.). If this flag is set to 'Disable' and AzCopy encounters a trailing dot file, it will warn customers in the scanning log but will not attempt to abort the operation."+
2139-
"If the destination does not support trailing dot files (Windows or Blob Storage), AzCopy will fail if the trailing dot file is the root of the transfer and skip any trailing dot paths encountered during enumeration.")
2137+
cpCmd.PersistentFlags().StringVar(&raw.trailingDot, "trailing-dot", "", "Available options: "+strings.Join(common.ValidTrailingDotOptions(), ", ")+". "+
2138+
"'Enable'(Default) treats trailing dot file operations in a safe manner between systems that support these files. On Windows, the transfers will not occur to stop risk of data corruption. See 'AllowToUnsafeDestination' to bypass this."+
2139+
"'Disable' reverts to the legacy functionality, where trailing dot files are ignored. This can result in potential data corruption if the transfer contains two paths that differ only by a trailing dot (E.g 'path/foo' and 'path/foo.'). If this flag is set to 'Disable' and AzCopy encounters a trailing dot file, it will warn customers in the scanning log but will not attempt to abort the operation."+
2140+
"If the destination does not support trailing dot files (Windows or Blob Storage), AzCopy will fail if the trailing dot file is the root of the transfer and skip any trailing dot paths encountered during enumeration."+
2141+
"'AllowToUnsafeDestination' supports transferring trailing dot files to systems that do not support them e.g Windows. Use with caution acknowledging risk of data corruption, when two files with different contents 'path/bar' and 'path/bar.' (differ only by a trailing dot) are seen as identical.")
21402142

21412143
// Public Documentation: https://docs.microsoft.com/en-us/azure/storage/blobs/encryption-customer-provided-keys
21422144
// Clients making requests against Azure Blob storage have the option to provide an encryption key on a per-request basis.

cmd/jobsResume.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,28 @@ func (rca resumeCmdArgs) getSourceAndDestinationServiceClients(
295295
}
296296

297297
options := createClientOptions(common.AzcopyCurrentJobLogger, nil)
298+
jobID, err := common.ParseJobID(rca.jobID)
299+
if err != nil {
300+
// Error for invalid JobId format
301+
return nil, nil, fmt.Errorf("error parsing the jobId %s. Failed with error %s", rca.jobID, err.Error())
302+
}
303+
304+
var getJobDetailsResponse common.GetJobDetailsResponse
305+
// Get job details from the STE
306+
Rpc(common.ERpcCmd.GetJobDetails(),
307+
&common.GetJobDetailsRequest{JobID: jobID},
308+
&getJobDetailsResponse)
309+
if getJobDetailsResponse.ErrorMsg != "" {
310+
glcm.Error(getJobDetailsResponse.ErrorMsg)
311+
}
298312

299-
srcServiceClient, err := common.GetServiceClientForLocation(fromTo.From(), source, srcCredType, tc, &options, nil)
313+
var fileSrcClientOptions any
314+
if fromTo.From() == common.ELocation.File() {
315+
fileSrcClientOptions = &common.FileClientOptions{
316+
AllowTrailingDot: getJobDetailsResponse.TrailingDot.IsEnabled(), //Access the trailingDot option of the job
317+
}
318+
}
319+
srcServiceClient, err := common.GetServiceClientForLocation(fromTo.From(), source, srcCredType, tc, &options, fileSrcClientOptions)
300320
if err != nil {
301321
return nil, nil, err
302322
}
@@ -306,11 +326,17 @@ func (rca resumeCmdArgs) getSourceAndDestinationServiceClients(
306326
srcCred = common.NewScopedCredential(tc, srcCredType)
307327
}
308328
options = createClientOptions(common.AzcopyCurrentJobLogger, srcCred)
309-
dstServiceClient, err := common.GetServiceClientForLocation(fromTo.To(), destination, dstCredType, tc, &options, nil)
329+
var fileClientOptions any
330+
if fromTo.To() == common.ELocation.File() {
331+
fileClientOptions = &common.FileClientOptions{
332+
AllowSourceTrailingDot: getJobDetailsResponse.TrailingDot.IsEnabled() && fromTo.From() == common.ELocation.File(),
333+
AllowTrailingDot: getJobDetailsResponse.TrailingDot.IsEnabled(),
334+
}
335+
}
336+
dstServiceClient, err := common.GetServiceClientForLocation(fromTo.To(), destination, dstCredType, tc, &options, fileClientOptions)
310337
if err != nil {
311338
return nil, nil, err
312339
}
313-
314340
return srcServiceClient, dstServiceClient, nil
315341
}
316342

@@ -357,9 +383,9 @@ func (rca resumeCmdArgs) process() error {
357383
}
358384

359385
// Get fromTo info, so we can decide what's the proper credential type to use.
360-
var getJobFromToResponse common.GetJobFromToResponse
361-
Rpc(common.ERpcCmd.GetJobFromTo(),
362-
&common.GetJobFromToRequest{JobID: jobID},
386+
var getJobFromToResponse common.GetJobDetailsResponse
387+
Rpc(common.ERpcCmd.GetJobDetails(),
388+
&common.GetJobDetailsRequest{JobID: jobID},
363389
&getJobFromToResponse)
364390
if getJobFromToResponse.ErrorMsg != "" {
365391
glcm.Error(getJobFromToResponse.ErrorMsg)

cmd/removeEnumerator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func newRemoveEnumerator(cca *CookedCopyCmdArgs) (enumerator *CopyEnumerator, er
9393
options := createClientOptions(common.AzcopyCurrentJobLogger, nil)
9494
var fileClientOptions any
9595
if cca.FromTo.From() == common.ELocation.File() {
96-
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable()}
96+
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot.IsEnabled()}
9797
}
9898
targetServiceClient, err := common.GetServiceClientForLocation(
9999
cca.FromTo.From(),

cmd/rpc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ func inprocSend(rpcCmd common.RpcCmd, requestData interface{}, responseData inte
5959
case common.ERpcCmd.ResumeJob():
6060
*(responseData.(*common.CancelPauseResumeResponse)) = jobsAdmin.ResumeJobOrder(*requestData.(*common.ResumeJobRequest))
6161

62-
case common.ERpcCmd.GetJobFromTo():
63-
*(responseData.(*common.GetJobFromToResponse)) = jobsAdmin.GetJobFromTo(*requestData.(*common.GetJobFromToRequest))
62+
case common.ERpcCmd.GetJobDetails():
63+
*(responseData.(*common.GetJobDetailsResponse)) = jobsAdmin.GetJobDetails(*requestData.(*common.GetJobDetailsRequest))
6464

6565
default:
6666
panic(fmt.Errorf("Unrecognized RpcCmd: %q", rpcCmd.String()))

cmd/setPropertiesEnumerator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func setPropertiesEnumerator(cca *CookedCopyCmdArgs) (enumerator *CopyEnumerator
7575
options := createClientOptions(common.AzcopyCurrentJobLogger, nil)
7676
var fileClientOptions any
7777
if cca.FromTo.From() == common.ELocation.File() {
78-
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable()}
78+
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot.IsEnabled()}
7979
}
8080

8181
targetServiceClient, err := common.GetServiceClientForLocation(

cmd/zc_enumerator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ func InitResourceTraverser(resource common.ResourceString, location common.Locat
495495
fileURLParts.ShareSnapshot = ""
496496
fileURLParts.DirectoryOrFilePath = ""
497497
fileOptions := &common.FileClientOptions{
498-
AllowTrailingDot: trailingDot == common.ETrailingDotOption.Enable(),
498+
AllowTrailingDot: trailingDot.IsEnabled(),
499499
}
500500

501501
res, err := SplitResourceString(fileURLParts.String(), common.ELocation.File())

cmd/zc_traverser_file.go

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ type fileTraverser struct {
4848

4949
// a generic function to notify that a new stored object has been enumerated
5050
incrementEnumerationCounter enumerationCounterFunc
51-
trailingDot common.TrailingDotOption
52-
destination *common.Location
51+
trailingDot common.TrailingDotOption
52+
destination *common.Location
5353
}
5454

5555
func createShareClientFromServiceClient(fileURLParts file.URLParts, client *service.Client) (*share.Client, error) {
@@ -123,6 +123,10 @@ func (t *fileTraverser) getPropertiesIfSingleFile() (*file.GetPropertiesResponse
123123
func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectProcessor, filters []ObjectFilter) (err error) {
124124
invalidBlobOrWindowsName := func(path string) bool {
125125
if t.destination != nil {
126+
if t.trailingDot == common.ETrailingDotOption.AllowToUnsafeDestination() && (*t.destination != common.ELocation.Blob() || *t.destination != common.ELocation.BlobFS()) { // Allow only Local, Trailing dot files not supported in Blob
127+
return false // Please let me shoot myself in the foot!
128+
}
129+
126130
if (t.destination.IsLocal() && runtime.GOOS == "windows") || *t.destination == common.ELocation.Blob() || *t.destination == common.ELocation.BlobFS() {
127131
/* Blob or Windows object name is invalid if it ends with period or
128132
one of (virtual) directories in path ends with period.
@@ -145,7 +149,7 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
145149
WarnStdoutAndScanningLog(fmt.Sprintf(invalidNameErrorMsg, targetURLParts.DirectoryOrFilePath))
146150
return common.EAzError.InvalidBlobOrWindowsName()
147151
}
148-
if t.trailingDot != common.ETrailingDotOption.Enable() && strings.HasSuffix(targetURLParts.DirectoryOrFilePath, ".") {
152+
if !t.trailingDot.IsEnabled() && strings.HasSuffix(targetURLParts.DirectoryOrFilePath, ".") {
149153
azcopyScanningLogger.Log(common.LogWarning, fmt.Sprintf(trailingDotErrMsg, getObjectNameOnly(targetURLParts.DirectoryOrFilePath)))
150154
}
151155
// check if the url points to a single file
@@ -288,7 +292,7 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
288292
WarnStdoutAndScanningLog(fmt.Sprintf(invalidNameErrorMsg, *fileInfo.Name))
289293
continue
290294
} else {
291-
if t.trailingDot != common.ETrailingDotOption.Enable() && strings.HasSuffix(*fileInfo.Name, ".") {
295+
if !t.trailingDot.IsEnabled() && strings.HasSuffix(*fileInfo.Name, ".") {
292296
azcopyScanningLogger.Log(common.LogWarning, fmt.Sprintf(trailingDotErrMsg, *fileInfo.Name))
293297
}
294298
}
@@ -301,13 +305,13 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
301305
WarnStdoutAndScanningLog(fmt.Sprintf(invalidNameErrorMsg, *dirInfo.Name))
302306
continue
303307
} else {
304-
if t.trailingDot != common.ETrailingDotOption.Enable() && strings.HasSuffix(*dirInfo.Name, ".") {
308+
if !t.trailingDot.IsEnabled() && strings.HasSuffix(*dirInfo.Name, ".") {
305309
azcopyScanningLogger.Log(common.LogWarning, fmt.Sprintf(trailingDotErrMsg, *dirInfo.Name))
306310
}
307311
}
308312
enqueueOutput(newAzFileSubdirectoryEntity(currentDirectoryClient, *dirInfo.Name), nil)
309313
if t.recursive {
310-
// If recursive is turned on, add sub directories to be processed
314+
// If recursive is turned on, add sub directories to be processed
311315
enqueueDir(currentDirectoryClient.NewSubdirectoryClient(*dirInfo.Name))
312316
}
313317

@@ -381,14 +385,14 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
381385

382386
func newFileTraverser(rawURL string, serviceClient *service.Client, ctx context.Context, recursive, getProperties bool, incrementEnumerationCounter enumerationCounterFunc, trailingDot common.TrailingDotOption, destination *common.Location) (t *fileTraverser) {
383387
t = &fileTraverser{
384-
rawURL: rawURL,
385-
serviceClient: serviceClient,
386-
ctx: ctx,
387-
recursive: recursive,
388-
getProperties: getProperties,
388+
rawURL: rawURL,
389+
serviceClient: serviceClient,
390+
ctx: ctx,
391+
recursive: recursive,
392+
getProperties: getProperties,
389393
incrementEnumerationCounter: incrementEnumerationCounter,
390-
trailingDot: trailingDot,
391-
destination: destination,
394+
trailingDot: trailingDot,
395+
destination: destination,
392396
}
393397
return
394398
}

cmd/zt_interceptors_for_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (i *interceptor) intercept(cmd common.RpcCmd, request interface{}, response
5353
case common.ERpcCmd.PauseJob():
5454
case common.ERpcCmd.CancelJob():
5555
case common.ERpcCmd.ResumeJob():
56-
case common.ERpcCmd.GetJobFromTo():
56+
case common.ERpcCmd.GetJobDetails():
5757
fallthrough
5858
default:
5959
panic("RPC mock not implemented")

common/fe-ste-models.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,15 @@ var ETrailingDotOption = TrailingDotOption(0)
153153

154154
type TrailingDotOption uint8
155155

156-
func (TrailingDotOption) Enable() TrailingDotOption { return TrailingDotOption(0) }
157-
func (TrailingDotOption) Disable() TrailingDotOption { return TrailingDotOption(1) }
156+
func (TrailingDotOption) Enable() TrailingDotOption { return TrailingDotOption(0) }
157+
func (TrailingDotOption) Disable() TrailingDotOption { return TrailingDotOption(1) }
158+
func (TrailingDotOption) AllowToUnsafeDestination() TrailingDotOption { return TrailingDotOption(2) }
159+
160+
// Trailing dots are supported in the Enable and AllowToUnsafeDestination options
161+
func (d TrailingDotOption) IsEnabled() bool {
162+
return d == d.Enable() ||
163+
d == d.AllowToUnsafeDestination()
164+
}
158165

159166
func (d TrailingDotOption) String() string {
160167
return enum.StringInt(d, reflect.TypeOf(d))
@@ -178,6 +185,7 @@ func ValidTrailingDotOptions() []string {
178185
return []string{
179186
ETrailingDotOption.Enable().String(),
180187
ETrailingDotOption.Disable().String(),
188+
ETrailingDotOption.AllowToUnsafeDestination().String(),
181189
}
182190
}
183191

common/rpc-models.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func (RpcCmd) ListJobTransfers() RpcCmd { return RpcCmd("ListJobTransfers") }
2727
func (RpcCmd) CancelJob() RpcCmd { return RpcCmd("Cancel") }
2828
func (RpcCmd) PauseJob() RpcCmd { return RpcCmd("PauseJob") }
2929
func (RpcCmd) ResumeJob() RpcCmd { return RpcCmd("ResumeJob") }
30-
func (RpcCmd) GetJobFromTo() RpcCmd { return RpcCmd("GetJobFromTo") }
30+
func (RpcCmd) GetJobDetails() RpcCmd { return RpcCmd("GetJobDetails") }
3131

3232
func (c RpcCmd) String() string {
3333
return enum.String(c, reflect.TypeOf(c))
@@ -375,15 +375,16 @@ type ListJobTransfersResponse struct {
375375
Details []TransferDetail
376376
}
377377

378-
// GetJobFromToRequest indicates request to get job's FromTo info from job part plan header
379-
type GetJobFromToRequest struct {
378+
// GetJobDetailsRequest indicates request to get job's FromTo and TrailingDot info from job part plan header
379+
type GetJobDetailsRequest struct {
380380
JobID JobID
381381
}
382382

383-
// GetJobFromToResponse indicates response to get job's FromTo info.
384-
type GetJobFromToResponse struct {
383+
// GetJobDetailsResponse indicates response to get job's FromTo and TrailingDot info.
384+
type GetJobDetailsResponse struct {
385385
ErrorMsg string
386386
FromTo FromTo
387387
Source string
388388
Destination string
389+
TrailingDot TrailingDotOption
389390
}

0 commit comments

Comments
 (0)