github.com/munnerz/test-infra@v0.0.0-20190108210205-ce3d181dc989/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  	"io/ioutil"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  	"time"
    30  
    31  	"k8s.io/test-infra/greenhouse/diskutil"
    32  
    33  	"github.com/sirupsen/logrus"
    34  )
    35  
    36  // ReadHandler should be implemented by cache users for use with Cache.Get
    37  type ReadHandler func(exists bool, contents io.ReadSeeker) error
    38  
    39  // Cache implements disk backed cache storage
    40  type Cache struct {
    41  	diskRoot string
    42  	logger   *logrus.Entry
    43  }
    44  
    45  // NewCache returns a new Cache given the root directory that should be used
    46  // on disk for cache storage
    47  func NewCache(diskRoot string) *Cache {
    48  	return &Cache{
    49  		diskRoot: strings.TrimSuffix(diskRoot, string(os.PathListSeparator)),
    50  	}
    51  }
    52  
    53  // KeyToPath converts a cache entry key to a path on disk
    54  func (c *Cache) KeyToPath(key string) string {
    55  	return filepath.Join(c.diskRoot, key)
    56  }
    57  
    58  // PathToKey converts a path on disk to a key, assuming the path is actually
    59  // under DiskRoot() ...
    60  func (c *Cache) PathToKey(key string) string {
    61  	return strings.TrimPrefix(key, c.diskRoot+string(os.PathSeparator))
    62  }
    63  
    64  // DiskRoot returns the root directory containing all on-disk cache entries
    65  func (c *Cache) DiskRoot() string {
    66  	return c.diskRoot
    67  }
    68  
    69  // file path helper
    70  func exists(path string) bool {
    71  	_, err := os.Stat(path)
    72  	return !os.IsNotExist(err)
    73  }
    74  
    75  // file path helper
    76  func ensureDir(dir string) error {
    77  	if exists(dir) {
    78  		return nil
    79  	}
    80  	return os.MkdirAll(dir, os.FileMode(0744))
    81  }
    82  
    83  func removeTemp(path string) {
    84  	err := os.Remove(path)
    85  	if err != nil {
    86  		logrus.WithError(err).Errorf("Failed to remove a temp file: %v", path)
    87  	}
    88  }
    89  
    90  // Put copies the content reader until the end into the cache at key
    91  // if contentSHA256 is not "" then the contents will only be stored in the
    92  // cache if the content's hex string SHA256 matches
    93  func (c *Cache) Put(key string, content io.Reader, contentSHA256 string) error {
    94  	// make sure directory exists
    95  	path := c.KeyToPath(key)
    96  	dir := filepath.Dir(path)
    97  	err := ensureDir(dir)
    98  	if err != nil {
    99  		logrus.WithError(err).Errorf("error ensuring directory '%s' exists", dir)
   100  	}
   101  
   102  	// create a temp file to get the content on disk
   103  	temp, err := ioutil.TempFile(dir, "temp-put")
   104  	if err != nil {
   105  		return fmt.Errorf("failed to create cache entry: %v", err)
   106  	}
   107  
   108  	// fast path copying when not hashing content,s
   109  	if contentSHA256 == "" {
   110  		_, err = io.Copy(temp, content)
   111  		if err != nil {
   112  			removeTemp(temp.Name())
   113  			return fmt.Errorf("failed to copy into cache entry: %v", err)
   114  		}
   115  
   116  	} else {
   117  		hasher := sha256.New()
   118  		_, err = io.Copy(io.MultiWriter(temp, hasher), content)
   119  		if err != nil {
   120  			removeTemp(temp.Name())
   121  			return fmt.Errorf("failed to copy into cache entry: %v", err)
   122  		}
   123  		actualContentSHA256 := hex.EncodeToString(hasher.Sum(nil))
   124  		if actualContentSHA256 != contentSHA256 {
   125  			removeTemp(temp.Name())
   126  			return fmt.Errorf(
   127  				"hashes did not match for '%s', given: '%s' actual: '%s",
   128  				key, contentSHA256, actualContentSHA256)
   129  		}
   130  	}
   131  
   132  	// move the content to the key location
   133  	err = temp.Sync()
   134  	if err != nil {
   135  		removeTemp(temp.Name())
   136  		return fmt.Errorf("failed to sync cache entry: %v", err)
   137  	}
   138  	temp.Close()
   139  	err = os.Rename(temp.Name(), path)
   140  	if err != nil {
   141  		removeTemp(temp.Name())
   142  		return fmt.Errorf("failed to insert contents into cache: %v", err)
   143  	}
   144  	return nil
   145  }
   146  
   147  // Get provides your readHandler with the contents at key
   148  func (c *Cache) Get(key string, readHandler ReadHandler) error {
   149  	path := c.KeyToPath(key)
   150  	f, err := os.Open(path)
   151  	if err != nil {
   152  		if os.IsNotExist(err) {
   153  			return readHandler(false, nil)
   154  		}
   155  		return fmt.Errorf("failed to get key: %v", err)
   156  	}
   157  	return readHandler(true, f)
   158  }
   159  
   160  // EntryInfo are returned when getting entries from the cache
   161  type EntryInfo struct {
   162  	Path       string
   163  	LastAccess time.Time
   164  }
   165  
   166  // GetEntries walks the cache dir and returns all paths that exist
   167  // In the future this *may* be made smarter
   168  func (c *Cache) GetEntries() []EntryInfo {
   169  	entries := []EntryInfo{}
   170  	// note we swallow errors because we just need to know what keys exist
   171  	// some keys missing is OK since this is used for eviction, but not returning
   172  	// any of the keys due to some error is NOT
   173  	_ = filepath.Walk(c.diskRoot, func(path string, f os.FileInfo, err error) error {
   174  		if err != nil {
   175  			logrus.WithError(err).Error("error getting some entries")
   176  			return nil
   177  		}
   178  		if !f.IsDir() {
   179  			atime := diskutil.GetATime(path, time.Now())
   180  			entries = append(entries, EntryInfo{
   181  				Path:       path,
   182  				LastAccess: atime,
   183  			})
   184  		}
   185  		return nil
   186  	})
   187  	return entries
   188  }
   189  
   190  // Delete deletes the file at key
   191  func (c *Cache) Delete(key string) error {
   192  	return os.Remove(c.KeyToPath(key))
   193  }