github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/resources/resource_spec.go (about) 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 nil, 196 ) 197 } 198 199 func (r *Spec) newGenericResourceWithBase( 200 sourceFs afero.Fs, 201 openReadSeekerCloser resource.OpenReadSeekCloser, 202 targetPathBaseDirs []string, 203 targetPathBuilder func() page.TargetPaths, 204 osFileInfo os.FileInfo, 205 sourceFilename, 206 baseFilename string, 207 mediaType media.Type, 208 data map[string]any, 209 ) *genericResource { 210 if osFileInfo != nil && osFileInfo.IsDir() { 211 panic(fmt.Sprintf("dirs not supported resource types: %v", osFileInfo)) 212 } 213 214 // This value is used both to construct URLs and file paths, but start 215 // with a Unix-styled path. 216 baseFilename = helpers.ToSlashTrimLeading(baseFilename) 217 fpath, fname := path.Split(baseFilename) 218 219 resourceType := mediaType.MainType 220 221 pathDescriptor := &resourcePathDescriptor{ 222 baseTargetPathDirs: helpers.UniqueStringsReuse(targetPathBaseDirs), 223 targetPathBuilder: targetPathBuilder, 224 relTargetDirFile: dirFile{dir: fpath, file: fname}, 225 } 226 227 var fim hugofs.FileMetaInfo 228 if osFileInfo != nil { 229 fim = osFileInfo.(hugofs.FileMetaInfo) 230 } 231 232 gfi := &resourceFileInfo{ 233 fi: fim, 234 openReadSeekerCloser: openReadSeekerCloser, 235 sourceFs: sourceFs, 236 sourceFilename: sourceFilename, 237 h: &resourceHash{}, 238 } 239 240 g := &genericResource{ 241 resourceFileInfo: gfi, 242 resourcePathDescriptor: pathDescriptor, 243 mediaType: mediaType, 244 resourceType: resourceType, 245 spec: r, 246 params: make(map[string]any), 247 name: baseFilename, 248 title: baseFilename, 249 resourceContent: &resourceContent{}, 250 data: data, 251 } 252 253 return g 254 } 255 256 func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (resource.Resource, error) { 257 fi := fd.FileInfo 258 var sourceFilename string 259 260 if fd.OpenReadSeekCloser != nil { 261 } else if fd.SourceFilename != "" { 262 var err error 263 fi, err = sourceFs.Stat(fd.SourceFilename) 264 if err != nil { 265 if herrors.IsNotExist(err) { 266 return nil, nil 267 } 268 return nil, err 269 } 270 sourceFilename = fd.SourceFilename 271 } else { 272 sourceFilename = fd.SourceFile.Filename() 273 } 274 275 if fd.RelTargetFilename == "" { 276 fd.RelTargetFilename = sourceFilename 277 } 278 279 mimeType := fd.MediaType 280 if mimeType.IsZero() { 281 ext := strings.ToLower(filepath.Ext(fd.RelTargetFilename)) 282 var ( 283 found bool 284 suffixInfo media.SuffixInfo 285 ) 286 mimeType, suffixInfo, found = r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")) 287 // TODO(bep) we need to handle these ambiguous types better, but in this context 288 // we most likely want the application/xml type. 289 if suffixInfo.Suffix == "xml" && mimeType.SubType == "rss" { 290 mimeType, found = r.MediaTypes.GetByType("application/xml") 291 } 292 293 if !found { 294 // A fallback. Note that mime.TypeByExtension is slow by Hugo standards, 295 // so we should configure media types to avoid this lookup for most 296 // situations. 297 mimeStr := mime.TypeByExtension(ext) 298 if mimeStr != "" { 299 mimeType, _ = media.FromStringAndExt(mimeStr, ext) 300 } 301 } 302 } 303 304 gr := r.newGenericResourceWithBase( 305 sourceFs, 306 fd.OpenReadSeekCloser, 307 fd.TargetBasePaths, 308 fd.TargetPaths, 309 fi, 310 sourceFilename, 311 fd.RelTargetFilename, 312 mimeType, 313 fd.Data) 314 315 if mimeType.MainType == "image" { 316 imgFormat, ok := images.ImageFormatFromMediaSubType(mimeType.SubType) 317 if ok { 318 ir := &imageResource{ 319 Image: images.NewImage(imgFormat, r.imaging, nil, gr), 320 baseResource: gr, 321 } 322 ir.root = ir 323 return newResourceAdapter(gr.spec, fd.LazyPublish, ir), nil 324 } 325 326 } 327 328 return newResourceAdapter(gr.spec, fd.LazyPublish, gr), nil 329 } 330 331 func (r *Spec) newResourceFor(fd ResourceSourceDescriptor) (resource.Resource, error) { 332 if fd.OpenReadSeekCloser == nil { 333 if fd.SourceFile != nil && fd.SourceFilename != "" { 334 return nil, errors.New("both SourceFile and AbsSourceFilename provided") 335 } else if fd.SourceFile == nil && fd.SourceFilename == "" { 336 return nil, errors.New("either SourceFile or AbsSourceFilename must be provided") 337 } 338 } 339 340 if fd.RelTargetFilename == "" { 341 fd.RelTargetFilename = fd.Filename() 342 } 343 344 if len(fd.TargetBasePaths) == 0 { 345 // If not set, we publish the same resource to all hosts. 346 fd.TargetBasePaths = r.MultihostTargetBasePaths 347 } 348 349 return r.newResource(fd.Fs, fd) 350 }