aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/fs/fs.go
diff options
context:
space:
mode:
Diffstat (limited to 'cli/internal/fs/fs.go')
-rw-r--r--cli/internal/fs/fs.go191
1 files changed, 191 insertions, 0 deletions
diff --git a/cli/internal/fs/fs.go b/cli/internal/fs/fs.go
new file mode 100644
index 0000000..77804c0
--- /dev/null
+++ b/cli/internal/fs/fs.go
@@ -0,0 +1,191 @@
+package fs
+
+import (
+ "io"
+ "io/ioutil"
+ "log"
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+
+ "github.com/pkg/errors"
+ "github.com/vercel/turbo/cli/internal/util"
+)
+
+// https://github.com/thought-machine/please/blob/master/src/fs/fs.go
+
+// DirPermissions are the default permission bits we apply to directories.
+const DirPermissions = os.ModeDir | 0775
+
+// EnsureDir ensures that the directory of the given file has been created.
+func EnsureDir(filename string) error {
+ dir := filepath.Dir(filename)
+ err := os.MkdirAll(dir, DirPermissions)
+ if err != nil && FileExists(dir) {
+ // It looks like this is a file and not a directory. Attempt to remove it; this can
+ // happen in some cases if you change a rule from outputting a file to a directory.
+ log.Printf("Attempting to remove file %s; a subdirectory is required", dir)
+ if err2 := os.Remove(dir); err2 == nil {
+ err = os.MkdirAll(dir, DirPermissions)
+ } else {
+ return err
+ }
+ }
+ return err
+}
+
+var nonRelativeSentinel string = ".." + string(filepath.Separator)
+
+// DirContainsPath returns true if the path 'target' is contained within 'dir'
+// Expects both paths to be absolute and does not verify that either path exists.
+func DirContainsPath(dir string, target string) (bool, error) {
+ // On windows, trying to get a relative path between files on different volumes
+ // is an error. We don't care about the error, it's good enough for us to say
+ // that one path doesn't contain the other if they're on different volumes.
+ if runtime.GOOS == "windows" && filepath.VolumeName(dir) != filepath.VolumeName(target) {
+ return false, nil
+ }
+ // In Go, filepath.Rel can return a path that starts with "../" or equivalent.
+ // Checking filesystem-level contains can get extremely complicated
+ // (see https://github.com/golang/dep/blob/f13583b555deaa6742f141a9c1185af947720d60/internal/fs/fs.go#L33)
+ // As a compromise, rely on the stdlib to generate a relative path and then check
+ // if the first step is "../".
+ rel, err := filepath.Rel(dir, target)
+ if err != nil {
+ return false, err
+ }
+ return !strings.HasPrefix(rel, nonRelativeSentinel), nil
+}
+
+// PathExists returns true if the given path exists, as a file or a directory.
+func PathExists(filename string) bool {
+ _, err := os.Lstat(filename)
+ return err == nil
+}
+
+// FileExists returns true if the given path exists and is a file.
+func FileExists(filename string) bool {
+ info, err := os.Lstat(filename)
+ return err == nil && !info.IsDir()
+}
+
+// CopyFile copies a file from 'from' to 'to', with an attempt to perform a copy & rename
+// to avoid chaos if anything goes wrong partway.
+func CopyFile(from *LstatCachedFile, to string) error {
+ fromMode, err := from.GetMode()
+ if err != nil {
+ return errors.Wrapf(err, "getting mode for %v", from.Path)
+ }
+ if fromMode&os.ModeSymlink != 0 {
+ target, err := from.Path.Readlink()
+ if err != nil {
+ return errors.Wrapf(err, "reading link target for %v", from.Path)
+ }
+ if err := EnsureDir(to); err != nil {
+ return err
+ }
+ if _, err := os.Lstat(to); err == nil {
+ // target link file exist, should remove it first
+ err := os.Remove(to)
+ if err != nil {
+ return err
+ }
+ }
+ return os.Symlink(target, to)
+ }
+ fromFile, err := from.Path.Open()
+ if err != nil {
+ return err
+ }
+ defer util.CloseAndIgnoreError(fromFile)
+ return writeFileFromStream(fromFile, to, fromMode)
+}
+
+// writeFileFromStream writes data from a reader to the file named 'to', with an attempt to perform
+// a copy & rename to avoid chaos if anything goes wrong partway.
+func writeFileFromStream(fromFile io.Reader, to string, mode os.FileMode) error {
+ dir, file := filepath.Split(to)
+ if dir != "" {
+ if err := os.MkdirAll(dir, DirPermissions); err != nil {
+ return err
+ }
+ }
+ tempFile, err := ioutil.TempFile(dir, file)
+ if err != nil {
+ return err
+ }
+ if _, err := io.Copy(tempFile, fromFile); err != nil {
+ return err
+ }
+ if err := tempFile.Close(); err != nil {
+ return err
+ }
+ // OK, now file is written; adjust permissions appropriately.
+ if mode == 0 {
+ mode = 0664
+ }
+ if err := os.Chmod(tempFile.Name(), mode); err != nil {
+ return err
+ }
+ // And move it to its final destination.
+ return renameFile(tempFile.Name(), to)
+}
+
+// IsDirectory checks if a given path is a directory
+func IsDirectory(path string) bool {
+ info, err := os.Stat(path)
+ return err == nil && info.IsDir()
+}
+
+// Try to gracefully rename the file as the os.Rename does not work across
+// filesystems and on most Linux systems /tmp is mounted as tmpfs
+func renameFile(from, to string) (err error) {
+ err = os.Rename(from, to)
+ if err == nil {
+ return nil
+ }
+ err = copyFile(from, to)
+ if err != nil {
+ return err
+ }
+ err = os.RemoveAll(from)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func copyFile(from, to string) (err error) {
+ in, err := os.Open(from)
+ if err != nil {
+ return err
+ }
+ defer in.Close()
+
+ out, err := os.Create(to)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if e := out.Close(); e != nil {
+ err = e
+ }
+ }()
+
+ _, err = io.Copy(out, in)
+ if err != nil {
+ return err
+ }
+
+ si, err := os.Stat(from)
+ if err != nil {
+ return err
+ }
+ err = os.Chmod(to, si.Mode())
+ if err != nil {
+ return err
+ }
+
+ return nil
+}