internal/copydir: Factor out our recursive directory copy for reuse
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.
This commit is contained in:
parent
d13001830b
commit
072c6d9aed
|
@ -1,4 +1,4 @@
|
||||||
package initwd
|
package copydir
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
@ -7,9 +7,32 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// copyDir copies the src directory contents into dst. Both directories
|
// CopyDir recursively copies all of the files within the directory given in
|
||||||
// should already exist.
|
// src to the directory given in dst.
|
||||||
func copyDir(dst, src string) error {
|
//
|
||||||
|
// 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)
|
src, err := filepath.EvalSymlinks(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -38,7 +61,7 @@ func copyDir(dst, src string) error {
|
||||||
dstPath := filepath.Join(dst, path[len(src):])
|
dstPath := filepath.Join(dst, path[len(src):])
|
||||||
|
|
||||||
// we don't want to try and copy the same file over itself.
|
// we don't want to try and copy the same file over itself.
|
||||||
if eq, err := sameFile(path, dstPath); eq {
|
if eq, err := SameFile(path, dstPath); eq {
|
||||||
return nil
|
return nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -94,14 +117,16 @@ func copyDir(dst, src string) error {
|
||||||
return filepath.Walk(src, walkFn)
|
return filepath.Walk(src, walkFn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sameFile tried to determine if to paths are the same file.
|
// SameFile returns true if the two given paths refer to the same physical
|
||||||
// If the paths don't match, we lookup the inode on supported systems.
|
// file on disk, using the unique file identifiers from the underlying
|
||||||
func sameFile(a, b string) (bool, error) {
|
// 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 {
|
if a == b {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
aIno, err := inode(a)
|
aInfo, err := os.Lstat(a)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -109,7 +134,7 @@ func sameFile(a, b string) (bool, error) {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
bIno, err := inode(b)
|
bInfo, err := os.Lstat(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return false, nil
|
return false, nil
|
||||||
|
@ -117,9 +142,5 @@ func sameFile(a, b string) (bool, error) {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if aIno > 0 && aIno == bIno {
|
return os.SameFile(aInfo, bInfo), nil
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package initwd
|
package copydir
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -52,7 +52,7 @@ func TestCopyDir_symlinks(t *testing.T) {
|
||||||
targetDir := filepath.Join(tmpdir, "target")
|
targetDir := filepath.Join(tmpdir, "target")
|
||||||
os.Mkdir(targetDir, os.ModePerm)
|
os.Mkdir(targetDir, os.ModePerm)
|
||||||
|
|
||||||
err = copyDir(targetDir, moduleDir)
|
err = CopyDir(targetDir, moduleDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ func TestCopyDir_symlink_file(t *testing.T) {
|
||||||
targetDir := filepath.Join(tmpdir, "target")
|
targetDir := filepath.Join(tmpdir, "target")
|
||||||
os.Mkdir(targetDir, os.ModePerm)
|
os.Mkdir(targetDir, os.ModePerm)
|
||||||
|
|
||||||
err = copyDir(targetDir, moduleDir)
|
err = CopyDir(targetDir, moduleDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
|
@ -2,7 +2,6 @@ package initwd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/hashicorp/terraform/internal/earlyconfig"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
@ -10,8 +9,11 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/internal/earlyconfig"
|
||||||
|
|
||||||
version "github.com/hashicorp/go-version"
|
version "github.com/hashicorp/go-version"
|
||||||
"github.com/hashicorp/terraform-config-inspect/tfconfig"
|
"github.com/hashicorp/terraform-config-inspect/tfconfig"
|
||||||
|
"github.com/hashicorp/terraform/internal/copydir"
|
||||||
"github.com/hashicorp/terraform/internal/modsdir"
|
"github.com/hashicorp/terraform/internal/modsdir"
|
||||||
"github.com/hashicorp/terraform/registry"
|
"github.com/hashicorp/terraform/registry"
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
|
@ -172,7 +174,7 @@ func DirFromModule(rootDir, modulesDir, sourceAddr string, reg *registry.Client,
|
||||||
// We've found the module the user requested, which we must
|
// We've found the module the user requested, which we must
|
||||||
// now copy into rootDir so it can be used directly.
|
// now copy into rootDir so it can be used directly.
|
||||||
log.Printf("[TRACE] copying new root module from %s to %s", record.Dir, rootDir)
|
log.Printf("[TRACE] copying new root module from %s to %s", record.Dir, rootDir)
|
||||||
err := copyDir(rootDir, record.Dir)
|
err := copydir.CopyDir(rootDir, record.Dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
diags = diags.Append(tfdiags.Sourceless(
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
|
@ -292,7 +294,7 @@ func DirFromModule(rootDir, modulesDir, sourceAddr string, reg *registry.Client,
|
||||||
// We copy rather than "rename" here because renaming between directories
|
// We copy rather than "rename" here because renaming between directories
|
||||||
// can be tricky in edge-cases like network filesystems, etc.
|
// can be tricky in edge-cases like network filesystems, etc.
|
||||||
log.Printf("[TRACE] copying new module %s from %s to %s", newKey, record.Dir, instPath)
|
log.Printf("[TRACE] copying new module %s from %s to %s", newKey, record.Dir, instPath)
|
||||||
err := copyDir(instPath, tempPath)
|
err := copydir.CopyDir(instPath, tempPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
diags = diags.Append(tfdiags.Sourceless(
|
diags = diags.Append(tfdiags.Sourceless(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||||
getter "github.com/hashicorp/go-getter"
|
getter "github.com/hashicorp/go-getter"
|
||||||
|
"github.com/hashicorp/terraform/internal/copydir"
|
||||||
"github.com/hashicorp/terraform/registry/regsrc"
|
"github.com/hashicorp/terraform/registry/regsrc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@ func (g reusingGetter) getWithGoGetter(instPath, addr string) (string, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create directory %s: %s", instPath, err)
|
return "", fmt.Errorf("failed to create directory %s: %s", instPath, err)
|
||||||
}
|
}
|
||||||
err = copyDir(instPath, prevDir)
|
err = copydir.CopyDir(instPath, prevDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to copy from %s to %s: %s", prevDir, instPath, err)
|
return "", fmt.Errorf("failed to copy from %s to %s: %s", prevDir, instPath, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
// +build linux darwin openbsd netbsd solaris dragonfly
|
|
||||||
|
|
||||||
package initwd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
// lookup the inode of a file on posix systems
|
|
||||||
func inode(path string) (uint64, error) {
|
|
||||||
stat, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if st, ok := stat.Sys().(*syscall.Stat_t); ok {
|
|
||||||
return st.Ino, nil
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("could not determine file inode")
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
// +build freebsd
|
|
||||||
|
|
||||||
package initwd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"syscall"
|
|
||||||
)
|
|
||||||
|
|
||||||
// lookup the inode of a file on posix systems
|
|
||||||
func inode(path string) (uint64, error) {
|
|
||||||
stat, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if st, ok := stat.Sys().(*syscall.Stat_t); ok {
|
|
||||||
return uint64(st.Ino), nil
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("could not determine file inode")
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package initwd
|
|
||||||
|
|
||||||
// no syscall.Stat_t on windows, return 0 for inodes
|
|
||||||
func inode(path string) (uint64, error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/hashicorp/terraform/configs"
|
"github.com/hashicorp/terraform/configs"
|
||||||
"github.com/hashicorp/terraform/configs/configload"
|
"github.com/hashicorp/terraform/configs/configload"
|
||||||
"github.com/hashicorp/terraform/helper/logging"
|
"github.com/hashicorp/terraform/helper/logging"
|
||||||
|
"github.com/hashicorp/terraform/internal/copydir"
|
||||||
"github.com/hashicorp/terraform/registry"
|
"github.com/hashicorp/terraform/registry"
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
)
|
)
|
||||||
|
@ -543,7 +544,7 @@ func tempChdir(t *testing.T, sourceDir string) (string, func()) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := copyDir(tmpDir, sourceDir); err != nil {
|
if err := copydir.CopyDir(tmpDir, sourceDir); err != nil {
|
||||||
t.Fatalf("failed to copy fixture to temporary directory: %s", err)
|
t.Fatalf("failed to copy fixture to temporary directory: %s", err)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue