github.com/joshdk/godel@v0.0.0-20170529232908-862138a45aee/layout/operations.go (about)

     1  // Copyright 2016 Palantir Technologies, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package layout
    16  
    17  import (
    18  	"crypto/sha256"
    19  	"encoding/hex"
    20  	"io"
    21  	"io/ioutil"
    22  	"os"
    23  	"path"
    24  
    25  	"github.com/pkg/errors"
    26  )
    27  
    28  // Move the file or directory at src to dst. Conceptually, this is equivalent to executing "mv src dst". dst must not
    29  // already exist and the path up to it must already exist. Uses os.Rename to execute the move, which means that there
    30  // may be platform-specific restrictions such as not being able to move the directory between different volumes.
    31  func Move(src, dst string) error {
    32  	if err := verifyDstPathSafe(dst); err != nil {
    33  		return errors.Wrapf(err, "Cannot move directory to path %s", dst)
    34  	}
    35  	if err := os.Rename(src, dst); err != nil {
    36  		return errors.Wrapf(err, "Failed to rename %s to %s", src, dst)
    37  	}
    38  	return nil
    39  }
    40  
    41  // CopyDir recursively copies the src directory to the path specified by dst. dst must not already exist and the path up
    42  // to it must already exist.
    43  func CopyDir(src, dst string) error {
    44  	if err := verifyDstPathSafe(dst); err != nil {
    45  		return errors.Wrapf(err, "Cannot copy directory to path %s", dst)
    46  	}
    47  
    48  	srcInfo, err := os.Stat(src)
    49  	if err != nil {
    50  		return errors.Wrapf(err, "Failed to stat source directory %s", src)
    51  	}
    52  
    53  	if err := os.Mkdir(dst, srcInfo.Mode()); err != nil {
    54  		return errors.Wrapf(err, "Failed to create destination directory %s", dst)
    55  	}
    56  
    57  	files, err := ioutil.ReadDir(src)
    58  	if err != nil {
    59  		return errors.Wrapf(err, "Failed to read directory %s", src)
    60  	}
    61  
    62  	for _, f := range files {
    63  		srcPath := path.Join(src, f.Name())
    64  		dstPath := path.Join(dst, f.Name())
    65  
    66  		if f.IsDir() {
    67  			err = CopyDir(srcPath, dstPath)
    68  		} else {
    69  			err = CopyFile(srcPath, dstPath)
    70  		}
    71  
    72  		if err != nil {
    73  			return errors.Wrapf(err, "Failed to copy %s to %s", srcPath, dstPath)
    74  		}
    75  	}
    76  
    77  	return nil
    78  }
    79  
    80  // CopyFile copies the file at src to dst. src must specify a regular file (non-directory, no special mode) that exists,
    81  // and dst must specify a path that does not yet exist, but whose parent directory does exist. The copied file will have
    82  // the same permissions as the original.
    83  func CopyFile(src, dst string) (rErr error) {
    84  	srcInfo, err := os.Stat(src)
    85  	if err != nil {
    86  		return errors.Wrapf(err, "Failed to stat source file %s", src)
    87  	}
    88  	if !srcInfo.Mode().IsRegular() {
    89  		return errors.Wrapf(err, "Source file %s is not a regular file, had mode: %v", src, srcInfo.Mode())
    90  	}
    91  	srcPerms := srcInfo.Mode().Perm()
    92  
    93  	srcFile, err := os.Open(src)
    94  	if err != nil {
    95  		return errors.Wrapf(err, "Failed to open %s", src)
    96  	}
    97  	defer func() {
    98  		if err := srcFile.Close(); err != nil {
    99  			rErr = errors.Wrapf(err, "failed to close %s in defer", src)
   100  		}
   101  	}()
   102  
   103  	if err := verifyDstPathSafe(dst); err != nil {
   104  		return errors.Wrapf(err, "cannot copy to destination path %s", dst)
   105  	}
   106  
   107  	dstFile, err := os.Create(dst)
   108  	if err != nil {
   109  		return errors.Wrapf(err, "failed to open %s", dst)
   110  	}
   111  	defer func() {
   112  		if err := dstFile.Close(); err != nil {
   113  			rErr = errors.Wrapf(err, "failed to close %s in defer")
   114  		}
   115  	}()
   116  
   117  	if _, err := io.Copy(dstFile, srcFile); err != nil {
   118  		return errors.Wrapf(err, "failed to copy %s to %s", src, dst)
   119  	}
   120  
   121  	if err := dstFile.Chmod(srcPerms); err != nil {
   122  		return errors.Wrapf(err, "failed to chmod %s to have permissions %v", dst, srcPerms)
   123  	}
   124  
   125  	return nil
   126  }
   127  
   128  // SyncDir syncs the contents of the provided directories such that the content of dstDir matches that of srcDir (except
   129  // for files that match a name in the provided skip slice).
   130  func SyncDir(srcDir, dstDir string, skip []string) (bool, error) {
   131  	modified := false
   132  
   133  	srcFiles, err := ioutil.ReadDir(srcDir)
   134  	if err != nil {
   135  		return modified, errors.Wrapf(err, "failed to read directory %s", srcDir)
   136  	}
   137  	srcFilesMap := toMap(srcFiles)
   138  
   139  	dstFiles, err := ioutil.ReadDir(dstDir)
   140  	if err != nil {
   141  		return modified, errors.Wrapf(err, "failed to read directory %s", dstDir)
   142  	}
   143  	dstFilesMap := toMap(dstFiles)
   144  
   145  	skipSet := toSet(skip)
   146  	for dstFileName, dstFileInfo := range dstFilesMap {
   147  		if _, ok := skipSet[dstFileName]; ok {
   148  			// skip if file name is in skip list
   149  			continue
   150  		}
   151  
   152  		remove := false
   153  		srcFilePath := path.Join(srcDir, dstFileName)
   154  		dstFilePath := path.Join(dstDir, dstFileName)
   155  
   156  		if currSrcFileInfo, ok := srcFilesMap[dstFileName]; !ok {
   157  			// if dst exists but src does not, remove dst
   158  			remove = true
   159  		} else if dstFileInfo.IsDir() != currSrcFileInfo.IsDir() {
   160  			// if dst file and src file are different types, remove dst
   161  			remove = true
   162  		} else if !dstFileInfo.IsDir() {
   163  			srcChecksum, err := checksum(srcFilePath)
   164  			if err != nil {
   165  				return modified, errors.Wrapf(err, "failed to compute checksum for %s", srcFilePath)
   166  			}
   167  			dstChecksum, err := checksum(dstFilePath)
   168  			if err != nil {
   169  				return modified, errors.Wrapf(err, "failed to compute checksum for %s", dstFilePath)
   170  			}
   171  
   172  			// if dst and src are both files and their checksums differ, remove dst
   173  			if srcChecksum != dstChecksum {
   174  				remove = true
   175  			}
   176  		} else {
   177  			// if dst and src both exist and are both directories, sync them recursively
   178  			recursiveModified, err := SyncDir(srcFilePath, dstFilePath, skip)
   179  			if err != nil {
   180  				return modified, errors.Wrapf(err, "failed to sync %s with %s", dstFilePath, srcFilePath)
   181  			}
   182  			modified = modified || recursiveModified
   183  		}
   184  
   185  		// remove path if it was marked for removal
   186  		if remove {
   187  			if err := os.RemoveAll(dstFilePath); err != nil {
   188  				return modified, errors.Wrapf(err, "failed to remove %s", dstFilePath)
   189  			}
   190  			modified = true
   191  		}
   192  	}
   193  
   194  	for srcFileName, srcFileInfo := range srcFilesMap {
   195  		if _, ok := skipSet[srcFileName]; ok {
   196  			// skip if file name is in skip list
   197  			continue
   198  		}
   199  
   200  		srcFilePath := path.Join(srcDir, srcFileName)
   201  		dstFilePath := path.Join(dstDir, srcFileName)
   202  
   203  		if _, err := os.Stat(dstFilePath); os.IsNotExist(err) {
   204  			// if path does not exist at destination, copy source version
   205  			var err error
   206  			if srcFileInfo.IsDir() {
   207  				err = CopyDir(srcFilePath, dstFilePath)
   208  			} else {
   209  				err = CopyFile(srcFilePath, dstFilePath)
   210  			}
   211  			if err != nil {
   212  				return modified, errors.Wrapf(err, "failed to copy %s to %s", srcFilePath, dstFilePath)
   213  			}
   214  			modified = true
   215  		} else if err != nil {
   216  			return modified, errors.Wrapf(err, "failed to stat %s", dstFilePath)
   217  		}
   218  	}
   219  
   220  	return modified, nil
   221  }
   222  
   223  func toSet(input []string) map[string]struct{} {
   224  	s := make(map[string]struct{}, len(input))
   225  	for _, curr := range input {
   226  		s[curr] = struct{}{}
   227  	}
   228  	return s
   229  }
   230  
   231  func toMap(input []os.FileInfo) map[string]os.FileInfo {
   232  	m := make(map[string]os.FileInfo, len(input))
   233  	for _, curr := range input {
   234  		m[curr.Name()] = curr
   235  	}
   236  	return m
   237  }
   238  
   239  func checksum(p string) (string, error) {
   240  	f, err := os.Open(p)
   241  	if err != nil {
   242  		return "", errors.Wrapf(err, "failed to open %s", p)
   243  	}
   244  	h := sha256.New()
   245  	if _, err := io.Copy(h, f); err != nil {
   246  		return "", errors.Wrapf(err, "failed to copy file to hash buffer")
   247  	}
   248  	return hex.EncodeToString(h.Sum(nil)), nil
   249  }
   250  
   251  // SyncDirAdditive copies all of the files and directories in src that are not in dst. Directories that are present in
   252  // both are handled recursively. Basically a recursive merge with source preservation.
   253  func SyncDirAdditive(src, dst string) error {
   254  	srcInfos, err := ioutil.ReadDir(src)
   255  	if err != nil {
   256  		return errors.Wrapf(err, "failed to open %s", src)
   257  	}
   258  
   259  	for _, srcInfo := range srcInfos {
   260  		srcPath := path.Join(src, srcInfo.Name())
   261  		dstPath := path.Join(dst, srcInfo.Name())
   262  
   263  		if dstInfo, err := os.Stat(dstPath); os.IsNotExist(err) {
   264  			// safe to copy
   265  			if srcInfo.IsDir() {
   266  				err = CopyDir(srcPath, dstPath)
   267  			} else {
   268  				err = CopyFile(srcPath, dstPath)
   269  			}
   270  			if err != nil {
   271  				return errors.Wrapf(err, "failed to copy %s to %s", srcPath, dstPath)
   272  			}
   273  		} else if err != nil {
   274  			return errors.Wrapf(err, "failed to stat %s", dstPath)
   275  		} else if srcInfo.IsDir() && dstInfo.IsDir() {
   276  			// if source and destination are both directories, sync recursively
   277  			if err = SyncDirAdditive(srcPath, dstPath); err != nil {
   278  				return errors.Wrapf(err, "failed to sync %s to %s", srcPath, dstPath)
   279  			}
   280  		}
   281  	}
   282  	return nil
   283  }
   284  
   285  func VerifyDirExists(dir string) error {
   286  	return verifyPath(dir, path.Base(dir), true, false)
   287  }
   288  
   289  func verifyPath(p, expectedName string, isDir bool, optional bool) error {
   290  	if path.Base(p) != expectedName {
   291  		return errors.Errorf("%s is not a path to %s", p, expectedName)
   292  	}
   293  
   294  	if fi, err := os.Stat(p); err != nil {
   295  		if os.IsNotExist(err) {
   296  			if !optional {
   297  				return errors.Wrapf(err, "%s does not exist", p)
   298  			}
   299  			// path does not exist, but it is optional so is okay
   300  			return nil
   301  		}
   302  		return errors.Wrapf(err, "failed to stat %s", p)
   303  	} else if currIsDir := fi.IsDir(); currIsDir != isDir {
   304  		return errors.Errorf("IsDir for %s returned wrong value: expected %v, was %v", p, isDir, currIsDir)
   305  	}
   306  
   307  	return nil
   308  }
   309  
   310  // verifyDstPathSafe verifies that the provided destination path does not exist, but that the path to its parent does.
   311  func verifyDstPathSafe(dst string) error {
   312  	if _, err := os.Stat(dst); !os.IsNotExist(err) {
   313  		return errors.Wrapf(err, "Destination path %s already exists", dst)
   314  	}
   315  	if _, err := os.Stat(path.Dir(dst)); os.IsNotExist(err) {
   316  		return errors.Wrapf(err, "Parent directory of destination path %s does not exist", dst)
   317  	}
   318  	return nil
   319  }