URI: 
       transform.go - hugo - [fork] hugo port for 9front
  HTML git clone https://git.drkhsh.at/hugo.git
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
   DIR README
   DIR LICENSE
       ---
       transform.go (21097B)
       ---
            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         "context"
           19         "fmt"
           20         "image"
           21         "io"
           22         "path"
           23         "strings"
           24         "sync"
           25 
           26         "github.com/gohugoio/hugo/common/constants"
           27         "github.com/gohugoio/hugo/common/hashing"
           28         "github.com/gohugoio/hugo/common/paths"
           29         "github.com/gohugoio/hugo/identity"
           30 
           31         "github.com/gohugoio/hugo/resources/images"
           32         "github.com/gohugoio/hugo/resources/images/exif"
           33         "github.com/spf13/afero"
           34 
           35         bp "github.com/gohugoio/hugo/bufferpool"
           36 
           37         "github.com/gohugoio/hugo/common/herrors"
           38         "github.com/gohugoio/hugo/common/hugio"
           39         "github.com/gohugoio/hugo/common/maps"
           40         "github.com/gohugoio/hugo/resources/internal"
           41         "github.com/gohugoio/hugo/resources/resource"
           42 
           43         "github.com/gohugoio/hugo/media"
           44 )
           45 
           46 var (
           47         _ resource.ContentResource           = (*resourceAdapter)(nil)
           48         _ resourceCopier                     = (*resourceAdapter)(nil)
           49         _ resource.ReadSeekCloserResource    = (*resourceAdapter)(nil)
           50         _ resource.Resource                  = (*resourceAdapter)(nil)
           51         _ resource.Staler                    = (*resourceAdapterInner)(nil)
           52         _ identity.IdentityGroupProvider     = (*resourceAdapterInner)(nil)
           53         _ resource.Source                    = (*resourceAdapter)(nil)
           54         _ resource.Identifier                = (*resourceAdapter)(nil)
           55         _ resource.TransientIdentifier       = (*resourceAdapter)(nil)
           56         _ targetPathProvider                 = (*resourceAdapter)(nil)
           57         _ sourcePathProvider                 = (*resourceAdapter)(nil)
           58         _ resource.Identifier                = (*resourceAdapter)(nil)
           59         _ resource.ResourceNameTitleProvider = (*resourceAdapter)(nil)
           60         _ resource.WithResourceMetaProvider  = (*resourceAdapter)(nil)
           61         _ identity.DependencyManagerProvider = (*resourceAdapter)(nil)
           62         _ identity.IdentityGroupProvider     = (*resourceAdapter)(nil)
           63         _ resource.NameNormalizedProvider    = (*resourceAdapter)(nil)
           64         _ isPublishedProvider                = (*resourceAdapter)(nil)
           65 )
           66 
           67 // These are transformations that need special support in Hugo that may not
           68 // be available when building the theme/site so we write the transformation
           69 // result to disk and reuse if needed for these,
           70 // TODO(bep) it's a little fragile having these constants redefined here.
           71 var transformationsToCacheOnDisk = map[string]bool{
           72         "postcss":    true,
           73         "tocss":      true,
           74         "tocss-dart": true,
           75 }
           76 
           77 func newResourceAdapter(spec *Spec, lazyPublish bool, target transformableResource) *resourceAdapter {
           78         var po *publishOnce
           79         if lazyPublish {
           80                 po = &publishOnce{}
           81         }
           82         return &resourceAdapter{
           83                 resourceTransformations: &resourceTransformations{},
           84                 metaProvider:            target,
           85                 resourceAdapterInner: &resourceAdapterInner{
           86                         ctx:         context.Background(),
           87                         spec:        spec,
           88                         publishOnce: po,
           89                         target:      target,
           90                         Staler:      &AtomicStaler{},
           91                 },
           92         }
           93 }
           94 
           95 // ResourceTransformation is the interface that a resource transformation step
           96 // needs to implement.
           97 type ResourceTransformation interface {
           98         Key() internal.ResourceTransformationKey
           99         Transform(ctx *ResourceTransformationCtx) error
          100 }
          101 
          102 type ResourceTransformationCtx struct {
          103         // The context that started the transformation.
          104         Ctx context.Context
          105 
          106         // The dependency manager to use for dependency tracking.
          107         DependencyManager identity.Manager
          108 
          109         // The content to transform.
          110         From io.Reader
          111 
          112         // The target of content transformation.
          113         // The current implementation requires that r is written to w
          114         // even if no transformation is performed.
          115         To io.Writer
          116 
          117         // This is the relative path to the original source. Unix styled slashes.
          118         SourcePath string
          119 
          120         // This is the relative target path to the resource. Unix styled slashes.
          121         InPath string
          122 
          123         // The relative target path to the transformed resource. Unix styled slashes.
          124         OutPath string
          125 
          126         // The input media type
          127         InMediaType media.Type
          128 
          129         // The media type of the transformed resource.
          130         OutMediaType media.Type
          131 
          132         // Data data can be set on the transformed Resource. Not that this need
          133         // to be simple types, as it needs to be serialized to JSON and back.
          134         Data map[string]any
          135 
          136         // This is used to publish additional artifacts, e.g. source maps.
          137         // We may improve this.
          138         OpenResourcePublisher func(relTargetPath string) (io.WriteCloser, error)
          139 }
          140 
          141 // AddOutPathIdentifier transforming InPath to OutPath adding an identifier,
          142 // eg '.min' before any extension.
          143 func (ctx *ResourceTransformationCtx) AddOutPathIdentifier(identifier string) {
          144         ctx.OutPath = ctx.addPathIdentifier(ctx.InPath, identifier)
          145 }
          146 
          147 // PublishSourceMap writes the content to the target folder of the main resource
          148 // with the ".map" extension added.
          149 func (ctx *ResourceTransformationCtx) PublishSourceMap(content string) error {
          150         target := ctx.OutPath + ".map"
          151         f, err := ctx.OpenResourcePublisher(target)
          152         if err != nil {
          153                 return err
          154         }
          155         defer f.Close()
          156         _, err = f.Write([]byte(content))
          157         return err
          158 }
          159 
          160 // ReplaceOutPathExtension transforming InPath to OutPath replacing the file
          161 // extension, e.g. ".scss"
          162 func (ctx *ResourceTransformationCtx) ReplaceOutPathExtension(newExt string) {
          163         dir, file := path.Split(ctx.InPath)
          164         base, _ := paths.PathAndExt(file)
          165         ctx.OutPath = path.Join(dir, (base + newExt))
          166 }
          167 
          168 func (ctx *ResourceTransformationCtx) addPathIdentifier(inPath, identifier string) string {
          169         dir, file := path.Split(inPath)
          170         base, ext := paths.PathAndExt(file)
          171         return path.Join(dir, (base + identifier + ext))
          172 }
          173 
          174 type publishOnce struct {
          175         publisherInit sync.Once
          176         publisherErr  error
          177 }
          178 
          179 type resourceAdapter struct {
          180         commonResource
          181         *resourceTransformations
          182         *resourceAdapterInner
          183         metaProvider resource.ResourceMetaProvider
          184 }
          185 
          186 var _ identity.ForEeachIdentityByNameProvider = (*resourceAdapter)(nil)
          187 
          188 func (r *resourceAdapter) Content(ctx context.Context) (any, error) {
          189         r.init(false, true)
          190         if r.transformationsErr != nil {
          191                 return nil, r.transformationsErr
          192         }
          193         return r.target.Content(ctx)
          194 }
          195 
          196 func (r *resourceAdapter) GetIdentity() identity.Identity {
          197         return identity.FirstIdentity(r.target)
          198 }
          199 
          200 func (r *resourceAdapter) Data() any {
          201         r.init(false, false)
          202         return r.target.Data()
          203 }
          204 
          205 func (r *resourceAdapter) ForEeachIdentityByName(name string, f func(identity.Identity) bool) {
          206         if constants.IsFieldRelOrPermalink(name) && !r.resourceTransformations.hasTransformationPermalinkHash() {
          207                 // Special case for links without any content hash in the URL.
          208                 // We don't need to rebuild all pages that use this resource,
          209                 // but we want to make sure that the resource is accessed at least once.
          210                 f(identity.NewFindFirstManagerIdentityProvider(r.target.GetDependencyManager(), r.target.GetIdentityGroup()))
          211                 return
          212         }
          213         f(r.target.GetIdentityGroup())
          214         f(r.target.GetDependencyManager())
          215 }
          216 
          217 func (r *resourceAdapter) GetIdentityGroup() identity.Identity {
          218         return r.target.GetIdentityGroup()
          219 }
          220 
          221 func (r *resourceAdapter) GetDependencyManager() identity.Manager {
          222         return r.target.GetDependencyManager()
          223 }
          224 
          225 func (r resourceAdapter) cloneTo(targetPath string) resource.Resource {
          226         newtTarget := r.target.cloneTo(targetPath)
          227         newInner := &resourceAdapterInner{
          228                 ctx:    r.ctx,
          229                 spec:   r.spec,
          230                 Staler: r.Staler,
          231                 target: newtTarget.(transformableResource),
          232         }
          233         if r.resourceAdapterInner.publishOnce != nil {
          234                 newInner.publishOnce = &publishOnce{}
          235         }
          236         r.resourceAdapterInner = newInner
          237         return &r
          238 }
          239 
          240 func (r *resourceAdapter) Process(spec string) (images.ImageResource, error) {
          241         return r.getImageOps().Process(spec)
          242 }
          243 
          244 func (r *resourceAdapter) Crop(spec string) (images.ImageResource, error) {
          245         return r.getImageOps().Crop(spec)
          246 }
          247 
          248 func (r *resourceAdapter) Fill(spec string) (images.ImageResource, error) {
          249         return r.getImageOps().Fill(spec)
          250 }
          251 
          252 func (r *resourceAdapter) Fit(spec string) (images.ImageResource, error) {
          253         return r.getImageOps().Fit(spec)
          254 }
          255 
          256 func (r *resourceAdapter) Filter(filters ...any) (images.ImageResource, error) {
          257         return r.getImageOps().Filter(filters...)
          258 }
          259 
          260 func (r *resourceAdapter) Resize(spec string) (images.ImageResource, error) {
          261         return r.getImageOps().Resize(spec)
          262 }
          263 
          264 func (r *resourceAdapter) Height() int {
          265         return r.getImageOps().Height()
          266 }
          267 
          268 func (r *resourceAdapter) Exif() *exif.ExifInfo {
          269         return r.getImageOps().Exif()
          270 }
          271 
          272 func (r *resourceAdapter) Colors() ([]images.Color, error) {
          273         return r.getImageOps().Colors()
          274 }
          275 
          276 func (r *resourceAdapter) Key() string {
          277         r.init(false, false)
          278         return r.target.(resource.Identifier).Key()
          279 }
          280 
          281 func (r *resourceAdapter) TransientKey() string {
          282         return r.Key()
          283 }
          284 
          285 func (r *resourceAdapter) targetPath() string {
          286         r.init(false, false)
          287         return r.target.(targetPathProvider).targetPath()
          288 }
          289 
          290 func (r *resourceAdapter) sourcePath() string {
          291         r.init(false, false)
          292         if sp, ok := r.target.(sourcePathProvider); ok {
          293                 return sp.sourcePath()
          294         }
          295         return ""
          296 }
          297 
          298 func (r *resourceAdapter) MediaType() media.Type {
          299         r.init(false, false)
          300         return r.target.MediaType()
          301 }
          302 
          303 func (r *resourceAdapter) Name() string {
          304         r.init(false, false)
          305         return r.metaProvider.Name()
          306 }
          307 
          308 func (r *resourceAdapter) NameNormalized() string {
          309         r.init(false, false)
          310         return r.target.(resource.NameNormalizedProvider).NameNormalized()
          311 }
          312 
          313 func (r *resourceAdapter) Params() maps.Params {
          314         r.init(false, false)
          315         return r.metaProvider.Params()
          316 }
          317 
          318 func (r *resourceAdapter) Permalink() string {
          319         r.init(true, false)
          320         return r.target.Permalink()
          321 }
          322 
          323 func (r *resourceAdapter) Publish() error {
          324         r.init(false, false)
          325 
          326         return r.target.Publish()
          327 }
          328 
          329 func (r *resourceAdapter) isPublished() bool {
          330         r.init(false, false)
          331         return r.target.isPublished()
          332 }
          333 
          334 func (r *resourceAdapter) ReadSeekCloser() (hugio.ReadSeekCloser, error) {
          335         r.init(false, false)
          336         return r.target.ReadSeekCloser()
          337 }
          338 
          339 func (r *resourceAdapter) RelPermalink() string {
          340         r.init(true, false)
          341         return r.target.RelPermalink()
          342 }
          343 
          344 func (r *resourceAdapter) ResourceType() string {
          345         r.init(false, false)
          346         return r.target.ResourceType()
          347 }
          348 
          349 func (r *resourceAdapter) String() string {
          350         return r.Name()
          351 }
          352 
          353 func (r *resourceAdapter) Title() string {
          354         r.init(false, false)
          355         return r.metaProvider.Title()
          356 }
          357 
          358 func (r resourceAdapter) Transform(t ...ResourceTransformation) (ResourceTransformer, error) {
          359         return r.TransformWithContext(context.Background(), t...)
          360 }
          361 
          362 func (r resourceAdapter) TransformWithContext(ctx context.Context, t ...ResourceTransformation) (ResourceTransformer, error) {
          363         r.resourceTransformations = &resourceTransformations{
          364                 transformations: append(r.transformations, t...),
          365         }
          366 
          367         r.resourceAdapterInner = &resourceAdapterInner{
          368                 ctx:         ctx,
          369                 spec:        r.spec,
          370                 Staler:      r.Staler,
          371                 publishOnce: &publishOnce{},
          372                 target:      r.target,
          373         }
          374 
          375         return &r, nil
          376 }
          377 
          378 func (r *resourceAdapter) Width() int {
          379         return r.getImageOps().Width()
          380 }
          381 
          382 func (r *resourceAdapter) DecodeImage() (image.Image, error) {
          383         return r.getImageOps().DecodeImage()
          384 }
          385 
          386 func (r resourceAdapter) WithResourceMeta(mp resource.ResourceMetaProvider) resource.Resource {
          387         r.metaProvider = mp
          388         return &r
          389 }
          390 
          391 func (r *resourceAdapter) getImageOps() images.ImageResourceOps {
          392         img, ok := r.target.(images.ImageResourceOps)
          393         if !ok {
          394                 if r.MediaType().SubType == "svg" {
          395                         panic("this method is only available for raster images. To determine if an image is SVG, you can do {{ if eq .MediaType.SubType \"svg\" }}{{ end }}")
          396                 }
          397                 panic("this method is only available for image resources")
          398         }
          399         r.init(false, false)
          400         return img
          401 }
          402 
          403 func (r *resourceAdapter) publish() {
          404         if r.publishOnce == nil {
          405                 return
          406         }
          407 
          408         r.publisherInit.Do(func() {
          409                 r.publisherErr = r.target.Publish()
          410 
          411                 if r.publisherErr != nil {
          412                         r.spec.Logger.Errorf("Failed to publish Resource: %s", r.publisherErr)
          413                 }
          414         })
          415 }
          416 
          417 func (r *resourceAdapter) TransformationKey() string {
          418         var key string
          419         for _, tr := range r.transformations {
          420                 key = key + "_" + tr.Key().Value()
          421         }
          422         return r.spec.ResourceCache.cleanKey(r.target.Key()) + "_" + hashing.MD5FromStringHexEncoded(key)
          423 }
          424 
          425 func (r *resourceAdapter) getOrTransform(publish, setContent bool) error {
          426         key := r.TransformationKey()
          427         res, err := r.spec.ResourceCache.cacheResourceTransformation.GetOrCreate(key, func(string) (*resourceAdapterInner, error) {
          428                 return r.transform(key, publish, setContent)
          429         })
          430         if err != nil {
          431                 return err
          432         }
          433 
          434         r.resourceAdapterInner = res
          435         return nil
          436 }
          437 
          438 func (r *resourceAdapter) transform(key string, publish, setContent bool) (*resourceAdapterInner, error) {
          439         cache := r.spec.ResourceCache
          440 
          441         b1 := bp.GetBuffer()
          442         b2 := bp.GetBuffer()
          443         defer bp.PutBuffer(b1)
          444         defer bp.PutBuffer(b2)
          445 
          446         tctx := &ResourceTransformationCtx{
          447                 Ctx:                   r.ctx,
          448                 Data:                  make(map[string]any),
          449                 OpenResourcePublisher: r.target.openPublishFileForWriting,
          450                 DependencyManager:     r.target.GetDependencyManager(),
          451         }
          452 
          453         tctx.InMediaType = r.target.MediaType()
          454         tctx.OutMediaType = r.target.MediaType()
          455 
          456         startCtx := *tctx
          457         updates := &transformationUpdate{startCtx: startCtx}
          458 
          459         var contentrc hugio.ReadSeekCloser
          460 
          461         contentrc, err := contentReadSeekerCloser(r.target)
          462         if err != nil {
          463                 return nil, err
          464         }
          465 
          466         defer contentrc.Close()
          467 
          468         tctx.From = contentrc
          469         tctx.To = b1
          470 
          471         tctx.InPath = r.target.TargetPath()
          472         tctx.SourcePath = strings.TrimPrefix(tctx.InPath, "/")
          473 
          474         counter := 0
          475         writeToFileCache := false
          476 
          477         var transformedContentr io.Reader
          478 
          479         for i, tr := range r.transformations {
          480                 if i != 0 {
          481                         tctx.InMediaType = tctx.OutMediaType
          482                 }
          483 
          484                 mayBeCachedOnDisk := transformationsToCacheOnDisk[tr.Key().Name]
          485                 if !writeToFileCache {
          486                         writeToFileCache = mayBeCachedOnDisk
          487                 }
          488 
          489                 if i > 0 {
          490                         hasWrites := tctx.To.(*bytes.Buffer).Len() > 0
          491                         if hasWrites {
          492                                 counter++
          493                                 // Switch the buffers
          494                                 if counter%2 == 0 {
          495                                         tctx.From = b2
          496                                         b1.Reset()
          497                                         tctx.To = b1
          498                                 } else {
          499                                         tctx.From = b1
          500                                         b2.Reset()
          501                                         tctx.To = b2
          502                                 }
          503                         }
          504                 }
          505 
          506                 newErr := func(err error) error {
          507                         msg := fmt.Sprintf("%s: failed to transform %q (%s)", strings.ToUpper(tr.Key().Name), tctx.InPath, tctx.InMediaType.Type)
          508 
          509                         if herrors.IsFeatureNotAvailableError(err) {
          510                                 var errMsg string
          511                                 switch strings.ToLower(tr.Key().Name) {
          512                                 case "postcss":
          513                                         // This transformation is not available in this
          514                                         // Most likely because PostCSS is not installed.
          515                                         errMsg = ". You need to install PostCSS. See https://gohugo.io/functions/css/postcss/"
          516                                 case "tailwindcss":
          517                                         errMsg = ". You need to install TailwindCSS CLI. See https://gohugo.io/functions/css/tailwindcss/"
          518                                 case "tocss":
          519                                         errMsg = ". Check your Hugo installation; you need the extended version to build SCSS/SASS with transpiler set to 'libsass'."
          520                                 case "tocss-dart":
          521                                         errMsg = ". You need to install Dart Sass, see https://gohugo.io//functions/css/sass/#dart-sass"
          522                                 case "babel":
          523                                         errMsg = ". You need to install Babel, see https://gohugo.io/functions/js/babel/"
          524 
          525                                 }
          526 
          527                                 return fmt.Errorf(msg+errMsg+": %w", err)
          528                         }
          529 
          530                         return fmt.Errorf(msg+": %w", err)
          531                 }
          532 
          533                 bcfg := r.spec.BuildConfig()
          534                 var tryFileCache bool
          535                 if mayBeCachedOnDisk && bcfg.UseResourceCache(nil) {
          536                         tryFileCache = true
          537                 } else {
          538                         err = tr.Transform(tctx)
          539                         if err != nil && err != herrors.ErrFeatureNotAvailable {
          540                                 return nil, newErr(err)
          541                         }
          542 
          543                         if mayBeCachedOnDisk {
          544                                 tryFileCache = bcfg.UseResourceCache(err)
          545                         }
          546                         if err != nil && !tryFileCache {
          547                                 return nil, newErr(err)
          548                         }
          549                 }
          550 
          551                 if tryFileCache {
          552                         f := r.target.tryTransformedFileCache(key, updates)
          553                         if f == nil {
          554                                 if err != nil {
          555                                         return nil, newErr(err)
          556                                 }
          557                                 return nil, newErr(fmt.Errorf("resource %q not found in file cache", key))
          558                         }
          559                         transformedContentr = f
          560                         updates.sourceFs = cache.fileCache.Fs
          561                         defer f.Close()
          562 
          563                         // The reader above is all we need.
          564                         break
          565                 }
          566 
          567                 if tctx.OutPath != "" {
          568                         tctx.InPath = tctx.OutPath
          569                         tctx.OutPath = ""
          570                 }
          571         }
          572 
          573         if transformedContentr == nil {
          574                 updates.updateFromCtx(tctx)
          575         }
          576 
          577         var publishwriters []io.WriteCloser
          578 
          579         if publish {
          580                 publicw, err := r.target.openPublishFileForWriting(updates.targetPath)
          581                 if err != nil {
          582                         return nil, err
          583                 }
          584                 publishwriters = append(publishwriters, publicw)
          585         }
          586 
          587         if transformedContentr == nil {
          588                 if writeToFileCache {
          589                         // Also write it to the cache
          590                         fi, metaw, err := cache.writeMeta(key, updates.toTransformedResourceMetadata())
          591                         if err != nil {
          592                                 return nil, err
          593                         }
          594                         updates.sourceFilename = &fi.Name
          595                         updates.sourceFs = cache.fileCache.Fs
          596                         publishwriters = append(publishwriters, metaw)
          597                 }
          598 
          599                 // Any transformations reading from From must also write to To.
          600                 // This means that if the target buffer is empty, we can just reuse
          601                 // the original reader.
          602                 if b, ok := tctx.To.(*bytes.Buffer); ok && b.Len() > 0 {
          603                         transformedContentr = tctx.To.(*bytes.Buffer)
          604                 } else {
          605                         transformedContentr = contentrc
          606                 }
          607         }
          608 
          609         // Also write it to memory
          610         var contentmemw *bytes.Buffer
          611 
          612         setContent = setContent || !writeToFileCache
          613 
          614         if setContent {
          615                 contentmemw = bp.GetBuffer()
          616                 defer bp.PutBuffer(contentmemw)
          617                 publishwriters = append(publishwriters, hugio.ToWriteCloser(contentmemw))
          618         }
          619 
          620         publishw := hugio.NewMultiWriteCloser(publishwriters...)
          621         _, err = io.Copy(publishw, transformedContentr)
          622         if err != nil {
          623                 return nil, err
          624         }
          625         publishw.Close()
          626 
          627         if setContent {
          628                 s := contentmemw.String()
          629                 updates.content = &s
          630         }
          631 
          632         newTarget, err := r.target.cloneWithUpdates(updates)
          633         if err != nil {
          634                 return nil, err
          635         }
          636         r.target = newTarget
          637 
          638         return r.resourceAdapterInner, nil
          639 }
          640 
          641 func (r *resourceAdapter) init(publish, setContent bool) {
          642         r.initTransform(publish, setContent)
          643 }
          644 
          645 func (r *resourceAdapter) initTransform(publish, setContent bool) {
          646         r.transformationsInit.Do(func() {
          647                 if len(r.transformations) == 0 {
          648                         // Nothing to do.
          649                         return
          650                 }
          651 
          652                 if publish {
          653                         // The transformation will write the content directly to
          654                         // the destination.
          655                         r.publishOnce = nil
          656                 }
          657 
          658                 r.transformationsErr = r.getOrTransform(publish, setContent)
          659                 if r.transformationsErr != nil {
          660                         if r.spec.ErrorSender != nil {
          661                                 r.spec.ErrorSender.SendError(r.transformationsErr)
          662                         } else {
          663                                 r.spec.Logger.Errorf("Transformation failed: %s", r.transformationsErr)
          664                         }
          665                 }
          666         })
          667 
          668         if publish && r.publishOnce != nil {
          669                 r.publish()
          670         }
          671 }
          672 
          673 type resourceAdapterInner struct {
          674         // The context that started this transformation.
          675         ctx context.Context
          676 
          677         target transformableResource
          678 
          679         resource.Staler
          680 
          681         spec *Spec
          682 
          683         // Handles publishing (to /public) if needed.
          684         *publishOnce
          685 }
          686 
          687 func (r *resourceAdapterInner) GetIdentityGroup() identity.Identity {
          688         return r.target.GetIdentityGroup()
          689 }
          690 
          691 func (r *resourceAdapterInner) StaleVersion() uint32 {
          692         // Both of these are incremented on change.
          693         return r.Staler.StaleVersion() + r.target.StaleVersion()
          694 }
          695 
          696 type resourceTransformations struct {
          697         transformationsInit sync.Once
          698         transformationsErr  error
          699         transformations     []ResourceTransformation
          700 }
          701 
          702 // hasTransformationPermalinkHash reports whether any of the transformations
          703 // in the chain creates a permalink that's based on the content, e.g. fingerprint.
          704 func (r *resourceTransformations) hasTransformationPermalinkHash() bool {
          705         for _, t := range r.transformations {
          706                 if constants.IsResourceTransformationPermalinkHash(t.Key().Name) {
          707                         return true
          708                 }
          709         }
          710         return false
          711 }
          712 
          713 type transformableResource interface {
          714         baseResourceInternal
          715 
          716         resource.ContentProvider
          717         resource.Resource
          718         resource.Identifier
          719         resource.Staler
          720         resourceCopier
          721 }
          722 
          723 type transformationUpdate struct {
          724         content        *string
          725         sourceFilename *string
          726         sourceFs       afero.Fs
          727         targetPath     string
          728         mediaType      media.Type
          729         data           map[string]any
          730 
          731         startCtx ResourceTransformationCtx
          732 }
          733 
          734 func (u *transformationUpdate) isContentChanged() bool {
          735         return u.content != nil || u.sourceFilename != nil
          736 }
          737 
          738 func (u *transformationUpdate) toTransformedResourceMetadata() transformedResourceMetadata {
          739         return transformedResourceMetadata{
          740                 MediaTypeV: u.mediaType.Type,
          741                 Target:     u.targetPath,
          742                 MetaData:   u.data,
          743         }
          744 }
          745 
          746 func (u *transformationUpdate) updateFromCtx(ctx *ResourceTransformationCtx) {
          747         u.targetPath = ctx.OutPath
          748         u.mediaType = ctx.OutMediaType
          749         u.data = ctx.Data
          750         u.targetPath = ctx.InPath
          751 }
          752 
          753 // We will persist this information to disk.
          754 type transformedResourceMetadata struct {
          755         Target     string         `json:"Target"`
          756         MediaTypeV string         `json:"MediaType"`
          757         MetaData   map[string]any `json:"Data"`
          758 }
          759 
          760 // contentReadSeekerCloser returns a ReadSeekerCloser if possible for a given Resource.
          761 func contentReadSeekerCloser(r resource.Resource) (hugio.ReadSeekCloser, error) {
          762         switch rr := r.(type) {
          763         case resource.ReadSeekCloserResource:
          764                 rc, err := rr.ReadSeekCloser()
          765                 if err != nil {
          766                         return nil, err
          767                 }
          768                 return rc, nil
          769         default:
          770                 return nil, fmt.Errorf("cannot transform content of Resource of type %T", r)
          771 
          772         }
          773 }