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 }