github.com/saymoon/flop@v0.1.6-0.20201205092451-00912199cc96/copy.go (about) 1 // Package flop implements file operations, taking most queues from GNU cp while trying to be 2 // more programmatically friendly. 3 package flop 4 5 import ( 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path/filepath" 11 "regexp" 12 "strconv" 13 14 "github.com/pkg/errors" 15 ) 16 17 // numberedBackupFile matches files that looks like file.ext.~1~ and uses a capture group to grab the number 18 var numberedBackupFile = regexp.MustCompile(`^.*\.~([0-9]{1,5})~$`) 19 20 // File describes a file on the filesystem. 21 type File struct { 22 // Path is the path to the src file. 23 Path string 24 // fileInfoOnInit is os.FileInfo for file when initialized. 25 fileInfoOnInit os.FileInfo 26 // existOnInit is true if the file exists when initialized. 27 existOnInit bool 28 // isDir is true if the file object is a directory. 29 isDir bool 30 } 31 32 // NewFile creates a new File. 33 func NewFile(path string) *File { 34 return &File{Path: path} 35 } 36 37 // setInfo will collect information about a File and populate the necessary fields. 38 func (f *File) setInfo() error { 39 info, err := os.Lstat(f.Path) 40 f.fileInfoOnInit = info 41 if err != nil { 42 if !os.IsNotExist(err) { 43 // if we are here then we have an error, but not one indicating the file does not exist 44 return err 45 } 46 } else { 47 f.existOnInit = true 48 if f.fileInfoOnInit.IsDir() { 49 f.isDir = true 50 } 51 } 52 return nil 53 } 54 55 func (f *File) isSymlink() bool { 56 if f.fileInfoOnInit.Mode()&os.ModeSymlink != 0 { 57 return true 58 } 59 return false 60 } 61 62 // shouldMakeParents returns true if we should make parent directories up to the dst 63 func (f *File) shouldMakeParents(opts Options) bool { 64 if opts.MkdirAll || opts.mkdirAll { 65 return true 66 } 67 68 if opts.Parents { 69 return true 70 } 71 72 if f.existOnInit { 73 return false 74 } 75 76 parent := filepath.Dir(filepath.Clean(f.Path)) 77 if _, err := os.Stat(parent); !os.IsNotExist(err) { 78 // dst does not exist but the direct parent does. make the target dir. 79 return true 80 } 81 82 return false 83 } 84 85 // shouldCopyParents returns true if parent directories from src should be copied into dst. 86 func (f *File) shouldCopyParents(opts Options) bool { 87 if !opts.Parents { 88 return false 89 } 90 return true 91 } 92 93 // SimpleCopy will src to dst with default Options. 94 func SimpleCopy(src, dst string) error { 95 return Copy(src, dst, Options{}) 96 } 97 98 // Copy will copy src to dst. Behavior is determined by the given Options. 99 func Copy(src, dst string, opts Options) (err error) { 100 opts.setLoggers() 101 srcFile, dstFile := NewFile(src), NewFile(dst) 102 103 // set src attributes 104 if err := srcFile.setInfo(); err != nil { 105 return errors.Wrapf(ErrCannotStatFile, "source file %s: %s", srcFile.Path, err) 106 } 107 if !srcFile.existOnInit { 108 return errors.Wrapf(ErrFileNotExist, "source file %s", srcFile.Path) 109 } 110 opts.logDebug("src %s existOnInit: %t", srcFile.Path, srcFile.existOnInit) 111 112 // stat dst attributes. handle errors later 113 _ = dstFile.setInfo() 114 opts.logDebug("dst %s existOnInit: %t", dstFile.Path, dstFile.existOnInit) 115 116 if dstFile.shouldMakeParents(opts) { 117 opts.mkdirAll = true 118 opts.DebugLogFunc("dst mkdirAll: true") 119 } 120 121 if opts.Parents { 122 if dstFile.existOnInit && !dstFile.isDir { 123 return ErrWithParentsDstMustBeDir 124 } 125 // TODO: figure out how to handle windows paths where they reference the full path like c:/dir 126 dstFile.Path = filepath.Join(dstFile.Path, srcFile.Path) 127 opts.logDebug("because of Parents option, setting dst Path to %s", dstFile.Path) 128 dstFile.isDir = srcFile.isDir 129 opts.Parents = false // ensure we don't keep creating parents on recursive calls 130 } 131 132 // copying src directory requires dst is also a directory, if it existOnInit 133 if srcFile.isDir && dstFile.existOnInit && !dstFile.isDir { 134 return errors.Wrapf( 135 ErrCannotOverwriteNonDir, "source directory %s, destination file %s", srcFile.Path, dstFile.Path) 136 } 137 138 // divide and conquer 139 switch { 140 case opts.Link: 141 return hardLink(srcFile, dstFile, opts.logDebug) 142 case srcFile.isSymlink(): 143 // FIXME: we really need to copy the pass through dest unless they specify otherwise...check the docs 144 return copyLink(srcFile, dstFile, opts.logDebug) 145 case srcFile.isDir: 146 return copyDir(srcFile, dstFile, opts) 147 default: 148 return copyFile(srcFile, dstFile, opts) 149 } 150 } 151 152 // hardLink creates a hard link to src at dst. 153 func hardLink(src, dst *File, logFunc func(format string, a ...interface{})) error { 154 logFunc("creating hard link to src %s at dst %s", src.Path, dst.Path) 155 return os.Link(src.Path, dst.Path) 156 } 157 158 // copyLink copies a symbolic link from src to dst. 159 func copyLink(src, dst *File, logFunc func(format string, a ...interface{})) error { 160 logFunc("copying sym link %s to %s", src.Path, dst.Path) 161 linkSrc, err := os.Readlink(src.Path) 162 if err != nil { 163 return err 164 } 165 return os.Symlink(linkSrc, dst.Path) 166 } 167 168 func copyDir(srcFile, dstFile *File, opts Options) error { 169 if !opts.Recursive { 170 return errors.Wrapf(ErrOmittingDir, "source directory %s", srcFile.Path) 171 } 172 if opts.mkdirAll { 173 opts.logDebug("making all dirs up to %s", dstFile.Path) 174 if err := os.MkdirAll(dstFile.Path, srcFile.fileInfoOnInit.Mode()); err != nil { 175 return err 176 } 177 } 178 179 srcDirEntries, err := ioutil.ReadDir(srcFile.Path) 180 if err != nil { 181 return errors.Wrapf(ErrReadingSrcDir, "source directory %s: %s", srcFile.Path, err) 182 } 183 184 for _, entry := range srcDirEntries { 185 newSrc := filepath.Join(srcFile.Path, entry.Name()) 186 newDst := filepath.Join(dstFile.Path, entry.Name()) 187 opts.logDebug("recursive cp with src %s and dst %s", newSrc, newDst) 188 if err := Copy( 189 newSrc, 190 newDst, 191 opts, 192 ); err != nil { 193 return err 194 } 195 } 196 return nil 197 } 198 199 func copyFile(srcFile, dstFile *File, opts Options) (err error) { 200 // shortcut if files are the same file 201 if os.SameFile(srcFile.fileInfoOnInit, dstFile.fileInfoOnInit) { 202 opts.logDebug("src %s is same file as dst %s", srcFile.Path, dstFile.Path) 203 return nil 204 } 205 206 // optionally make dst parent directories 207 if dstFile.shouldMakeParents(opts) { 208 // TODO: permissive perms here to ensure tmp file can write on nix.. ensure we are setting these correctly down the line or fix here 209 if err := os.MkdirAll(filepath.Dir(dstFile.Path), 0777); err != nil { 210 return err 211 } 212 } 213 214 if dstFile.existOnInit { 215 if dstFile.isDir { 216 // optionally append src file name to dst dir like cp does 217 if opts.AppendNameToPath { 218 dstFile.Path = filepath.Join(dstFile.Path, filepath.Base(srcFile.Path)) 219 opts.logDebug("because of AppendNameToPath option, setting dst path to %s", dstFile.Path) 220 } else { 221 return errors.Wrapf(ErrWritingFileToExistingDir, "destination directory %s", dstFile.Path) 222 } 223 } 224 225 // optionally do not clobber existing dst file 226 if opts.NoClobber { 227 opts.logDebug("dst %s exists, will not clobber", dstFile.Path) 228 return nil 229 } 230 231 if opts.Backup != "" { 232 if err := backupFile(dstFile, opts.Backup, opts); err != nil { 233 return err 234 } 235 } 236 237 } 238 239 srcFD, err := os.Open(srcFile.Path) 240 if err != nil { 241 return errors.Wrapf(ErrCannotOpenSrc, "source file %s: %s", srcFile.Path, err) 242 } 243 defer func() { 244 if closeErr := srcFD.Close(); closeErr != nil { 245 err = closeErr 246 } 247 }() 248 249 if opts.Atomic { 250 dstDir := filepath.Dir(dstFile.Path) 251 tmpFD, err := ioutil.TempFile(dstDir, "copyfile-") 252 defer closeAndRemove(tmpFD, opts.logDebug) 253 if err != nil { 254 return errors.Wrapf(ErrCannotCreateTmpFile, "destination directory %s: %s", dstDir, err) 255 } 256 opts.logDebug("created tmp file %s", tmpFD.Name()) 257 258 //copy src to tmp and cleanup on any error 259 opts.logInfo("copying src file %s to tmp file %s", srcFD.Name(), tmpFD.Name()) 260 if _, err := io.Copy(tmpFD, srcFD); err != nil { 261 return err 262 } 263 if err := tmpFD.Sync(); err != nil { 264 return err 265 } 266 if err := tmpFD.Close(); err != nil { 267 return err 268 } 269 270 // move tmp to dst 271 opts.logInfo("renaming tmp file %s to dst %s", tmpFD.Name(), dstFile.Path) 272 if err := os.Rename(tmpFD.Name(), dstFile.Path); err != nil { 273 return errors.Wrapf(ErrCannotRenameTempFile, "attempted to rename temp transfer file %s to %s", tmpFD.Name(), dstFile.Path) 274 } 275 } else { 276 dstFD, err := os.Create(dstFile.Path) 277 if err != nil { 278 return errors.Wrapf(ErrCannotOpenOrCreateDstFile, "destination file %s: %s", dstFile.Path, err) 279 } 280 defer func() { 281 if closeErr := dstFD.Close(); closeErr != nil { 282 err = closeErr 283 } 284 }() 285 286 opts.logInfo("copying src file %s to dst file %s", srcFD.Name(), dstFD.Name()) 287 if _, err = io.Copy(dstFD, srcFD); err != nil { 288 return err 289 } 290 if err := dstFD.Sync(); err != nil { 291 return err 292 } 293 } 294 295 return SetPermissions(dstFile, srcFile.fileInfoOnInit.Mode(), opts) 296 } 297 298 // backupFile will create a backup of the file using the chosen control method. See Options.Backup. 299 func backupFile(file *File, control string, opts Options) error { 300 // TODO: this func could be more efficient if it used file instead of the path but right now this causes panic 301 // do not copy if the file did not exist 302 if !file.existOnInit { 303 return nil 304 } 305 306 // simple backup 307 simple := func() error { 308 bkp := file.Path + "~" 309 opts.logDebug("creating simple backup file %s", bkp) 310 return Copy(file.Path, bkp, opts) 311 } 312 313 // next gives the next unused backup file number, 1 above the current highest 314 next := func() (int, error) { 315 // find general matches that look like numbered backup files 316 m, err := filepath.Glob(file.Path + ".~[0-9]*~") 317 if err != nil { 318 return -1, err 319 } 320 321 // get each backup file num substring, convert to int, track highest num 322 var highest int 323 for _, f := range m { 324 subs := numberedBackupFile.FindStringSubmatch(filepath.Base(f)) 325 if len(subs) > 1 { 326 if i, _ := strconv.Atoi(string(subs[1])); i > highest { 327 highest = i 328 } 329 } 330 } 331 return highest + 1, nil 332 } 333 334 // numbered backup 335 numbered := func(n int) error { 336 return Copy(file.Path, fmt.Sprintf("%s.~%d~", file.Path, n), opts) 337 } 338 339 switch control { 340 default: 341 return errors.Wrapf(ErrInvalidBackupControlValue, "backup value '%s'", control) 342 case "off": 343 return nil 344 case "simple": 345 return simple() 346 case "numbered": 347 i, err := next() 348 if err != nil { 349 return err 350 } 351 return numbered(i) 352 case "existing": 353 i, err := next() 354 if err != nil { 355 return err 356 } 357 358 if i > 1 { 359 return numbered(i) 360 } 361 return simple() 362 } 363 } 364 365 func closeAndRemove(file *os.File, logFunc func(format string, a ...interface{})) { 366 if file != nil { 367 if err := file.Close(); err != nil { 368 logFunc("err closing file %s: %s", file.Name(), err) 369 } 370 if err := os.Remove(file.Name()); err != nil { 371 logFunc("err removing file %s: %s", file.Name(), err) 372 } 373 } 374 }