Skip to content

Commit

Permalink
Merge branch 'main' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
wonwuakpa-msft authored Jan 6, 2025
2 parents 4b85a36 + bc74a22 commit 586f6ca
Show file tree
Hide file tree
Showing 20 changed files with 201 additions and 81 deletions.
14 changes: 8 additions & 6 deletions cmd/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -1568,7 +1568,7 @@ func (cca *CookedCopyCmdArgs) processCopyJobPartOrders() (err error) {
var azureFileSpecificOptions any
if cca.FromTo.From() == common.ELocation.File() {
azureFileSpecificOptions = &common.FileClientOptions{
AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable(),
AllowTrailingDot: cca.trailingDot.IsEnabled(),
}
}

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

if cca.FromTo.To() == common.ELocation.File() {
azureFileSpecificOptions = &common.FileClientOptions{
AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable(),
AllowSourceTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable() && cca.FromTo.From() == common.ELocation.File(),
AllowTrailingDot: cca.trailingDot.IsEnabled(),
AllowSourceTrailingDot: cca.trailingDot.IsEnabled() && cca.FromTo.From() == common.ELocation.File(),
}
}

Expand Down Expand Up @@ -2134,9 +2134,11 @@ func init() {
// so properties can be get in parallel, at same time no additional go routines are created for this specific job.
// The usage of this hidden flag is to provide fallback to traditional behavior, when service supports returning full properties during list.
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.")
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(), ", ")+". "+
"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."+
"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.")
cpCmd.PersistentFlags().StringVar(&raw.trailingDot, "trailing-dot", "", "Available options: "+strings.Join(common.ValidTrailingDotOptions(), ", ")+". "+
"'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."+
"'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."+
"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."+
"'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.")

// Public Documentation: https://docs.microsoft.com/en-us/azure/storage/blobs/encryption-customer-provided-keys
// Clients making requests against Azure Blob storage have the option to provide an encryption key on a per-request basis.
Expand Down
38 changes: 32 additions & 6 deletions cmd/jobsResume.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,28 @@ func (rca resumeCmdArgs) getSourceAndDestinationServiceClients(
}

options := createClientOptions(common.AzcopyCurrentJobLogger, nil)
jobID, err := common.ParseJobID(rca.jobID)
if err != nil {
// Error for invalid JobId format
return nil, nil, fmt.Errorf("error parsing the jobId %s. Failed with error %s", rca.jobID, err.Error())
}

var getJobDetailsResponse common.GetJobDetailsResponse
// Get job details from the STE
Rpc(common.ERpcCmd.GetJobDetails(),
&common.GetJobDetailsRequest{JobID: jobID},
&getJobDetailsResponse)
if getJobDetailsResponse.ErrorMsg != "" {
glcm.Error(getJobDetailsResponse.ErrorMsg)
}

srcServiceClient, err := common.GetServiceClientForLocation(fromTo.From(), source, srcCredType, tc, &options, nil)
var fileSrcClientOptions any
if fromTo.From() == common.ELocation.File() {
fileSrcClientOptions = &common.FileClientOptions{
AllowTrailingDot: getJobDetailsResponse.TrailingDot.IsEnabled(), //Access the trailingDot option of the job
}
}
srcServiceClient, err := common.GetServiceClientForLocation(fromTo.From(), source, srcCredType, tc, &options, fileSrcClientOptions)
if err != nil {
return nil, nil, err
}
Expand All @@ -306,11 +326,17 @@ func (rca resumeCmdArgs) getSourceAndDestinationServiceClients(
srcCred = common.NewScopedCredential(tc, srcCredType)
}
options = createClientOptions(common.AzcopyCurrentJobLogger, srcCred)
dstServiceClient, err := common.GetServiceClientForLocation(fromTo.To(), destination, dstCredType, tc, &options, nil)
var fileClientOptions any
if fromTo.To() == common.ELocation.File() {
fileClientOptions = &common.FileClientOptions{
AllowSourceTrailingDot: getJobDetailsResponse.TrailingDot.IsEnabled() && fromTo.From() == common.ELocation.File(),
AllowTrailingDot: getJobDetailsResponse.TrailingDot.IsEnabled(),
}
}
dstServiceClient, err := common.GetServiceClientForLocation(fromTo.To(), destination, dstCredType, tc, &options, fileClientOptions)
if err != nil {
return nil, nil, err
}

