github.com/puellanivis/breton@v0.2.16/lib/files/cachefiles/cache.go (about)

     1  // Package cachefiles implements a caching filestore accessable through "cache:opaqueURL".
     2  package cachefiles
     3  
     4  import (
     5  	"bytes"
     6  	"context"
     7  	"net/url"
     8  	"os"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/puellanivis/breton/lib/files"
    13  	"github.com/puellanivis/breton/lib/files/wrapper"
    14  )
    15  
    16  type line struct {
    17  	info os.FileInfo
    18  	data []byte
    19  }
    20  
    21  // FileStore is a caching structure that holds copies of the content of files.
    22  type FileStore struct {
    23  	mu sync.RWMutex
    24  
    25  	cache map[string]*line
    26  }
    27  
    28  // New returns a new caching FileStore, which can be registered into lib/files
    29  //
    30  // Deprecated: Now that we use sensible defaults, and lazy initialization,
    31  // a simple &cachefiles.FileStore{} or new(cachefiles.FileStore) is enough now.
    32  func New() *FileStore {
    33  	return &FileStore{}
    34  }
    35  
    36  // Default is the default cache attached to the "cache" Scheme
    37  var Default = new(FileStore)
    38  
    39  func init() {
    40  	files.RegisterScheme(Default, "cache")
    41  }
    42  
    43  func (h *FileStore) expire(filename string) {
    44  	h.mu.Lock()
    45  	defer h.mu.Unlock()
    46  
    47  	delete(h.cache, filename)
    48  }
    49  
    50  func trimScheme(uri *url.URL) string {
    51  	u := *uri
    52  	u.Scheme = ""
    53  
    54  	return u.String()
    55  }
    56  
    57  // Open implements the files.FileStore Open. It returns a buffered copy of the files.Reader returned from reading the uri escaped by the "cache:" scheme. Any access within the next ExpireTime set by the context.Context (5 minutes by default) will return a new copy of an bytes.Reader of the same buffer.
    58  func (h *FileStore) Open(ctx context.Context, uri *url.URL) (files.Reader, error) {
    59  	filename := trimScheme(uri)
    60  
    61  	ctx, reentrant := isReentrant(ctx)
    62  	if reentrant {
    63  		// We are in a reentrant caching scenario.
    64  		// Continuing will deadlock, so we won’t even try to cache at all.
    65  		return files.Open(ctx, filename)
    66  	}
    67  
    68  	h.mu.RLock()
    69  	f := h.cache[filename]
    70  	h.mu.RUnlock()
    71  
    72  	if f != nil {
    73  		return wrapper.NewReaderWithInfo(bytes.NewReader(f.data), f.info), nil
    74  	}
    75  
    76  	h.mu.Lock()
    77  	defer h.mu.Unlock()
    78  
    79  	f = h.cache[filename]
    80  	if f != nil {
    81  		// Another goroutine already did our work.
    82  		return wrapper.NewReaderWithInfo(bytes.NewReader(f.data), f.info), nil
    83  	}
    84  
    85  	raw, err := files.Open(ctx, filename)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	info, err := raw.Stat()
    91  	if err != nil {
    92  		// Just in case, if we got an err != nil return,
    93  		// we want to be absolutely sure we don’t try and use the returned `info`.
    94  		// Instead, we will make up our own.
    95  		info = nil
    96  	}
    97  
    98  	data, err := files.ReadFrom(raw)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	if info == nil {
   104  		info = wrapper.NewInfo(uri, len(data), time.Now())
   105  	}
   106  
   107  	f = &line{
   108  		data: data,
   109  		info: info,
   110  	}
   111  
   112  	if h.cache == nil {
   113  		h.cache = make(map[string]*line)
   114  	}
   115  
   116  	h.cache[filename] = f
   117  
   118  	// default 5 minute expiration
   119  	expiration := 5 * time.Minute
   120  	if d, ok := GetExpire(ctx); ok {
   121  		expiration = d
   122  	}
   123  	timer := time.NewTimer(expiration)
   124  
   125  	go func() {
   126  		<-timer.C
   127  		h.expire(filename)
   128  	}()
   129  
   130  	return wrapper.NewReaderWithInfo(bytes.NewReader(data), info), nil
   131  }
   132  
   133  // Create implements the files.FileStore Create. At this time, it just returns the files.Create() from the wrapped url.
   134  func (h *FileStore) Create(ctx context.Context, uri *url.URL) (files.Writer, error) {
   135  	return files.Create(ctx, trimScheme(uri))
   136  }
   137  
   138  // List implements the files.FileStore List. It does not cache anything and just returns the files.List() from the wrapped url.
   139  func (h *FileStore) List(ctx context.Context, uri *url.URL) ([]os.FileInfo, error) {
   140  	return files.List(ctx, trimScheme(uri))
   141  }