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  }