github.com/x-oss-byte/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 }