mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
44da086082
Artifacts are the additional output files published as part of the build: source maps and files emitted by ESBuild's file loader (e.g. fonts). Each artifact provides Permalink, RelPermalink and MediaType, so e.g. font preload links can be constructed in templates. Also add media type definitions for source maps (application/source-map), font/woff and font/woff2. Fixes #15173 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
841 lines
22 KiB
Go
841 lines
22 KiB
Go
// Copyright 2019 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 (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/gohugoio/hugo/common/constants"
|
|
"github.com/gohugoio/hugo/common/hashing"
|
|
"github.com/gohugoio/hugo/common/hmaps"
|
|
"github.com/gohugoio/hugo/common/paths"
|
|
"github.com/gohugoio/hugo/identity"
|
|
|
|
"github.com/gohugoio/hugo/resources/images"
|
|
"github.com/gohugoio/hugo/resources/images/meta"
|
|
"github.com/spf13/afero"
|
|
|
|
bp "github.com/gohugoio/hugo/bufferpool"
|
|
|
|
"github.com/gohugoio/hugo/common/herrors"
|
|
"github.com/gohugoio/hugo/common/hugio"
|
|
"github.com/gohugoio/hugo/resources/internal"
|
|
"github.com/gohugoio/hugo/resources/resource"
|
|
|
|
"github.com/gohugoio/hugo/media"
|
|
)
|
|
|
|
var (
|
|
_ resource.ContentResource = (*resourceAdapter)(nil)
|
|
_ resourceCopier = (*resourceAdapter)(nil)
|
|
_ resource.ReadSeekCloserResource = (*resourceAdapter)(nil)
|
|
_ resource.Resource = (*resourceAdapter)(nil)
|
|
_ resource.Staler = (*resourceAdapterInner)(nil)
|
|
_ identity.IdentityGroupProvider = (*resourceAdapterInner)(nil)
|
|
_ resource.Source = (*resourceAdapter)(nil)
|
|
_ resource.Identifier = (*resourceAdapter)(nil)
|
|
_ resource.TransientIdentifier = (*resourceAdapter)(nil)
|
|
_ targetPathProvider = (*resourceAdapter)(nil)
|
|
_ sourcePathProvider = (*resourceAdapter)(nil)
|
|
_ resource.Identifier = (*resourceAdapter)(nil)
|
|
_ resource.ResourceNameTitleProvider = (*resourceAdapter)(nil)
|
|
_ resource.WithResourceMetaProvider = (*resourceAdapter)(nil)
|
|
_ identity.DependencyManagerProvider = (*resourceAdapter)(nil)
|
|
_ identity.IdentityGroupProvider = (*resourceAdapter)(nil)
|
|
_ resource.NameNormalizedProvider = (*resourceAdapter)(nil)
|
|
_ isPublishedProvider = (*resourceAdapter)(nil)
|
|
)
|
|
|
|
// These are transformations that need special support in Hugo that may not
|
|
// be available when building the theme/site so we write the transformation
|
|
// result to disk and reuse if needed for these,
|
|
// TODO(bep) it's a little fragile having these constants redefined here.
|
|
var transformationsToCacheOnDisk = map[string]bool{
|
|
"postcss": true,
|
|
"tocss": true,
|
|
"tocss-dart": true,
|
|
}
|
|
|
|
func newResourceAdapter(spec *Spec, lazyPublish bool, target transformableResource) *resourceAdapter {
|
|
var po *publishOnce
|
|
if lazyPublish {
|
|
po = &publishOnce{}
|
|
}
|
|
return &resourceAdapter{
|
|
resourceTransformations: &resourceTransformations{},
|
|
metaProvider: target,
|
|
resourceAdapterInner: &resourceAdapterInner{
|
|
ctx: context.Background(),
|
|
spec: spec,
|
|
publishOnce: po,
|
|
target: target,
|
|
Staler: &AtomicStaler{},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ResourceTransformation is the interface that a resource transformation step
|
|
// needs to implement.
|
|
type ResourceTransformation interface {
|
|
Key() internal.ResourceTransformationKey
|
|
Transform(ctx *ResourceTransformationCtx) error
|
|
}
|
|
|
|
type ResourceTransformationCtx struct {
|
|
// The context that started the transformation.
|
|
Ctx context.Context
|
|
|
|
// The dependency manager to use for dependency tracking.
|
|
DependencyManager identity.Manager
|
|
|
|
// The content to transform.
|
|
From io.Reader
|
|
|
|
// The target of content transformation.
|
|
// The current implementation requires that r is written to w
|
|
// even if no transformation is performed.
|
|
To io.Writer
|
|
|
|
// This is the relative path to the original source. Unix styled slashes.
|
|
SourcePath string
|
|
|
|
// This is the relative target path to the resource. Unix styled slashes.
|
|
InPath string
|
|
|
|
// The relative target path to the transformed resource. Unix styled slashes.
|
|
OutPath string
|
|
|
|
// The input media type
|
|
InMediaType media.Type
|
|
|
|
// The media type of the transformed resource.
|
|
OutMediaType media.Type
|
|
|
|
// Data can be set on the transformed Resource. For transformations
|
|
// cached to disk (see transformationsToCacheOnDisk), this needs to be
|
|
// simple types, as it will be serialized to JSON and back.
|
|
Data map[string]any
|
|
|
|
// This is used to publish additional artifacts, e.g. source maps.
|
|
// We may improve this.
|
|
OpenResourcePublisher func(relTargetPath string) (io.WriteCloser, error)
|
|
}
|
|
|
|
// AddOutPathIdentifier transforming InPath to OutPath adding an identifier,
|
|
// eg '.min' before any extension.
|
|
func (ctx *ResourceTransformationCtx) AddOutPathIdentifier(identifier string) {
|
|
ctx.OutPath = ctx.addPathIdentifier(ctx.InPath, identifier)
|
|
}
|
|
|
|
// PublishSourceMap writes the content to the target folder of the main resource
|
|
// with the ".map" extension added.
|
|
func (ctx *ResourceTransformationCtx) PublishSourceMap(content []byte) error {
|
|
target := ctx.OutPath + ".map"
|
|
return ctx.PublishTo(target, content)
|
|
}
|
|
|
|
// PublishTo writes the content to the target folder.
|
|
func (ctx *ResourceTransformationCtx) PublishTo(target string, content []byte) error {
|
|
f, err := ctx.OpenResourcePublisher(target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
_, err = f.Write(content)
|
|
return err
|
|
}
|
|
|
|
// ReplaceOutPathExtension transforming InPath to OutPath replacing the file
|
|
// extension, e.g. ".scss"
|
|
func (ctx *ResourceTransformationCtx) ReplaceOutPathExtension(newExt string) {
|
|
dir, file := path.Split(ctx.InPath)
|
|
base, _ := paths.PathAndExt(file)
|
|
ctx.OutPath = path.Join(dir, (base + newExt))
|
|
}
|
|
|
|
func (ctx *ResourceTransformationCtx) addPathIdentifier(inPath, identifier string) string {
|
|
dir, file := path.Split(inPath)
|
|
base, ext := paths.PathAndExt(file)
|
|
return path.Join(dir, (base + identifier + ext))
|
|
}
|
|
|
|
type publishOnce struct {
|
|
publisherInit sync.Once
|
|
publisherErr error
|
|
}
|
|
|
|
type resourceAdapter struct {
|
|
commonResource
|
|
*resourceTransformations
|
|
*resourceAdapterInner
|
|
metaProvider resource.ResourceMetaProvider
|
|
}
|
|
|
|
var _ identity.ForEeachIdentityByNameProvider = (*resourceAdapter)(nil)
|
|
|
|
func (r *resourceAdapter) Content(ctx context.Context) (any, error) {
|
|
r.init(false, true)
|
|
if r.transformationsErr != nil {
|
|
return nil, r.transformationsErr
|
|
}
|
|
return r.target.Content(ctx)
|
|
}
|
|
|
|
func (r *resourceAdapter) GetIdentity() identity.Identity {
|
|
return identity.FirstIdentity(r.target)
|
|
}
|
|
|
|
func (r *resourceAdapter) Data() any {
|
|
r.init(false, false)
|
|
return r.target.Data()
|
|
}
|
|
|
|
func (r *resourceAdapter) ForEeachIdentityByName(name string, f func(identity.Identity) bool) {
|
|
if constants.IsFieldRelOrPermalink(name) && !r.resourceTransformations.hasTransformationPermalinkHash() {
|
|
// Special case for links without any content hash in the URL.
|
|
// We don't need to rebuild all pages that use this resource,
|
|
// but we want to make sure that the resource is accessed at least once.
|
|
f(identity.NewFindFirstManagerIdentityProvider(r.target.GetDependencyManager(), r.target.GetIdentityGroup()))
|
|
return
|
|
}
|
|
f(r.target.GetIdentityGroup())
|
|
f(r.target.GetDependencyManager())
|
|
}
|
|
|
|
func (r *resourceAdapter) GetIdentityGroup() identity.Identity {
|
|
return r.target.GetIdentityGroup()
|
|
}
|
|
|
|
func (r *resourceAdapter) GetDependencyManager() identity.Manager {
|
|
return r.target.GetDependencyManager()
|
|
}
|
|
|
|
func (r resourceAdapter) cloneTo(targetPath string) resource.Resource {
|
|
newtTarget := r.target.cloneTo(targetPath)
|
|
newInner := &resourceAdapterInner{
|
|
ctx: r.ctx,
|
|
spec: r.spec,
|
|
Staler: r.Staler,
|
|
target: newtTarget.(transformableResource),
|
|
}
|
|
if r.resourceAdapterInner.publishOnce != nil {
|
|
newInner.publishOnce = &publishOnce{}
|
|
}
|
|
r.resourceAdapterInner = newInner
|
|
return &r
|
|
}
|
|
|
|
func (r *resourceAdapter) Process(spec string) (images.ImageResource, error) {
|
|
return r.getImageOps().Process(spec)
|
|
}
|
|
|
|
func (r *resourceAdapter) Crop(spec string) (images.ImageResource, error) {
|
|
return r.getImageOps().Crop(spec)
|
|
}
|
|
|
|
func (r *resourceAdapter) Fill(spec string) (images.ImageResource, error) {
|
|
return r.getImageOps().Fill(spec)
|
|
}
|
|
|
|
func (r *resourceAdapter) Fit(spec string) (images.ImageResource, error) {
|
|
return r.getImageOps().Fit(spec)
|
|
}
|
|
|
|
func (r *resourceAdapter) Filter(filters ...any) (images.ImageResource, error) {
|
|
return r.getImageOps().Filter(filters...)
|
|
}
|
|
|
|
func (r *resourceAdapter) Resize(spec string) (images.ImageResource, error) {
|
|
return r.getImageOps().Resize(spec)
|
|
}
|
|
|
|
func (r *resourceAdapter) Height() int {
|
|
return r.getImageOps().Height()
|
|
}
|
|
|
|
func (r *resourceAdapter) Exif() *meta.ExifInfo {
|
|
return r.getImageOps().Exif()
|
|
}
|
|
|
|
func (r *resourceAdapter) Meta() *meta.MetaInfo {
|
|
return r.getImageOps().Meta()
|
|
}
|
|
|
|
func (r *resourceAdapter) Colors() ([]images.Color, error) {
|
|
return r.getImageOps().Colors()
|
|
}
|
|
|
|
func (r *resourceAdapter) Key() string {
|
|
r.init(false, false)
|
|
return r.target.(resource.Identifier).Key()
|
|
}
|
|
|
|
func (r *resourceAdapter) TransientKey() string {
|
|
return r.Key()
|
|
}
|
|
|
|
func (r *resourceAdapter) targetPath() string {
|
|
r.init(false, false)
|
|
return r.target.(targetPathProvider).targetPath()
|
|
}
|
|
|
|
func (r *resourceAdapter) sourcePath() string {
|
|
r.init(false, false)
|
|
if sp, ok := r.target.(sourcePathProvider); ok {
|
|
return sp.sourcePath()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (r *resourceAdapter) MediaType() media.Type {
|
|
r.init(false, false)
|
|
return r.target.MediaType()
|
|
}
|
|
|
|
func (r *resourceAdapter) Name() string {
|
|
r.init(false, false)
|
|
return r.metaProvider.Name()
|
|
}
|
|
|
|
func (r *resourceAdapter) NameNormalized() string {
|
|
r.init(false, false)
|
|
return r.target.(resource.NameNormalizedProvider).NameNormalized()
|
|
}
|
|
|
|
func (r *resourceAdapter) Params() hmaps.Params {
|
|
r.init(false, false)
|
|
return r.metaProvider.Params()
|
|
}
|
|
|
|
func (r *resourceAdapter) Permalink() string {
|
|
r.init(true, false)
|
|
return r.target.Permalink()
|
|
}
|
|
|
|
func (r *resourceAdapter) Publish() error {
|
|
r.init(false, false)
|
|
|
|
return r.target.Publish()
|
|
}
|
|
|
|
func (r *resourceAdapter) isPublished() bool {
|
|
r.init(false, false)
|
|
return r.target.isPublished()
|
|
}
|
|
|
|
func (r *resourceAdapter) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
|
|
r.init(false, false)
|
|
return r.target.ReadSeekCloser()
|
|
}
|
|
|
|
func (r *resourceAdapter) RelPermalink() string {
|
|
r.init(true, false)
|
|
return r.target.RelPermalink()
|
|
}
|
|
|
|
func (r *resourceAdapter) ResourceType() string {
|
|
r.init(false, false)
|
|
return r.target.ResourceType()
|
|
}
|
|
|
|
func (r *resourceAdapter) String() string {
|
|
return r.Name()
|
|
}
|
|
|
|
func (r *resourceAdapter) Title() string {
|
|
r.init(false, false)
|
|
return r.metaProvider.Title()
|
|
}
|
|
|
|
func (r resourceAdapter) Transform(t ...ResourceTransformation) (ResourceTransformer, error) {
|
|
return r.TransformWithContext(context.Background(), t...)
|
|
}
|
|
|
|
func (r resourceAdapter) TransformWithContext(ctx context.Context, t ...ResourceTransformation) (ResourceTransformer, error) {
|
|
r.resourceTransformations = &resourceTransformations{
|
|
transformations: append(r.transformations, t...),
|
|
}
|
|
|
|
r.resourceAdapterInner = &resourceAdapterInner{
|
|
ctx: ctx,
|
|
spec: r.spec,
|
|
Staler: r.Staler,
|
|
publishOnce: &publishOnce{},
|
|
target: r.target,
|
|
}
|
|
|
|
return &r, nil
|
|
}
|
|
|
|
func (r *resourceAdapter) Width() int {
|
|
return r.getImageOps().Width()
|
|
}
|
|
|
|
func (r *resourceAdapter) DecodeImage() (image.Image, error) {
|
|
return r.getImageOps().DecodeImage()
|
|
}
|
|
|
|
func (r resourceAdapter) WithResourceMeta(mp resource.ResourceMetaProvider) resource.Resource {
|
|
r.metaProvider = mp
|
|
return &r
|
|
}
|
|
|
|
func (r *resourceAdapter) getImageOps() images.ImageResourceOps {
|
|
img, ok := r.target.(images.ImageResourceOps)
|
|
if !ok {
|
|
instructions := "use reflect.IsImageResource, " +
|
|
"reflect.IsImageResourceProcessable, or " +
|
|
"reflect.IsImageResourceWithMeta to check if the resource " +
|
|
"supports this method before calling it"
|
|
msg := fmt.Sprintf(
|
|
"resource %q of media type %q does not support this method: %s",
|
|
r.Name(),
|
|
r.MediaType(),
|
|
instructions,
|
|
)
|
|
panic(msg)
|
|
}
|
|
r.init(false, false)
|
|
|
|
return img
|
|
}
|
|
|
|
// ResolveImageOpsSupport reports the ImageOpsSupport for the given resource. This can be used to determine if a resource supports image operations like Resize, Crop, etc.
|
|
func ResolveImageOpsSupport(v any) images.ImageResourceType {
|
|
r, ok := v.(resource.Resource)
|
|
if !ok {
|
|
return images.ImageResourceTypeNone
|
|
}
|
|
mt := r.MediaType()
|
|
if mt.MainType != "image" {
|
|
return images.ImageResourceTypeNone
|
|
}
|
|
_, support := images.ImageFormatFromMediaSubType(mt.SubType)
|
|
return support
|
|
}
|
|
|
|
func (r *resourceAdapter) publish() {
|
|
if r.publishOnce == nil {
|
|
return
|
|
}
|
|
|
|
r.publisherInit.Do(func() {
|
|
r.publisherErr = r.target.Publish()
|
|
|
|
if r.publisherErr != nil {
|
|
r.spec.Logger.Errorf("Failed to publish Resource: %s", r.publisherErr)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (r *resourceAdapter) TransformationKey() string {
|
|
var key string
|
|
for _, tr := range r.transformations {
|
|
key = key + "_" + tr.Key().Value()
|
|
}
|
|
return r.spec.ResourceCache.cleanKey(r.target.Key()) + "_" + hashing.MD5FromStringHexEncoded(key)
|
|
}
|
|
|
|
func (r *resourceAdapter) getOrTransform(publish, setContent bool) error {
|
|
key := r.TransformationKey()
|
|
|
|
var created bool
|
|
res, err := r.spec.ResourceCache.cacheResourceTransformation.GetOrCreate(key, func(string) (*resourceAdapterInner, error) {
|
|
created = true
|
|
return r.transform(key, publish, setContent)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.resourceAdapterInner = res
|
|
|
|
if publish && r.spec.Rebuilder.IsRebuild() {
|
|
targetPath := r.target.TargetPath()
|
|
var republish bool
|
|
|
|
r.spec.ResourceCache.cacheResourceTransformationPublished.WithWriteLock(func(m map[string]string) error {
|
|
if created {
|
|
m[targetPath] = key
|
|
} else {
|
|
key2, found := m[targetPath]
|
|
republish = !found || key2 != key
|
|
m[targetPath] = key
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if !created && republish {
|
|
src, err := contentReadSeekerCloser(r.target)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
dest, err := r.target.openPublishFileForWriting(targetPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dest.Close()
|
|
_, err = io.Copy(dest, src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *resourceAdapter) transform(key string, publish, setContent bool) (*resourceAdapterInner, error) {
|
|
cache := r.spec.ResourceCache
|
|
|
|
b1 := bp.GetBuffer()
|
|
b2 := bp.GetBuffer()
|
|
defer bp.PutBuffer(b1)
|
|
defer bp.PutBuffer(b2)
|
|
|
|
tctx := &ResourceTransformationCtx{
|
|
Ctx: r.ctx,
|
|
Data: make(map[string]any),
|
|
OpenResourcePublisher: r.target.openPublishFileForWriting,
|
|
DependencyManager: r.target.GetDependencyManager(),
|
|
}
|
|
|
|
tctx.InMediaType = r.target.MediaType()
|
|
tctx.OutMediaType = r.target.MediaType()
|
|
|
|
startCtx := *tctx
|
|
updates := &transformationUpdate{startCtx: startCtx}
|
|
|
|
var contentrc hugio.ReadSeekCloser
|
|
|
|
contentrc, err := contentReadSeekerCloser(r.target)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defer contentrc.Close()
|
|
|
|
tctx.From = contentrc
|
|
tctx.To = b1
|
|
|
|
tctx.InPath = r.target.TargetPath()
|
|
tctx.SourcePath = strings.TrimPrefix(tctx.InPath, "/")
|
|
|
|
counter := 0
|
|
writeToFileCache := false
|
|
|
|
var transformedContentr io.Reader
|
|
|
|
for i, tr := range r.transformations {
|
|
if i != 0 {
|
|
tctx.InMediaType = tctx.OutMediaType
|
|
}
|
|
|
|
mayBeCachedOnDisk := transformationsToCacheOnDisk[tr.Key().Name]
|
|
if !writeToFileCache {
|
|
writeToFileCache = mayBeCachedOnDisk
|
|
}
|
|
|
|
if i > 0 {
|
|
hasWrites := tctx.To.(*bytes.Buffer).Len() > 0
|
|
if hasWrites {
|
|
counter++
|
|
// Switch the buffers
|
|
if counter%2 == 0 {
|
|
tctx.From = b2
|
|
b1.Reset()
|
|
tctx.To = b1
|
|
} else {
|
|
tctx.From = b1
|
|
b2.Reset()
|
|
tctx.To = b2
|
|
}
|
|
}
|
|
}
|
|
|
|
newErr := func(err error) error {
|
|
msg := fmt.Sprintf("%s: failed to transform %q (%s)", strings.ToUpper(tr.Key().Name), tctx.InPath, tctx.InMediaType.Type)
|
|
|
|
if herrors.IsFeatureNotAvailableError(err) {
|
|
var errMsg string
|
|
switch strings.ToLower(tr.Key().Name) {
|
|
case "postcss":
|
|
// This transformation is not available in this
|
|
// Most likely because PostCSS is not installed.
|
|
errMsg = ". You need to install PostCSS. See https://gohugo.io/functions/css/postcss/"
|
|
case "tailwindcss":
|
|
errMsg = ". You need to install TailwindCSS CLI. See https://gohugo.io/functions/css/tailwindcss/"
|
|
case "tocss":
|
|
errMsg = ". Check your Hugo installation; you need the extended version to build SCSS/SASS with transpiler set to 'libsass'."
|
|
case "tocss-dart":
|
|
errMsg = ". You need to install Dart Sass, see https://gohugo.io//functions/css/sass/#dart-sass"
|
|
case "babel":
|
|
errMsg = ". You need to install Babel, see https://gohugo.io/functions/js/babel/"
|
|
|
|
}
|
|
|
|
return fmt.Errorf(msg+errMsg+": %w", err)
|
|
}
|
|
|
|
return fmt.Errorf(msg+": %w", err)
|
|
}
|
|
|
|
bcfg := r.spec.BuildConfig()
|
|
var tryFileCache bool
|
|
if mayBeCachedOnDisk && bcfg.UseResourceCache(nil) {
|
|
tryFileCache = true
|
|
} else {
|
|
err = tr.Transform(tctx)
|
|
if err != nil && err != herrors.ErrFeatureNotAvailable {
|
|
return nil, newErr(err)
|
|
}
|
|
|
|
if mayBeCachedOnDisk {
|
|
tryFileCache = bcfg.UseResourceCache(err)
|
|
}
|
|
if err != nil && !tryFileCache {
|
|
return nil, newErr(err)
|
|
}
|
|
}
|
|
|
|
if tryFileCache {
|
|
f := r.target.tryTransformedFileCache(key, updates)
|
|
if f == nil {
|
|
if err != nil {
|
|
return nil, newErr(err)
|
|
}
|
|
return nil, newErr(fmt.Errorf("resource %q not found in file cache", key))
|
|
}
|
|
transformedContentr = f
|
|
updates.sourceFs = cache.fileCache.Fs
|
|
defer f.Close()
|
|
|
|
// The reader above is all we need.
|
|
break
|
|
}
|
|
|
|
if tctx.OutPath != "" {
|
|
tctx.InPath = tctx.OutPath
|
|
tctx.OutPath = ""
|
|
}
|
|
}
|
|
|
|
if transformedContentr == nil {
|
|
updates.updateFromCtx(tctx)
|
|
}
|
|
|
|
var publishwriters []io.WriteCloser
|
|
|
|
if publish {
|
|
publicw, err := r.target.openPublishFileForWriting(updates.targetPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
publishwriters = append(publishwriters, publicw)
|
|
}
|
|
|
|
if transformedContentr == nil {
|
|
if writeToFileCache {
|
|
// Also write it to the cache
|
|
fi, metaw, err := cache.writeMeta(key, updates.toTransformedResourceMetadata())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updates.sourceFilename = &fi.Name
|
|
updates.sourceFs = cache.fileCache.Fs
|
|
publishwriters = append(publishwriters, metaw)
|
|
}
|
|
|
|
// Any transformations reading from From must also write to To.
|
|
// This means that if the target buffer is empty, we can just reuse
|
|
// the original reader.
|
|
if b, ok := tctx.To.(*bytes.Buffer); ok && b.Len() > 0 {
|
|
transformedContentr = tctx.To.(*bytes.Buffer)
|
|
} else {
|
|
transformedContentr = contentrc
|
|
}
|
|
}
|
|
|
|
// Also write it to memory
|
|
var contentmemw *bytes.Buffer
|
|
|
|
setContent = setContent || !writeToFileCache
|
|
|
|
if setContent {
|
|
contentmemw = bp.GetBuffer()
|
|
defer bp.PutBuffer(contentmemw)
|
|
publishwriters = append(publishwriters, hugio.ToWriteCloser(contentmemw))
|
|
}
|
|
|
|
publishw := hugio.NewMultiWriteCloser(publishwriters...)
|
|
_, err = io.Copy(publishw, transformedContentr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
publishw.Close()
|
|
|
|
if setContent {
|
|
s := contentmemw.String()
|
|
updates.content = &s
|
|
}
|
|
|
|
newTarget, err := r.target.cloneWithUpdates(updates)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.target = newTarget
|
|
|
|
return r.resourceAdapterInner, nil
|
|
}
|
|
|
|
func (r *resourceAdapter) init(publish, setContent bool) {
|
|
if err := r.doInit(publish, setContent); err != nil {
|
|
// The panic will be handled and converted to an error by the template framework.
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func (r *resourceAdapter) doInit(publish, setContent bool) error {
|
|
r.transformationsInit.Do(func() {
|
|
if len(r.transformations) == 0 {
|
|
// Nothing to do.
|
|
return
|
|
}
|
|
|
|
if publish {
|
|
// The transformation will write the content directly to
|
|
// the destination.
|
|
r.publishOnce = nil
|
|
}
|
|
|
|
r.transformationsErr = r.getOrTransform(publish, setContent)
|
|
})
|
|
|
|
if publish && r.publishOnce != nil {
|
|
r.publish()
|
|
}
|
|
|
|
return r.transformationsErr
|
|
}
|
|
|
|
type resourceAdapterInner struct {
|
|
// The context that started this transformation.
|
|
ctx context.Context
|
|
|
|
target transformableResource
|
|
|
|
resource.Staler
|
|
|
|
spec *Spec
|
|
|
|
// Handles publishing (to /public) if needed.
|
|
*publishOnce
|
|
}
|
|
|
|
func (r *resourceAdapterInner) GetIdentityGroup() identity.Identity {
|
|
return r.target.GetIdentityGroup()
|
|
}
|
|
|
|
func (r *resourceAdapterInner) StaleVersion() uint32 {
|
|
// Both of these are incremented on change.
|
|
return r.Staler.StaleVersion() + r.target.StaleVersion()
|
|
}
|
|
|
|
type resourceTransformations struct {
|
|
transformationsInit sync.Once
|
|
transformationsErr error
|
|
transformations []ResourceTransformation
|
|
}
|
|
|
|
// hasTransformationPermalinkHash reports whether any of the transformations
|
|
// in the chain creates a permalink that's based on the content, e.g. fingerprint.
|
|
func (r *resourceTransformations) hasTransformationPermalinkHash() bool {
|
|
for _, t := range r.transformations {
|
|
if constants.IsResourceTransformationPermalinkHash(t.Key().Name) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
type transformableResource interface {
|
|
baseResourceInternal
|
|
|
|
resource.ContentProvider
|
|
resource.Resource
|
|
resource.Identifier
|
|
resource.Staler
|
|
resourceCopier
|
|
}
|
|
|
|
type transformationUpdate struct {
|
|
content *string
|
|
sourceFilename *string
|
|
sourceFs afero.Fs
|
|
targetPath string
|
|
mediaType media.Type
|
|
data map[string]any
|
|
|
|
startCtx ResourceTransformationCtx
|
|
}
|
|
|
|
func (u *transformationUpdate) isContentChanged() bool {
|
|
return u.content != nil || u.sourceFilename != nil
|
|
}
|
|
|
|
func (u *transformationUpdate) toTransformedResourceMetadata() transformedResourceMetadata {
|
|
return transformedResourceMetadata{
|
|
MediaTypeV: u.mediaType.Type,
|
|
Target: u.targetPath,
|
|
MetaData: u.data,
|
|
}
|
|
}
|
|
|
|
func (u *transformationUpdate) updateFromCtx(ctx *ResourceTransformationCtx) {
|
|
u.targetPath = ctx.OutPath
|
|
u.mediaType = ctx.OutMediaType
|
|
u.data = ctx.Data
|
|
u.targetPath = ctx.InPath
|
|
}
|
|
|
|
// We will persist this information to disk.
|
|
type transformedResourceMetadata struct {
|
|
Target string `json:"Target"`
|
|
MediaTypeV string `json:"MediaType"`
|
|
MetaData map[string]any `json:"Data"`
|
|
}
|
|
|
|
// contentReadSeekerCloser returns a ReadSeekerCloser if possible for a given Resource.
|
|
func contentReadSeekerCloser(r resource.Resource) (hugio.ReadSeekCloser, error) {
|
|
switch rr := r.(type) {
|
|
case resource.ReadSeekCloserResource:
|
|
rc, err := rr.ReadSeekCloser()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return rc, nil
|
|
default:
|
|
return nil, fmt.Errorf("cannot transform content of Resource of type %T", r)
|
|
|
|
}
|
|
}
|