github.com/dominikszabo/hugo-ds-clean@v0.47.1/resource/resource.go (about)

     1  // Copyright 2018 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 resource
    15  
    16  import (
    17  	"errors"
    18  	"fmt"
    19  	"io"
    20  	"io/ioutil"
    21  	"mime"
    22  	"os"
    23  	"path"
    24  	"path/filepath"
    25  	"strings"
    26  	"sync"
    27  
    28  	"github.com/gohugoio/hugo/output"
    29  	"github.com/gohugoio/hugo/tpl"
    30  
    31  	"github.com/gohugoio/hugo/common/loggers"
    32  
    33  	jww "github.com/spf13/jwalterweatherman"
    34  
    35  	"github.com/spf13/afero"
    36  
    37  	"github.com/gobwas/glob"
    38  	"github.com/gohugoio/hugo/helpers"
    39  	"github.com/gohugoio/hugo/media"
    40  	"github.com/gohugoio/hugo/source"
    41  )
    42  
    43  var (
    44  	_ ContentResource         = (*genericResource)(nil)
    45  	_ ReadSeekCloserResource  = (*genericResource)(nil)
    46  	_ Resource                = (*genericResource)(nil)
    47  	_ Source                  = (*genericResource)(nil)
    48  	_ Cloner                  = (*genericResource)(nil)
    49  	_ ResourcesLanguageMerger = (*Resources)(nil)
    50  	_ permalinker             = (*genericResource)(nil)
    51  )
    52  
    53  const DefaultResourceType = "unknown"
    54  
    55  var noData = make(map[string]interface{})
    56  
    57  // Source is an internal template and not meant for use in the templates. It
    58  // may change without notice.
    59  type Source interface {
    60  	Publish() error
    61  }
    62  
    63  type permalinker interface {
    64  	relPermalinkFor(target string) string
    65  	permalinkFor(target string) string
    66  	relTargetPathsFor(target string) []string
    67  	relTargetPaths() []string
    68  	targetPath() string
    69  }
    70  
    71  // Cloner is an internal template and not meant for use in the templates. It
    72  // may change without notice.
    73  type Cloner interface {
    74  	WithNewBase(base string) Resource
    75  }
    76  
    77  // Resource represents a linkable resource, i.e. a content page, image etc.
    78  type Resource interface {
    79  	// Permalink represents the absolute link to this resource.
    80  	Permalink() string
    81  
    82  	// RelPermalink represents the host relative link to this resource.
    83  	RelPermalink() string
    84  
    85  	// ResourceType is the resource type. For most file types, this is the main
    86  	// part of the MIME type, e.g. "image", "application", "text" etc.
    87  	// For content pages, this value is "page".
    88  	ResourceType() string
    89  
    90  	// MediaType is this resource's MIME type.
    91  	MediaType() media.Type
    92  
    93  	// Name is the logical name of this resource. This can be set in the front matter
    94  	// metadata for this resource. If not set, Hugo will assign a value.
    95  	// This will in most cases be the base filename.
    96  	// So, for the image "/some/path/sunset.jpg" this will be "sunset.jpg".
    97  	// The value returned by this method will be used in the GetByPrefix and ByPrefix methods
    98  	// on Resources.
    99  	Name() string
   100  
   101  	// Title returns the title if set in front matter. For content pages, this will be the expected value.
   102  	Title() string
   103  
   104  	// Resource specific data set by Hugo.
   105  	// One example would be.Data.Digest for fingerprinted resources.
   106  	Data() interface{}
   107  
   108  	// Params set in front matter for this resource.
   109  	Params() map[string]interface{}
   110  }
   111  
   112  type ResourcesLanguageMerger interface {
   113  	MergeByLanguage(other Resources) Resources
   114  	// Needed for integration with the tpl package.
   115  	MergeByLanguageInterface(other interface{}) (interface{}, error)
   116  }
   117  
   118  type translatedResource interface {
   119  	TranslationKey() string
   120  }
   121  
   122  // ContentResource represents a Resource that provides a way to get to its content.
   123  // Most Resource types in Hugo implements this interface, including Page.
   124  // This should be used with care, as it will read the file content into memory, but it
   125  // should be cached as effectively as possible by the implementation.
   126  type ContentResource interface {
   127  	Resource
   128  
   129  	// Content returns this resource's content. It will be equivalent to reading the content
   130  	// that RelPermalink points to in the published folder.
   131  	// The return type will be contextual, and should be what you would expect:
   132  	// * Page: template.HTML
   133  	// * JSON: String
   134  	// * Etc.
   135  	Content() (interface{}, error)
   136  }
   137  
   138  // OpenReadSeekeCloser allows setting some other way (than reading from a filesystem)
   139  // to open or create a ReadSeekCloser.
   140  type OpenReadSeekCloser func() (ReadSeekCloser, error)
   141  
   142  // ReadSeekCloserResource is a Resource that supports loading its content.
   143  type ReadSeekCloserResource interface {
   144  	Resource
   145  	ReadSeekCloser() (ReadSeekCloser, error)
   146  }
   147  
   148  // Resources represents a slice of resources, which can be a mix of different types.
   149  // I.e. both pages and images etc.
   150  type Resources []Resource
   151  
   152  func (r Resources) ByType(tp string) Resources {
   153  	var filtered Resources
   154  
   155  	for _, resource := range r {
   156  		if resource.ResourceType() == tp {
   157  			filtered = append(filtered, resource)
   158  		}
   159  	}
   160  	return filtered
   161  }
   162  
   163  // GetMatch finds the first Resource matching the given pattern, or nil if none found.
   164  // See Match for a more complete explanation about the rules used.
   165  func (r Resources) GetMatch(pattern string) Resource {
   166  	g, err := getGlob(pattern)
   167  	if err != nil {
   168  		return nil
   169  	}
   170  
   171  	for _, resource := range r {
   172  		if g.Match(strings.ToLower(resource.Name())) {
   173  			return resource
   174  		}
   175  	}
   176  
   177  	return nil
   178  }
   179  
   180  // Match gets all resources matching the given base filename prefix, e.g
   181  // "*.png" will match all png files. The "*" does not match path delimiters (/),
   182  // so if you organize your resources in sub-folders, you need to be explicit about it, e.g.:
   183  // "images/*.png". To match any PNG image anywhere in the bundle you can do "**.png", and
   184  // to match all PNG images below the images folder, use "images/**.jpg".
   185  // The matching is case insensitive.
   186  // Match matches by using the value of Resource.Name, which, by default, is a filename with
   187  // path relative to the bundle root with Unix style slashes (/) and no leading slash, e.g. "images/logo.png".
   188  // See https://github.com/gobwas/glob for the full rules set.
   189  func (r Resources) Match(pattern string) Resources {
   190  	g, err := getGlob(pattern)
   191  	if err != nil {
   192  		return nil
   193  	}
   194  
   195  	var matches Resources
   196  	for _, resource := range r {
   197  		if g.Match(strings.ToLower(resource.Name())) {
   198  			matches = append(matches, resource)
   199  		}
   200  	}
   201  	return matches
   202  }
   203  
   204  var (
   205  	globCache = make(map[string]glob.Glob)
   206  	globMu    sync.RWMutex
   207  )
   208  
   209  func getGlob(pattern string) (glob.Glob, error) {
   210  	var g glob.Glob
   211  
   212  	globMu.RLock()
   213  	g, found := globCache[pattern]
   214  	globMu.RUnlock()
   215  	if !found {
   216  		var err error
   217  		g, err = glob.Compile(strings.ToLower(pattern), '/')
   218  		if err != nil {
   219  			return nil, err
   220  		}
   221  
   222  		globMu.Lock()
   223  		globCache[pattern] = g
   224  		globMu.Unlock()
   225  	}
   226  
   227  	return g, nil
   228  
   229  }
   230  
   231  // MergeByLanguage adds missing translations in r1 from r2.
   232  func (r1 Resources) MergeByLanguage(r2 Resources) Resources {
   233  	result := append(Resources(nil), r1...)
   234  	m := make(map[string]bool)
   235  	for _, r := range r1 {
   236  		if translated, ok := r.(translatedResource); ok {
   237  			m[translated.TranslationKey()] = true
   238  		}
   239  	}
   240  
   241  	for _, r := range r2 {
   242  		if translated, ok := r.(translatedResource); ok {
   243  			if _, found := m[translated.TranslationKey()]; !found {
   244  				result = append(result, r)
   245  			}
   246  		}
   247  	}
   248  	return result
   249  }
   250  
   251  // MergeByLanguageInterface is the generic version of MergeByLanguage. It
   252  // is here just so it can be called from the tpl package.
   253  func (r1 Resources) MergeByLanguageInterface(in interface{}) (interface{}, error) {
   254  	r2, ok := in.(Resources)
   255  	if !ok {
   256  		return nil, fmt.Errorf("%T cannot be merged by language", in)
   257  	}
   258  	return r1.MergeByLanguage(r2), nil
   259  }
   260  
   261  type Spec struct {
   262  	*helpers.PathSpec
   263  
   264  	MediaTypes    media.Types
   265  	OutputFormats output.Formats
   266  
   267  	Logger *jww.Notepad
   268  
   269  	TextTemplates tpl.TemplateParseFinder
   270  
   271  	// Holds default filter settings etc.
   272  	imaging *Imaging
   273  
   274  	imageCache    *imageCache
   275  	ResourceCache *ResourceCache
   276  
   277  	GenImagePath  string
   278  	GenAssetsPath string
   279  }
   280  
   281  func NewSpec(s *helpers.PathSpec, logger *jww.Notepad, outputFormats output.Formats, mimeTypes media.Types) (*Spec, error) {
   282  
   283  	imaging, err := decodeImaging(s.Cfg.GetStringMap("imaging"))
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	if logger == nil {
   289  		logger = loggers.NewErrorLogger()
   290  	}
   291  
   292  	genImagePath := filepath.FromSlash("_gen/images")
   293  	// The transformed assets (CSS etc.)
   294  	genAssetsPath := filepath.FromSlash("_gen/assets")
   295  
   296  	rs := &Spec{PathSpec: s,
   297  		Logger:        logger,
   298  		GenImagePath:  genImagePath,
   299  		GenAssetsPath: genAssetsPath,
   300  		imaging:       &imaging,
   301  		MediaTypes:    mimeTypes,
   302  		OutputFormats: outputFormats,
   303  		imageCache: newImageCache(
   304  			s,
   305  			// We're going to write a cache pruning routine later, so make it extremely
   306  			// unlikely that the user shoots him or herself in the foot
   307  			// and this is set to a value that represents data he/she
   308  			// cares about. This should be set in stone once released.
   309  			genImagePath,
   310  		)}
   311  
   312  	rs.ResourceCache = newResourceCache(rs)
   313  
   314  	return rs, nil
   315  
   316  }
   317  
   318  type ResourceSourceDescriptor struct {
   319  	// TargetPathBuilder is a callback to create target paths's relative to its owner.
   320  	TargetPathBuilder func(base string) string
   321  
   322  	// Need one of these to load the resource content.
   323  	SourceFile         source.File
   324  	OpenReadSeekCloser OpenReadSeekCloser
   325  
   326  	// If OpenReadSeekerCloser is not set, we use this to open the file.
   327  	SourceFilename string
   328  
   329  	// The relative target filename without any language code.
   330  	RelTargetFilename string
   331  
   332  	// Any base path prepeneded to the permalink.
   333  	// Typically the language code if this resource should be published to its sub-folder.
   334  	URLBase string
   335  
   336  	// Any base paths prepended to the target path. This will also typically be the
   337  	// language code, but setting it here means that it should not have any effect on
   338  	// the permalink.
   339  	// This may be several values. In multihost mode we may publish the same resources to
   340  	// multiple targets.
   341  	TargetBasePaths []string
   342  
   343  	// Delay publishing until either Permalink or RelPermalink is called. Maybe never.
   344  	LazyPublish bool
   345  }
   346  
   347  func (r ResourceSourceDescriptor) Filename() string {
   348  	if r.SourceFile != nil {
   349  		return r.SourceFile.Filename()
   350  	}
   351  	return r.SourceFilename
   352  }
   353  
   354  func (r *Spec) sourceFs() afero.Fs {
   355  	return r.PathSpec.BaseFs.Content.Fs
   356  }
   357  
   358  func (r *Spec) New(fd ResourceSourceDescriptor) (Resource, error) {
   359  	return r.newResourceForFs(r.sourceFs(), fd)
   360  }
   361  
   362  func (r *Spec) NewForFs(sourceFs afero.Fs, fd ResourceSourceDescriptor) (Resource, error) {
   363  	return r.newResourceForFs(sourceFs, fd)
   364  }
   365  
   366  func (r *Spec) newResourceForFs(sourceFs afero.Fs, fd ResourceSourceDescriptor) (Resource, error) {
   367  	if fd.OpenReadSeekCloser == nil {
   368  		if fd.SourceFile != nil && fd.SourceFilename != "" {
   369  			return nil, errors.New("both SourceFile and AbsSourceFilename provided")
   370  		} else if fd.SourceFile == nil && fd.SourceFilename == "" {
   371  			return nil, errors.New("either SourceFile or AbsSourceFilename must be provided")
   372  		}
   373  	}
   374  
   375  	if fd.RelTargetFilename == "" {
   376  		fd.RelTargetFilename = fd.Filename()
   377  	}
   378  
   379  	if len(fd.TargetBasePaths) == 0 {
   380  		// If not set, we publish the same resource to all hosts.
   381  		fd.TargetBasePaths = r.MultihostTargetBasePaths
   382  	}
   383  
   384  	return r.newResource(sourceFs, fd)
   385  }
   386  
   387  func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (Resource, error) {
   388  	var fi os.FileInfo
   389  	var sourceFilename string
   390  
   391  	if fd.OpenReadSeekCloser != nil {
   392  
   393  	} else if fd.SourceFilename != "" {
   394  		var err error
   395  		fi, err = sourceFs.Stat(fd.SourceFilename)
   396  		if err != nil {
   397  			return nil, err
   398  		}
   399  		sourceFilename = fd.SourceFilename
   400  	} else {
   401  		fi = fd.SourceFile.FileInfo()
   402  		sourceFilename = fd.SourceFile.Filename()
   403  	}
   404  
   405  	if fd.RelTargetFilename == "" {
   406  		fd.RelTargetFilename = sourceFilename
   407  	}
   408  
   409  	ext := filepath.Ext(fd.RelTargetFilename)
   410  	mimeType, found := r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, "."))
   411  	// TODO(bep) we need to handle these ambigous types better, but in this context
   412  	// we most likely want the application/xml type.
   413  	if mimeType.Suffix() == "xml" && mimeType.SubType == "rss" {
   414  		mimeType, found = r.MediaTypes.GetByType("application/xml")
   415  	}
   416  
   417  	if !found {
   418  		mimeStr := mime.TypeByExtension(ext)
   419  		if mimeStr != "" {
   420  			mimeType, _ = media.FromStringAndExt(mimeStr, ext)
   421  		}
   422  	}
   423  
   424  	gr := r.newGenericResourceWithBase(
   425  		sourceFs,
   426  		fd.LazyPublish,
   427  		fd.OpenReadSeekCloser,
   428  		fd.URLBase,
   429  		fd.TargetBasePaths,
   430  		fd.TargetPathBuilder,
   431  		fi,
   432  		sourceFilename,
   433  		fd.RelTargetFilename,
   434  		mimeType)
   435  
   436  	if mimeType.MainType == "image" {
   437  		ext := strings.ToLower(helpers.Ext(sourceFilename))
   438  
   439  		imgFormat, ok := imageFormats[ext]
   440  		if !ok {
   441  			// This allows SVG etc. to be used as resources. They will not have the methods of the Image, but
   442  			// that would not (currently) have worked.
   443  			return gr, nil
   444  		}
   445  
   446  		if err := gr.initHash(); err != nil {
   447  			return nil, err
   448  		}
   449  
   450  		return &Image{
   451  			format:          imgFormat,
   452  			imaging:         r.imaging,
   453  			genericResource: gr}, nil
   454  	}
   455  	return gr, nil
   456  
   457  }
   458  
   459  // TODO(bep) unify
   460  func (r *Spec) IsInImageCache(key string) bool {
   461  	// This is used for cache pruning. We currently only have images, but we could
   462  	// imagine expanding on this.
   463  	return r.imageCache.isInCache(key)
   464  }
   465  
   466  func (r *Spec) DeleteCacheByPrefix(prefix string) {
   467  	r.imageCache.deleteByPrefix(prefix)
   468  }
   469  
   470  func (r *Spec) ClearCaches() {
   471  	r.imageCache.clear()
   472  	r.ResourceCache.clear()
   473  }
   474  
   475  func (r *Spec) CacheStats() string {
   476  	r.imageCache.mu.RLock()
   477  	defer r.imageCache.mu.RUnlock()
   478  
   479  	s := fmt.Sprintf("Cache entries: %d", len(r.imageCache.store))
   480  
   481  	count := 0
   482  	for k := range r.imageCache.store {
   483  		if count > 5 {
   484  			break
   485  		}
   486  		s += "\n" + k
   487  		count++
   488  	}
   489  
   490  	return s
   491  }
   492  
   493  type dirFile struct {
   494  	// This is the directory component with Unix-style slashes.
   495  	dir string
   496  	// This is the file component.
   497  	file string
   498  }
   499  
   500  func (d dirFile) path() string {
   501  	return path.Join(d.dir, d.file)
   502  }
   503  
   504  type resourcePathDescriptor struct {
   505  	// The relative target directory and filename.
   506  	relTargetDirFile dirFile
   507  
   508  	// Callback used to construct a target path relative to its owner.
   509  	targetPathBuilder func(rel string) string
   510  
   511  	// baseURLDir is the fixed sub-folder for a resource in permalinks. This will typically
   512  	// be the language code if we publish to the language's sub-folder.
   513  	baseURLDir string
   514  
   515  	// This will normally be the same as above, but this will only apply to publishing
   516  	// of resources. It may be mulltiple values when in multihost mode.
   517  	baseTargetPathDirs []string
   518  
   519  	// baseOffset is set when the output format's path has a offset, e.g. for AMP.
   520  	baseOffset string
   521  }
   522  
   523  type resourceContent struct {
   524  	content     string
   525  	contentInit sync.Once
   526  }
   527  
   528  type resourceHash struct {
   529  	hash     string
   530  	hashInit sync.Once
   531  }
   532  
   533  type publishOnce struct {
   534  	publisherInit sync.Once
   535  	publisherErr  error
   536  	logger        *jww.Notepad
   537  }
   538  
   539  func (l *publishOnce) publish(s Source) error {
   540  	l.publisherInit.Do(func() {
   541  		l.publisherErr = s.Publish()
   542  		if l.publisherErr != nil {
   543  			l.logger.ERROR.Printf("failed to publish Resource: %s", l.publisherErr)
   544  		}
   545  	})
   546  	return l.publisherErr
   547  }
   548  
   549  // genericResource represents a generic linkable resource.
   550  type genericResource struct {
   551  	resourcePathDescriptor
   552  
   553  	title  string
   554  	name   string
   555  	params map[string]interface{}
   556  
   557  	// Absolute filename to the source, including any content folder path.
   558  	// Note that this is absolute in relation to the filesystem it is stored in.
   559  	// It can be a base path filesystem, and then this filename will not match
   560  	// the path to the file on the real filesystem.
   561  	sourceFilename string
   562  
   563  	// Will be set if this resource is backed by something other than a file.
   564  	openReadSeekerCloser OpenReadSeekCloser
   565  
   566  	// A hash of the source content. Is only calculated in caching situations.
   567  	*resourceHash
   568  
   569  	// This may be set to tell us to look in another filesystem for this resource.
   570  	// We, by default, use the sourceFs filesystem in the spec below.
   571  	overriddenSourceFs afero.Fs
   572  
   573  	spec *Spec
   574  
   575  	resourceType string
   576  	mediaType    media.Type
   577  
   578  	osFileInfo os.FileInfo
   579  
   580  	// We create copies of this struct, so this needs to be a pointer.
   581  	*resourceContent
   582  
   583  	// May be set to signal lazy/delayed publishing.
   584  	*publishOnce
   585  }
   586  
   587  func (l *genericResource) Data() interface{} {
   588  	return noData
   589  }
   590  
   591  func (l *genericResource) Content() (interface{}, error) {
   592  	if err := l.initContent(); err != nil {
   593  		return nil, err
   594  	}
   595  
   596  	return l.content, nil
   597  }
   598  
   599  func (l *genericResource) ReadSeekCloser() (ReadSeekCloser, error) {
   600  	if l.openReadSeekerCloser != nil {
   601  		return l.openReadSeekerCloser()
   602  	}
   603  	f, err := l.sourceFs().Open(l.sourceFilename)
   604  	if err != nil {
   605  		return nil, err
   606  	}
   607  	return f, nil
   608  
   609  }
   610  
   611  func (l *genericResource) MediaType() media.Type {
   612  	return l.mediaType
   613  }
   614  
   615  // Implement the Cloner interface.
   616  func (l genericResource) WithNewBase(base string) Resource {
   617  	l.baseOffset = base
   618  	l.resourceContent = &resourceContent{}
   619  	return &l
   620  }
   621  
   622  func (l *genericResource) initHash() error {
   623  	var err error
   624  	l.hashInit.Do(func() {
   625  		var hash string
   626  		var f ReadSeekCloser
   627  		f, err = l.ReadSeekCloser()
   628  		if err != nil {
   629  			err = fmt.Errorf("failed to open source file: %s", err)
   630  			return
   631  		}
   632  		defer f.Close()
   633  
   634  		hash, err = helpers.MD5FromFileFast(f)
   635  		if err != nil {
   636  			return
   637  		}
   638  		l.hash = hash
   639  
   640  	})
   641  
   642  	return err
   643  }
   644  
   645  func (l *genericResource) initContent() error {
   646  	var err error
   647  	l.contentInit.Do(func() {
   648  		var r ReadSeekCloser
   649  		r, err = l.ReadSeekCloser()
   650  		if err != nil {
   651  			return
   652  		}
   653  		defer r.Close()
   654  
   655  		var b []byte
   656  		b, err = ioutil.ReadAll(r)
   657  		if err != nil {
   658  			return
   659  		}
   660  
   661  		l.content = string(b)
   662  
   663  	})
   664  
   665  	return err
   666  }
   667  
   668  func (l *genericResource) sourceFs() afero.Fs {
   669  	if l.overriddenSourceFs != nil {
   670  		return l.overriddenSourceFs
   671  	}
   672  	return l.spec.sourceFs()
   673  }
   674  
   675  func (l *genericResource) publishIfNeeded() {
   676  	if l.publishOnce != nil {
   677  		l.publishOnce.publish(l)
   678  	}
   679  }
   680  
   681  func (l *genericResource) Permalink() string {
   682  	l.publishIfNeeded()
   683  	return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(l.relTargetDirFile.path()), l.spec.BaseURL.HostURL())
   684  }
   685  
   686  func (l *genericResource) RelPermalink() string {
   687  	l.publishIfNeeded()
   688  	return l.relPermalinkFor(l.relTargetDirFile.path())
   689  }
   690  
   691  func (l *genericResource) relPermalinkFor(target string) string {
   692  	return l.relPermalinkForRel(target)
   693  
   694  }
   695  func (l *genericResource) permalinkFor(target string) string {
   696  	return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(target), l.spec.BaseURL.HostURL())
   697  
   698  }
   699  func (l *genericResource) relTargetPathsFor(target string) []string {
   700  	return l.relTargetPathsForRel(target)
   701  }
   702  
   703  func (l *genericResource) relTargetPaths() []string {
   704  	return l.relTargetPathsForRel(l.targetPath())
   705  }
   706  
   707  func (l *genericResource) Name() string {
   708  	return l.name
   709  }
   710  
   711  func (l *genericResource) Title() string {
   712  	return l.title
   713  }
   714  
   715  func (l *genericResource) Params() map[string]interface{} {
   716  	return l.params
   717  }
   718  
   719  func (l *genericResource) setTitle(title string) {
   720  	l.title = title
   721  }
   722  
   723  func (l *genericResource) setName(name string) {
   724  	l.name = name
   725  }
   726  
   727  func (l *genericResource) updateParams(params map[string]interface{}) {
   728  	if l.params == nil {
   729  		l.params = params
   730  		return
   731  	}
   732  
   733  	// Sets the params not already set
   734  	for k, v := range params {
   735  		if _, found := l.params[k]; !found {
   736  			l.params[k] = v
   737  		}
   738  	}
   739  }
   740  
   741  func (l *genericResource) relPermalinkForRel(rel string) string {
   742  	return l.spec.PathSpec.URLizeFilename(l.relTargetPathForRel(rel, false, true))
   743  }
   744  
   745  func (l *genericResource) relTargetPathsForRel(rel string) []string {
   746  	if len(l.baseTargetPathDirs) == 0 {
   747  		return []string{l.relTargetPathForRelAndBasePath(rel, "", false)}
   748  	}
   749  
   750  	var targetPaths = make([]string, len(l.baseTargetPathDirs))
   751  	for i, dir := range l.baseTargetPathDirs {
   752  		targetPaths[i] = l.relTargetPathForRelAndBasePath(rel, dir, false)
   753  	}
   754  	return targetPaths
   755  }
   756  
   757  func (l *genericResource) relTargetPathForRel(rel string, addBaseTargetPath, isURL bool) string {
   758  	if addBaseTargetPath && len(l.baseTargetPathDirs) > 1 {
   759  		panic("multiple baseTargetPathDirs")
   760  	}
   761  	var basePath string
   762  	if addBaseTargetPath && len(l.baseTargetPathDirs) > 0 {
   763  		basePath = l.baseTargetPathDirs[0]
   764  	}
   765  
   766  	return l.relTargetPathForRelAndBasePath(rel, basePath, isURL)
   767  }
   768  
   769  func (l *genericResource) relTargetPathForRelAndBasePath(rel, basePath string, isURL bool) string {
   770  	if l.targetPathBuilder != nil {
   771  		rel = l.targetPathBuilder(rel)
   772  	}
   773  
   774  	if isURL && l.baseURLDir != "" {
   775  		rel = path.Join(l.baseURLDir, rel)
   776  	}
   777  
   778  	if basePath != "" {
   779  		rel = path.Join(basePath, rel)
   780  	}
   781  
   782  	if l.baseOffset != "" {
   783  		rel = path.Join(l.baseOffset, rel)
   784  	}
   785  
   786  	if isURL && l.spec.PathSpec.BasePath != "" {
   787  		rel = path.Join(l.spec.PathSpec.BasePath, rel)
   788  	}
   789  
   790  	if len(rel) == 0 || rel[0] != '/' {
   791  		rel = "/" + rel
   792  	}
   793  
   794  	return rel
   795  }
   796  
   797  func (l *genericResource) ResourceType() string {
   798  	return l.resourceType
   799  }
   800  
   801  func (l *genericResource) String() string {
   802  	return fmt.Sprintf("Resource(%s: %s)", l.resourceType, l.name)
   803  }
   804  
   805  func (l *genericResource) Publish() error {
   806  	fr, err := l.ReadSeekCloser()
   807  	if err != nil {
   808  		return err
   809  	}
   810  	defer fr.Close()
   811  	fw, err := helpers.OpenFilesForWriting(l.spec.BaseFs.PublishFs, l.targetFilenames()...)
   812  	if err != nil {
   813  		return err
   814  	}
   815  	defer fw.Close()
   816  
   817  	_, err = io.Copy(fw, fr)
   818  	return err
   819  }
   820  
   821  // Path is stored with Unix style slashes.
   822  func (l *genericResource) targetPath() string {
   823  	return l.relTargetDirFile.path()
   824  }
   825  
   826  func (l *genericResource) targetFilenames() []string {
   827  	paths := l.relTargetPaths()
   828  	for i, p := range paths {
   829  		paths[i] = filepath.Clean(p)
   830  	}
   831  	return paths
   832  }
   833  
   834  // TODO(bep) clean up below
   835  func (r *Spec) newGenericResource(sourceFs afero.Fs,
   836  	targetPathBuilder func(base string) string,
   837  	osFileInfo os.FileInfo,
   838  	sourceFilename,
   839  	baseFilename string,
   840  	mediaType media.Type) *genericResource {
   841  	return r.newGenericResourceWithBase(
   842  		sourceFs,
   843  		false,
   844  		nil,
   845  		"",
   846  		nil,
   847  		targetPathBuilder,
   848  		osFileInfo,
   849  		sourceFilename,
   850  		baseFilename,
   851  		mediaType,
   852  	)
   853  
   854  }
   855  
   856  func (r *Spec) newGenericResourceWithBase(
   857  	sourceFs afero.Fs,
   858  	lazyPublish bool,
   859  	openReadSeekerCloser OpenReadSeekCloser,
   860  	urlBaseDir string,
   861  	targetPathBaseDirs []string,
   862  	targetPathBuilder func(base string) string,
   863  	osFileInfo os.FileInfo,
   864  	sourceFilename,
   865  	baseFilename string,
   866  	mediaType media.Type) *genericResource {
   867  
   868  	// This value is used both to construct URLs and file paths, but start
   869  	// with a Unix-styled path.
   870  	baseFilename = helpers.ToSlashTrimLeading(baseFilename)
   871  	fpath, fname := path.Split(baseFilename)
   872  
   873  	var resourceType string
   874  	if mediaType.MainType == "image" {
   875  		resourceType = mediaType.MainType
   876  	} else {
   877  		resourceType = mediaType.SubType
   878  	}
   879  
   880  	pathDescriptor := resourcePathDescriptor{
   881  		baseURLDir:         urlBaseDir,
   882  		baseTargetPathDirs: targetPathBaseDirs,
   883  		targetPathBuilder:  targetPathBuilder,
   884  		relTargetDirFile:   dirFile{dir: fpath, file: fname},
   885  	}
   886  
   887  	var po *publishOnce
   888  	if lazyPublish {
   889  		po = &publishOnce{logger: r.Logger}
   890  	}
   891  
   892  	return &genericResource{
   893  		openReadSeekerCloser:   openReadSeekerCloser,
   894  		publishOnce:            po,
   895  		resourcePathDescriptor: pathDescriptor,
   896  		overriddenSourceFs:     sourceFs,
   897  		osFileInfo:             osFileInfo,
   898  		sourceFilename:         sourceFilename,
   899  		mediaType:              mediaType,
   900  		resourceType:           resourceType,
   901  		spec:                   r,
   902  		params:                 make(map[string]interface{}),
   903  		name:                   baseFilename,
   904  		title:                  baseFilename,
   905  		resourceContent:        &resourceContent{},
   906  		resourceHash:           &resourceHash{},
   907  	}
   908  }