oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/internal/cas/proxy.go (about)

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package cas
    17  
    18  import (
    19  	"context"
    20  	"io"
    21  	"sync"
    22  
    23  	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
    24  	"oras.land/oras-go/v2/content"
    25  	"oras.land/oras-go/v2/internal/ioutil"
    26  )
    27  
    28  // Proxy is a caching proxy for the storage.
    29  // The first fetch call of a described content will read from the remote and
    30  // cache the fetched content.
    31  // The subsequent fetch call will read from the local cache.
    32  type Proxy struct {
    33  	content.ReadOnlyStorage
    34  	Cache       content.Storage
    35  	StopCaching bool
    36  }
    37  
    38  // NewProxy creates a proxy for the `base` storage, using the `cache` storage as
    39  // the cache.
    40  func NewProxy(base content.ReadOnlyStorage, cache content.Storage) *Proxy {
    41  	return &Proxy{
    42  		ReadOnlyStorage: base,
    43  		Cache:           cache,
    44  	}
    45  }
    46  
    47  // NewProxyWithLimit creates a proxy for the `base` storage, using the `cache`
    48  // storage with a push size limit as the cache.
    49  func NewProxyWithLimit(base content.ReadOnlyStorage, cache content.Storage, pushLimit int64) *Proxy {
    50  	limitedCache := content.LimitStorage(cache, pushLimit)
    51  	return &Proxy{
    52  		ReadOnlyStorage: base,
    53  		Cache:           limitedCache,
    54  	}
    55  }
    56  
    57  // Fetch fetches the content identified by the descriptor.
    58  func (p *Proxy) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
    59  	if p.StopCaching {
    60  		return p.FetchCached(ctx, target)
    61  	}
    62  
    63  	rc, err := p.Cache.Fetch(ctx, target)
    64  	if err == nil {
    65  		return rc, nil
    66  	}
    67  
    68  	rc, err = p.ReadOnlyStorage.Fetch(ctx, target)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  	pr, pw := io.Pipe()
    73  	var wg sync.WaitGroup
    74  	wg.Add(1)
    75  	var pushErr error
    76  	go func() {
    77  		defer wg.Done()
    78  		pushErr = p.Cache.Push(ctx, target, pr)
    79  		if pushErr != nil {
    80  			pr.CloseWithError(pushErr)
    81  		}
    82  	}()
    83  	closer := ioutil.CloserFunc(func() error {
    84  		rcErr := rc.Close()
    85  		if err := pw.Close(); err != nil {
    86  			return err
    87  		}
    88  		wg.Wait()
    89  		if pushErr != nil {
    90  			return pushErr
    91  		}
    92  		return rcErr
    93  	})
    94  
    95  	return struct {
    96  		io.Reader
    97  		io.Closer
    98  	}{
    99  		Reader: io.TeeReader(rc, pw),
   100  		Closer: closer,
   101  	}, nil
   102  }
   103  
   104  // FetchCached fetches the content identified by the descriptor.
   105  // If the content is not cached, it will be fetched from the remote without
   106  // caching.
   107  func (p *Proxy) FetchCached(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
   108  	exists, err := p.Cache.Exists(ctx, target)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  	if exists {
   113  		return p.Cache.Fetch(ctx, target)
   114  	}
   115  	return p.ReadOnlyStorage.Fetch(ctx, target)
   116  }
   117  
   118  // Exists returns true if the described content exists.
   119  func (p *Proxy) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
   120  	exists, err := p.Cache.Exists(ctx, target)
   121  	if err == nil && exists {
   122  		return true, nil
   123  	}
   124  	return p.ReadOnlyStorage.Exists(ctx, target)
   125  }