mirror of
https://github.com/gohugoio/hugo.git
synced 2024-12-04 21:32:51 -05:00
708 lines
17 KiB
Go
708 lines
17 KiB
Go
// Copyright 2022 The Hugo Authors. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package resources
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/gohugoio/hugo/resources/internal"
|
|
|
|
"github.com/gohugoio/hugo/common/herrors"
|
|
|
|
"github.com/gohugoio/hugo/hugofs"
|
|
|
|
"github.com/gohugoio/hugo/media"
|
|
"github.com/gohugoio/hugo/source"
|
|
|
|
"errors"
|
|
|
|
"github.com/gohugoio/hugo/common/hugio"
|
|
"github.com/gohugoio/hugo/common/maps"
|
|
"github.com/gohugoio/hugo/resources/page"
|
|
"github.com/gohugoio/hugo/resources/resource"
|
|
"github.com/spf13/afero"
|
|
|
|
"github.com/gohugoio/hugo/helpers"
|
|
)
|
|
|
|
var (
|
|
_ resource.ContentResource = (*genericResource)(nil)
|
|
_ resource.ReadSeekCloserResource = (*genericResource)(nil)
|
|
_ resource.Resource = (*genericResource)(nil)
|
|
_ resource.Source = (*genericResource)(nil)
|
|
_ resource.Cloner = (*genericResource)(nil)
|
|
_ resource.ResourcesLanguageMerger = (*resource.Resources)(nil)
|
|
_ permalinker = (*genericResource)(nil)
|
|
_ resource.Identifier = (*genericResource)(nil)
|
|
_ fileInfo = (*genericResource)(nil)
|
|
)
|
|
|
|
type ResourceSourceDescriptor struct {
|
|
// TargetPaths is a callback to fetch paths's relative to its owner.
|
|
TargetPaths func() page.TargetPaths
|
|
|
|
// Need one of these to load the resource content.
|
|
SourceFile source.File
|
|
OpenReadSeekCloser resource.OpenReadSeekCloser
|
|
|
|
FileInfo os.FileInfo
|
|
|
|
// If OpenReadSeekerCloser is not set, we use this to open the file.
|
|
SourceFilename string
|
|
|
|
Fs afero.Fs
|
|
|
|
Data map[string]any
|
|
|
|
// Set when its known up front, else it's resolved from the target filename.
|
|
MediaType media.Type
|
|
|
|
// The relative target filename without any language code.
|
|
RelTargetFilename string
|
|
|
|
// Any base paths prepended to the target path. This will also typically be the
|
|
// language code, but setting it here means that it should not have any effect on
|
|
// the permalink.
|
|
// This may be several values. In multihost mode we may publish the same resources to
|
|
// multiple targets.
|
|
TargetBasePaths []string
|
|
|
|
// Delay publishing until either Permalink or RelPermalink is called. Maybe never.
|
|
LazyPublish bool
|
|
}
|
|
|
|
func (r ResourceSourceDescriptor) Filename() string {
|
|
if r.SourceFile != nil {
|
|
return r.SourceFile.Filename()
|
|
}
|
|
return r.SourceFilename
|
|
}
|
|
|
|
type ResourceTransformer interface {
|
|
resource.Resource
|
|
Transformer
|
|
}
|
|
|
|
type Transformer interface {
|
|
Transform(...ResourceTransformation) (ResourceTransformer, error)
|
|
TransformWithContext(context.Context, ...ResourceTransformation) (ResourceTransformer, error)
|
|
}
|
|
|
|
func NewFeatureNotAvailableTransformer(key string, elements ...any) ResourceTransformation {
|
|
return transformerNotAvailable{
|
|
key: internal.NewResourceTransformationKey(key, elements...),
|
|
}
|
|
}
|
|
|
|
type transformerNotAvailable struct {
|
|
key internal.ResourceTransformationKey
|
|
}
|
|
|
|
func (t transformerNotAvailable) Transform(ctx *ResourceTransformationCtx) error {
|
|
return herrors.ErrFeatureNotAvailable
|
|
}
|
|
|
|
func (t transformerNotAvailable) Key() internal.ResourceTransformationKey {
|
|
return t.key
|
|
}
|
|
|
|
// resourceCopier is for internal use.
|
|
type resourceCopier interface {
|
|
cloneTo(targetPath string) resource.Resource
|
|
}
|
|
|
|
// Copy copies r to the targetPath given.
|
|
func Copy(r resource.Resource, targetPath string) resource.Resource {
|
|
if r.Err() != nil {
|
|
panic(fmt.Sprintf("Resource has an .Err: %s", r.Err()))
|
|
}
|
|
return r.(resourceCopier).cloneTo(targetPath)
|
|
}
|
|
|
|
type baseResourceResource interface {
|
|
resource.Cloner
|
|
resourceCopier
|
|
resource.ContentProvider
|
|
resource.Resource
|
|
resource.Identifier
|
|
}
|
|
|
|
type baseResourceInternal interface {
|
|
resource.Source
|
|
|
|
fileInfo
|
|
metaAssigner
|
|
targetPather
|
|
|
|
ReadSeekCloser() (hugio.ReadSeekCloser, error)
|
|
|
|
// For internal use.
|
|
cloneWithUpdates(*transformationUpdate) (baseResource, error)
|
|
tryTransformedFileCache(key string, u *transformationUpdate) io.ReadCloser
|
|
|
|
specProvider
|
|
getResourcePaths() *resourcePathDescriptor
|
|
getTargetFilenames() []string
|
|
openPublishFileForWriting(relTargetPath string) (io.WriteCloser, error)
|
|
|
|
relTargetPathForRel(rel string, addBaseTargetPath, isAbs, isURL bool) string
|
|
}
|
|
|
|
type specProvider interface {
|
|
getSpec() *Spec
|
|
}
|
|
|
|
type baseResource interface {
|
|
baseResourceResource
|
|
baseResourceInternal
|
|
}
|
|
|
|
type commonResource struct {
|
|
}
|
|
|
|
// Slice is for internal use.
|
|
// for the template functions. See collections.Slice.
|
|
func (commonResource) Slice(in any) (any, error) {
|
|
switch items := in.(type) {
|
|
case resource.Resources:
|
|
return items, nil
|
|
case []any:
|
|
groups := make(resource.Resources, len(items))
|
|
for i, v := range items {
|
|
g, ok := v.(resource.Resource)
|
|
if !ok {
|
|
return nil, fmt.Errorf("type %T is not a Resource", v)
|
|
}
|
|
groups[i] = g
|
|
{
|
|
}
|
|
}
|
|
return groups, nil
|
|
default:
|
|
return nil, fmt.Errorf("invalid slice type %T", items)
|
|
}
|
|
}
|
|
|
|
type dirFile struct {
|
|
// This is the directory component with Unix-style slashes.
|
|
dir string
|
|
// This is the file component.
|
|
file string
|
|
}
|
|
|
|
func (d dirFile) path() string {
|
|
return path.Join(d.dir, d.file)
|
|
}
|
|
|
|
type fileInfo interface {
|
|
getSourceFilename() string
|
|
setSourceFilename(string)
|
|
setSourceFilenameIsHash(bool)
|
|
setSourceFs(afero.Fs)
|
|
getFileInfo() hugofs.FileMetaInfo
|
|
hash() (string, error)
|
|
size() int
|
|
}
|
|
|
|
// genericResource represents a generic linkable resource.
|
|
type genericResource struct {
|
|
*resourcePathDescriptor
|
|
*resourceFileInfo
|
|
*resourceContent
|
|
|
|
spec *Spec
|
|
|
|
title string
|
|
name string
|
|
params map[string]any
|
|
data map[string]any
|
|
|
|
resourceType string
|
|
mediaType media.Type
|
|
}
|
|
|
|
func (l *genericResource) Clone() resource.Resource {
|
|
return l.clone()
|
|
}
|
|
|
|
func (l *genericResource) cloneTo(targetPath string) resource.Resource {
|
|
c := l.clone()
|
|
|
|
targetPath = helpers.ToSlashTrimLeading(targetPath)
|
|
dir, file := path.Split(targetPath)
|
|
|
|
c.resourcePathDescriptor = &resourcePathDescriptor{
|
|
relTargetDirFile: dirFile{dir: dir, file: file},
|
|
}
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
func (l *genericResource) Content(context.Context) (any, error) {
|
|
if err := l.initContent(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return l.content, nil
|
|
}
|
|
|
|
func (r *genericResource) Err() resource.ResourceError {
|
|
return nil
|
|
}
|
|
|
|
func (l *genericResource) Data() any {
|
|
return l.data
|
|
}
|
|
|
|
func (l *genericResource) Key() string {
|
|
basePath := l.spec.Cfg.BaseURL().BasePath
|
|
if basePath == "" {
|
|
return l.RelPermalink()
|
|
}
|
|
return strings.TrimPrefix(l.RelPermalink(), basePath)
|
|
}
|
|
|
|
func (l *genericResource) MediaType() media.Type {
|
|
return l.mediaType
|
|
}
|
|
|
|
func (l *genericResource) setMediaType(mediaType media.Type) {
|
|
l.mediaType = mediaType
|
|
}
|
|
|
|
func (l *genericResource) Name() string {
|
|
return l.name
|
|
}
|
|
|
|
func (l *genericResource) Params() maps.Params {
|
|
return l.params
|
|
}
|
|
|
|
func (l *genericResource) Permalink() string {
|
|
return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(l.relTargetDirFile.path(), true), l.spec.Cfg.BaseURL().HostURL())
|
|
}
|
|
|
|
func (l *genericResource) Publish() error {
|
|
var err error
|
|
l.publishInit.Do(func() {
|
|
targetFilenames := l.getTargetFilenames()
|
|
if l.sourceFilenameIsHash {
|
|
// This is a processed image. We want to avoid copying it if it hasn't changed.
|
|
var changedFilenames []string
|
|
for _, targetFilename := range targetFilenames {
|
|
if _, err := l.getSpec().BaseFs.PublishFs.Stat(targetFilename); err == nil {
|
|
continue
|
|
}
|
|
changedFilenames = append(changedFilenames, targetFilename)
|
|
}
|
|
if len(changedFilenames) == 0 {
|
|
return
|
|
}
|
|
targetFilenames = changedFilenames
|
|
}
|
|
var fr hugio.ReadSeekCloser
|
|
fr, err = l.ReadSeekCloser()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer fr.Close()
|
|
|
|
var fw io.WriteCloser
|
|
fw, err = helpers.OpenFilesForWriting(l.spec.BaseFs.PublishFs, targetFilenames...)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer fw.Close()
|
|
|
|
_, err = io.Copy(fw, fr)
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
func (l *genericResource) RelPermalink() string {
|
|
return l.relPermalinkFor(l.relTargetDirFile.path())
|
|
}
|
|
|
|
func (l *genericResource) ResourceType() string {
|
|
return l.resourceType
|
|
}
|
|
|
|
func (l *genericResource) String() string {
|
|
return fmt.Sprintf("Resource(%s: %s)", l.resourceType, l.name)
|
|
}
|
|
|
|
// Path is stored with Unix style slashes.
|
|
func (l *genericResource) TargetPath() string {
|
|
return l.relTargetDirFile.path()
|
|
}
|
|
|
|
func (l *genericResource) Title() string {
|
|
return l.title
|
|
}
|
|
|
|
func (l *genericResource) createBasePath(rel string, isURL bool) string {
|
|
if l.targetPathBuilder == nil {
|
|
return rel
|
|
}
|
|
tp := l.targetPathBuilder()
|
|
|
|
if isURL {
|
|
return path.Join(tp.SubResourceBaseLink, rel)
|
|
}
|
|
|
|
// TODO(bep) path
|
|
return path.Join(filepath.ToSlash(tp.SubResourceBaseTarget), rel)
|
|
}
|
|
|
|
func (l *genericResource) initContent() error {
|
|
var err error
|
|
l.contentInit.Do(func() {
|
|
var r hugio.ReadSeekCloser
|
|
r, err = l.ReadSeekCloser()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer r.Close()
|
|
|
|
var b []byte
|
|
b, err = io.ReadAll(r)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
l.content = string(b)
|
|
})
|
|
|
|
return err
|
|
}
|
|
|
|
func (l *genericResource) setName(name string) {
|
|
l.name = name
|
|
}
|
|
|
|
func (l *genericResource) getResourcePaths() *resourcePathDescriptor {
|
|
return l.resourcePathDescriptor
|
|
}
|
|
|
|
func (l *genericResource) getSpec() *Spec {
|
|
return l.spec
|
|
}
|
|
|
|
func (l *genericResource) getTargetFilenames() []string {
|
|
paths := l.relTargetPaths()
|
|
for i, p := range paths {
|
|
paths[i] = filepath.Clean(p)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func (l *genericResource) setTitle(title string) {
|
|
l.title = title
|
|
}
|
|
|
|
func (r *genericResource) tryTransformedFileCache(key string, u *transformationUpdate) io.ReadCloser {
|
|
fi, f, meta, found := r.spec.ResourceCache.getFromFile(key)
|
|
if !found {
|
|
return nil
|
|
}
|
|
u.sourceFilename = &fi.Name
|
|
mt, _ := r.spec.MediaTypes().GetByType(meta.MediaTypeV)
|
|
u.mediaType = mt
|
|
u.data = meta.MetaData
|
|
u.targetPath = meta.Target
|
|
return f
|
|
}
|
|
|
|
func (r *genericResource) mergeData(in map[string]any) {
|
|
if len(in) == 0 {
|
|
return
|
|
}
|
|
if r.data == nil {
|
|
r.data = make(map[string]any)
|
|
}
|
|
for k, v := range in {
|
|
if _, found := r.data[k]; !found {
|
|
r.data[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rc *genericResource) cloneWithUpdates(u *transformationUpdate) (baseResource, error) {
|
|
r := rc.clone()
|
|
|
|
if u.content != nil {
|
|
r.contentInit.Do(func() {
|
|
r.content = *u.content
|
|
r.openReadSeekerCloser = func() (hugio.ReadSeekCloser, error) {
|
|
return hugio.NewReadSeekerNoOpCloserFromString(r.content), nil
|
|
}
|
|
})
|
|
}
|
|
|
|
r.mediaType = u.mediaType
|
|
|
|
if u.sourceFilename != nil {
|
|
r.setSourceFilename(*u.sourceFilename)
|
|
}
|
|
|
|
if u.sourceFs != nil {
|
|
r.setSourceFs(u.sourceFs)
|
|
}
|
|
|
|
if u.targetPath == "" {
|
|
return nil, errors.New("missing targetPath")
|
|
}
|
|
|
|
fpath, fname := path.Split(u.targetPath)
|
|
r.resourcePathDescriptor.relTargetDirFile = dirFile{dir: fpath, file: fname}
|
|
|
|
r.mergeData(u.data)
|
|
|
|
return r, nil
|
|
}
|
|
|
|
func (l genericResource) clone() *genericResource {
|
|
gi := *l.resourceFileInfo
|
|
rp := *l.resourcePathDescriptor
|
|
l.resourceFileInfo = &gi
|
|
l.resourcePathDescriptor = &rp
|
|
l.resourceContent = &resourceContent{}
|
|
return &l
|
|
}
|
|
|
|
func (r *genericResource) openPublishFileForWriting(relTargetPath string) (io.WriteCloser, error) {
|
|
return helpers.OpenFilesForWriting(r.spec.BaseFs.PublishFs, r.relTargetPathsFor(relTargetPath)...)
|
|
}
|
|
|
|
func (l *genericResource) permalinkFor(target string) string {
|
|
return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(target, true), l.spec.Cfg.BaseURL().HostURL())
|
|
}
|
|
|
|
func (l *genericResource) relPermalinkFor(target string) string {
|
|
return l.relPermalinkForRel(target, false)
|
|
}
|
|
|
|
func (l *genericResource) relPermalinkForRel(rel string, isAbs bool) string {
|
|
return l.spec.PathSpec.URLizeFilename(l.relTargetPathForRel(rel, false, isAbs, true))
|
|
}
|
|
|
|
func (l *genericResource) relTargetPathForRel(rel string, addBaseTargetPath, isAbs, isURL bool) string {
|
|
if addBaseTargetPath && len(l.baseTargetPathDirs) > 1 {
|
|
panic("multiple baseTargetPathDirs")
|
|
}
|
|
var basePath string
|
|
if addBaseTargetPath && len(l.baseTargetPathDirs) > 0 {
|
|
basePath = l.baseTargetPathDirs[0]
|
|
}
|
|
|
|
return l.relTargetPathForRelAndBasePath(rel, basePath, isAbs, isURL)
|
|
}
|
|
|
|
func (l *genericResource) relTargetPathForRelAndBasePath(rel, basePath string, isAbs, isURL bool) string {
|
|
rel = l.createBasePath(rel, isURL)
|
|
|
|
if basePath != "" {
|
|
rel = path.Join(basePath, rel)
|
|
}
|
|
|
|
if l.baseOffset != "" {
|
|
rel = path.Join(l.baseOffset, rel)
|
|
}
|
|
|
|
if isURL {
|
|
bp := l.spec.PathSpec.GetBasePath(!isAbs)
|
|
if bp != "" {
|
|
rel = path.Join(bp, rel)
|
|
}
|
|
}
|
|
|
|
if len(rel) == 0 || rel[0] != '/' {
|
|
rel = "/" + rel
|
|
}
|
|
|
|
return rel
|
|
}
|
|
|
|
func (l *genericResource) relTargetPaths() []string {
|
|
return l.relTargetPathsForRel(l.TargetPath())
|
|
}
|
|
|
|
func (l *genericResource) relTargetPathsFor(target string) []string {
|
|
return l.relTargetPathsForRel(target)
|
|
}
|
|
|
|
func (l *genericResource) relTargetPathsForRel(rel string) []string {
|
|
if len(l.baseTargetPathDirs) == 0 {
|
|
return []string{l.relTargetPathForRelAndBasePath(rel, "", false, false)}
|
|
}
|
|
|
|
targetPaths := make([]string, len(l.baseTargetPathDirs))
|
|
for i, dir := range l.baseTargetPathDirs {
|
|
targetPaths[i] = l.relTargetPathForRelAndBasePath(rel, dir, false, false)
|
|
}
|
|
return targetPaths
|
|
}
|
|
|
|
func (l *genericResource) updateParams(params map[string]any) {
|
|
if l.params == nil {
|
|
l.params = params
|
|
return
|
|
}
|
|
|
|
// Sets the params not already set
|
|
for k, v := range params {
|
|
if _, found := l.params[k]; !found {
|
|
l.params[k] = v
|
|
}
|
|
}
|
|
}
|
|
|
|
type targetPather interface {
|
|
TargetPath() string
|
|
}
|
|
|
|
type permalinker interface {
|
|
targetPather
|
|
permalinkFor(target string) string
|
|
relPermalinkFor(target string) string
|
|
relTargetPaths() []string
|
|
relTargetPathsFor(target string) []string
|
|
}
|
|
|
|
type resourceContent struct {
|
|
content string
|
|
contentInit sync.Once
|
|
|
|
publishInit sync.Once
|
|
}
|
|
|
|
type resourceFileInfo struct {
|
|
// Will be set if this resource is backed by something other than a file.
|
|
openReadSeekerCloser resource.OpenReadSeekCloser
|
|
|
|
// This may be set to tell us to look in another filesystem for this resource.
|
|
// We, by default, use the sourceFs filesystem in the spec below.
|
|
sourceFs afero.Fs
|
|
|
|
// Absolute filename to the source, including any content folder path.
|
|
// Note that this is absolute in relation to the filesystem it is stored in.
|
|
// It can be a base path filesystem, and then this filename will not match
|
|
// the path to the file on the real filesystem.
|
|
sourceFilename string
|
|
|
|
// For performance. This means that whenever the content changes, the filename changes.
|
|
sourceFilenameIsHash bool
|
|
|
|
fi hugofs.FileMetaInfo
|
|
|
|
// A hash of the source content. Is only calculated in caching situations.
|
|
h *resourceHash
|
|
}
|
|
|
|
func (fi *resourceFileInfo) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
|
|
if fi.openReadSeekerCloser != nil {
|
|
return fi.openReadSeekerCloser()
|
|
}
|
|
|
|
f, err := fi.getSourceFs().Open(fi.getSourceFilename())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func (fi *resourceFileInfo) getFileInfo() hugofs.FileMetaInfo {
|
|
return fi.fi
|
|
}
|
|
|
|
func (fi *resourceFileInfo) getSourceFilename() string {
|
|
return fi.sourceFilename
|
|
}
|
|
|
|
func (fi *resourceFileInfo) setSourceFilename(s string) {
|
|
// Make sure it's always loaded by sourceFilename.
|
|
fi.openReadSeekerCloser = nil
|
|
fi.sourceFilename = s
|
|
}
|
|
|
|
func (fi *resourceFileInfo) setSourceFilenameIsHash(b bool) {
|
|
fi.sourceFilenameIsHash = b
|
|
}
|
|
|
|
func (fi *resourceFileInfo) getSourceFs() afero.Fs {
|
|
return fi.sourceFs
|
|
}
|
|
|
|
func (fi *resourceFileInfo) setSourceFs(fs afero.Fs) {
|
|
fi.sourceFs = fs
|
|
}
|
|
|
|
func (fi *resourceFileInfo) hash() (string, error) {
|
|
var err error
|
|
fi.h.init.Do(func() {
|
|
var hash string
|
|
var f hugio.ReadSeekCloser
|
|
f, err = fi.ReadSeekCloser()
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to open source file: %w", err)
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
hash, err = helpers.MD5FromFileFast(f)
|
|
if err != nil {
|
|
return
|
|
}
|
|
fi.h.value = hash
|
|
})
|
|
|
|
return fi.h.value, err
|
|
}
|
|
|
|
func (fi *resourceFileInfo) size() int {
|
|
if fi.fi == nil {
|
|
return 0
|
|
}
|
|
|
|
return int(fi.fi.Size())
|
|
}
|
|
|
|
type resourceHash struct {
|
|
value string
|
|
init sync.Once
|
|
}
|
|
|
|
type resourcePathDescriptor struct {
|
|
// The relative target directory and filename.
|
|
relTargetDirFile dirFile
|
|
|
|
// Callback used to construct a target path relative to its owner.
|
|
targetPathBuilder func() page.TargetPaths
|
|
|
|
// This will normally be the same as above, but this will only apply to publishing
|
|
// of resources. It may be multiple values when in multihost mode.
|
|
baseTargetPathDirs []string
|
|
|
|
// baseOffset is set when the output format's path has a offset, e.g. for AMP.
|
|
baseOffset string
|
|
}
|