image_cache.go (4352B)
1 // Copyright 2019 The Hugo Authors. All rights reserved. 2 // 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 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package resources 15 16 import ( 17 "image" 18 "io" 19 "path/filepath" 20 "strings" 21 "sync" 22 23 "github.com/gohugoio/hugo/resources/images" 24 25 "github.com/gohugoio/hugo/cache/filecache" 26 "github.com/gohugoio/hugo/helpers" 27 ) 28 29 type imageCache struct { 30 pathSpec *helpers.PathSpec 31 32 fileCache *filecache.Cache 33 34 mu sync.RWMutex 35 store map[string]*resourceAdapter 36 } 37 38 func (c *imageCache) deleteIfContains(s string) { 39 c.mu.Lock() 40 defer c.mu.Unlock() 41 s = c.normalizeKeyBase(s) 42 for k := range c.store { 43 if strings.Contains(k, s) { 44 delete(c.store, k) 45 } 46 } 47 } 48 49 // The cache key is a lowercase path with Unix style slashes and it always starts with 50 // a leading slash. 51 func (c *imageCache) normalizeKey(key string) string { 52 return "/" + c.normalizeKeyBase(key) 53 } 54 55 func (c *imageCache) normalizeKeyBase(key string) string { 56 return strings.Trim(strings.ToLower(filepath.ToSlash(key)), "/") 57 } 58 59 func (c *imageCache) clear() { 60 c.mu.Lock() 61 defer c.mu.Unlock() 62 c.store = make(map[string]*resourceAdapter) 63 } 64 65 func (c *imageCache) getOrCreate( 66 parent *imageResource, conf images.ImageConfig, 67 createImage func() (*imageResource, image.Image, error)) (*resourceAdapter, error) { 68 relTarget := parent.relTargetPathFromConfig(conf) 69 memKey := parent.relTargetPathForRel(relTarget.path(), false, false, false) 70 memKey = c.normalizeKey(memKey) 71 72 // For the file cache we want to generate and store it once if possible. 73 fileKeyPath := relTarget 74 if fi := parent.root.getFileInfo(); fi != nil { 75 fileKeyPath.dir = filepath.ToSlash(filepath.Dir(fi.Meta().Path)) 76 } 77 fileKey := fileKeyPath.path() 78 79 // First check the in-memory store, then the disk. 80 c.mu.RLock() 81 cachedImage, found := c.store[memKey] 82 c.mu.RUnlock() 83 84 if found { 85 return cachedImage, nil 86 } 87 88 var img *imageResource 89 90 // These funcs are protected by a named lock. 91 // read clones the parent to its new name and copies 92 // the content to the destinations. 93 read := func(info filecache.ItemInfo, r io.ReadSeeker) error { 94 img = parent.clone(nil) 95 rp := img.getResourcePaths() 96 rp.relTargetDirFile.file = relTarget.file 97 img.setSourceFilename(info.Name) 98 img.setMediaType(conf.TargetFormat.MediaType()) 99 100 if err := img.InitConfig(r); err != nil { 101 return err 102 } 103 104 r.Seek(0, 0) 105 106 w, err := img.openDestinationsForWriting() 107 if err != nil { 108 return err 109 } 110 111 if w == nil { 112 // Nothing to write. 113 return nil 114 } 115 116 defer w.Close() 117 _, err = io.Copy(w, r) 118 119 return err 120 } 121 122 // create creates the image and encodes it to the cache (w). 123 create := func(info filecache.ItemInfo, w io.WriteCloser) (err error) { 124 defer w.Close() 125 126 var conv image.Image 127 img, conv, err = createImage() 128 if err != nil { 129 return 130 } 131 rp := img.getResourcePaths() 132 rp.relTargetDirFile.file = relTarget.file 133 img.setSourceFilename(info.Name) 134 135 return img.EncodeTo(conf, conv, w) 136 } 137 138 // Now look in the file cache. 139 140 // The definition of this counter is not that we have processed that amount 141 // (e.g. resized etc.), it can be fetched from file cache, 142 // but the count of processed image variations for this site. 143 c.pathSpec.ProcessingStats.Incr(&c.pathSpec.ProcessingStats.ProcessedImages) 144 145 _, err := c.fileCache.ReadOrCreate(fileKey, read, create) 146 if err != nil { 147 return nil, err 148 } 149 150 // The file is now stored in this cache. 151 img.setSourceFs(c.fileCache.Fs) 152 153 c.mu.Lock() 154 if cachedImage, found = c.store[memKey]; found { 155 c.mu.Unlock() 156 return cachedImage, nil 157 } 158 159 imgAdapter := newResourceAdapter(parent.getSpec(), true, img) 160 c.store[memKey] = imgAdapter 161 c.mu.Unlock() 162 163 return imgAdapter, nil 164 } 165 166 func newImageCache(fileCache *filecache.Cache, ps *helpers.PathSpec) *imageCache { 167 return &imageCache{fileCache: fileCache, pathSpec: ps, store: make(map[string]*resourceAdapter)} 168 }