github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/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  	)
   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]interface{}),
   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  }