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 }