hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

resource.go (16883B)

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