github.com/opentofu/opentofu@v1.7.1/internal/copy/copy_dir.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package copy
     7  
     8  import (
     9  	"io"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  )
    14  
    15  // CopyDir recursively copies all of the files within the directory given in
    16  // src to the directory given in dst.
    17  //
    18  // Both directories should already exist. If the destination directory is
    19  // non-empty then the new files will merge in with the old, overwriting any
    20  // files that have a relative path in common between source and destination.
    21  //
    22  // Recursive copying of directories is inevitably a rather opinionated sort of
    23  // operation, so this function won't be appropriate for all use-cases. Some
    24  // of the "opinions" it has are described in the following paragraphs:
    25  //
    26  // Symlinks in the source directory are recreated with the same target in the
    27  // destination directory. If the symlink is to a directory itself, that
    28  // directory is not recursively visited for further copying.
    29  //
    30  // File and directory modes are not preserved exactly, but the executable
    31  // flag is preserved for files on operating systems where it is significant.
    32  //
    33  // Any "dot files" it encounters along the way are skipped, even on platforms
    34  // that do not normally ascribe special meaning to files with names starting
    35  // with dots.
    36  //
    37  // Callers may rely on the above details and other undocumented details of
    38  // this function, so if you intend to change it be sure to review the callers
    39  // first and make sure they are compatible with the change you intend to make.
    40  func CopyDir(dst, src string) error {
    41  	src, err := filepath.EvalSymlinks(src)
    42  	if err != nil {
    43  		return err
    44  	}
    45  
    46  	walkFn := func(path string, info os.FileInfo, err error) error {
    47  		if err != nil {
    48  			return err
    49  		}
    50  
    51  		if path == src {
    52  			return nil
    53  		}
    54  
    55  		if strings.HasPrefix(filepath.Base(path), ".") {
    56  			// Skip any dot files
    57  			if info.IsDir() {
    58  				return filepath.SkipDir
    59  			} else {
    60  				return nil
    61  			}
    62  		}
    63  
    64  		// The "path" has the src prefixed to it. We need to join our
    65  		// destination with the path without the src on it.
    66  		dstPath := filepath.Join(dst, path[len(src):])
    67  
    68  		// we don't want to try and copy the same file over itself.
    69  		if eq, err := SameFile(path, dstPath); eq {
    70  			return nil
    71  		} else if err != nil {
    72  			return err
    73  		}
    74  
    75  		// If we have a directory, make that subdirectory, then continue
    76  		// the walk.
    77  		if info.IsDir() {
    78  			if path == filepath.Join(src, dst) {
    79  				// dst is in src; don't walk it.
    80  				return nil
    81  			}
    82  
    83  			if err := os.MkdirAll(dstPath, 0755); err != nil {
    84  				return err
    85  			}
    86  
    87  			return nil
    88  		}
    89  
    90  		// If the current path is a symlink, recreate the symlink relative to
    91  		// the dst directory
    92  		if info.Mode()&os.ModeSymlink == os.ModeSymlink {
    93  			target, err := os.Readlink(path)
    94  			if err != nil {
    95  				return err
    96  			}
    97  
    98  			return os.Symlink(target, dstPath)
    99  		}
   100  
   101  		// If we have a file, copy the contents.
   102  		srcF, err := os.Open(path)
   103  		if err != nil {
   104  			return err
   105  		}
   106  		defer srcF.Close()
   107  
   108  		dstF, err := os.Create(dstPath)
   109  		if err != nil {
   110  			return err
   111  		}
   112  		defer dstF.Close()
   113  
   114  		if _, err := io.Copy(dstF, srcF); err != nil {
   115  			return err
   116  		}
   117  
   118  		// Chmod it
   119  		return os.Chmod(dstPath, info.Mode())
   120  	}
   121  
   122  	return filepath.Walk(src, walkFn)
   123  }
   124  
   125  // SameFile returns true if the two given paths refer to the same physical
   126  // file on disk, using the unique file identifiers from the underlying
   127  // operating system. For example, on Unix systems this checks whether the
   128  // two files are on the same device and have the same inode.
   129  func SameFile(a, b string) (bool, error) {
   130  	if a == b {
   131  		return true, nil
   132  	}
   133  
   134  	aInfo, err := os.Lstat(a)
   135  	if err != nil {
   136  		if os.IsNotExist(err) {
   137  			return false, nil
   138  		}
   139  		return false, err
   140  	}
   141  
   142  	bInfo, err := os.Lstat(b)
   143  	if err != nil {
   144  		if os.IsNotExist(err) {
   145  			return false, nil
   146  		}
   147  		return false, err
   148  	}
   149  
   150  	return os.SameFile(aInfo, bInfo), nil
   151  }