diff --git a/pkg/minikube/assets/vm_assets.go b/pkg/minikube/assets/vm_assets.go index fbc405e19704..c10ff6f79cf3 100644 --- a/pkg/minikube/assets/vm_assets.go +++ b/pkg/minikube/assets/vm_assets.go @@ -23,6 +23,7 @@ import ( "io" "os" "path" + "time" "github.com/golang/glog" "github.com/pkg/errors" @@ -36,6 +37,7 @@ type CopyableFile interface { GetTargetDir() string GetTargetName() string GetPermissions() string + GetModTime() time.Time } // BaseAsset is the base asset class @@ -44,6 +46,7 @@ type BaseAsset struct { TargetDir string TargetName string Permissions string + ModTime time.Time } // GetAssetName returns asset name @@ -66,6 +69,11 @@ func (b *BaseAsset) GetPermissions() string { return b.Permissions } +// GetModTime returns mod time +func (b *BaseAsset) GetModTime() time.Time { + return b.ModTime +} + // FileAsset is an asset using a file type FileAsset struct { BaseAsset @@ -104,6 +112,15 @@ func (f *FileAsset) GetLength() (flen int) { return int(fi.Size()) } +// GetModTime returns modification timeof the file +func (f *FileAsset) GetModTime() time.Time { + fi, err := os.Stat(f.AssetName) + if err != nil { + return time.Time{} + } + return fi.ModTime() +} + func (f *FileAsset) Read(p []byte) (int, error) { if f.reader == nil { return 0, errors.New("Error attempting FileAsset.Read, FileAsset.reader uninitialized") diff --git a/pkg/minikube/command/ssh_runner.go b/pkg/minikube/command/ssh_runner.go index a341afb0498c..a0e74a75573c 100644 --- a/pkg/minikube/command/ssh_runner.go +++ b/pkg/minikube/command/ssh_runner.go @@ -23,6 +23,8 @@ import ( "io" "os/exec" "path" + "strconv" + "strings" "sync" "time" @@ -143,6 +145,11 @@ func (s *SSHRunner) RunCmd(cmd *exec.Cmd) (*RunResult, error) { // Copy copies a file to the remote over SSH. func (s *SSHRunner) Copy(f assets.CopyableFile) error { + dst := path.Join(path.Join(f.GetTargetDir(), f.GetTargetName())) + if s.fileExistsRemotely(f, dst) { + glog.Infof("Skipping copying %s as it already exists", f.GetAssetName()) + } + sess, err := s.c.NewSession() if err != nil { return errors.Wrap(err, "NewSession") @@ -156,7 +163,6 @@ func (s *SSHRunner) Copy(f assets.CopyableFile) error { // StdinPipe is closed. But let's use errgroup to make it explicit. var g errgroup.Group var copied int64 - dst := path.Join(path.Join(f.GetTargetDir(), f.GetTargetName())) glog.Infof("Transferring %d bytes to %s", f.GetLength(), dst) g.Go(func() error { @@ -189,6 +195,49 @@ func (s *SSHRunner) Copy(f assets.CopyableFile) error { return g.Wait() } +func (s *SSHRunner) fileExistsRemotely(f assets.CopyableFile, dst string) bool { + sess, err := s.c.NewSession() + if err != nil { + return false + } + + // check if sizes of the two files are the same + srcSize := f.GetLength() + size := fmt.Sprintf("ls -l %s | cut -d \" \" -f5", dst) + out, err := sess.CombinedOutput(size) + if err != nil { + return false + } + dstSize, err := strconv.Atoi(strings.Trim(string(out), "\n")) + if err != nil { + return false + } + if srcSize != dstSize { + return false + } + + sess, err = s.c.NewSession() + if err != nil { + return false + } + // ensure src file hasn't been modified since dst was copied over + srcModTime := f.GetModTime() + stat := "stat -c %Y" + fmt.Sprintf(" %s", dst) + out, err = sess.CombinedOutput(stat) + if err != nil { + return false + } + unix, err := strconv.Atoi(strings.Trim(string(out), "\n")) + if err != nil { + return false + } + dstModTime := time.Unix(int64(unix), 0) + if err != nil { + return false + } + return srcModTime.Before(dstModTime) +} + // teePrefix copies bytes from a reader to writer, logging each new line. func teePrefix(prefix string, r io.Reader, w io.Writer, logger func(format string, args ...interface{})) error { scanner := bufio.NewScanner(r)