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 }