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 }