github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/resources/resource.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  	"fmt"
    18  	"io"
    19  	"io/ioutil"
    20  	"os"
    21  	"path"
    22  	"path/filepath"
    23  	"sync"
    24  
    25  	"github.com/gohugoio/hugo/resources/internal"
    26  
    27  	"github.com/gohugoio/hugo/common/herrors"
    28  
    29  	"github.com/gohugoio/hugo/hugofs"
    30  
    31  	"github.com/gohugoio/hugo/media"
    32  	"github.com/gohugoio/hugo/source"
    33  
    34  	"github.com/pkg/errors"
    35  
    36  	"github.com/gohugoio/hugo/common/hugio"
    37  	"github.com/gohugoio/hugo/common/maps"
    38  	"github.com/gohugoio/hugo/resources/page"
    39  	"github.com/gohugoio/hugo/resources/resource"
    40  	"github.com/spf13/afero"
    41  
    42  	"github.com/gohugoio/hugo/helpers"
    43  )
    44  
    45  var (
    46  	_ resource.ContentResource         = (*genericResource)(nil)
    47  	_ resource.ReadSeekCloserResource  = (*genericResource)(nil)
    48  	_ resource.Resource                = (*genericResource)(nil)
    49  	_ resource.Source                  = (*genericResource)(nil)
    50  	_ resource.Cloner                  = (*genericResource)(nil)
    51  	_ resource.ResourcesLanguageMerger = (*resource.Resources)(nil)
    52  	_ permalinker                      = (*genericResource)(nil)
    53  	_ resource.Identifier              = (*genericResource)(nil)
    54  	_ fileInfo                         = (*genericResource)(nil)
    55  )
    56  
    57  type ResourceSourceDescriptor struct {
    58  	// TargetPaths is a callback to fetch paths's relative to its owner.
    59  	TargetPaths func() page.TargetPaths
    60  
    61  	// Need one of these to load the resource content.
    62  	SourceFile         source.File
    63  	OpenReadSeekCloser resource.OpenReadSeekCloser
    64  
    65  	FileInfo os.FileInfo
    66  
    67  	// If OpenReadSeekerCloser is not set, we use this to open the file.
    68  	SourceFilename string
    69  
    70  	Fs afero.Fs
    71  
    72  	// The relative target filename without any language code.
    73  	RelTargetFilename string
    74  
    75  	// Any base paths prepended to the target path. This will also typically be the
    76  	// language code, but setting it here means that it should not have any effect on
    77  	// the permalink.
    78  	// This may be several values. In multihost mode we may publish the same resources to
    79  	// multiple targets.
    80  	TargetBasePaths []string
    81  
    82  	// Delay publishing until either Permalink or RelPermalink is called. Maybe never.
    83  	LazyPublish bool
    84  }
    85  
    86  func (r ResourceSourceDescriptor) Filename() string {
    87  	if r.SourceFile != nil {
    88  		return r.SourceFile.Filename()
    89  	}
    90  	return r.SourceFilename
    91  }
    92  
    93  type ResourceTransformer interface {
    94  	resource.Resource
    95  	Transformer
    96  }
    97  
    98  type Transformer interface {
    99  	Transform(...ResourceTransformation) (ResourceTransformer, error)
   100  }
   101  
   102  func NewFeatureNotAvailableTransformer(key string, elements ...interface{}) ResourceTransformation {
   103  	return transformerNotAvailable{
   104  		key: internal.NewResourceTransformationKey(key, elements...),
   105  	}
   106  }
   107  
   108  type transformerNotAvailable struct {
   109  	key internal.ResourceTransformationKey
   110  }
   111  
   112  func (t transformerNotAvailable) Transform(ctx *ResourceTransformationCtx) error {
   113  	return herrors.ErrFeatureNotAvailable
   114  }
   115  
   116  func (t transformerNotAvailable) Key() internal.ResourceTransformationKey {
   117  	return t.key
   118  }
   119  
   120  type baseResourceResource interface {
   121  	resource.Cloner
   122  	resource.ContentProvider
   123  	resource.Resource
   124  	resource.Identifier
   125  }
   126  
   127  type baseResourceInternal interface {
   128  	resource.Source
   129  
   130  	fileInfo
   131  	metaAssigner
   132  	targetPather
   133  
   134  	ReadSeekCloser() (hugio.ReadSeekCloser, error)
   135  
   136  	// Internal
   137  	cloneWithUpdates(*transformationUpdate) (baseResource, error)
   138  	tryTransformedFileCache(key string, u *transformationUpdate) io.ReadCloser
   139  
   140  	specProvider
   141  	getResourcePaths() *resourcePathDescriptor
   142  	getTargetFilenames() []string
   143  	openDestinationsForWriting() (io.WriteCloser, error)
   144  	openPublishFileForWriting(relTargetPath string) (io.WriteCloser, error)
   145  
   146  	relTargetPathForRel(rel string, addBaseTargetPath, isAbs, isURL bool) string
   147  }
   148  
   149  type specProvider interface {
   150  	getSpec() *Spec
   151  }
   152  
   153  type baseResource interface {
   154  	baseResourceResource
   155  	baseResourceInternal
   156  }
   157  
   158  type commonResource struct {
   159  }
   160  
   161  // Slice is not meant to be used externally. It's a bridge function
   162  // for the template functions. See collections.Slice.
   163  func (commonResource) Slice(in interface{}) (interface{}, error) {
   164  	switch items := in.(type) {
   165  	case resource.Resources:
   166  		return items, nil
   167  	case []interface{}:
   168  		groups := make(resource.Resources, len(items))
   169  		for i, v := range items {
   170  			g, ok := v.(resource.Resource)
   171  			if !ok {
   172  				return nil, fmt.Errorf("type %T is not a Resource", v)
   173  			}
   174  			groups[i] = g
   175  			{
   176  			}
   177  		}
   178  		return groups, nil
   179  	default:
   180  		return nil, fmt.Errorf("invalid slice type %T", items)
   181  	}
   182  }
   183  
   184  type dirFile struct {
   185  	// This is the directory component with Unix-style slashes.
   186  	dir string
   187  	// This is the file component.
   188  	file string
   189  }
   190  
   191  func (d dirFile) path() string {
   192  	return path.Join(d.dir, d.file)
   193  }
   194  
   195  type fileInfo interface {
   196  	getSourceFilename() string
   197  	setSourceFilename(string)
   198  	setSourceFs(afero.Fs)
   199  	getFileInfo() hugofs.FileMetaInfo
   200  	hash() (string, error)
   201  	size() int
   202  }
   203  
   204  // genericResource represents a generic linkable resource.
   205  type genericResource struct {
   206  	*resourcePathDescriptor
   207  	*resourceFileInfo
   208  	*resourceContent
   209  
   210  	spec *Spec
   211  
   212  	title  string
   213  	name   string
   214  	params map[string]interface{}
   215  	data   map[string]interface{}
   216  
   217  	resourceType string
   218  	mediaType    media.Type
   219  }
   220  
   221  func (l *genericResource) Clone() resource.Resource {
   222  	return l.clone()
   223  }
   224  
   225  func (l *genericResource) Content() (interface{}, error) {
   226  	if err := l.initContent(); err != nil {
   227  		return nil, err
   228  	}
   229  
   230  	return l.content, nil
   231  }
   232  
   233  func (l *genericResource) Data() interface{} {
   234  	return l.data
   235  }
   236  
   237  func (l *genericResource) Key() string {
   238  	return l.RelPermalink()
   239  }
   240  
   241  func (l *genericResource) MediaType() media.Type {
   242  	return l.mediaType
   243  }
   244  
   245  func (l *genericResource) setMediaType(mediaType media.Type) {
   246  	l.mediaType = mediaType
   247  }
   248  
   249  func (l *genericResource) Name() string {
   250  	return l.name
   251  }
   252  
   253  func (l *genericResource) Params() maps.Params {
   254  	return l.params
   255  }
   256  
   257  func (l *genericResource) Permalink() string {
   258  	return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(l.relTargetDirFile.path(), true), l.spec.BaseURL.HostURL())
   259  }
   260  
   261  func (l *genericResource) Publish() error {
   262  	var err error
   263  	l.publishInit.Do(func() {
   264  		var fr hugio.ReadSeekCloser
   265  		fr, err = l.ReadSeekCloser()
   266  		if err != nil {
   267  			return
   268  		}
   269  		defer fr.Close()
   270  
   271  		var fw io.WriteCloser
   272  		fw, err = helpers.OpenFilesForWriting(l.spec.BaseFs.PublishFs, l.getTargetFilenames()...)
   273  		if err != nil {
   274  			return
   275  		}
   276  		defer fw.Close()
   277  
   278  		_, err = io.Copy(fw, fr)
   279  	})
   280  
   281  	return err
   282  }
   283  
   284  func (l *genericResource) RelPermalink() string {
   285  	return l.relPermalinkFor(l.relTargetDirFile.path())
   286  }
   287  
   288  func (l *genericResource) ResourceType() string {
   289  	return l.resourceType
   290  }
   291  
   292  func (l *genericResource) String() string {
   293  	return fmt.Sprintf("Resource(%s: %s)", l.resourceType, l.name)
   294  }
   295  
   296  // Path is stored with Unix style slashes.
   297  func (l *genericResource) TargetPath() string {
   298  	return l.relTargetDirFile.path()
   299  }
   300  
   301  func (l *genericResource) Title() string {
   302  	return l.title
   303  }
   304  
   305  func (l *genericResource) createBasePath(rel string, isURL bool) string {
   306  	if l.targetPathBuilder == nil {
   307  		return rel
   308  	}
   309  	tp := l.targetPathBuilder()
   310  
   311  	if isURL {
   312  		return path.Join(tp.SubResourceBaseLink, rel)
   313  	}
   314  
   315  	// TODO(bep) path
   316  	return path.Join(filepath.ToSlash(tp.SubResourceBaseTarget), rel)
   317  }
   318  
   319  func (l *genericResource) initContent() error {
   320  	var err error
   321  	l.contentInit.Do(func() {
   322  		var r hugio.ReadSeekCloser
   323  		r, err = l.ReadSeekCloser()
   324  		if err != nil {
   325  			return
   326  		}
   327  		defer r.Close()
   328  
   329  		var b []byte
   330  		b, err = ioutil.ReadAll(r)
   331  		if err != nil {
   332  			return
   333  		}
   334  
   335  		l.content = string(b)
   336  	})
   337  
   338  	return err
   339  }
   340  
   341  func (l *genericResource) setName(name string) {
   342  	l.name = name
   343  }
   344  
   345  func (l *genericResource) getResourcePaths() *resourcePathDescriptor {
   346  	return l.resourcePathDescriptor
   347  }
   348  
   349  func (l *genericResource) getSpec() *Spec {
   350  	return l.spec
   351  }
   352  
   353  func (l *genericResource) getTargetFilenames() []string {
   354  	paths := l.relTargetPaths()
   355  	for i, p := range paths {
   356  		paths[i] = filepath.Clean(p)
   357  	}
   358  	return paths
   359  }
   360  
   361  func (l *genericResource) setTitle(title string) {
   362  	l.title = title
   363  }
   364  
   365  func (r *genericResource) tryTransformedFileCache(key string, u *transformationUpdate) io.ReadCloser {
   366  	fi, f, meta, found := r.spec.ResourceCache.getFromFile(key)
   367  	if !found {
   368  		return nil
   369  	}
   370  	u.sourceFilename = &fi.Name
   371  	mt, _ := r.spec.MediaTypes.GetByType(meta.MediaTypeV)
   372  	u.mediaType = mt
   373  	u.data = meta.MetaData
   374  	u.targetPath = meta.Target
   375  	return f
   376  }
   377  
   378  func (r *genericResource) mergeData(in map[string]interface{}) {
   379  	if len(in) == 0 {
   380  		return
   381  	}
   382  	if r.data == nil {
   383  		r.data = make(map[string]interface{})
   384  	}
   385  	for k, v := range in {
   386  		if _, found := r.data[k]; !found {
   387  			r.data[k] = v
   388  		}
   389  	}
   390  }
   391  
   392  func (rc *genericResource) cloneWithUpdates(u *transformationUpdate) (baseResource, error) {
   393  	r := rc.clone()
   394  
   395  	if u.content != nil {
   396  		r.contentInit.Do(func() {
   397  			r.content = *u.content
   398  			r.openReadSeekerCloser = func() (hugio.ReadSeekCloser, error) {
   399  				return hugio.NewReadSeekerNoOpCloserFromString(r.content), nil
   400  			}
   401  		})
   402  	}
   403  
   404  	r.mediaType = u.mediaType
   405  
   406  	if u.sourceFilename != nil {
   407  		r.setSourceFilename(*u.sourceFilename)
   408  	}
   409  
   410  	if u.sourceFs != nil {
   411  		r.setSourceFs(u.sourceFs)
   412  	}
   413  
   414  	if u.targetPath == "" {
   415  		return nil, errors.New("missing targetPath")
   416  	}
   417  
   418  	fpath, fname := path.Split(u.targetPath)
   419  	r.resourcePathDescriptor.relTargetDirFile = dirFile{dir: fpath, file: fname}
   420  
   421  	r.mergeData(u.data)
   422  
   423  	return r, nil
   424  }
   425  
   426  func (l genericResource) clone() *genericResource {
   427  	gi := *l.resourceFileInfo
   428  	rp := *l.resourcePathDescriptor
   429  	l.resourceFileInfo = &gi
   430  	l.resourcePathDescriptor = &rp
   431  	l.resourceContent = &resourceContent{}
   432  	return &l
   433  }
   434  
   435  // returns an opened file or nil if nothing to write (it may already be published).
   436  func (l *genericResource) openDestinationsForWriting() (w io.WriteCloser, err error) {
   437  	l.publishInit.Do(func() {
   438  		targetFilenames := l.getTargetFilenames()
   439  		var changedFilenames []string
   440  
   441  		// Fast path:
   442  		// This is a processed version of the original;
   443  		// check if it already exists at the destination.
   444  		for _, targetFilename := range targetFilenames {
   445  			if _, err := l.getSpec().BaseFs.PublishFs.Stat(targetFilename); err == nil {
   446  				continue
   447  			}
   448  
   449  			changedFilenames = append(changedFilenames, targetFilename)
   450  		}
   451  
   452  		if len(changedFilenames) == 0 {
   453  			return
   454  		}
   455  
   456  		w, err = helpers.OpenFilesForWriting(l.getSpec().BaseFs.PublishFs, changedFilenames...)
   457  	})
   458  
   459  	return
   460  }
   461  
   462  func (r *genericResource) openPublishFileForWriting(relTargetPath string) (io.WriteCloser, error) {
   463  	return helpers.OpenFilesForWriting(r.spec.BaseFs.PublishFs, r.relTargetPathsFor(relTargetPath)...)
   464  }
   465  
   466  func (l *genericResource) permalinkFor(target string) string {
   467  	return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(target, true), l.spec.BaseURL.HostURL())
   468  }
   469  
   470  func (l *genericResource) relPermalinkFor(target string) string {
   471  	return l.relPermalinkForRel(target, false)
   472  }
   473  
   474  func (l *genericResource) relPermalinkForRel(rel string, isAbs bool) string {
   475  	return l.spec.PathSpec.URLizeFilename(l.relTargetPathForRel(rel, false, isAbs, true))
   476  }
   477  
   478  func (l *genericResource) relTargetPathForRel(rel string, addBaseTargetPath, isAbs, isURL bool) string {
   479  	if addBaseTargetPath && len(l.baseTargetPathDirs) > 1 {
   480  		panic("multiple baseTargetPathDirs")
   481  	}
   482  	var basePath string
   483  	if addBaseTargetPath && len(l.baseTargetPathDirs) > 0 {
   484  		basePath = l.baseTargetPathDirs[0]
   485  	}
   486  
   487  	return l.relTargetPathForRelAndBasePath(rel, basePath, isAbs, isURL)
   488  }
   489  
   490  func (l *genericResource) relTargetPathForRelAndBasePath(rel, basePath string, isAbs, isURL bool) string {
   491  	rel = l.createBasePath(rel, isURL)
   492  
   493  	if basePath != "" {
   494  		rel = path.Join(basePath, rel)
   495  	}
   496  
   497  	if l.baseOffset != "" {
   498  		rel = path.Join(l.baseOffset, rel)
   499  	}
   500  
   501  	if isURL {
   502  		bp := l.spec.PathSpec.GetBasePath(!isAbs)
   503  		if bp != "" {
   504  			rel = path.Join(bp, rel)
   505  		}
   506  	}
   507  
   508  	if len(rel) == 0 || rel[0] != '/' {
   509  		rel = "/" + rel
   510  	}
   511  
   512  	return rel
   513  }
   514  
   515  func (l *genericResource) relTargetPaths() []string {
   516  	return l.relTargetPathsForRel(l.TargetPath())
   517  }
   518  
   519  func (l *genericResource) relTargetPathsFor(target string) []string {
   520  	return l.relTargetPathsForRel(target)
   521  }
   522  
   523  func (l *genericResource) relTargetPathsForRel(rel string) []string {
   524  	if len(l.baseTargetPathDirs) == 0 {
   525  		return []string{l.relTargetPathForRelAndBasePath(rel, "", false, false)}
   526  	}
   527  
   528  	targetPaths := make([]string, len(l.baseTargetPathDirs))
   529  	for i, dir := range l.baseTargetPathDirs {
   530  		targetPaths[i] = l.relTargetPathForRelAndBasePath(rel, dir, false, false)
   531  	}
   532  	return targetPaths
   533  }
   534  
   535  func (l *genericResource) updateParams(params map[string]interface{}) {
   536  	if l.params == nil {
   537  		l.params = params
   538  		return
   539  	}
   540  
   541  	// Sets the params not already set
   542  	for k, v := range params {
   543  		if _, found := l.params[k]; !found {
   544  			l.params[k] = v
   545  		}
   546  	}
   547  }
   548  
   549  type targetPather interface {
   550  	TargetPath() string
   551  }
   552  
   553  type permalinker interface {
   554  	targetPather
   555  	permalinkFor(target string) string
   556  	relPermalinkFor(target string) string
   557  	relTargetPaths() []string
   558  	relTargetPathsFor(target string) []string
   559  }
   560  
   561  type resourceContent struct {
   562  	content     string
   563  	contentInit sync.Once
   564  
   565  	publishInit sync.Once
   566  }
   567  
   568  type resourceFileInfo struct {
   569  	// Will be set if this resource is backed by something other than a file.
   570  	openReadSeekerCloser resource.OpenReadSeekCloser
   571  
   572  	// This may be set to tell us to look in another filesystem for this resource.
   573  	// We, by default, use the sourceFs filesystem in the spec below.
   574  	sourceFs afero.Fs
   575  
   576  	// Absolute filename to the source, including any content folder path.
   577  	// Note that this is absolute in relation to the filesystem it is stored in.
   578  	// It can be a base path filesystem, and then this filename will not match
   579  	// the path to the file on the real filesystem.
   580  	sourceFilename string
   581  
   582  	fi hugofs.FileMetaInfo
   583  
   584  	// A hash of the source content. Is only calculated in caching situations.
   585  	h *resourceHash
   586  }
   587  
   588  func (fi *resourceFileInfo) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
   589  	if fi.openReadSeekerCloser != nil {
   590  		return fi.openReadSeekerCloser()
   591  	}
   592  
   593  	f, err := fi.getSourceFs().Open(fi.getSourceFilename())
   594  	if err != nil {
   595  		return nil, err
   596  	}
   597  	return f, nil
   598  }
   599  
   600  func (fi *resourceFileInfo) getFileInfo() hugofs.FileMetaInfo {
   601  	return fi.fi
   602  }
   603  
   604  func (fi *resourceFileInfo) getSourceFilename() string {
   605  	return fi.sourceFilename
   606  }
   607  
   608  func (fi *resourceFileInfo) setSourceFilename(s string) {
   609  	// Make sure it's always loaded by sourceFilename.
   610  	fi.openReadSeekerCloser = nil
   611  	fi.sourceFilename = s
   612  }
   613  
   614  func (fi *resourceFileInfo) getSourceFs() afero.Fs {
   615  	return fi.sourceFs
   616  }
   617  
   618  func (fi *resourceFileInfo) setSourceFs(fs afero.Fs) {
   619  	fi.sourceFs = fs
   620  }
   621  
   622  func (fi *resourceFileInfo) hash() (string, error) {
   623  	var err error
   624  	fi.h.init.Do(func() {
   625  		var hash string
   626  		var f hugio.ReadSeekCloser
   627  		f, err = fi.ReadSeekCloser()
   628  		if err != nil {
   629  			err = errors.Wrap(err, "failed to open source file")
   630  			return
   631  		}
   632  		defer f.Close()
   633  
   634  		hash, err = helpers.MD5FromFileFast(f)
   635  		if err != nil {
   636  			return
   637  		}
   638  		fi.h.value = hash
   639  	})
   640  
   641  	return fi.h.value, err
   642  }
   643  
   644  func (fi *resourceFileInfo) size() int {
   645  	if fi.fi == nil {
   646  		return 0
   647  	}
   648  
   649  	return int(fi.fi.Size())
   650  }
   651  
   652  type resourceHash struct {
   653  	value string
   654  	init  sync.Once
   655  }
   656  
   657  type resourcePathDescriptor struct {
   658  	// The relative target directory and filename.
   659  	relTargetDirFile dirFile
   660  
   661  	// Callback used to construct a target path relative to its owner.
   662  	targetPathBuilder func() page.TargetPaths
   663  
   664  	// This will normally be the same as above, but this will only apply to publishing
   665  	// of resources. It may be multiple values when in multihost mode.
   666  	baseTargetPathDirs []string
   667  
   668  	// baseOffset is set when the output format's path has a offset, e.g. for AMP.
   669  	baseOffset string
   670  }