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  }