mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-15 19:22:46 -06:00
072c6d9aed
We've previously been copying this function around so it could remain unexported while being used in various packages. However, it's a non-trivial function with lots of specific assumptions built into it, so here we'll put it somewhere that other packages can depend on it _and_ document the assumptions it seems to be making for future reference. As a bonus, this now uses os.SameFile to detect when two paths point to the same physical file, instead of the slightly buggy local implementation we had before which only worked on Unix systems and did not correctly handle when the paths were on different physical devices. The copy of the function I extracted here is the one from internal/initwd, so this commit also includes the removal of that unexported version and updating the callers in that package to use at at this new location.
147 lines
3.7 KiB
Go
147 lines
3.7 KiB
Go
package copydir
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// CopyDir recursively copies all of the files within the directory given in
|
|
// src to the directory given in dst.
|
|
//
|
|
// Both directories should already exist. If the destination directory is
|
|
// non-empty then the new files will merge in with the old, overwriting any
|
|
// files that have a relative path in common between source and destination.
|
|
//
|
|
// Recursive copying of directories is inevitably a rather opinionated sort of
|
|
// operation, so this function won't be appropriate for all use-cases. Some
|
|
// of the "opinions" it has are described in the following paragraphs:
|
|
//
|
|
// Symlinks in the source directory are recreated with the same target in the
|
|
// destination directory. If the symlink is to a directory itself, that
|
|
// directory is not recursively visited for further copying.
|
|
//
|
|
// File and directory modes are not preserved exactly, but the executable
|
|
// flag is preserved for files on operating systems where it is significant.
|
|
//
|
|
// Any "dot files" it encounters along the way are skipped, even on platforms
|
|
// that do not normally ascribe special meaning to files with names starting
|
|
// with dots.
|
|
//
|
|
// Callers may rely on the above details and other undocumented details of
|
|
// this function, so if you intend to change it be sure to review the callers
|
|
// first and make sure they are compatible with the change you intend to make.
|
|
func CopyDir(dst, src string) error {
|
|
src, err := filepath.EvalSymlinks(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
walkFn := func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if path == src {
|
|
return nil
|
|
}
|
|
|
|
if strings.HasPrefix(filepath.Base(path), ".") {
|
|
// Skip any dot files
|
|
if info.IsDir() {
|
|
return filepath.SkipDir
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// The "path" has the src prefixed to it. We need to join our
|
|
// destination with the path without the src on it.
|
|
dstPath := filepath.Join(dst, path[len(src):])
|
|
|
|
// we don't want to try and copy the same file over itself.
|
|
if eq, err := SameFile(path, dstPath); eq {
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
// If we have a directory, make that subdirectory, then continue
|
|
// the walk.
|
|
if info.IsDir() {
|
|
if path == filepath.Join(src, dst) {
|
|
// dst is in src; don't walk it.
|
|
return nil
|
|
}
|
|
|
|
if err := os.MkdirAll(dstPath, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// If the current path is a symlink, recreate the symlink relative to
|
|
// the dst directory
|
|
if info.Mode()&os.ModeSymlink == os.ModeSymlink {
|
|
target, err := os.Readlink(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.Symlink(target, dstPath)
|
|
}
|
|
|
|
// If we have a file, copy the contents.
|
|
srcF, err := os.Open(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer srcF.Close()
|
|
|
|
dstF, err := os.Create(dstPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dstF.Close()
|
|
|
|
if _, err := io.Copy(dstF, srcF); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Chmod it
|
|
return os.Chmod(dstPath, info.Mode())
|
|
}
|
|
|
|
return filepath.Walk(src, walkFn)
|
|
}
|
|
|
|
// SameFile returns true if the two given paths refer to the same physical
|
|
// file on disk, using the unique file identifiers from the underlying
|
|
// operating system. For example, on Unix systems this checks whether the
|
|
// two files are on the same device and have the same inode.
|
|
func SameFile(a, b string) (bool, error) {
|
|
if a == b {
|
|
return true, nil
|
|
}
|
|
|
|
aInfo, err := os.Lstat(a)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
bInfo, err := os.Lstat(b)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
return os.SameFile(aInfo, bInfo), nil
|
|
}
|