resource_spec.go (8621B)
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 "errors"
18 "fmt"
19 "mime"
20 "os"
21 "path"
22 "path/filepath"
23 "strings"
24 "sync"
25
26 "github.com/gohugoio/hugo/resources/jsconfig"
27
28 "github.com/gohugoio/hugo/common/herrors"
29 "github.com/gohugoio/hugo/common/hexec"
30
31 "github.com/gohugoio/hugo/config"
32 "github.com/gohugoio/hugo/identity"
33
34 "github.com/gohugoio/hugo/helpers"
35 "github.com/gohugoio/hugo/hugofs"
36 "github.com/gohugoio/hugo/resources/postpub"
37
38 "github.com/gohugoio/hugo/cache/filecache"
39 "github.com/gohugoio/hugo/common/loggers"
40 "github.com/gohugoio/hugo/media"
41 "github.com/gohugoio/hugo/output"
42 "github.com/gohugoio/hugo/resources/images"
43 "github.com/gohugoio/hugo/resources/page"
44 "github.com/gohugoio/hugo/resources/resource"
45 "github.com/gohugoio/hugo/tpl"
46 "github.com/spf13/afero"
47 )
48
49 func NewSpec(
50 s *helpers.PathSpec,
51 fileCaches filecache.Caches,
52 incr identity.Incrementer,
53 logger loggers.Logger,
54 errorHandler herrors.ErrorSender,
55 execHelper *hexec.Exec,
56 outputFormats output.Formats,
57 mimeTypes media.Types) (*Spec, error) {
58 imgConfig, err := images.DecodeConfig(s.Cfg.GetStringMap("imaging"))
59 if err != nil {
60 return nil, err
61 }
62
63 imaging, err := images.NewImageProcessor(imgConfig)
64 if err != nil {
65 return nil, err
66 }
67
68 if incr == nil {
69 incr = &identity.IncrementByOne{}
70 }
71
72 if logger == nil {
73 logger = loggers.NewErrorLogger()
74 }
75
76 permalinks, err := page.NewPermalinkExpander(s)
77 if err != nil {
78 return nil, err
79 }
80
81 rs := &Spec{
82 PathSpec: s,
83 Logger: logger,
84 ErrorSender: errorHandler,
85 imaging: imaging,
86 ExecHelper: execHelper,
87 incr: incr,
88 MediaTypes: mimeTypes,
89 OutputFormats: outputFormats,
90 Permalinks: permalinks,
91 BuildConfig: config.DecodeBuild(s.Cfg),
92 FileCaches: fileCaches,
93 PostBuildAssets: &PostBuildAssets{
94 PostProcessResources: make(map[string]postpub.PostPublishedResource),
95 JSConfigBuilder: jsconfig.NewBuilder(),
96 },
97 imageCache: newImageCache(
98 fileCaches.ImageCache(),
99
100 s,
101 ),
102 }
103
104 rs.ResourceCache = newResourceCache(rs)
105
106 return rs, nil
107 }
108
109 type Spec struct {
110 *helpers.PathSpec
111
112 MediaTypes media.Types
113 OutputFormats output.Formats
114
115 Logger loggers.Logger
116 ErrorSender herrors.ErrorSender
117
118 TextTemplates tpl.TemplateParseFinder
119
120 Permalinks page.PermalinkExpander
121 BuildConfig config.Build
122
123 // Holds default filter settings etc.
124 imaging *images.ImageProcessor
125
126 ExecHelper *hexec.Exec
127
128 incr identity.Incrementer
129 imageCache *imageCache
130 ResourceCache *ResourceCache
131 FileCaches filecache.Caches
132
133 // Assets used after the build is done.
134 // This is shared between all sites.
135 *PostBuildAssets
136 }
137
138 type PostBuildAssets struct {
139 postProcessMu sync.RWMutex
140 PostProcessResources map[string]postpub.PostPublishedResource
141 JSConfigBuilder *jsconfig.Builder
142 }
143
144 func (r *Spec) New(fd ResourceSourceDescriptor) (resource.Resource, error) {
145 return r.newResourceFor(fd)
146 }
147
148 func (r *Spec) CacheStats() string {
149 r.imageCache.mu.RLock()
150 defer r.imageCache.mu.RUnlock()
151
152 s := fmt.Sprintf("Cache entries: %d", len(r.imageCache.store))
153
154 count := 0
155 for k := range r.imageCache.store {
156 if count > 5 {
157 break
158 }
159 s += "\n" + k
160 count++
161 }
162
163 return s
164 }
165
166 func (r *Spec) ClearCaches() {
167 r.imageCache.clear()
168 r.ResourceCache.clear()
169 }
170
171 func (r *Spec) DeleteBySubstring(s string) {
172 r.imageCache.deleteIfContains(s)
173 }
174
175 func (s *Spec) String() string {
176 return "spec"
177 }
178
179 // TODO(bep) clean up below
180 func (r *Spec) newGenericResource(sourceFs afero.Fs,
181 targetPathBuilder func() page.TargetPaths,
182 osFileInfo os.FileInfo,
183 sourceFilename,
184 baseFilename string,
185 mediaType media.Type) *genericResource {
186 return r.newGenericResourceWithBase(
187 sourceFs,
188 nil,
189 nil,
190 targetPathBuilder,
191 osFileInfo,
192 sourceFilename,
193 baseFilename,
194 mediaType,
195 )
196 }
197
198 func (r *Spec) newGenericResourceWithBase(
199 sourceFs afero.Fs,
200 openReadSeekerCloser resource.OpenReadSeekCloser,
201 targetPathBaseDirs []string,
202 targetPathBuilder func() page.TargetPaths,
203 osFileInfo os.FileInfo,
204 sourceFilename,
205 baseFilename string,
206 mediaType media.Type) *genericResource {
207 if osFileInfo != nil && osFileInfo.IsDir() {
208 panic(fmt.Sprintf("dirs not supported resource types: %v", osFileInfo))
209 }
210
211 // This value is used both to construct URLs and file paths, but start
212 // with a Unix-styled path.
213 baseFilename = helpers.ToSlashTrimLeading(baseFilename)
214 fpath, fname := path.Split(baseFilename)
215
216 resourceType := mediaType.MainType
217
218 pathDescriptor := &resourcePathDescriptor{
219 baseTargetPathDirs: helpers.UniqueStringsReuse(targetPathBaseDirs),
220 targetPathBuilder: targetPathBuilder,
221 relTargetDirFile: dirFile{dir: fpath, file: fname},
222 }
223
224 var fim hugofs.FileMetaInfo
225 if osFileInfo != nil {
226 fim = osFileInfo.(hugofs.FileMetaInfo)
227 }
228
229 gfi := &resourceFileInfo{
230 fi: fim,
231 openReadSeekerCloser: openReadSeekerCloser,
232 sourceFs: sourceFs,
233 sourceFilename: sourceFilename,
234 h: &resourceHash{},
235 }
236
237 g := &genericResource{
238 resourceFileInfo: gfi,
239 resourcePathDescriptor: pathDescriptor,
240 mediaType: mediaType,
241 resourceType: resourceType,
242 spec: r,
243 params: make(map[string]any),
244 name: baseFilename,
245 title: baseFilename,
246 resourceContent: &resourceContent{},
247 }
248
249 return g
250 }
251
252 func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (resource.Resource, error) {
253 fi := fd.FileInfo
254 var sourceFilename string
255
256 if fd.OpenReadSeekCloser != nil {
257 } else if fd.SourceFilename != "" {
258 var err error
259 fi, err = sourceFs.Stat(fd.SourceFilename)
260 if err != nil {
261 if os.IsNotExist(err) {
262 return nil, nil
263 }
264 return nil, err
265 }
266 sourceFilename = fd.SourceFilename
267 } else {
268 sourceFilename = fd.SourceFile.Filename()
269 }
270
271 if fd.RelTargetFilename == "" {
272 fd.RelTargetFilename = sourceFilename
273 }
274
275 mimeType := fd.MediaType
276 if mimeType.IsZero() {
277 ext := strings.ToLower(filepath.Ext(fd.RelTargetFilename))
278 var (
279 found bool
280 suffixInfo media.SuffixInfo
281 )
282 mimeType, suffixInfo, found = r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, "."))
283 // TODO(bep) we need to handle these ambiguous types better, but in this context
284 // we most likely want the application/xml type.
285 if suffixInfo.Suffix == "xml" && mimeType.SubType == "rss" {
286 mimeType, found = r.MediaTypes.GetByType("application/xml")
287 }
288
289 if !found {
290 // A fallback. Note that mime.TypeByExtension is slow by Hugo standards,
291 // so we should configure media types to avoid this lookup for most
292 // situations.
293 mimeStr := mime.TypeByExtension(ext)
294 if mimeStr != "" {
295 mimeType, _ = media.FromStringAndExt(mimeStr, ext)
296 }
297 }
298 }
299
300 gr := r.newGenericResourceWithBase(
301 sourceFs,
302 fd.OpenReadSeekCloser,
303 fd.TargetBasePaths,
304 fd.TargetPaths,
305 fi,
306 sourceFilename,
307 fd.RelTargetFilename,
308 mimeType)
309
310 if mimeType.MainType == "image" {
311 imgFormat, ok := images.ImageFormatFromMediaSubType(mimeType.SubType)
312 if ok {
313 ir := &imageResource{
314 Image: images.NewImage(imgFormat, r.imaging, nil, gr),
315 baseResource: gr,
316 }
317 ir.root = ir
318 return newResourceAdapter(gr.spec, fd.LazyPublish, ir), nil
319 }
320
321 }
322
323 return newResourceAdapter(gr.spec, fd.LazyPublish, gr), nil
324 }
325
326 func (r *Spec) newResourceFor(fd ResourceSourceDescriptor) (resource.Resource, error) {
327 if fd.OpenReadSeekCloser == nil {
328 if fd.SourceFile != nil && fd.SourceFilename != "" {
329 return nil, errors.New("both SourceFile and AbsSourceFilename provided")
330 } else if fd.SourceFile == nil && fd.SourceFilename == "" {
331 return nil, errors.New("either SourceFile or AbsSourceFilename must be provided")
332 }
333 }
334
335 if fd.RelTargetFilename == "" {
336 fd.RelTargetFilename = fd.Filename()
337 }
338
339 if len(fd.TargetBasePaths) == 0 {
340 // If not set, we publish the same resource to all hosts.
341 fd.TargetBasePaths = r.MultihostTargetBasePaths
342 }
343
344 return r.newResource(fd.Fs, fd)
345 }