hugo/resources/resource_spec.go
2023-06-18 13:03:04 +02:00

383 lines
9.2 KiB
Go

// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package resources
import (
"errors"
"fmt"
"mime"
"os"
"path"
"path/filepath"
"strings"
"sync"
"github.com/BurntSushi/locker"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources/jsconfig"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/resources/postpub"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/tpl"
"github.com/spf13/afero"
)
func NewSpec(
s *helpers.PathSpec,
common *SpecCommon, // may be nil
imageCache *ImageCache, // may be nil
incr identity.Incrementer,
logger loggers.Logger,
errorHandler herrors.ErrorSender,
execHelper *hexec.Exec) (*Spec, error) {
fileCaches, err := filecache.NewCaches(s)
if err != nil {
return nil, fmt.Errorf("failed to create file caches from configuration: %w", err)
}
conf := s.Cfg.GetConfig().(*allconfig.Config)
imgConfig := conf.Imaging
imaging, err := images.NewImageProcessor(imgConfig)
if err != nil {
return nil, err
}
if incr == nil {
incr = &identity.IncrementByOne{}
}
if logger == nil {
logger = loggers.NewDefault()
}
permalinks, err := page.NewPermalinkExpander(s.URLize, conf.Permalinks)
if err != nil {
return nil, err
}
if common == nil {
common = &SpecCommon{
incr: incr,
FileCaches: fileCaches,
PostBuildAssets: &PostBuildAssets{
PostProcessResources: make(map[string]postpub.PostPublishedResource),
JSConfigBuilder: jsconfig.NewBuilder(),
},
ResourceCache: &ResourceCache{
fileCache: fileCaches.AssetsCache(),
cache: make(map[string]any),
nlocker: locker.NewLocker(),
},
}
}
if imageCache == nil {
imageCache = newImageCache(
fileCaches.ImageCache(),
s,
)
} else {
imageCache = imageCache.WithPathSpec(s)
}
rs := &Spec{
PathSpec: s,
Logger: logger,
ErrorSender: errorHandler,
imaging: imaging,
ImageCache: imageCache,
ExecHelper: execHelper,
Permalinks: permalinks,
SpecCommon: common,
}
return rs, nil
}
type Spec struct {
*helpers.PathSpec
Logger loggers.Logger
ErrorSender herrors.ErrorSender
TextTemplates tpl.TemplateParseFinder
Permalinks page.PermalinkExpander
ImageCache *ImageCache
// Holds default filter settings etc.
imaging *images.ImageProcessor
ExecHelper *hexec.Exec
*SpecCommon
}
// The parts of Spec that's comoon for all sites.
type SpecCommon struct {
incr identity.Incrementer
ResourceCache *ResourceCache
FileCaches filecache.Caches
// Assets used after the build is done.
// This is shared between all sites.
*PostBuildAssets
}
type PostBuildAssets struct {
postProcessMu sync.RWMutex
PostProcessResources map[string]postpub.PostPublishedResource
JSConfigBuilder *jsconfig.Builder
}
func (r *Spec) New(fd ResourceSourceDescriptor) (resource.Resource, error) {
return r.newResourceFor(fd)
}
func (r *Spec) MediaTypes() media.Types {
return r.Cfg.GetConfigSection("mediaTypes").(media.Types)
}
func (r *Spec) OutputFormats() output.Formats {
return r.Cfg.GetConfigSection("outputFormats").(output.Formats)
}
func (r *Spec) BuildConfig() config.BuildConfig {
return r.Cfg.GetConfigSection("build").(config.BuildConfig)
}
func (r *Spec) CacheStats() string {
r.ImageCache.mu.RLock()
defer r.ImageCache.mu.RUnlock()
s := fmt.Sprintf("Cache entries: %d", len(r.ImageCache.store))
count := 0
for k := range r.ImageCache.store {
if count > 5 {
break
}
s += "\n" + k
count++
}
return s
}
func (r *Spec) ClearCaches() {
r.ImageCache.clear()
r.ResourceCache.clear()
}
func (r *Spec) DeleteBySubstring(s string) {
r.ImageCache.deleteIfContains(s)
}
func (s *Spec) String() string {
return "spec"
}
// TODO(bep) clean up below
func (r *Spec) newGenericResource(sourceFs afero.Fs,
targetPathBuilder func() page.TargetPaths,
osFileInfo os.FileInfo,
sourceFilename,
baseFilename string,
mediaType media.Type) *genericResource {
return r.newGenericResourceWithBase(
sourceFs,
nil,
nil,
targetPathBuilder,
osFileInfo,
sourceFilename,
baseFilename,
mediaType,
nil,
)
}
func (r *Spec) newGenericResourceWithBase(
sourceFs afero.Fs,
openReadSeekerCloser resource.OpenReadSeekCloser,
targetPathBaseDirs []string,
targetPathBuilder func() page.TargetPaths,
osFileInfo os.FileInfo,
sourceFilename,
baseFilename string,
mediaType media.Type,
data map[string]any,
) *genericResource {
if osFileInfo != nil && osFileInfo.IsDir() {
panic(fmt.Sprintf("dirs not supported resource types: %v", osFileInfo))
}
// This value is used both to construct URLs and file paths, but start
// with a Unix-styled path.
baseFilename = helpers.ToSlashTrimLeading(baseFilename)
fpath, fname := path.Split(baseFilename)
resourceType := mediaType.MainType
pathDescriptor := &resourcePathDescriptor{
baseTargetPathDirs: helpers.UniqueStringsReuse(targetPathBaseDirs),
targetPathBuilder: targetPathBuilder,
relTargetDirFile: dirFile{dir: fpath, file: fname},
}
var fim hugofs.FileMetaInfo
if osFileInfo != nil {
fim = osFileInfo.(hugofs.FileMetaInfo)
}
gfi := &resourceFileInfo{
fi: fim,
openReadSeekerCloser: openReadSeekerCloser,
sourceFs: sourceFs,
sourceFilename: sourceFilename,
h: &resourceHash{},
}
g := &genericResource{
resourceFileInfo: gfi,
resourcePathDescriptor: pathDescriptor,
mediaType: mediaType,
resourceType: resourceType,
spec: r,
params: make(map[string]any),
name: baseFilename,
title: baseFilename,
resourceContent: &resourceContent{},
data: data,
}
return g
}
func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (resource.Resource, error) {
fi := fd.FileInfo
var sourceFilename string
if fd.OpenReadSeekCloser != nil {
} else if fd.SourceFilename != "" {
var err error
fi, err = sourceFs.Stat(fd.SourceFilename)
if err != nil {
if herrors.IsNotExist(err) {
return nil, nil
}
return nil, err
}
sourceFilename = fd.SourceFilename
} else {
sourceFilename = fd.SourceFile.Filename()
}
if fd.RelTargetFilename == "" {
fd.RelTargetFilename = sourceFilename
}
mimeType := fd.MediaType
if mimeType.IsZero() {
ext := strings.ToLower(filepath.Ext(fd.RelTargetFilename))
var (
found bool
suffixInfo media.SuffixInfo
)
mimeType, suffixInfo, found = r.MediaTypes().GetFirstBySuffix(strings.TrimPrefix(ext, "."))
// TODO(bep) we need to handle these ambiguous types better, but in this context
// we most likely want the application/xml type.
if suffixInfo.Suffix == "xml" && mimeType.SubType == "rss" {
mimeType, found = r.MediaTypes().GetByType("application/xml")
}
if !found {
// A fallback. Note that mime.TypeByExtension is slow by Hugo standards,
// so we should configure media types to avoid this lookup for most
// situations.
mimeStr := mime.TypeByExtension(ext)
if mimeStr != "" {
mimeType, _ = media.FromStringAndExt(mimeStr, ext)
}
}
}
gr := r.newGenericResourceWithBase(
sourceFs,
fd.OpenReadSeekCloser,
fd.TargetBasePaths,
fd.TargetPaths,
fi,
sourceFilename,
fd.RelTargetFilename,
mimeType,
fd.Data)
if mimeType.MainType == "image" {
imgFormat, ok := images.ImageFormatFromMediaSubType(mimeType.SubType)
if ok {
ir := &imageResource{
Image: images.NewImage(imgFormat, r.imaging, nil, gr),
baseResource: gr,
}
ir.root = ir
return newResourceAdapter(gr.spec, fd.LazyPublish, ir), nil
}
}
return newResourceAdapter(gr.spec, fd.LazyPublish, gr), nil
}
func (r *Spec) newResourceFor(fd ResourceSourceDescriptor) (resource.Resource, error) {
if fd.OpenReadSeekCloser == nil {
if fd.SourceFile != nil && fd.SourceFilename != "" {
return nil, errors.New("both SourceFile and AbsSourceFilename provided")
} else if fd.SourceFile == nil && fd.SourceFilename == "" {
return nil, errors.New("either SourceFile or AbsSourceFilename must be provided")
}
}
if fd.RelTargetFilename == "" {
fd.RelTargetFilename = fd.Filename()
}
if len(fd.TargetBasePaths) == 0 {
// If not set, we publish the same resource to all hosts.
fd.TargetBasePaths = r.MultihostTargetBasePaths
}
return r.newResource(fd.Fs, fd)
}