return srcServiceClient, dstServiceClient, nil
}

Expand Down Expand Up @@ -357,9 +383,9 @@ func (rca resumeCmdArgs) process() error {
}

// Get fromTo info, so we can decide what's the proper credential type to use.
var getJobFromToResponse common.GetJobFromToResponse
Rpc(common.ERpcCmd.GetJobFromTo(),
&common.GetJobFromToRequest{JobID: jobID},
var getJobFromToResponse common.GetJobDetailsResponse
Rpc(common.ERpcCmd.GetJobDetails(),
&common.GetJobDetailsRequest{JobID: jobID},
&getJobFromToResponse)
if getJobFromToResponse.ErrorMsg != "" {
glcm.Error(getJobFromToResponse.ErrorMsg)
Expand Down
2 changes: 1 addition & 1 deletion cmd/removeEnumerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func newRemoveEnumerator(cca *CookedCopyCmdArgs) (enumerator *CopyEnumerator, er
options := createClientOptions(common.AzcopyCurrentJobLogger, nil)
var fileClientOptions any
if cca.FromTo.From() == common.ELocation.File() {
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable()}
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot.IsEnabled()}
}
targetServiceClient, err := common.GetServiceClientForLocation(
cca.FromTo.From(),
Expand Down
4 changes: 2 additions & 2 deletions cmd/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ func inprocSend(rpcCmd common.RpcCmd, requestData interface{}, responseData inte
case common.ERpcCmd.ResumeJob():
*(responseData.(*common.CancelPauseResumeResponse)) = jobsAdmin.ResumeJobOrder(*requestData.(*common.ResumeJobRequest))

case common.ERpcCmd.GetJobFromTo():
*(responseData.(*common.GetJobFromToResponse)) = jobsAdmin.GetJobFromTo(*requestData.(*common.GetJobFromToRequest))
case common.ERpcCmd.GetJobDetails():
*(responseData.(*common.GetJobDetailsResponse)) = jobsAdmin.GetJobDetails(*requestData.(*common.GetJobDetailsRequest))

default:
panic(fmt.Errorf("Unrecognized RpcCmd: %q", rpcCmd.String()))
Expand Down
2 changes: 1 addition & 1 deletion cmd/setPropertiesEnumerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func setPropertiesEnumerator(cca *CookedCopyCmdArgs) (enumerator *CopyEnumerator
options := createClientOptions(common.AzcopyCurrentJobLogger, nil)
var fileClientOptions any
if cca.FromTo.From() == common.ELocation.File() {
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot == common.ETrailingDotOption.Enable()}
fileClientOptions = &common.FileClientOptions{AllowTrailingDot: cca.trailingDot.IsEnabled()}
}

targetServiceClient, err := common.GetServiceClientForLocation(
Expand Down
2 changes: 1 addition & 1 deletion cmd/zc_enumerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ func InitResourceTraverser(resource common.ResourceString, location common.Locat
fileURLParts.ShareSnapshot = ""
fileURLParts.DirectoryOrFilePath = ""
fileOptions := &common.FileClientOptions{
AllowTrailingDot: trailingDot == common.ETrailingDotOption.Enable(),
AllowTrailingDot: trailingDot.IsEnabled(),
}

res, err := SplitResourceString(fileURLParts.String(), common.ELocation.File())
Expand Down
30 changes: 17 additions & 13 deletions cmd/zc_traverser_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,8 @@ type fileTraverser struct {

// a generic function to notify that a new stored object has been enumerated
incrementEnumerationCounter enumerationCounterFunc
trailingDot common.TrailingDotOption
destination *common.Location
trailingDot common.TrailingDotOption
destination *common.Location
}

func createShareClientFromServiceClient(fileURLParts file.URLParts, client *service.Client) (*share.Client, error) {
Expand Down Expand Up @@ -123,6 +123,10 @@ func (t *fileTraverser) getPropertiesIfSingleFile() (*file.GetPropertiesResponse
func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectProcessor, filters []ObjectFilter) (err error) {
invalidBlobOrWindowsName := func(path string) bool {
if t.destination != nil {
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
return false // Please let me shoot myself in the foot!
}

if (t.destination.IsLocal() && runtime.GOOS == "windows") || *t.destination == common.ELocation.Blob() || *t.destination == common.ELocation.BlobFS() {
/* Blob or Windows object name is invalid if it ends with period or
one of (virtual) directories in path ends with period.
Expand All @@ -145,7 +149,7 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
WarnStdoutAndScanningLog(fmt.Sprintf(invalidNameErrorMsg, targetURLParts.DirectoryOrFilePath))
return common.EAzError.InvalidBlobOrWindowsName()
}
if t.trailingDot != common.ETrailingDotOption.Enable() && strings.HasSuffix(targetURLParts.DirectoryOrFilePath, ".") {
if !t.trailingDot.IsEnabled() && strings.HasSuffix(targetURLParts.DirectoryOrFilePath, ".") {
azcopyScanningLogger.Log(common.LogWarning, fmt.Sprintf(trailingDotErrMsg, getObjectNameOnly(targetURLParts.DirectoryOrFilePath)))
}
// check if the url points to a single file
Expand Down Expand Up @@ -288,7 +292,7 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
WarnStdoutAndScanningLog(fmt.Sprintf(invalidNameErrorMsg, *fileInfo.Name))
continue
} else {
if t.trailingDot != common.ETrailingDotOption.Enable() && strings.HasSuffix(*fileInfo.Name, ".") {
if !t.trailingDot.IsEnabled() && strings.HasSuffix(*fileInfo.Name, ".") {
azcopyScanningLogger.Log(common.LogWarning, fmt.Sprintf(trailingDotErrMsg, *fileInfo.Name))
}
}
Expand All @@ -301,13 +305,13 @@ func (t *fileTraverser) Traverse(preprocessor objectMorpher, processor objectPro
WarnStdoutAndScanningLog(fmt.Sprintf(invalidNameErrorMsg, *dirInfo.Name))
continue
} else {
if t.trailingDot != common.ETrailingDotOption.Enable() && strings.HasSuffix(*dirInfo.Name, ".") {
if !t.trailingDot.IsEnabled() && strings.HasSuffix(*dirInfo.Name, ".") {
azcopyScanningLogger.Log(common.LogWarning, fmt.Sprintf(trailingDotErrMsg, *dirInfo.Name))
}
}
enqueueOutput(newAzFileSubdirectoryEntity(currentDirectoryClient, *dirInfo.Name), nil)
if t.recursive {
// If recursive is turned on, add sub directories to be processed
// If recursive is turned on, add sub directories to be processed
enqueueDir(currentDirectoryClient.NewSubdirectoryClient(*dirInfo.Name))
}

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

func newFileTraverser(rawURL string, serviceClient *service.Client, ctx context.Context, recursive, getProperties bool, incrementEnumerationCounter enumerationCounterFunc, trailingDot common.TrailingDotOption, destination *common.Location) (t *fileTraverser) {
t = &fileTraverser{
rawURL: rawURL,
serviceClient: serviceClient,
ctx: ctx,
recursive: recursive,
getProperties: getProperties,
rawURL: rawURL,
serviceClient: serviceClient,
ctx: ctx,
recursive: recursive,
getProperties: getProperties,
incrementEnumerationCounter: incrementEnumerationCounter,
trailingDot: trailingDot,
destination: destination,
trailingDot: trailingDot,
destination: destination,
}
return
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/zt_interceptors_for_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (i *interceptor) intercept(cmd common.RpcCmd, request interface{}, response
case common.ERpcCmd.PauseJob():
case common.ERpcCmd.CancelJob():
case common.ERpcCmd.ResumeJob():
case common.ERpcCmd.GetJobFromTo():
case common.ERpcCmd.GetJobDetails():
fallthrough
default:
panic("RPC mock not implemented")
Expand Down
12 changes: 10 additions & 2 deletions common/fe-ste-models.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,15 @@ var ETrailingDotOption = TrailingDotOption(0)

type TrailingDotOption uint8

func (TrailingDotOption) Enable() TrailingDotOption { return TrailingDotOption(0) }
func (TrailingDotOption) Disable() TrailingDotOption { return TrailingDotOption(1) }
func (TrailingDotOption) Enable() TrailingDotOption { return TrailingDotOption(0) }
func (TrailingDotOption) Disable() TrailingDotOption { return TrailingDotOption(1) }
func (TrailingDotOption) AllowToUnsafeDestination() TrailingDotOption { return TrailingDotOption(2) }

// Trailing dots are supported in the Enable and AllowToUnsafeDestination options
func (d TrailingDotOption) IsEnabled() bool {
return d == d.Enable() ||
d == d.AllowToUnsafeDestination()
}

func (d TrailingDotOption) String() string {
return enum.StringInt(d, reflect.TypeOf(d))
Expand All @@ -178,6 +185,7 @@ func ValidTrailingDotOptions() []string {
return []string{
ETrailingDotOption.Enable().String(),
ETrailingDotOption.Disable().String(),
ETrailingDotOption.AllowToUnsafeDestination().String(),
}
}

Expand Down
11 changes: 6 additions & 5 deletions common/rpc-models.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (RpcCmd) ListJobTransfers() RpcCmd { return RpcCmd("ListJobTransfers") }
func (RpcCmd) CancelJob() RpcCmd { return RpcCmd("Cancel") }
func (RpcCmd) PauseJob() RpcCmd { return RpcCmd("PauseJob") }
func (RpcCmd) ResumeJob() RpcCmd { return RpcCmd("ResumeJob") }
func (RpcCmd) GetJobFromTo() RpcCmd { return RpcCmd("GetJobFromTo") }
func (RpcCmd) GetJobDetails() RpcCmd { return RpcCmd("GetJobDetails") }

func (c RpcCmd) String() string {
return enum.String(c, reflect.TypeOf(c))
Expand Down Expand Up @@ -375,15 +375,16 @@ type ListJobTransfersResponse struct {
Details []TransferDetail
}

// GetJobFromToRequest indicates request to get job's FromTo info from job part plan header
type GetJobFromToRequest struct {
// GetJobDetailsRequest indicates request to get job's FromTo and TrailingDot info from job part plan header
type GetJobDetailsRequest struct {
JobID JobID
}

// GetJobFromToResponse indicates response to get job's FromTo info.
type GetJobFromToResponse struct {
// GetJobDetailsResponse indicates response to get job's FromTo and TrailingDot info.
type GetJobDetailsResponse struct {
ErrorMsg string
FromTo FromTo
Source string
Destination string
TrailingDot TrailingDotOption
}
8 changes: 4 additions & 4 deletions e2etest/newe2e_resource_managers_local.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,11 @@ func (l *LocalObjectResourceManager) ListChildren(a Asserter, recursive bool) ma
func (l *LocalObjectResourceManager) GetProperties(a Asserter) ObjectProperties {
a.HelperMarker().Helper()
stats, err := os.Stat(l.getWorkingPath())
a.NoError("failed to get stat", err)
var lmt *time.Time
if stats != nil {
lmt = PtrOf(stats.ModTime())
if err != nil { // Prevent nil dereferences
a.NoError("failed to get stat", err)
return ObjectProperties{}
}
lmt := common.Iff(stats == nil, nil, PtrOf(stats.ModTime()))
out := ObjectProperties{
LastModifiedTime: lmt,
}
Expand Down
2 changes: 1 addition & 1 deletion e2etest/newe2e_task_runazcopy_parameters.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ type GlobalFlags struct {
OutputLevel *common.OutputVerbosity `flag:"output-level,default:DEFAULT"`

// TODO: reconsider/reengineer this flag; WI#26475473
//DebugSkipFiles []string `flag:"debug-skip-files"`
// DebugSkipFiles []string `flag:"debug-skip-files"`

// TODO: handle prompting and input; WI#26475441
//CancelFromStdin *bool `flag:"cancel-from-stdin"`
Expand Down
52 changes: 52 additions & 0 deletions e2etest/zt_newe2e_basic_functionality_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package e2etest

import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
blobsas "github.com/Azure/azure-sdk-for-go/sdk/storage/azblob/sas"
"github.com/Azure/azure-storage-azcopy/v10/common"
"strconv"
Expand Down Expand Up @@ -412,6 +413,57 @@ func (s *BasicFunctionalitySuite) Scenario_Copy_EmptySASErrorCodes(svm *Scenario
ValidateContainsError(svm, stdout, []string{"https://aka.ms/AzCopyError/NoAuthenticationInformation", "https://aka.ms/AzCopyError/ResourceNotFound"})
}

// Test of Copy to UnsafeDestinations (Windows Local dest)
func (s *BasicFunctionalitySuite) Scenario_CopyUnSafeDest(svm *ScenarioVariationManager) {
azCopyVerb := AzCopyVerbCopy

fileName := "file."
body := NewRandomObjectContentContainer(SizeFromString("10K"))

// Scale up from service to object
srcObj := CreateResource[ObjectResourceManager](svm,
GetRootResource(svm, common.ELocation.File()), // Only source is File - Windows Local & Blob doesn't support T.D
ResourceDefinitionObject{
ObjectName: pointerTo(fileName),
Body: body,
})

dstObj := CreateResource[ContainerResourceManager](svm,
GetRootResource(svm,
ResolveVariation(svm, []common.Location{common.ELocation.File(),
common.ELocation.Local()})),
ResourceDefinitionContainer{}).GetObject(svm, "file", common.EEntityType.File())

sasOpts := GenericAccountSignatureValues{}

RunAzCopy(
svm,
AzCopyCommand{
Verb: azCopyVerb,
Targets: []ResourceManager{
TryApplySpecificAuthType(srcObj, EExplicitCredentialType.SASToken(), svm,
CreateAzCopyTargetOptions{
SASTokenOptions: sasOpts,
}),
TryApplySpecificAuthType(dstObj, EExplicitCredentialType.SASToken(), svm,
CreateAzCopyTargetOptions{
SASTokenOptions: sasOpts,
}),
},
Flags: CopyFlags{
CopySyncCommonFlags: CopySyncCommonFlags{
Recursive: pointerTo(true),
TrailingDot: to.Ptr(common.ETrailingDotOption.AllowToUnsafeDestination()), // Allow download to unsafe Local destination
},
},
})

ValidateResource[ObjectResourceManager](svm, dstObj,
ResourceDefinitionObject{
Body: body,
}, false)
}

func (s *BasicFunctionalitySuite) Scenario_TagsPermission(svm *ScenarioVariationManager) {
objectType := ResolveVariation(svm, []common.EntityType{common.EEntityType.File(), common.EEntityType.Folder(), common.EEntityType.Symlink()})
srcLoc := ResolveVariation(svm, []common.Location{common.ELocation.Local(), common.ELocation.Blob()})
Expand Down
Loading

0 comments on commit 586f6ca

Please sign in to comment.