github.com/sdboyer/gps@v0.16.3/internal/fs/fs.go (about)

     1  package fs
     2  
     3  import (
     4  	"errors"
     5  	"io"
     6  	"io/ioutil"
     7  	"os"
     8  	"path/filepath"
     9  	"runtime"
    10  	"syscall"
    11  )
    12  
    13  // RenameWithFallback attempts to rename a file or directory, but falls back to
    14  // copying in the event of a cross-link device error. If the fallback copy
    15  // succeeds, src is still removed, emulating normal rename behavior.
    16  func RenameWithFallback(src, dest string) error {
    17  	fi, err := os.Stat(src)
    18  	if err != nil {
    19  		return err
    20  	}
    21  
    22  	err = os.Rename(src, dest)
    23  	if err == nil {
    24  		return nil
    25  	}
    26  
    27  	terr, ok := err.(*os.LinkError)
    28  	if !ok {
    29  		return err
    30  	}
    31  
    32  	// Rename may fail if src and dest are on different devices; fall back to
    33  	// copy if we detect that case. syscall.EXDEV is the common name for the
    34  	// cross device link error which has varying output text across different
    35  	// operating systems.
    36  	var cerr error
    37  	if terr.Err == syscall.EXDEV {
    38  		if fi.IsDir() {
    39  			cerr = CopyDir(src, dest)
    40  		} else {
    41  			cerr = CopyFile(src, dest)
    42  		}
    43  	} else if runtime.GOOS == "windows" {
    44  		// In windows it can drop down to an operating system call that
    45  		// returns an operating system error with a different number and
    46  		// message. Checking for that as a fall back.
    47  		noerr, ok := terr.Err.(syscall.Errno)
    48  		// 0x11 (ERROR_NOT_SAME_DEVICE) is the windows error.
    49  		// See https://msdn.microsoft.com/en-us/library/cc231199.aspx
    50  		if ok && noerr == 0x11 {
    51  			if fi.IsDir() {
    52  				cerr = CopyDir(src, dest)
    53  			} else {
    54  				cerr = CopyFile(src, dest)
    55  			}
    56  		}
    57  	} else {
    58  		return terr
    59  	}
    60  
    61  	if cerr != nil {
    62  		return cerr
    63  	}
    64  
    65  	return os.RemoveAll(src)
    66  }
    67  
    68  var (
    69  	errSrcNotDir = errors.New("source is not a directory")
    70  	errDestExist = errors.New("destination already exists")
    71  )
    72  
    73  // CopyDir recursively copies a directory tree, attempting to preserve permissions.
    74  // Source directory must exist, destination directory must *not* exist.
    75  // Symlinks are ignored and skipped.
    76  func CopyDir(src string, dst string) (err error) {
    77  	src = filepath.Clean(src)
    78  	dst = filepath.Clean(dst)
    79  
    80  	si, err := os.Stat(src)
    81  	if err != nil {
    82  		return err
    83  	}
    84  	if !si.IsDir() {
    85  		return errSrcNotDir
    86  	}
    87  
    88  	_, err = os.Stat(dst)
    89  	if err != nil && !os.IsNotExist(err) {
    90  		return
    91  	}
    92  	if err == nil {
    93  		return errDestExist
    94  	}
    95  
    96  	err = os.MkdirAll(dst, si.Mode())
    97  	if err != nil {
    98  		return
    99  	}
   100  
   101  	entries, err := ioutil.ReadDir(src)
   102  	if err != nil {
   103  		return
   104  	}
   105  
   106  	for _, entry := range entries {
   107  		srcPath := filepath.Join(src, entry.Name())
   108  		dstPath := filepath.Join(dst, entry.Name())
   109  
   110  		if entry.IsDir() {
   111  			err = CopyDir(srcPath, dstPath)
   112  			if err != nil {
   113  				return
   114  			}
   115  		} else {
   116  			// This will include symlinks, which is what we want in all cases
   117  			// where gps is copying things.
   118  			err = CopyFile(srcPath, dstPath)
   119  			if err != nil {
   120  				return
   121  			}
   122  		}
   123  	}
   124  
   125  	return
   126  }
   127  
   128  // CopyFile copies the contents of the file named src to the file named
   129  // by dst. The file will be created if it does not already exist. If the
   130  // destination file exists, all it's contents will be replaced by the contents
   131  // of the source file. The file mode will be copied from the source and
   132  // the copied data is synced/flushed to stable storage.
   133  func CopyFile(src, dst string) (err error) {
   134  	in, err := os.Open(src)
   135  	if err != nil {
   136  		return
   137  	}
   138  	defer in.Close()
   139  
   140  	out, err := os.Create(dst)
   141  	if err != nil {
   142  		return
   143  	}
   144  	defer func() {
   145  		if e := out.Close(); e != nil {
   146  			err = e
   147  		}
   148  	}()
   149  
   150  	_, err = io.Copy(out, in)
   151  	if err != nil {
   152  		return
   153  	}
   154  
   155  	err = out.Sync()
   156  	if err != nil {
   157  		return
   158  	}
   159  
   160  	si, err := os.Stat(src)
   161  	if err != nil {
   162  		return
   163  	}
   164  	err = os.Chmod(dst, si.Mode())
   165  	if err != nil {
   166  		return
   167  	}
   168  
   169  	return
   170  }
   171