package tools
import (
"encoding/hex"
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"github.com/git-lfs/git-lfs/v3/errors"
"github.com/git-lfs/git-lfs/v3/filepathfilter"
"github.com/git-lfs/git-lfs/v3/tr"
)
func FileOrDirExists(path string) (exists bool, isDir bool) {
fi, err := os.Stat(path)
if err != nil {
return false, false
} else {
return true, fi.IsDir()
}
}
func FileExists(path string) bool {
ret, isDir := FileOrDirExists(path)
return ret && !isDir
}
func DirExists(path string) bool {
ret, isDir := FileOrDirExists(path)
return ret && isDir
}
func FileExistsOfSize(path string, sz int64) bool {
fi, err := os.Stat(path)
if err != nil {
return false
}
return !fi.IsDir() && fi.Size() == sz
}
func ResolveSymlinks(path string) string {
if len(path) == 0 {
return path
}
if resolved, err := CanonicalizeSystemPath(path); err == nil {
return resolved
}
return path
}
func RenameFileCopyPermissions(srcfile, destfile string) error {
info, err := os.Stat(destfile)
if os.IsNotExist(err) {
} else if err != nil {
return err
} else {
if err := os.Chmod(srcfile, info.Mode()); err != nil {
return errors.New(tr.Tr.Get("can't set filemode on file %q: %v", srcfile, err))
}
}
if err := RobustRename(srcfile, destfile); err != nil {
return errors.New(tr.Tr.Get("cannot replace %q with %q: %v", destfile, srcfile, err))
}
return nil
}
func CleanPaths(paths, delim string) (cleaned []string) {
if paths = strings.TrimSpace(paths); len(paths) == 0 {
return
}
for _, part := range strings.Split(paths, delim) {
part = strings.TrimSpace(part)
for _, sep := range []string{`/`, `\`} {
if strings.HasSuffix(part, sep) {
part = strings.TrimSuffix(part, sep)
break
}
}
cleaned = append(cleaned, part)
}
return cleaned
}
type repositoryPermissionFetcher interface {
RepositoryPermissions(executable bool) os.FileMode
}
func MkdirAll(path string, config repositoryPermissionFetcher) error {
umask := 0777 & ^config.RepositoryPermissions(true)
return doWithUmask(int(umask), func() error {
return os.MkdirAll(path, config.RepositoryPermissions(true))
})
}
var (
currentUser func() (*user.User, error) = func() (*user.User, error) {
u := &user.User{}
u.HomeDir = os.Getenv("HOME")
return u, nil
}
lookupUser func(who string) (*user.User, error) = user.Lookup
lookupConfigHome func() string = func() string {
return os.Getenv("XDG_CONFIG_HOME")
}
)
func ExpandPath(path string, expand bool) (string, error) {
if len(path) == 0 || path[0] != '~' {
return path, nil
}
var username string
if slash := strings.Index(path[1:], "/"); slash > -1 {
username = path[1 : slash+1]
} else {
username = path[1:]
}
var (
who *user.User
err error
)
if len(username) == 0 {
who, err = currentUser()
} else {
who, err = lookupUser(username)
}
if err != nil {
return "", errors.Wrapf(err, tr.Tr.Get("could not find user %s", username))
}
homedir := who.HomeDir
if expand {
homedir, err = filepath.EvalSymlinks(homedir)
if err != nil {
return "", errors.Wrapf(err, tr.Tr.Get("cannot eval symlinks for %s", homedir))
}
}
return filepath.Join(homedir, path[len(username)+1:]), nil
}
func ExpandConfigPath(path, defaultPath string) (string, error) {
if path != "" {
return ExpandPath(path, false)
}
configHome := lookupConfigHome()
if configHome != "" {
return filepath.Join(configHome, defaultPath), nil
}
return ExpandPath(fmt.Sprintf("~/.config/%s", defaultPath), false)
}
func VerifyFileHash(oid, path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
h := NewLfsContentHash()
_, err = io.Copy(h, f)
if err != nil {
return err
}
calcOid := hex.EncodeToString(h.Sum(nil))
if calcOid != oid {
return errors.New(tr.Tr.Get("file %q has an invalid hash %s, expected %s", path, calcOid, oid))
}
return nil
}
type FastWalkCallback func(parentDir string, info os.FileInfo, err error)
func FastWalkDir(rootDir string, cb FastWalkCallback) {
fastWalkCallback(fastWalkWithExcludeFiles(rootDir), cb)
}
func fastWalkCallback(walker *fastWalker, cb FastWalkCallback) {
for file := range walker.ch {
cb(file.ParentDir, file.Info, file.Err)
}
}
type fastWalkInfo struct {
ParentDir string
Info os.FileInfo
Err error
}
type fastWalker struct {
rootDir string
ch chan fastWalkInfo
limit int32
cur *int32
wg *sync.WaitGroup
}
func fastWalkWithExcludeFiles(rootDir string) *fastWalker {
excludePaths := []filepathfilter.Pattern{
filepathfilter.NewPattern(".git", filepathfilter.GitIgnore),
filepathfilter.NewPattern("**/.git", filepathfilter.GitIgnore),
}
limit, _ := strconv.Atoi(os.Getenv("LFS_FASTWALK_LIMIT"))
if limit < 1 {
limit = runtime.GOMAXPROCS(-1) * 20
}
c := int32(0)
w := &fastWalker{
rootDir: rootDir,
limit: int32(limit),
cur: &c,
ch: make(chan fastWalkInfo, 256),
wg: &sync.WaitGroup{},
}
go func() {
defer w.Wait()
dirFi, err := os.Stat(w.rootDir)
if err != nil {
w.ch <- fastWalkInfo{Err: err}
return
}
w.Walk(true, "", dirFi, excludePaths)
}()
return w
}
func (w *fastWalker) Walk(isRoot bool, workDir string, itemFi os.FileInfo,
excludePaths []filepathfilter.Pattern) {
var fullPath string
var parentWorkDir string
if isRoot {
fullPath = w.rootDir
} else {
parentWorkDir = join(w.rootDir, workDir)
fullPath = join(parentWorkDir, itemFi.Name())
}
if !isRoot && itemFi.IsDir() {
_, err := os.Stat(filepath.Join(fullPath, ".git"))
if err == nil {
return
}
}
workPath := join(workDir, itemFi.Name())
if !filepathfilter.NewFromPatterns(nil, excludePaths).Allows(workPath) {
return
}
w.ch <- fastWalkInfo{ParentDir: parentWorkDir, Info: itemFi}
if !itemFi.IsDir() {
return
}
var childWorkDir string
if !isRoot {
childWorkDir = join(workDir, itemFi.Name())
}
df, err := os.Open(fullPath)
if err != nil {
w.ch <- fastWalkInfo{Err: err}
return
}
jobSize := 100
for children, err := df.Readdir(jobSize); err == nil; children, err = df.Readdir(jobSize) {
w.walk(children, func(subitems []os.FileInfo) {
for _, childFi := range subitems {
w.Walk(false, childWorkDir, childFi, excludePaths)
}
})
}
df.Close()
if err != nil && err != io.EOF {
w.ch <- fastWalkInfo{Err: err}
}
}
func (w *fastWalker) walk(children []os.FileInfo, fn func([]os.FileInfo)) {
cur := atomic.AddInt32(w.cur, 1)
if cur > w.limit {
fn(children)
atomic.AddInt32(w.cur, -1)
return
}
w.wg.Add(1)
go func() {
fn(children)
w.wg.Done()
atomic.AddInt32(w.cur, -1)
}()
}
func (w *fastWalker) Wait() {
w.wg.Wait()
close(w.ch)
}
func join(paths ...string) string {
ne := make([]string, 0, len(paths))
for _, p := range paths {
if len(p) > 0 {
ne = append(ne, p)
}
}
return strings.Join(ne, "/")
}
func SetFileWriteFlag(path string, writeEnabled bool) error {
stat, err := os.Stat(path)
if err != nil {
return err
}
mode := uint32(stat.Mode())
if (writeEnabled && (mode&0200) > 0) ||
(!writeEnabled && (mode&0222) == 0) {
return nil
}
if writeEnabled {
mode = mode | 0200
} else {
mode = mode &^ 0222
}
return os.Chmod(path, os.FileMode(mode))
}
func TempFile(dir, pattern string, cfg repositoryPermissionFetcher) (*os.File, error) {
tmp, err := os.CreateTemp(dir, pattern)
if err != nil {
return nil, err
}
perms := cfg.RepositoryPermissions(false)
err = os.Chmod(tmp.Name(), perms)
if err != nil {
tmp.Close()
os.Remove(tmp.Name())
return nil, err
}
return tmp, nil
}
func ExecutablePermissions(perms os.FileMode) os.FileMode {
return perms | ((perms & 0444) >> 2)
}
func CanonicalizePath(path string, missingOk bool) (string, error) {
path, err := TranslateCygwinPath(path)
if err != nil {
return "", err
}
if len(path) > 0 {
path, err := filepath.Abs(path)
if err != nil {
return "", err
}
result, err := CanonicalizeSystemPath(path)
if err != nil && os.IsNotExist(err) && missingOk {
return path, nil
}
return result, err
}
return "", nil
}
const (
windowsPrefix = `.\`
nixPrefix = `./`
)
func TrimCurrentPrefix(p string) string {
if strings.HasPrefix(p, windowsPrefix) {
return strings.TrimPrefix(p, windowsPrefix)
}
return strings.TrimPrefix(p, nixPrefix)
}