k8s.io/test-infra@v0.0.0-20240520184403-27c6b4c223d8/greenhouse/diskcache/cache.go (about)

     1  /*
     2  Copyright 2018 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package diskcache implements disk backed cache storage for use in greenhouse
    18  package diskcache
    19  
    20  import (
    21  	"crypto/sha256"
    22  	"encoding/hex"
    23  	"fmt"
    24  	"io"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  	"time"
    29  
    30  	"k8s.io/test-infra/greenhouse/diskutil"
    31  
    32  	"github.com/sirupsen/logrus"
    33  )
    34  
    35  // ReadHandler should be implemented by cache users for use with Cache.Get
    36  type ReadHandler func(exists bool, contents io.ReadSeeker) error
    37  
    38  // Cache implements disk backed cache storage
    39  type Cache struct {
    40  	diskRoot string
    41  }
    42  
    43  // NewCache returns a new Cache given the root directory that should be used
    44  // on disk for cache storage
    45  func NewCache(diskRoot string) *Cache {
    46  	return &Cache{
    47  		diskRoot: strings.TrimSuffix(diskRoot, string(os.PathListSeparator)),
    48  	}
    49  }
    50  
    51  // KeyToPath converts a cache entry key to a path on disk
    52  func (c *Cache) KeyToPath(key string) string {
    53  	return filepath.Join(c.diskRoot, key)
    54  }
    55  
    56  // PathToKey converts a path on disk to a key, assuming the path is actually
    57  // under DiskRoot() ...
    58  func (c *Cache) PathToKey(key string) string {
    59  	return strings.TrimPrefix(key, c.diskRoot+string(os.PathSeparator))
    60  }
    61  
    62  // DiskRoot returns the root directory containing all on-disk cache entries
    63  func (c *Cache) DiskRoot() string {
    64  	return c.diskRoot
    65  }
    66  
    67  // file path helper
    68  func exists(path string) bool {
    69  	_, err := os.Stat(path)
    70  	return !os.IsNotExist(err)
    71  }
    72  
    73  // file path helper
    74  func ensureDir(dir string) error {
    75  	if exists(dir) {
    76  		return nil
    77  	}
    78  	return os.MkdirAll(dir, os.FileMode(0744))
    79  }
    80  
    81  func removeTemp(path string) {
    82  	err := os.Remove(path)
    83  	if err != nil {
    84  		logrus.WithError(err).Errorf("Failed to remove a temp file: %v", path)
    85  	}
    86  }
    87  
    88  // Put copies the content reader until the end into the cache at key
    89  // if contentSHA256 is not "" then the contents will only be stored in the
    90  // cache if the content's hex string SHA256 matches
    91  func (c *Cache) Put(key string, content io.Reader, contentSHA256 string) error {
    92  	// make sure directory exists
    93  	path := c.KeyToPath(key)
    94  	dir := filepath.Dir(path)
    95  	err := ensureDir(dir)
    96  	if err != nil {
    97  		logrus.WithError(err).Errorf("error ensuring directory '%s' exists", dir)
    98  	}
    99  
   100  	// create a temp file to get the content on disk
   101  	temp, err := os.CreateTemp(dir, "temp-put")
   102  	if err != nil {
   103  		return fmt.Errorf("failed to create cache entry: %w", err)
   104  	}
   105  
   106  	// fast path copying when not hashing content,s
   107  	if contentSHA256 == "" {
   108  		_, err = io.Copy(temp, content)
   109  		if err != nil {
   110  			removeTemp(temp.Name())
   111  			return fmt.Errorf("failed to copy into cache entry: %w", err)
   112  		}
   113  
   114  	} else {
   115  		hasher := sha256.New()
   116  		_, err = io.Copy(io.MultiWriter(temp, hasher), content)
   117  		if err != nil {
   118  			removeTemp(temp.Name())
   119  			return fmt.Errorf("failed to copy into cache entry: %w", err)
   120  		}
   121  		actualContentSHA256 := hex.EncodeToString(hasher.Sum(nil))
   122  		if actualContentSHA256 != contentSHA256 {
   123  			removeTemp(temp.Name())
   124  			return fmt.Errorf(
   125  				"hashes did not match for '%s', given: '%s' actual: '%s",
   126  				key, contentSHA256, actualContentSHA256)
   127  		}
   128  	}
   129  
   130  	// move the content to the key location
   131  	err = temp.Sync()
   132  	if err != nil {
   133  		removeTemp(temp.Name())
   134  		return fmt.Errorf("failed to sync cache entry: %w", err)
   135  	}
   136  	temp.Close()
   137  	err = os.Rename(temp.Name(), path)
   138  	if err != nil {
   139  		removeTemp(temp.Name())
   140  		return fmt.Errorf("failed to insert contents into cache: %w", err)
   141  	}
   142  	return nil
   143  }
   144  
   145  // Get provides your readHandler with the contents at key
   146  func (c *Cache) Get(key string, readHandler ReadHandler) error {
   147  	path := c.KeyToPath(key)
   148  	f, err := os.Open(path)
   149  	if err != nil {
   150  		if os.IsNotExist(err) {
   151  			return readHandler(false, nil)
   152  		}
   153  		return fmt.Errorf("failed to get key: %w", err)
   154  	}
   155  	return readHandler(true, f)
   156  }
   157  
   158  // EntryInfo are returned when getting entries from the cache
   159  type EntryInfo struct {
   160  	Path       string
   161  	LastAccess time.Time
   162  }
   163  
   164  // GetEntries walks the cache dir and returns all paths that exist
   165  // In the future this *may* be made smarter
   166  func (c *Cache) GetEntries() []EntryInfo {
   167  	entries := []EntryInfo{}
   168  	// note we swallow errors because we just need to know what keys exist
   169  	// some keys missing is OK since this is used for eviction, but not returning
   170  	// any of the keys due to some error is NOT
   171  	_ = filepath.Walk(c.diskRoot, func(path string, f os.FileInfo, err error) error {
   172  		if err != nil {
   173  			logrus.WithError(err).Error("error getting some entries")
   174  			return nil
   175  		}
   176  		if !f.IsDir() {
   177  			atime := diskutil.GetATime(path, time.Now())
   178  			entries = append(entries, EntryInfo{
   179  				Path:       path,
   180  				LastAccess: atime,
   181  			})
   182  		}
   183  		return nil
   184  	})
   185  	return entries
   186  }
   187  
   188  // Delete deletes the file at key
   189  func (c *Cache) Delete(key string) error {
   190  	return os.Remove(c.KeyToPath(key))
   191  }