github.com/git-lfs/git-lfs@v2.5.2+incompatible/fs/fs.go (about)

     1  package fs
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  
    15  	"github.com/git-lfs/git-lfs/tools"
    16  	"github.com/rubyist/tracerx"
    17  )
    18  
    19  var oidRE = regexp.MustCompile(`\A[[:alnum:]]{64}`)
    20  
    21  // Environment is a copy of a subset of the interface
    22  // github.com/git-lfs/git-lfs/config.Environment.
    23  //
    24  // For more information, see config/environment.go.
    25  type Environment interface {
    26  	Get(key string) (val string, ok bool)
    27  }
    28  
    29  // Object represents a locally stored LFS object.
    30  type Object struct {
    31  	Oid  string
    32  	Size int64
    33  }
    34  
    35  type Filesystem struct {
    36  	GitStorageDir string   // parent of objects/lfs (may be same as GitDir but may not)
    37  	LFSStorageDir string   // parent of lfs objects and tmp dirs. Default: ".git/lfs"
    38  	ReferenceDirs []string // alternative local media dirs (relative to clone reference repo)
    39  	lfsobjdir     string
    40  	tmpdir        string
    41  	logdir        string
    42  	mu            sync.Mutex
    43  }
    44  
    45  func (f *Filesystem) EachObject(fn func(Object) error) error {
    46  	var eachErr error
    47  	tools.FastWalkGitRepo(f.LFSObjectDir(), func(parentDir string, info os.FileInfo, err error) {
    48  		if err != nil {
    49  			eachErr = err
    50  			return
    51  		}
    52  		if eachErr != nil || info.IsDir() {
    53  			return
    54  		}
    55  		if oidRE.MatchString(info.Name()) {
    56  			fn(Object{Oid: info.Name(), Size: info.Size()})
    57  		}
    58  	})
    59  	return eachErr
    60  }
    61  
    62  func (f *Filesystem) ObjectExists(oid string, size int64) bool {
    63  	return tools.FileExistsOfSize(f.ObjectPathname(oid), size)
    64  }
    65  
    66  func (f *Filesystem) ObjectPath(oid string) (string, error) {
    67  	dir := f.localObjectDir(oid)
    68  	if err := os.MkdirAll(dir, 0755); err != nil {
    69  		return "", fmt.Errorf("Error trying to create local storage directory in %q: %s", dir, err)
    70  	}
    71  	return filepath.Join(dir, oid), nil
    72  }
    73  
    74  func (f *Filesystem) ObjectPathname(oid string) string {
    75  	return filepath.Join(f.localObjectDir(oid), oid)
    76  }
    77  
    78  func (f *Filesystem) DecodePathname(path string) string {
    79  	return string(DecodePathBytes([]byte(path)))
    80  }
    81  
    82  /**
    83   * Revert non ascii chracters escaped by git or windows (as octal sequences \000) back to bytes.
    84   */
    85  func DecodePathBytes(path []byte) []byte {
    86  	var expression = regexp.MustCompile(`\\[0-9]{3}`)
    87  	var buffer bytes.Buffer
    88  
    89  	// strip quotes if any
    90  	if len(path) > 2 && path[0] == '"' && path[len(path)-1] == '"' {
    91  		path = path[1 : len(path)-1]
    92  	}
    93  
    94  	base := 0
    95  	for _, submatches := range expression.FindAllSubmatchIndex(path, -1) {
    96  		buffer.Write(path[base:submatches[0]])
    97  
    98  		match := string(path[submatches[0]+1 : submatches[0]+4])
    99  
   100  		k, err := strconv.ParseUint(match, 8, 64)
   101  		if err != nil {
   102  			return path
   103  		} // abort on error
   104  
   105  		buffer.Write([]byte{byte(k)})
   106  		base = submatches[1]
   107  	}
   108  
   109  	buffer.Write(path[base:len(path)])
   110  
   111  	return buffer.Bytes()
   112  }
   113  
   114  func (f *Filesystem) localObjectDir(oid string) string {
   115  	return filepath.Join(f.LFSObjectDir(), oid[0:2], oid[2:4])
   116  }
   117  
   118  func (f *Filesystem) ObjectReferencePaths(oid string) []string {
   119  	if len(f.ReferenceDirs) == 0 {
   120  		return nil
   121  	}
   122  
   123  	var paths []string
   124  	for _, ref := range f.ReferenceDirs {
   125  		paths = append(paths, filepath.Join(ref, oid[0:2], oid[2:4], oid))
   126  	}
   127  	return paths
   128  }
   129  
   130  func (f *Filesystem) LFSObjectDir() string {
   131  	f.mu.Lock()
   132  	defer f.mu.Unlock()
   133  
   134  	if len(f.lfsobjdir) == 0 {
   135  		f.lfsobjdir = filepath.Join(f.LFSStorageDir, "objects")
   136  		os.MkdirAll(f.lfsobjdir, 0755)
   137  	}
   138  
   139  	return f.lfsobjdir
   140  }
   141  
   142  func (f *Filesystem) LogDir() string {
   143  	f.mu.Lock()
   144  	defer f.mu.Unlock()
   145  
   146  	if len(f.logdir) == 0 {
   147  		f.logdir = filepath.Join(f.LFSStorageDir, "logs")
   148  		os.MkdirAll(f.logdir, 0755)
   149  	}
   150  
   151  	return f.logdir
   152  }
   153  
   154  func (f *Filesystem) TempDir() string {
   155  	f.mu.Lock()
   156  	defer f.mu.Unlock()
   157  
   158  	if len(f.tmpdir) == 0 {
   159  		f.tmpdir = filepath.Join(f.LFSStorageDir, "tmp")
   160  		os.MkdirAll(f.tmpdir, 0755)
   161  	}
   162  
   163  	return f.tmpdir
   164  }
   165  
   166  func (f *Filesystem) Cleanup() error {
   167  	if f == nil {
   168  		return nil
   169  	}
   170  	return f.cleanupTmp()
   171  }
   172  
   173  // New initializes a new *Filesystem with the given directories. gitdir is the
   174  // path to the bare repo, workdir is the path to the repository working
   175  // directory, and lfsdir is the optional path to the `.git/lfs` directory.
   176  func New(env Environment, gitdir, workdir, lfsdir string) *Filesystem {
   177  	fs := &Filesystem{
   178  		GitStorageDir: resolveGitStorageDir(gitdir),
   179  	}
   180  
   181  	fs.ReferenceDirs = resolveReferenceDirs(env, fs.GitStorageDir)
   182  
   183  	if len(lfsdir) == 0 {
   184  		lfsdir = "lfs"
   185  	}
   186  
   187  	if filepath.IsAbs(lfsdir) {
   188  		fs.LFSStorageDir = lfsdir
   189  	} else {
   190  		fs.LFSStorageDir = filepath.Join(fs.GitStorageDir, lfsdir)
   191  	}
   192  
   193  	return fs
   194  }
   195  
   196  func resolveReferenceDirs(env Environment, gitStorageDir string) []string {
   197  	var references []string
   198  
   199  	envAlternates, ok := env.Get("GIT_ALTERNATE_OBJECT_DIRECTORIES")
   200  	if ok {
   201  		splits := strings.Split(envAlternates, string(os.PathListSeparator))
   202  		for _, split := range splits {
   203  			if dir, ok := existsAlternate(split); ok {
   204  				references = append(references, dir)
   205  			}
   206  		}
   207  	}
   208  
   209  	cloneReferencePath := filepath.Join(gitStorageDir, "objects", "info", "alternates")
   210  	if tools.FileExists(cloneReferencePath) {
   211  		f, err := os.Open(cloneReferencePath)
   212  		if err != nil {
   213  			tracerx.Printf("could not open %s: %s",
   214  				cloneReferencePath, err)
   215  			return nil
   216  		}
   217  		defer f.Close()
   218  
   219  		scanner := bufio.NewScanner(f)
   220  		for scanner.Scan() {
   221  			text := strings.TrimSpace(scanner.Text())
   222  			if len(text) == 0 || strings.HasPrefix(text, "#") {
   223  				continue
   224  			}
   225  
   226  			if dir, ok := existsAlternate(text); ok {
   227  				references = append(references, dir)
   228  			}
   229  		}
   230  
   231  		if err := scanner.Err(); err != nil {
   232  			tracerx.Printf("could not scan %s: %s",
   233  				cloneReferencePath, err)
   234  		}
   235  	}
   236  	return references
   237  }
   238  
   239  // existsAlternate takes an object directory given in "objs" (read as a single,
   240  // line from .git/objects/info/alternates). If that is a satisfiable alternates
   241  // directory (i.e., it exists), the directory is returned along with "true". If
   242  // not, the empty string and false is returned instead.
   243  func existsAlternate(objs string) (string, bool) {
   244  	objs = strings.TrimSpace(objs)
   245  	if strings.HasPrefix(objs, "\"") {
   246  		var err error
   247  
   248  		unquote := strings.LastIndex(objs, "\"")
   249  		if unquote == 0 {
   250  			return "", false
   251  		}
   252  
   253  		objs, err = strconv.Unquote(objs[:unquote+1])
   254  		if err != nil {
   255  			return "", false
   256  		}
   257  	}
   258  
   259  	storage := filepath.Join(filepath.Dir(objs), "lfs", "objects")
   260  
   261  	if tools.DirExists(storage) {
   262  		return storage, true
   263  	}
   264  	return "", false
   265  }
   266  
   267  // From a git dir, get the location that objects are to be stored (we will store lfs alongside)
   268  // Sometimes there is an additional level of redirect on the .git folder by way of a commondir file
   269  // before you find object storage, e.g. 'git worktree' uses this. It redirects to gitdir either by GIT_DIR
   270  // (during setup) or .git/git-dir: (during use), but this only contains the index etc, the objects
   271  // are found in another git dir via 'commondir'.
   272  func resolveGitStorageDir(gitDir string) string {
   273  	commondirpath := filepath.Join(gitDir, "commondir")
   274  	if tools.FileExists(commondirpath) && !tools.DirExists(filepath.Join(gitDir, "objects")) {
   275  		// no git-dir: prefix in commondir
   276  		storage, err := processGitRedirectFile(commondirpath, "")
   277  		if err == nil {
   278  			return storage
   279  		}
   280  	}
   281  	return gitDir
   282  }
   283  
   284  func processGitRedirectFile(file, prefix string) (string, error) {
   285  	data, err := ioutil.ReadFile(file)
   286  	if err != nil {
   287  		return "", err
   288  	}
   289  
   290  	contents := string(data)
   291  	var dir string
   292  	if len(prefix) > 0 {
   293  		if !strings.HasPrefix(contents, prefix) {
   294  			// Prefix required & not found
   295  			return "", nil
   296  		}
   297  		dir = strings.TrimSpace(contents[len(prefix):])
   298  	} else {
   299  		dir = strings.TrimSpace(contents)
   300  	}
   301  
   302  	if !filepath.IsAbs(dir) {
   303  		// The .git file contains a relative path.
   304  		// Create an absolute path based on the directory the .git file is located in.
   305  		dir = filepath.Join(filepath.Dir(file), dir)
   306  	}
   307  
   308  	return dir, nil
   309  }