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