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 }