github.com/psexton/git-lfs@v2.1.1-0.20170517224304-289a18b2bc53+incompatible/tools/filetools.go (about)

     1  // Package tools contains other helper functions too small to justify their own package
     2  // NOTE: Subject to change, do not rely on this package from outside git-lfs source
     3  package tools
     4  
     5  import (
     6  	"bufio"
     7  	"encoding/hex"
     8  	"fmt"
     9  	"io"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/git-lfs/git-lfs/filepathfilter"
    17  )
    18  
    19  // FileOrDirExists determines if a file/dir exists, returns IsDir() results too.
    20  func FileOrDirExists(path string) (exists bool, isDir bool) {
    21  	fi, err := os.Stat(path)
    22  	if err != nil {
    23  		return false, false
    24  	} else {
    25  		return true, fi.IsDir()
    26  	}
    27  }
    28  
    29  // FileExists determines if a file (NOT dir) exists.
    30  func FileExists(path string) bool {
    31  	ret, isDir := FileOrDirExists(path)
    32  	return ret && !isDir
    33  }
    34  
    35  // DirExists determines if a dir (NOT file) exists.
    36  func DirExists(path string) bool {
    37  	ret, isDir := FileOrDirExists(path)
    38  	return ret && isDir
    39  }
    40  
    41  // FileExistsOfSize determines if a file exists and is of a specific size.
    42  func FileExistsOfSize(path string, sz int64) bool {
    43  	fi, err := os.Stat(path)
    44  
    45  	if err != nil {
    46  		return false
    47  	}
    48  
    49  	return !fi.IsDir() && fi.Size() == sz
    50  }
    51  
    52  // ResolveSymlinks ensures that if the path supplied is a symlink, it is
    53  // resolved to the actual concrete path
    54  func ResolveSymlinks(path string) string {
    55  	if len(path) == 0 {
    56  		return path
    57  	}
    58  
    59  	if resolved, err := filepath.EvalSymlinks(path); err == nil {
    60  		return resolved
    61  	}
    62  	return path
    63  }
    64  
    65  // RenameFileCopyPermissions moves srcfile to destfile, replacing destfile if
    66  // necessary and also copying the permissions of destfile if it already exists
    67  func RenameFileCopyPermissions(srcfile, destfile string) error {
    68  	info, err := os.Stat(destfile)
    69  	if os.IsNotExist(err) {
    70  		// no original file
    71  	} else if err != nil {
    72  		return err
    73  	} else {
    74  		if err := os.Chmod(srcfile, info.Mode()); err != nil {
    75  			return fmt.Errorf("can't set filemode on file %q: %v", srcfile, err)
    76  		}
    77  	}
    78  
    79  	if err := os.Rename(srcfile, destfile); err != nil {
    80  		return fmt.Errorf("cannot replace %q with %q: %v", destfile, srcfile, err)
    81  	}
    82  	return nil
    83  }
    84  
    85  // CleanPaths splits the given `paths` argument by the delimiter argument, and
    86  // then "cleans" that path according to the path.Clean function (see
    87  // https://golang.org/pkg/path#Clean).
    88  // Note always cleans to '/' path separators regardless of platform (git friendly)
    89  func CleanPaths(paths, delim string) (cleaned []string) {
    90  	// If paths is an empty string, splitting it will yield [""], which will
    91  	// become the path ".". To avoid this, bail out if trimmed paths
    92  	// argument is empty.
    93  	if paths = strings.TrimSpace(paths); len(paths) == 0 {
    94  		return
    95  	}
    96  
    97  	for _, part := range strings.Split(paths, delim) {
    98  		part = strings.TrimSpace(part)
    99  
   100  		cleaned = append(cleaned, path.Clean(part))
   101  	}
   102  
   103  	return cleaned
   104  }
   105  
   106  // VerifyFileHash reads a file and verifies whether the SHA is correct
   107  // Returns an error if there is a problem
   108  func VerifyFileHash(oid, path string) error {
   109  	f, err := os.Open(path)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	defer f.Close()
   114  
   115  	h := NewLfsContentHash()
   116  	_, err = io.Copy(h, f)
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	calcOid := hex.EncodeToString(h.Sum(nil))
   122  	if calcOid != oid {
   123  		return fmt.Errorf("File %q has an invalid hash %s, expected %s", path, calcOid, oid)
   124  	}
   125  
   126  	return nil
   127  }
   128  
   129  // FastWalkCallback is the signature for the callback given to FastWalkGitRepo()
   130  type FastWalkCallback func(parentDir string, info os.FileInfo, err error)
   131  
   132  // FastWalkGitRepo is a more optimal implementation of filepath.Walk for a Git
   133  // repo. The callback guaranteed to be called sequentially. The function returns
   134  // once all files and errors have triggered callbacks.
   135  // It differs in the following ways:
   136  //  * Uses goroutines to parallelise large dirs and descent into subdirs
   137  //  * Does not provide sorted output; parents will always be before children but
   138  //    there are no other guarantees. Use parentDir argument in the callback to
   139  //    determine absolute path rather than tracking it yourself
   140  //  * Automatically ignores any .git directories
   141  //  * Respects .gitignore contents and skips ignored files/dirs
   142  func FastWalkGitRepo(dir string, cb FastWalkCallback) {
   143  	// Ignore all git metadata including subrepos
   144  	excludePaths := []filepathfilter.Pattern{
   145  		filepathfilter.NewPattern(".git"),
   146  		filepathfilter.NewPattern(filepath.Join("**", ".git")),
   147  	}
   148  
   149  	fileCh := fastWalkWithExcludeFiles(dir, ".gitignore", excludePaths)
   150  	for file := range fileCh {
   151  		cb(file.ParentDir, file.Info, file.Err)
   152  	}
   153  }
   154  
   155  // Returned from FastWalk with parent directory context
   156  // This is needed because FastWalk can provide paths out of order so the
   157  // parent dir cannot be implied
   158  type fastWalkInfo struct {
   159  	ParentDir string
   160  	Info      os.FileInfo
   161  	Err       error
   162  }
   163  
   164  // fastWalkWithExcludeFiles walks the contents of a dir, respecting
   165  // include/exclude patterns and also loading new exlude patterns from files
   166  // named excludeFilename in directories walked
   167  func fastWalkWithExcludeFiles(dir, excludeFilename string,
   168  	excludePaths []filepathfilter.Pattern) <-chan fastWalkInfo {
   169  	fiChan := make(chan fastWalkInfo, 256)
   170  	go fastWalkFromRoot(dir, excludeFilename, excludePaths, fiChan)
   171  	return fiChan
   172  }
   173  
   174  func fastWalkFromRoot(dir string, excludeFilename string,
   175  	excludePaths []filepathfilter.Pattern, fiChan chan<- fastWalkInfo) {
   176  
   177  	dirFi, err := os.Stat(dir)
   178  	if err != nil {
   179  		fiChan <- fastWalkInfo{Err: err}
   180  		return
   181  	}
   182  
   183  	// This waitgroup will be incremented for each nested goroutine
   184  	var waitg sync.WaitGroup
   185  	fastWalkFileOrDir(filepath.Dir(dir), dirFi, excludeFilename, excludePaths, fiChan, &waitg)
   186  	waitg.Wait()
   187  	close(fiChan)
   188  }
   189  
   190  // fastWalkFileOrDir is the main recursive implementation of fast walk
   191  // Sends the file/dir and any contents to the channel so long as it passes the
   192  // include/exclude filter. If a dir, parses any excludeFilename found and updates
   193  // the excludePaths with its content before (parallel) recursing into contents
   194  // Also splits large directories into multiple goroutines.
   195  // Increments waitg.Add(1) for each new goroutine launched internally
   196  func fastWalkFileOrDir(parentDir string, itemFi os.FileInfo, excludeFilename string,
   197  	excludePaths []filepathfilter.Pattern, fiChan chan<- fastWalkInfo, waitg *sync.WaitGroup) {
   198  
   199  	fullPath := filepath.Join(parentDir, itemFi.Name())
   200  
   201  	if !filepathfilter.NewFromPatterns(nil, excludePaths).Allows(fullPath) {
   202  		return
   203  	}
   204  
   205  	fiChan <- fastWalkInfo{ParentDir: parentDir, Info: itemFi}
   206  
   207  	if !itemFi.IsDir() {
   208  		// Nothing more to do if this is not a dir
   209  		return
   210  	}
   211  
   212  	if len(excludeFilename) > 0 {
   213  		possibleExcludeFile := filepath.Join(fullPath, excludeFilename)
   214  		var err error
   215  		excludePaths, err = loadExcludeFilename(possibleExcludeFile, fullPath, excludePaths)
   216  		if err != nil {
   217  			fiChan <- fastWalkInfo{Err: err}
   218  		}
   219  	}
   220  
   221  	// The absolute optimal way to scan would be File.Readdirnames but we
   222  	// still need the Stat() to know whether something is a dir, so use
   223  	// File.Readdir instead. Means we can provide os.FileInfo to callers like
   224  	// filepath.Walk as a bonus.
   225  	df, err := os.Open(fullPath)
   226  	if err != nil {
   227  		fiChan <- fastWalkInfo{Err: err}
   228  		return
   229  	}
   230  	defer df.Close()
   231  
   232  	// The number of items in a dir we process in each goroutine
   233  	jobSize := 100
   234  	for children, err := df.Readdir(jobSize); err == nil; children, err = df.Readdir(jobSize) {
   235  		// Parallelise all dirs, and chop large dirs into batches
   236  		waitg.Add(1)
   237  		go func(subitems []os.FileInfo) {
   238  			for _, childFi := range subitems {
   239  				fastWalkFileOrDir(fullPath, childFi, excludeFilename, excludePaths, fiChan, waitg)
   240  			}
   241  			waitg.Done()
   242  		}(children)
   243  
   244  	}
   245  	if err != nil && err != io.EOF {
   246  		fiChan <- fastWalkInfo{Err: err}
   247  	}
   248  }
   249  
   250  // loadExcludeFilename reads the given file in gitignore format and returns a
   251  // revised array of exclude paths if there are any changes.
   252  // If any changes are made a copy of the array is taken so the original is not
   253  // modified
   254  func loadExcludeFilename(filename, parentDir string, excludePaths []filepathfilter.Pattern) ([]filepathfilter.Pattern, error) {
   255  	f, err := os.OpenFile(filename, os.O_RDONLY, 0644)
   256  	if err != nil {
   257  		if os.IsNotExist(err) {
   258  			return excludePaths, nil
   259  		}
   260  		return excludePaths, err
   261  	}
   262  	defer f.Close()
   263  
   264  	retPaths := excludePaths
   265  	modified := false
   266  
   267  	scanner := bufio.NewScanner(f)
   268  	for scanner.Scan() {
   269  		line := strings.TrimSpace(scanner.Text())
   270  		// Skip blanks, comments and negations (not supported right now)
   271  		if len(line) == 0 || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "!") {
   272  			continue
   273  		}
   274  
   275  		if !modified {
   276  			// copy on write
   277  			retPaths = make([]filepathfilter.Pattern, len(excludePaths))
   278  			copy(retPaths, excludePaths)
   279  			modified = true
   280  		}
   281  
   282  		path := line
   283  		// Add pattern in context if exclude has separator, or no wildcard
   284  		// Allow for both styles of separator at this point
   285  		if strings.ContainsAny(path, "/\\") ||
   286  			!strings.Contains(path, "*") {
   287  			path = filepath.Join(parentDir, line)
   288  		}
   289  		retPaths = append(retPaths, filepathfilter.NewPattern(path))
   290  	}
   291  
   292  	return retPaths, nil
   293  }
   294  
   295  // SetFileWriteFlag changes write permissions on a file
   296  // Used to make a file read-only or not. When writeEnabled = false, the write
   297  // bit is removed for all roles. When writeEnabled = true, the behaviour is
   298  // different per platform:
   299  // On Mac & Linux, the write bit is set only on the owner as per default umask.
   300  // All other bits are unaffected.
   301  // On Windows, all the write bits are set since Windows doesn't support Unix permissions.
   302  func SetFileWriteFlag(path string, writeEnabled bool) error {
   303  	stat, err := os.Stat(path)
   304  	if err != nil {
   305  		return err
   306  	}
   307  	mode := uint32(stat.Mode())
   308  
   309  	if (writeEnabled && (mode&0200) > 0) ||
   310  		(!writeEnabled && (mode&0222) == 0) {
   311  		// no change needed
   312  		return nil
   313  	}
   314  
   315  	if writeEnabled {
   316  		mode = mode | 0200 // set owner write only
   317  		// Go's own Chmod makes Windows set all though
   318  	} else {
   319  		mode = mode &^ 0222 // disable all write
   320  	}
   321  	return os.Chmod(path, os.FileMode(mode))
   322  }