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  }