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 }