github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/resources/transform.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  	"bytes"
    18  	"fmt"
    19  	"image"
    20  	"io"
    21  	"path"
    22  	"strings"
    23  	"sync"
    24  
    25  	"github.com/gohugoio/hugo/common/paths"
    26  
    27  	"github.com/pkg/errors"
    28  
    29  	"github.com/gohugoio/hugo/resources/images/exif"
    30  	"github.com/spf13/afero"
    31  
    32  	bp "github.com/gohugoio/hugo/bufferpool"
    33  
    34  	"github.com/gohugoio/hugo/common/herrors"
    35  	"github.com/gohugoio/hugo/common/hugio"
    36  	"github.com/gohugoio/hugo/common/maps"
    37  	"github.com/gohugoio/hugo/helpers"
    38  	"github.com/gohugoio/hugo/resources/internal"
    39  	"github.com/gohugoio/hugo/resources/resource"
    40  
    41  	"github.com/gohugoio/hugo/media"
    42  )
    43  
    44  var (
    45  	_ resource.ContentResource        = (*resourceAdapter)(nil)
    46  	_ resource.ReadSeekCloserResource = (*resourceAdapter)(nil)
    47  	_ resource.Resource               = (*resourceAdapter)(nil)
    48  	_ resource.Source                 = (*resourceAdapter)(nil)
    49  	_ resource.Identifier             = (*resourceAdapter)(nil)
    50  	_ resource.ResourceMetaProvider   = (*resourceAdapter)(nil)
    51  )
    52  
    53  // These are transformations that need special support in Hugo that may not
    54  // be available when building the theme/site so we write the transformation
    55  // result to disk and reuse if needed for these,
    56  // TODO(bep) it's a little fragile having these constants redefined here.
    57  var transformationsToCacheOnDisk = map[string]bool{
    58  	"postcss":    true,
    59  	"tocss":      true,
    60  	"tocss-dart": true,
    61  }
    62  
    63  func newResourceAdapter(spec *Spec, lazyPublish bool, target transformableResource) *resourceAdapter {
    64  	var po *publishOnce
    65  	if lazyPublish {
    66  		po = &publishOnce{}
    67  	}
    68  	return &resourceAdapter{
    69  		resourceTransformations: &resourceTransformations{},
    70  		resourceAdapterInner: &resourceAdapterInner{
    71  			spec:        spec,
    72  			publishOnce: po,
    73  			target:      target,
    74  		},
    75  	}
    76  }
    77  
    78  // ResourceTransformation is the interface that a resource transformation step
    79  // needs to implement.
    80  type ResourceTransformation interface {
    81  	Key() internal.ResourceTransformationKey
    82  	Transform(ctx *ResourceTransformationCtx) error
    83  }
    84  
    85  type ResourceTransformationCtx struct {
    86  	// The content to transform.
    87  	From io.Reader
    88  
    89  	// The target of content transformation.
    90  	// The current implementation requires that r is written to w
    91  	// even if no transformation is performed.
    92  	To io.Writer
    93  
    94  	// This is the relative path to the original source. Unix styled slashes.
    95  	SourcePath string
    96  
    97  	// This is the relative target path to the resource. Unix styled slashes.
    98  	InPath string
    99  
   100  	// The relative target path to the transformed resource. Unix styled slashes.
   101  	OutPath string
   102  
   103  	// The input media type
   104  	InMediaType media.Type
   105  
   106  	// The media type of the transformed resource.
   107  	OutMediaType media.Type
   108  
   109  	// Data data can be set on the transformed Resource. Not that this need
   110  	// to be simple types, as it needs to be serialized to JSON and back.
   111  	Data map[string]interface{}
   112  
   113  	// This is used to publish additional artifacts, e.g. source maps.
   114  	// We may improve this.
   115  	OpenResourcePublisher func(relTargetPath string) (io.WriteCloser, error)
   116  }
   117  
   118  // AddOutPathIdentifier transforming InPath to OutPath adding an identifier,
   119  // eg '.min' before any extension.
   120  func (ctx *ResourceTransformationCtx) AddOutPathIdentifier(identifier string) {
   121  	ctx.OutPath = ctx.addPathIdentifier(ctx.InPath, identifier)
   122  }
   123  
   124  // PublishSourceMap writes the content to the target folder of the main resource
   125  // with the ".map" extension added.
   126  func (ctx *ResourceTransformationCtx) PublishSourceMap(content string) error {
   127  	target := ctx.OutPath + ".map"
   128  	f, err := ctx.OpenResourcePublisher(target)
   129  	if err != nil {
   130  		return err
   131  	}
   132  	defer f.Close()
   133  	_, err = f.Write([]byte(content))
   134  	return err
   135  }
   136  
   137  // ReplaceOutPathExtension transforming InPath to OutPath replacing the file
   138  // extension, e.g. ".scss"
   139  func (ctx *ResourceTransformationCtx) ReplaceOutPathExtension(newExt string) {
   140  	dir, file := path.Split(ctx.InPath)
   141  	base, _ := paths.PathAndExt(file)
   142  	ctx.OutPath = path.Join(dir, (base + newExt))
   143  }
   144  
   145  func (ctx *ResourceTransformationCtx) addPathIdentifier(inPath, identifier string) string {
   146  	dir, file := path.Split(inPath)
   147  	base, ext := paths.PathAndExt(file)
   148  	return path.Join(dir, (base + identifier + ext))
   149  }
   150  
   151  type publishOnce struct {
   152  	publisherInit sync.Once
   153  	publisherErr  error
   154  }
   155  
   156  type resourceAdapter struct {
   157  	commonResource
   158  	*resourceTransformations
   159  	*resourceAdapterInner
   160  }
   161  
   162  func (r *resourceAdapter) Content() (interface{}, error) {
   163  	r.init(false, true)
   164  	if r.transformationsErr != nil {
   165  		return nil, r.transformationsErr
   166  	}
   167  	return r.target.Content()
   168  }
   169  
   170  func (r *resourceAdapter) Data() interface{} {
   171  	r.init(false, false)
   172  	return r.target.Data()
   173  }
   174  
   175  func (r *resourceAdapter) Fill(spec string) (resource.Image, error) {
   176  	return r.getImageOps().Fill(spec)
   177  }
   178  
   179  func (r *resourceAdapter) Fit(spec string) (resource.Image, error) {
   180  	return r.getImageOps().Fit(spec)
   181  }
   182  
   183  func (r *resourceAdapter) Filter(filters ...interface{}) (resource.Image, error) {
   184  	return r.getImageOps().Filter(filters...)
   185  }
   186  
   187  func (r *resourceAdapter) Height() int {
   188  	return r.getImageOps().Height()
   189  }
   190  
   191  func (r *resourceAdapter) Exif() *exif.Exif {
   192  	return r.getImageOps().Exif()
   193  }
   194  
   195  func (r *resourceAdapter) Key() string {
   196  	r.init(false, false)
   197  	return r.target.(resource.Identifier).Key()
   198  }
   199  
   200  func (r *resourceAdapter) MediaType() media.Type {
   201  	r.init(false, false)
   202  	return r.target.MediaType()
   203  }
   204  
   205  func (r *resourceAdapter) Name() string {
   206  	r.init(false, false)
   207  	return r.target.Name()
   208  }
   209  
   210  func (r *resourceAdapter) Params() maps.Params {
   211  	r.init(false, false)
   212  	return r.target.Params()
   213  }
   214  
   215  func (r *resourceAdapter) Permalink() string {
   216  	r.init(true, false)
   217  	return r.target.Permalink()
   218  }
   219  
   220  func (r *resourceAdapter) Publish() error {
   221  	r.init(false, false)
   222  
   223  	return r.target.Publish()
   224  }
   225  
   226  func (r *resourceAdapter) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
   227  	r.init(false, false)
   228  	return r.target.ReadSeekCloser()
   229  }
   230  
   231  func (r *resourceAdapter) RelPermalink() string {
   232  	r.init(true, false)
   233  	return r.target.RelPermalink()
   234  }
   235  
   236  func (r *resourceAdapter) Resize(spec string) (resource.Image, error) {
   237  	return r.getImageOps().Resize(spec)
   238  }
   239  
   240  func (r *resourceAdapter) ResourceType() string {
   241  	r.init(false, false)
   242  	return r.target.ResourceType()
   243  }
   244  
   245  func (r *resourceAdapter) String() string {
   246  	return r.Name()
   247  }
   248  
   249  func (r *resourceAdapter) Title() string {
   250  	r.init(false, false)
   251  	return r.target.Title()
   252  }
   253  
   254  func (r resourceAdapter) Transform(t ...ResourceTransformation) (ResourceTransformer, error) {
   255  	r.resourceTransformations = &resourceTransformations{
   256  		transformations: append(r.transformations, t...),
   257  	}
   258  
   259  	r.resourceAdapterInner = &resourceAdapterInner{
   260  		spec:        r.spec,
   261  		publishOnce: &publishOnce{},
   262  		target:      r.target,
   263  	}
   264  
   265  	return &r, nil
   266  }
   267  
   268  func (r *resourceAdapter) Width() int {
   269  	return r.getImageOps().Width()
   270  }
   271  
   272  func (r *resourceAdapter) DecodeImage() (image.Image, error) {
   273  	return r.getImageOps().DecodeImage()
   274  }
   275  
   276  func (r *resourceAdapter) getImageOps() resource.ImageOps {
   277  	img, ok := r.target.(resource.ImageOps)
   278  	if !ok {
   279  		panic(fmt.Sprintf("%T is not an image", r.target))
   280  	}
   281  	r.init(false, false)
   282  	return img
   283  }
   284  
   285  func (r *resourceAdapter) getMetaAssigner() metaAssigner {
   286  	return r.target
   287  }
   288  
   289  func (r *resourceAdapter) getSpec() *Spec {
   290  	return r.spec
   291  }
   292  
   293  func (r *resourceAdapter) publish() {
   294  	if r.publishOnce == nil {
   295  		return
   296  	}
   297  
   298  	r.publisherInit.Do(func() {
   299  		r.publisherErr = r.target.Publish()
   300  
   301  		if r.publisherErr != nil {
   302  			r.spec.Logger.Errorf("Failed to publish Resource: %s", r.publisherErr)
   303  		}
   304  	})
   305  }
   306  
   307  func (r *resourceAdapter) TransformationKey() string {
   308  	// Files with a suffix will be stored in cache (both on disk and in memory)
   309  	// partitioned by their suffix.
   310  	var key string
   311  	for _, tr := range r.transformations {
   312  		key = key + "_" + tr.Key().Value()
   313  	}
   314  
   315  	base := ResourceCacheKey(r.target.Key())
   316  	return r.spec.ResourceCache.cleanKey(base) + "_" + helpers.MD5String(key)
   317  }
   318  
   319  func (r *resourceAdapter) transform(publish, setContent bool) error {
   320  	cache := r.spec.ResourceCache
   321  
   322  	key := r.TransformationKey()
   323  
   324  	cached, found := cache.get(key)
   325  
   326  	if found {
   327  		r.resourceAdapterInner = cached.(*resourceAdapterInner)
   328  		return nil
   329  	}
   330  
   331  	// Acquire a write lock for the named transformation.
   332  	cache.nlocker.Lock(key)
   333  	// Check the cache again.
   334  	cached, found = cache.get(key)
   335  	if found {
   336  		r.resourceAdapterInner = cached.(*resourceAdapterInner)
   337  		cache.nlocker.Unlock(key)
   338  		return nil
   339  	}
   340  
   341  	defer cache.nlocker.Unlock(key)
   342  	defer cache.set(key, r.resourceAdapterInner)
   343  
   344  	b1 := bp.GetBuffer()
   345  	b2 := bp.GetBuffer()
   346  	defer bp.PutBuffer(b1)
   347  	defer bp.PutBuffer(b2)
   348  
   349  	tctx := &ResourceTransformationCtx{
   350  		Data:                  make(map[string]interface{}),
   351  		OpenResourcePublisher: r.target.openPublishFileForWriting,
   352  	}
   353  
   354  	tctx.InMediaType = r.target.MediaType()
   355  	tctx.OutMediaType = r.target.MediaType()
   356  
   357  	startCtx := *tctx
   358  	updates := &transformationUpdate{startCtx: startCtx}
   359  
   360  	var contentrc hugio.ReadSeekCloser
   361  
   362  	contentrc, err := contentReadSeekerCloser(r.target)
   363  	if err != nil {
   364  		return err
   365  	}
   366  
   367  	defer contentrc.Close()
   368  
   369  	tctx.From = contentrc
   370  	tctx.To = b1
   371  
   372  	tctx.InPath = r.target.TargetPath()
   373  	tctx.SourcePath = tctx.InPath
   374  
   375  	counter := 0
   376  	writeToFileCache := false
   377  
   378  	var transformedContentr io.Reader
   379  
   380  	for i, tr := range r.transformations {
   381  		if i != 0 {
   382  			tctx.InMediaType = tctx.OutMediaType
   383  		}
   384  
   385  		mayBeCachedOnDisk := transformationsToCacheOnDisk[tr.Key().Name]
   386  		if !writeToFileCache {
   387  			writeToFileCache = mayBeCachedOnDisk
   388  		}
   389  
   390  		if i > 0 {
   391  			hasWrites := tctx.To.(*bytes.Buffer).Len() > 0
   392  			if hasWrites {
   393  				counter++
   394  				// Switch the buffers
   395  				if counter%2 == 0 {
   396  					tctx.From = b2
   397  					b1.Reset()
   398  					tctx.To = b1
   399  				} else {
   400  					tctx.From = b1
   401  					b2.Reset()
   402  					tctx.To = b2
   403  				}
   404  			}
   405  		}
   406  
   407  		newErr := func(err error) error {
   408  			msg := fmt.Sprintf("%s: failed to transform %q (%s)", strings.ToUpper(tr.Key().Name), tctx.InPath, tctx.InMediaType.Type())
   409  
   410  			if err == herrors.ErrFeatureNotAvailable {
   411  				var errMsg string
   412  				if tr.Key().Name == "postcss" {
   413  					// This transformation is not available in this
   414  					// Most likely because PostCSS is not installed.
   415  					errMsg = ". Check your PostCSS installation; install with \"npm install postcss-cli\". See https://gohugo.io/hugo-pipes/postcss/"
   416  				} else if tr.Key().Name == "tocss" {
   417  					errMsg = ". Check your Hugo installation; you need the extended version to build SCSS/SASS."
   418  				} else if tr.Key().Name == "tocss-dart" {
   419  					errMsg = ". You need dart-sass-embedded in your system $PATH."
   420  
   421  				} else if tr.Key().Name == "babel" {
   422  					errMsg = ". You need to install Babel, see https://gohugo.io/hugo-pipes/babel/"
   423  				}
   424  
   425  				return errors.Wrap(err, msg+errMsg)
   426  			}
   427  
   428  			return errors.Wrap(err, msg)
   429  		}
   430  
   431  		var tryFileCache bool
   432  
   433  		if mayBeCachedOnDisk && r.spec.BuildConfig.UseResourceCache(nil) {
   434  			tryFileCache = true
   435  		} else {
   436  			err = tr.Transform(tctx)
   437  			if err != nil && err != herrors.ErrFeatureNotAvailable {
   438  				return newErr(err)
   439  			}
   440  
   441  			if mayBeCachedOnDisk {
   442  				tryFileCache = r.spec.BuildConfig.UseResourceCache(err)
   443  			}
   444  			if err != nil && !tryFileCache {
   445  				return newErr(err)
   446  			}
   447  		}
   448  
   449  		if tryFileCache {
   450  			f := r.target.tryTransformedFileCache(key, updates)
   451  			if f == nil {
   452  				if err != nil {
   453  					return newErr(err)
   454  				}
   455  				return newErr(errors.Errorf("resource %q not found in file cache", key))
   456  			}
   457  			transformedContentr = f
   458  			updates.sourceFs = cache.fileCache.Fs
   459  			defer f.Close()
   460  
   461  			// The reader above is all we need.
   462  			break
   463  		}
   464  
   465  		if tctx.OutPath != "" {
   466  			tctx.InPath = tctx.OutPath
   467  			tctx.OutPath = ""
   468  		}
   469  	}
   470  
   471  	if transformedContentr == nil {
   472  		updates.updateFromCtx(tctx)
   473  	}
   474  
   475  	var publishwriters []io.WriteCloser
   476  
   477  	if publish {
   478  		publicw, err := r.target.openPublishFileForWriting(updates.targetPath)
   479  		if err != nil {
   480  			return err
   481  		}
   482  		publishwriters = append(publishwriters, publicw)
   483  	}
   484  
   485  	if transformedContentr == nil {
   486  		if writeToFileCache {
   487  			// Also write it to the cache
   488  			fi, metaw, err := cache.writeMeta(key, updates.toTransformedResourceMetadata())
   489  			if err != nil {
   490  				return err
   491  			}
   492  			updates.sourceFilename = &fi.Name
   493  			updates.sourceFs = cache.fileCache.Fs
   494  			publishwriters = append(publishwriters, metaw)
   495  		}
   496  
   497  		// Any transformations reading from From must also write to To.
   498  		// This means that if the target buffer is empty, we can just reuse
   499  		// the original reader.
   500  		if b, ok := tctx.To.(*bytes.Buffer); ok && b.Len() > 0 {
   501  			transformedContentr = tctx.To.(*bytes.Buffer)
   502  		} else {
   503  			transformedContentr = contentrc
   504  		}
   505  	}
   506  
   507  	// Also write it to memory
   508  	var contentmemw *bytes.Buffer
   509  
   510  	setContent = setContent || !writeToFileCache
   511  
   512  	if setContent {
   513  		contentmemw = bp.GetBuffer()
   514  		defer bp.PutBuffer(contentmemw)
   515  		publishwriters = append(publishwriters, hugio.ToWriteCloser(contentmemw))
   516  	}
   517  
   518  	publishw := hugio.NewMultiWriteCloser(publishwriters...)
   519  	_, err = io.Copy(publishw, transformedContentr)
   520  	if err != nil {
   521  		return err
   522  	}
   523  	publishw.Close()
   524  
   525  	if setContent {
   526  		s := contentmemw.String()
   527  		updates.content = &s
   528  	}
   529  
   530  	newTarget, err := r.target.cloneWithUpdates(updates)
   531  	if err != nil {
   532  		return err
   533  	}
   534  	r.target = newTarget
   535  
   536  	return nil
   537  }
   538  
   539  func (r *resourceAdapter) init(publish, setContent bool) {
   540  	r.initTransform(publish, setContent)
   541  }
   542  
   543  func (r *resourceAdapter) initTransform(publish, setContent bool) {
   544  	r.transformationsInit.Do(func() {
   545  		if len(r.transformations) == 0 {
   546  			// Nothing to do.
   547  			return
   548  		}
   549  
   550  		if publish {
   551  			// The transformation will write the content directly to
   552  			// the destination.
   553  			r.publishOnce = nil
   554  		}
   555  
   556  		r.transformationsErr = r.transform(publish, setContent)
   557  		if r.transformationsErr != nil {
   558  			if r.spec.ErrorSender != nil {
   559  				r.spec.ErrorSender.SendError(r.transformationsErr)
   560  			} else {
   561  				r.spec.Logger.Errorf("Transformation failed: %s", r.transformationsErr)
   562  			}
   563  		}
   564  	})
   565  
   566  	if publish && r.publishOnce != nil {
   567  		r.publish()
   568  	}
   569  }
   570  
   571  type resourceAdapterInner struct {
   572  	target transformableResource
   573  
   574  	spec *Spec
   575  
   576  	// Handles publishing (to /public) if needed.
   577  	*publishOnce
   578  }
   579  
   580  type resourceTransformations struct {
   581  	transformationsInit sync.Once
   582  	transformationsErr  error
   583  	transformations     []ResourceTransformation
   584  }
   585  
   586  type transformableResource interface {
   587  	baseResourceInternal
   588  
   589  	resource.ContentProvider
   590  	resource.Resource
   591  	resource.Identifier
   592  }
   593  
   594  type transformationUpdate struct {
   595  	content        *string
   596  	sourceFilename *string
   597  	sourceFs       afero.Fs
   598  	targetPath     string
   599  	mediaType      media.Type
   600  	data           map[string]interface{}
   601  
   602  	startCtx ResourceTransformationCtx
   603  }
   604  
   605  func (u *transformationUpdate) isContentChanged() bool {
   606  	return u.content != nil || u.sourceFilename != nil
   607  }
   608  
   609  func (u *transformationUpdate) toTransformedResourceMetadata() transformedResourceMetadata {
   610  	return transformedResourceMetadata{
   611  		MediaTypeV: u.mediaType.Type(),
   612  		Target:     u.targetPath,
   613  		MetaData:   u.data,
   614  	}
   615  }
   616  
   617  func (u *transformationUpdate) updateFromCtx(ctx *ResourceTransformationCtx) {
   618  	u.targetPath = ctx.OutPath
   619  	u.mediaType = ctx.OutMediaType
   620  	u.data = ctx.Data
   621  	u.targetPath = ctx.InPath
   622  }
   623  
   624  // We will persist this information to disk.
   625  type transformedResourceMetadata struct {
   626  	Target     string                 `json:"Target"`
   627  	MediaTypeV string                 `json:"MediaType"`
   628  	MetaData   map[string]interface{} `json:"Data"`
   629  }
   630  
   631  // contentReadSeekerCloser returns a ReadSeekerCloser if possible for a given Resource.
   632  func contentReadSeekerCloser(r resource.Resource) (hugio.ReadSeekCloser, error) {
   633  	switch rr := r.(type) {
   634  	case resource.ReadSeekCloserResource:
   635  		rc, err := rr.ReadSeekCloser()
   636  		if err != nil {
   637  			return nil, err
   638  		}
   639  		return rc, nil
   640  	default:
   641  		return nil, fmt.Errorf("cannot transform content of Resource of type %T", r)
   642  
   643  	}
   644  }