Skip to content
Open
7 changes: 6 additions & 1 deletion cmd/copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -1980,5 +1980,10 @@ func init() {
"Specifies how hardlinks should be handled. "+
"\n This flag is only applicable when downloading from an Azure NFS file share, uploading "+
"to an Azure Files NFS share, or performing service-to-service copies involving Azure Files NFS. \n"+
"\n The only supported option is 'follow' (default), which copies hardlinks as regular, independent files at the destination.")
"\n The supported option are 'follow' (default), 'skip' and 'preserve'. \n"+
" 'follow' means that the hardlinked files are transferred as separate files. \n"+
" 'skip' means that all the hardlinked files are skipped. \n"+
" 'preserve' means that the first hardlinked file is transferred, and the other files are created as hardlinks to that file at the destination. \n"+
"\n Note: \n"+
" When using 'preserve', the source and destination must be on a file system that supports hardlinks. \n")
}
2 changes: 1 addition & 1 deletion cmd/copyValidation.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func (cooked *CookedCopyCmdArgs) validate() (err error) {
} else {
if err := performSMBSpecificValidation(
cooked.FromTo, cooked.preservePermissions, cooked.preserveInfo,
cooked.preservePOSIXProperties); err != nil {
cooked.preservePOSIXProperties, &cooked.hardlinks); err != nil {
return err
}

Expand Down
130 changes: 66 additions & 64 deletions cmd/flagsValidation.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,10 @@ func performNFSSpecificValidation(fromTo common.FromTo,
PreservePermissionsFlag); err != nil {
return err
}
// TODO: Add this check in Phase-3 which targets to support hardlinks for NFS copy.
// if err = validateAndAdjustHardlinksFlag(hardlinkHandling, fromTo); err != nil {
// return err
// }

if err = validateAndAdjustHardlinksFlag(hardlinkHandling, fromTo); err != nil {
return err
}

if err = validateSymlinkFlag(symlinkHandling == common.ESymlinkHandlingType.Follow(),
fromTo); err != nil {
Expand All @@ -198,7 +198,8 @@ func performNFSSpecificValidation(fromTo common.FromTo,
func performSMBSpecificValidation(fromTo common.FromTo,
preservePermissions common.PreservePermissionsOption,
preserveInfo bool,
preservePOSIXProperties bool) (err error) {
preservePOSIXProperties bool,
hardlinkHandling *common.HardlinkHandlingType) (err error) {

if err = validatePreserveSMBPropertyOption(preserveInfo,
fromTo,
Expand All @@ -214,10 +215,9 @@ func performSMBSpecificValidation(fromTo common.FromTo,
return err
}

// TODO: Add this check in Phase-3 which targets to support hardlinks for NFS copy.
// if err = validateAndAdjustHardlinksFlag(hardlinkHandling, fromTo); err != nil {
// return err
// }
if err = validateAndAdjustHardlinksFlag(hardlinkHandling, fromTo); err != nil {
return err
}
return nil
}

Expand All @@ -236,61 +236,63 @@ func validateSymlinkFlag(followSymlinks bool, fromTo common.FromTo) error {
// transfer direction (upload, download, S2S), and source/destination types (NFS, SMB, local).
// Returns an error if the configuration is unsupported.
// This function will be added as part of Phase-3 which targets to support hardlinks for NFS copy.
// func validateAndAdjustHardlinksFlag(option *common.HardlinkHandlingType, fromTo common.FromTo) error {
// if !fromTo.IsNFS() {
// return nil
// }

// // NFS<->SMB special case: force skip
// if (fromTo == common.EFromTo.FileNFSFileSMB() || fromTo == common.EFromTo.FileSMBFileNFS()) &&
// *option != common.SkipHardlinkHandlingType {
// return fmt.Errorf(
// "For NFS->SMB and SMB->NFS transfers, '--hardlinks' must be set to 'skip'. " +
// "Hardlinked files are not supported between NFS and SMB and will always be skipped. " +
// "Please re-run with '--hardlinks=skip'.",
// )
// }

// // OS check: hardlinks handling only supported on Linux in case of upload and download
// if runtime.GOOS != "linux" && !fromTo.IsS2S() {
// return fmt.Errorf("The --hardlinks option is only supported on Linux.")
// }

// switch {
// case fromTo.IsDownload():
// // Must be NFS -> Local Linux
// if fromTo.From() != common.ELocation.FileNFS() {
// return fmt.Errorf("For downloads, '--hardlinks' is only supported from an NFS file share to a Linux filesystem.")
// }

// case fromTo.IsUpload():
// // Must be Local Linux -> NFS
// if fromTo.To() != common.ELocation.FileNFS() {
// return fmt.Errorf("For uploads, '--hardlinks' is only supported from a Linux filesystem to an NFS file share.")
// }

// case fromTo.IsS2S():
// // Allowed: NFS<->NFS, NFS->SMB, SMB->NFS
// validPairs := map[common.FromTo]bool{
// common.EFromTo.FileNFSFileNFS(): true,
// common.EFromTo.FileNFSFileSMB(): true,
// common.EFromTo.FileSMBFileNFS(): true,
// }
// if !validPairs[fromTo] {
// return fmt.Errorf("For S2S transfers, '--hardlinks' is only supported for NFS<->NFS, NFS->SMB, and SMB->NFS.")
// }
// }

// // Info messages
// switch *option {
// case common.SkipHardlinkHandlingType:
// glcm.Info("The --hardlinks option is set to 'skip'. Hardlinked files will be skipped.")
// case common.DefaultHardlinkHandlingType:
// glcm.Info("The --hardlinks option is set to 'follow'. Hardlinked files will be copied as regular files.")
// }

// return nil
// }
func validateAndAdjustHardlinksFlag(option *common.HardlinkHandlingType, fromTo common.FromTo) error {
if !fromTo.IsNFS() {
return nil
}

// OS check: hardlinks handling only supported on Linux in case of upload and download
if runtime.GOOS != "linux" && !fromTo.IsS2S() {
return fmt.Errorf("The --hardlinks option is only supported on Linux.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fmt.Errorf("The --hardlinks option is only supported on Linux.")
return fmt.Errorf("For uploads and downloads, the --hardlinks option is only supported on Linux.")

}

// NFS<->SMB special case: force skip
if (fromTo == common.EFromTo.FileNFSFileSMB() || fromTo == common.EFromTo.FileSMBFileNFS()) &&
*option == common.PreserveHardlinkHandlingType {
return fmt.Errorf(
"For NFS->SMB and SMB->NFS transfers, '--hardlinks' must be set to 'skip'. " +
"Hardlinked files are not supported between NFS and SMB and will always be skipped. " +
"Please re-run with '--hardlinks=skip'.",
)
}

switch {
case fromTo.IsDownload():
// Must be NFS -> Local Linux
if fromTo.From() != common.ELocation.FileNFS() {
return fmt.Errorf("For downloads, '--hardlinks' is only supported from an NFS file share to a Linux filesystem.")
}

case fromTo.IsUpload():
// Must be Local Linux -> NFS
if fromTo.To() != common.ELocation.FileNFS() {
return fmt.Errorf("For uploads, '--hardlinks' is only supported from a Linux filesystem to an NFS file share.")
}

case fromTo.IsS2S():
// Allowed: NFS<->NFS, NFS->SMB, SMB->NFS
validPairs := map[common.FromTo]bool{
common.EFromTo.FileNFSFileNFS(): true,
common.EFromTo.FileNFSFileSMB(): true,
common.EFromTo.FileSMBFileNFS(): true,
}
if !validPairs[fromTo] {
return fmt.Errorf("for S2S transfers, '--hardlinks' is only supported for NFS<->NFS, NFS->SMB, and SMB->NFS.")
}
}

// Info messages
switch *option {
case common.SkipHardlinkHandlingType:
glcm.Info("The --hardlinks option is set to 'skip'. Hardlinked files will be skipped.")
case common.DefaultHardlinkHandlingType:
glcm.Info("The --hardlinks option is set to 'follow'. Hardlinked files will be copied as regular files.")
case common.PreserveHardlinkHandlingType:
glcm.Info("The --hardlinks option is set to 'preserve'. Hardlinked files will be preserved as hardlinks at the destination.")
}

return nil
}

func isUnsupportedPlatformForNFS(fromTo common.FromTo) (bool, error) {
// upload and download is not supported for NFS on non-linux systems
Expand Down
7 changes: 4 additions & 3 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,15 @@ func (cooked *cookedSyncCmdArgs) validate() (err error) {
} else {
if err := performSMBSpecificValidation(
cooked.fromTo, cooked.preservePermissions, cooked.preserveInfo,
cooked.preservePOSIXProperties); err != nil {
cooked.preservePOSIXProperties,
&cooked.hardlinks); err != nil {
return err
}

if cooked.symlinkHandling == common.ESymlinkHandlingType.Follow() {
return fmt.Errorf("The '--follow-symlink' flag is not applicable for sync operations.")
return fmt.Errorf("the '--follow-symlink' flag is not applicable for sync operations")
} else if cooked.symlinkHandling == common.ESymlinkHandlingType.Preserve() {
return fmt.Errorf("The '--preserve-symlink' flag is not applicable for sync operations.")
return fmt.Errorf("the '--preserve-symlink' flag is not applicable for sync operations")
}
}

Expand Down
6 changes: 6 additions & 0 deletions common/fe-ste-models.go
Original file line number Diff line number Diff line change
Expand Up @@ -1595,6 +1595,7 @@ var EHardlinkHandlingType = HardlinkHandlingType(0)

var DefaultHardlinkHandlingType = EHardlinkHandlingType.Follow()
var SkipHardlinkHandlingType = EHardlinkHandlingType.Skip()
var PreserveHardlinkHandlingType = EHardlinkHandlingType.Preserve()

type HardlinkHandlingType uint8

Expand All @@ -1608,6 +1609,11 @@ func (HardlinkHandlingType) Skip() HardlinkHandlingType {
return HardlinkHandlingType(1)
}

// Skip means skip the hardlinks and do not copy them to the destination
func (HardlinkHandlingType) Preserve() HardlinkHandlingType {
return HardlinkHandlingType(2)
}

func (pho HardlinkHandlingType) String() string {
return enum.StringInt(pho, reflect.TypeOf(pho))
}
Expand Down
13 changes: 13 additions & 0 deletions e2etest/newe2e_task_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,19 @@ func ValidateHardlinksConvertedCount(a Asserter, stdOut AzCopyStdout, expected u
return
}

func ValidateHardlinksSkippedCount(a Asserter, stdOut AzCopyStdout, expected uint32) {
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
}

parsedStdout := GetTypeOrAssert[*AzCopyParsedCopySyncRemoveStdout](a, stdOut)
hardlinksSkippedCount := parsedStdout.FinalStatus.SkippedHardlinkCount
if hardlinksSkippedCount != expected {
a.Error(fmt.Sprintf("expected hardlink skipped count (%d) received count (%d)", expected, hardlinksSkippedCount))
}
return
}

func ValidateMessageOutput(a Asserter, stdout AzCopyStdout, message string, shouldContain bool) {
if dryrunner, ok := a.(DryrunAsserter); ok && dryrunner.Dryrun() {
return
Expand Down
Loading
Loading