mirror of
https://github.com/gohugoio/hugo.git
synced 2024-12-04 21:32:51 -05:00
7c9fada778
Fixes #11124
382 lines
9.2 KiB
Go
382 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)
|
|
}
|