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