github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/filemetadata/filemetadata.go (about) 1 // Package filemetadata contains types of metadata for files, to be used for caching. 2 package filemetadata 3 4 import ( 5 "errors" 6 "fmt" 7 "os" 8 "strings" 9 "time" 10 11 "github.com/bazelbuild/remote-apis-sdks/go/pkg/digest" 12 "github.com/pkg/xattr" 13 ) 14 15 // SymlinkMetadata contains details if the given path is a symlink. 16 type SymlinkMetadata struct { 17 Target string 18 IsDangling bool 19 } 20 21 // Metadata contains details for a particular file. 22 type Metadata struct { 23 Digest digest.Digest 24 IsExecutable bool 25 IsDirectory bool 26 MTime time.Time 27 Err error 28 Symlink *SymlinkMetadata 29 } 30 31 // FileError is the error returned by the Compute function. 32 type FileError struct { 33 IsNotFound bool 34 Err error 35 } 36 37 // External xattr package can be mocked for testing through this interface. 38 type xattributeAccessorInterface interface { 39 isSupported() bool 40 getXAttr(path string, name string) ([]byte, error) 41 } 42 43 type xattributeAccessor struct{} 44 45 func (x xattributeAccessor) isSupported() bool { 46 return xattr.XATTR_SUPPORTED 47 } 48 49 func (x xattributeAccessor) getXAttr(path string, name string) ([]byte, error) { 50 return xattr.Get(path, name) 51 } 52 53 var ( 54 // XattrDigestName is the xattr name for the object digest. 55 XattrDigestName string 56 // XattrAccess is the object to control access of XattrDigestName. 57 XattrAccess xattributeAccessorInterface = xattributeAccessor{} 58 ) 59 60 // Error returns the error message. 61 func (e *FileError) Error() string { 62 return e.Err.Error() 63 } 64 65 func isSymlink(filename string) (bool, error) { 66 file, err := os.Lstat(filename) 67 if err != nil { 68 return false, err 69 } 70 return file.Mode()&os.ModeSymlink != 0, nil 71 } 72 73 // Compute computes a Metadata from a given file path. 74 // If an error is returned, it will be of type *FileError. 75 func Compute(filename string) *Metadata { 76 md := &Metadata{Digest: digest.Empty} 77 file, err := os.Stat(filename) 78 if isSym, _ := isSymlink(filename); isSym { 79 md.Symlink = &SymlinkMetadata{} 80 dest, rlErr := os.Readlink(filename) 81 if rlErr != nil { 82 md.Err = &FileError{Err: rlErr} 83 return md 84 } 85 // If Readlink was OK, we set Target, even if this could be a dangling symlink. 86 md.Symlink.Target = dest 87 if err != nil { 88 md.Err = &FileError{Err: err} 89 md.Symlink.IsDangling = true 90 return md 91 } 92 } 93 94 if err != nil { 95 fe := &FileError{Err: err} 96 if os.IsNotExist(err) { 97 fe.IsNotFound = true 98 } 99 md.Err = fe 100 return md 101 } 102 mode := file.Mode() 103 md.MTime = file.ModTime() 104 md.IsExecutable = (mode & 0100) != 0 105 if mode.IsDir() { 106 md.IsDirectory = true 107 return md 108 } 109 110 if len(XattrDigestName) > 0 { 111 if !XattrAccess.isSupported() { 112 md.Err = &FileError{Err: errors.New("x-attributes are not supported by the system")} 113 return md 114 } 115 xattrValue, err := XattrAccess.getXAttr(filename, XattrDigestName) 116 if err == nil { 117 xattrStr := string(xattrValue) 118 if !strings.Contains(xattrStr, "/") { 119 xattrStr = fmt.Sprintf("%s/%d", xattrStr, file.Size()) 120 } 121 md.Digest, err = digest.NewFromString(xattrStr) 122 if err != nil { 123 md.Err = &FileError{Err: err} 124 } 125 return md 126 } 127 } 128 md.Digest, err = digest.NewFromFile(filename) 129 if err != nil { 130 md.Err = &FileError{Err: err} 131 } 132 return md 133 } 134 135 // Cache is a cache for file contents->Metadata. 136 type Cache interface { 137 Get(path string) *Metadata 138 Delete(filename string) error 139 Update(path string, cacheEntry *Metadata) error 140 GetCacheHits() uint64 141 GetCacheMisses() uint64 142 } 143 144 type noopCache struct{} 145 146 // Get computes the metadata from the file contents. 147 // If an error is returned, it will be in Metadata.Err of type *FileError. 148 func (c *noopCache) Get(path string) *Metadata { 149 return Compute(path) 150 } 151 152 // Delete removes an entry from the cache. It is a noop for the Noop cache. 153 func (c *noopCache) Delete(string) error { 154 return nil 155 } 156 157 // Update updates a cache entry with the given value. It is a noop for Noop cache. 158 func (c *noopCache) Update(string, *Metadata) error { 159 return nil 160 } 161 162 // GetCacheHits returns the number of cache hits. It returns 0 for Noop cache. 163 func (c *noopCache) GetCacheHits() uint64 { 164 return 0 165 } 166 167 // GetCacheMisses returns the number of cache misses. 168 // It returns 0 for Noop cache. 169 func (c *noopCache) GetCacheMisses() uint64 { 170 return 0 171 } 172 173 // NewNoopCache returns a cache that doesn't cache (evaluates on every Get). 174 func NewNoopCache() Cache { 175 return &noopCache{} 176 }