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