github.com/rn2dy/hugo@v0.47.1/resource/transform.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  	"bytes"
    18  	"path"
    19  	"strconv"
    20  	"strings"
    21  
    22  	"github.com/gohugoio/hugo/common/errors"
    23  	"github.com/gohugoio/hugo/helpers"
    24  	"github.com/mitchellh/hashstructure"
    25  	"github.com/spf13/afero"
    26  
    27  	"fmt"
    28  	"io"
    29  	"sync"
    30  
    31  	"github.com/gohugoio/hugo/media"
    32  
    33  	bp "github.com/gohugoio/hugo/bufferpool"
    34  )
    35  
    36  var (
    37  	_ ContentResource        = (*transformedResource)(nil)
    38  	_ ReadSeekCloserResource = (*transformedResource)(nil)
    39  )
    40  
    41  func (s *Spec) Transform(r Resource, t ResourceTransformation) (Resource, error) {
    42  	return &transformedResource{
    43  		Resource:                    r,
    44  		transformation:              t,
    45  		transformedResourceMetadata: transformedResourceMetadata{MetaData: make(map[string]interface{})},
    46  		cache: s.ResourceCache}, nil
    47  }
    48  
    49  type ResourceTransformationCtx struct {
    50  	// The content to transform.
    51  	From io.Reader
    52  
    53  	// The target of content transformation.
    54  	// The current implementation requires that r is written to w
    55  	// even if no transformation is performed.
    56  	To io.Writer
    57  
    58  	// This is the relative path to the original source. Unix styled slashes.
    59  	SourcePath string
    60  
    61  	// This is the relative target path to the resource. Unix styled slashes.
    62  	InPath string
    63  
    64  	// The relative target path to the transformed resource. Unix styled slashes.
    65  	OutPath string
    66  
    67  	// The input media type
    68  	InMediaType media.Type
    69  
    70  	// The media type of the transformed resource.
    71  	OutMediaType media.Type
    72  
    73  	// Data data can be set on the transformed Resource. Not that this need
    74  	// to be simple types, as it needs to be serialized to JSON and back.
    75  	Data map[string]interface{}
    76  
    77  	// This is used to publis additional artifacts, e.g. source maps.
    78  	// We may improve this.
    79  	OpenResourcePublisher func(relTargetPath string) (io.WriteCloser, error)
    80  }
    81  
    82  // AddOutPathIdentifier transforming InPath to OutPath adding an identifier,
    83  // eg '.min' before any extension.
    84  func (ctx *ResourceTransformationCtx) AddOutPathIdentifier(identifier string) {
    85  	ctx.OutPath = ctx.addPathIdentifier(ctx.InPath, identifier)
    86  }
    87  
    88  func (ctx *ResourceTransformationCtx) addPathIdentifier(inPath, identifier string) string {
    89  	dir, file := path.Split(inPath)
    90  	base, ext := helpers.PathAndExt(file)
    91  	return path.Join(dir, (base + identifier + ext))
    92  }
    93  
    94  // ReplaceOutPathExtension transforming InPath to OutPath replacing the file
    95  // extension, e.g. ".scss"
    96  func (ctx *ResourceTransformationCtx) ReplaceOutPathExtension(newExt string) {
    97  	dir, file := path.Split(ctx.InPath)
    98  	base, _ := helpers.PathAndExt(file)
    99  	ctx.OutPath = path.Join(dir, (base + newExt))
   100  }
   101  
   102  // PublishSourceMap writes the content to the target folder of the main resource
   103  // with the ".map" extension added.
   104  func (ctx *ResourceTransformationCtx) PublishSourceMap(content string) error {
   105  	target := ctx.OutPath + ".map"
   106  	f, err := ctx.OpenResourcePublisher(target)
   107  	if err != nil {
   108  		return err
   109  	}
   110  	defer f.Close()
   111  	_, err = f.Write([]byte(content))
   112  	return err
   113  }
   114  
   115  // ResourceTransformationKey are provided by the different transformation implementations.
   116  // It identifies the transformation (name) and its configuration (elements).
   117  // We combine this in a chain with the rest of the transformations
   118  // with the target filename and a content hash of the origin to use as cache key.
   119  type ResourceTransformationKey struct {
   120  	name     string
   121  	elements []interface{}
   122  }
   123  
   124  // NewResourceTransformationKey creates a new ResourceTransformationKey from the transformation
   125  // name and elements. We will create a 64 bit FNV hash from the elements, which when combined
   126  // with the other key elements should be unique for all practical applications.
   127  func NewResourceTransformationKey(name string, elements ...interface{}) ResourceTransformationKey {
   128  	return ResourceTransformationKey{name: name, elements: elements}
   129  }
   130  
   131  // Do not change this without good reasons.
   132  func (k ResourceTransformationKey) key() string {
   133  	if len(k.elements) == 0 {
   134  		return k.name
   135  	}
   136  
   137  	sb := bp.GetBuffer()
   138  	defer bp.PutBuffer(sb)
   139  
   140  	sb.WriteString(k.name)
   141  	for _, element := range k.elements {
   142  		hash, err := hashstructure.Hash(element, nil)
   143  		if err != nil {
   144  			panic(err)
   145  		}
   146  		sb.WriteString("_")
   147  		sb.WriteString(strconv.FormatUint(hash, 10))
   148  	}
   149  
   150  	return sb.String()
   151  }
   152  
   153  // ResourceTransformation is the interface that a resource transformation step
   154  // needs to implement.
   155  type ResourceTransformation interface {
   156  	Key() ResourceTransformationKey
   157  	Transform(ctx *ResourceTransformationCtx) error
   158  }
   159  
   160  // We will persist this information to disk.
   161  type transformedResourceMetadata struct {
   162  	Target     string                 `json:"Target"`
   163  	MediaTypeV string                 `json:"MediaType"`
   164  	MetaData   map[string]interface{} `json:"Data"`
   165  }
   166  
   167  type transformedResource struct {
   168  	cache *ResourceCache
   169  
   170  	// This is the filename inside resources/_gen/assets
   171  	sourceFilename string
   172  
   173  	linker permalinker
   174  
   175  	// The transformation to apply.
   176  	transformation ResourceTransformation
   177  
   178  	// We apply the tranformations lazily.
   179  	transformInit sync.Once
   180  	transformErr  error
   181  
   182  	// The transformed values
   183  	content     string
   184  	contentInit sync.Once
   185  	transformedResourceMetadata
   186  
   187  	// The source
   188  	Resource
   189  }
   190  
   191  func (r *transformedResource) ReadSeekCloser() (ReadSeekCloser, error) {
   192  	if err := r.initContent(); err != nil {
   193  		return nil, err
   194  	}
   195  	return NewReadSeekerNoOpCloserFromString(r.content), nil
   196  }
   197  
   198  func (r *transformedResource) transferTransformedValues(another *transformedResource) {
   199  	if another.content != "" {
   200  		r.contentInit.Do(func() {
   201  			r.content = another.content
   202  		})
   203  	}
   204  	r.transformedResourceMetadata = another.transformedResourceMetadata
   205  }
   206  
   207  func (r *transformedResource) tryTransformedFileCache(key string) io.ReadCloser {
   208  	f, meta, found := r.cache.getFromFile(key)
   209  	if !found {
   210  		return nil
   211  	}
   212  	r.transformedResourceMetadata = meta
   213  	r.sourceFilename = f.Name()
   214  
   215  	return f
   216  }
   217  
   218  func (r *transformedResource) Content() (interface{}, error) {
   219  	if err := r.initTransform(true); err != nil {
   220  		return nil, err
   221  	}
   222  	if err := r.initContent(); err != nil {
   223  		return "", err
   224  	}
   225  	return r.content, nil
   226  }
   227  
   228  func (r *transformedResource) Data() interface{} {
   229  	return r.MetaData
   230  }
   231  
   232  func (r *transformedResource) MediaType() media.Type {
   233  	if err := r.initTransform(false); err != nil {
   234  		return media.Type{}
   235  	}
   236  	m, _ := r.cache.rs.MediaTypes.GetByType(r.MediaTypeV)
   237  	return m
   238  }
   239  
   240  func (r *transformedResource) Permalink() string {
   241  	if err := r.initTransform(false); err != nil {
   242  		return ""
   243  	}
   244  	return r.linker.permalinkFor(r.Target)
   245  }
   246  
   247  func (r *transformedResource) RelPermalink() string {
   248  	if err := r.initTransform(false); err != nil {
   249  		return ""
   250  	}
   251  	return r.linker.relPermalinkFor(r.Target)
   252  }
   253  
   254  func (r *transformedResource) initContent() error {
   255  	var err error
   256  	r.contentInit.Do(func() {
   257  		var b []byte
   258  		b, err := afero.ReadFile(r.cache.rs.Resources.Fs, r.sourceFilename)
   259  		if err != nil {
   260  			return
   261  		}
   262  		r.content = string(b)
   263  	})
   264  	return err
   265  }
   266  
   267  func (r *transformedResource) transform(setContent bool) (err error) {
   268  
   269  	openPublishFileForWriting := func(relTargetPath string) (io.WriteCloser, error) {
   270  		return helpers.OpenFilesForWriting(r.cache.rs.PublishFs, r.linker.relTargetPathsFor(relTargetPath)...)
   271  	}
   272  
   273  	// This can be the last resource in a chain.
   274  	// Rewind and create a processing chain.
   275  	var chain []Resource
   276  	current := r
   277  	for {
   278  		rr := current.Resource
   279  		chain = append(chain[:0], append([]Resource{rr}, chain[0:]...)...)
   280  		if tr, ok := rr.(*transformedResource); ok {
   281  			current = tr
   282  		} else {
   283  			break
   284  		}
   285  	}
   286  
   287  	// Append the current transformer at the end
   288  	chain = append(chain, r)
   289  
   290  	first := chain[0]
   291  
   292  	// Files with a suffix will be stored in cache (both on disk and in memory)
   293  	// partitioned by their suffix. There will be other files below /other.
   294  	// This partition is also how we determine what to delete on server reloads.
   295  	var key, base string
   296  	for _, element := range chain {
   297  		switch v := element.(type) {
   298  		case *transformedResource:
   299  			key = key + "_" + v.transformation.Key().key()
   300  		case permalinker:
   301  			r.linker = v
   302  			p := v.targetPath()
   303  			if p == "" {
   304  				panic("target path needed for key creation")
   305  			}
   306  			partition := ResourceKeyPartition(p)
   307  			base = partition + "/" + p
   308  		default:
   309  			return fmt.Errorf("transformation not supported for type %T", element)
   310  		}
   311  	}
   312  
   313  	key = r.cache.cleanKey(base + "_" + helpers.MD5String(key))
   314  
   315  	cached, found := r.cache.get(key)
   316  	if found {
   317  		r.transferTransformedValues(cached.(*transformedResource))
   318  		return
   319  	}
   320  
   321  	// Acquire a write lock for the named transformation.
   322  	r.cache.nlocker.Lock(key)
   323  	// Check the cache again.
   324  	cached, found = r.cache.get(key)
   325  	if found {
   326  		r.transferTransformedValues(cached.(*transformedResource))
   327  		r.cache.nlocker.Unlock(key)
   328  		return
   329  	}
   330  
   331  	defer r.cache.nlocker.Unlock(key)
   332  	defer r.cache.set(key, r)
   333  
   334  	b1 := bp.GetBuffer()
   335  	b2 := bp.GetBuffer()
   336  	defer bp.PutBuffer(b1)
   337  	defer bp.PutBuffer(b2)
   338  
   339  	tctx := &ResourceTransformationCtx{
   340  		Data: r.transformedResourceMetadata.MetaData,
   341  		OpenResourcePublisher: openPublishFileForWriting,
   342  	}
   343  
   344  	tctx.InMediaType = first.MediaType()
   345  	tctx.OutMediaType = first.MediaType()
   346  
   347  	contentrc, err := contentReadSeekerCloser(first)
   348  	if err != nil {
   349  		return err
   350  	}
   351  	defer contentrc.Close()
   352  
   353  	tctx.From = contentrc
   354  	tctx.To = b1
   355  
   356  	if r.linker != nil {
   357  		tctx.InPath = r.linker.targetPath()
   358  		tctx.SourcePath = tctx.InPath
   359  	}
   360  
   361  	counter := 0
   362  
   363  	var transformedContentr io.Reader
   364  
   365  	for _, element := range chain {
   366  		tr, ok := element.(*transformedResource)
   367  		if !ok {
   368  			continue
   369  		}
   370  		counter++
   371  		if counter != 1 {
   372  			tctx.InMediaType = tctx.OutMediaType
   373  		}
   374  		if counter%2 == 0 {
   375  			tctx.From = b1
   376  			b2.Reset()
   377  			tctx.To = b2
   378  		} else {
   379  			if counter != 1 {
   380  				// The first reader is the file.
   381  				tctx.From = b2
   382  			}
   383  			b1.Reset()
   384  			tctx.To = b1
   385  		}
   386  
   387  		if err := tr.transformation.Transform(tctx); err != nil {
   388  			if err == errors.FeatureNotAvailableErr {
   389  				// This transformation is not available in this
   390  				// Hugo installation (scss not compiled in, PostCSS not available etc.)
   391  				// If a prepared bundle for this transformation chain is available, use that.
   392  				f := r.tryTransformedFileCache(key)
   393  				if f == nil {
   394  					return fmt.Errorf("%s: failed to transform %q (%s): %s", strings.ToUpper(tr.transformation.Key().name), tctx.InPath, tctx.InMediaType.Type(), err)
   395  				}
   396  				transformedContentr = f
   397  				defer f.Close()
   398  
   399  				// The reader above is all we need.
   400  				break
   401  			}
   402  
   403  			// Abort.
   404  			return err
   405  		}
   406  
   407  		if tctx.OutPath != "" {
   408  			tctx.InPath = tctx.OutPath
   409  			tctx.OutPath = ""
   410  		}
   411  	}
   412  
   413  	if transformedContentr == nil {
   414  		r.Target = tctx.InPath
   415  		r.MediaTypeV = tctx.OutMediaType.Type()
   416  	}
   417  
   418  	publicw, err := openPublishFileForWriting(r.Target)
   419  	if err != nil {
   420  		r.transformErr = err
   421  		return
   422  	}
   423  	defer publicw.Close()
   424  
   425  	publishwriters := []io.Writer{publicw}
   426  
   427  	if transformedContentr == nil {
   428  		// Also write it to the cache
   429  		metaw, err := r.cache.writeMeta(key, r.transformedResourceMetadata)
   430  		if err != nil {
   431  			return err
   432  		}
   433  		r.sourceFilename = metaw.Name()
   434  		defer metaw.Close()
   435  
   436  		publishwriters = append(publishwriters, metaw)
   437  
   438  		if counter > 0 {
   439  			transformedContentr = tctx.To.(*bytes.Buffer)
   440  		} else {
   441  			transformedContentr = contentrc
   442  		}
   443  	}
   444  
   445  	// Also write it to memory
   446  	var contentmemw *bytes.Buffer
   447  
   448  	if setContent {
   449  		contentmemw = bp.GetBuffer()
   450  		defer bp.PutBuffer(contentmemw)
   451  		publishwriters = append(publishwriters, contentmemw)
   452  	}
   453  
   454  	publishw := io.MultiWriter(publishwriters...)
   455  	_, r.transformErr = io.Copy(publishw, transformedContentr)
   456  
   457  	if setContent {
   458  		r.contentInit.Do(func() {
   459  			r.content = contentmemw.String()
   460  		})
   461  	}
   462  
   463  	return nil
   464  
   465  }
   466  func (r *transformedResource) initTransform(setContent bool) error {
   467  	r.transformInit.Do(func() {
   468  		if err := r.transform(setContent); err != nil {
   469  			r.transformErr = err
   470  			r.cache.rs.Logger.ERROR.Println("error: failed to transform resource:", err)
   471  		}
   472  	})
   473  	return r.transformErr
   474  }
   475  
   476  // contentReadSeekerCloser returns a ReadSeekerCloser if possible for a given Resource.
   477  func contentReadSeekerCloser(r Resource) (ReadSeekCloser, error) {
   478  	switch rr := r.(type) {
   479  	case ReadSeekCloserResource:
   480  		rc, err := rr.ReadSeekCloser()
   481  		if err != nil {
   482  			return nil, err
   483  		}
   484  		return rc, nil
   485  	default:
   486  		return nil, fmt.Errorf("cannot transform content of Resource of type %T", r)
   487  
   488  	}
   489  }