github.com/dominikszabo/hugo-ds-clean@v0.47.1/resource/resource.go (about) 1 // Copyright 2018 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 resource 15 16 import ( 17 "errors" 18 "fmt" 19 "io" 20 "io/ioutil" 21 "mime" 22 "os" 23 "path" 24 "path/filepath" 25 "strings" 26 "sync" 27 28 "github.com/gohugoio/hugo/output" 29 "github.com/gohugoio/hugo/tpl" 30 31 "github.com/gohugoio/hugo/common/loggers" 32 33 jww "github.com/spf13/jwalterweatherman" 34 35 "github.com/spf13/afero" 36 37 "github.com/gobwas/glob" 38 "github.com/gohugoio/hugo/helpers" 39 "github.com/gohugoio/hugo/media" 40 "github.com/gohugoio/hugo/source" 41 ) 42 43 var ( 44 _ ContentResource = (*genericResource)(nil) 45 _ ReadSeekCloserResource = (*genericResource)(nil) 46 _ Resource = (*genericResource)(nil) 47 _ Source = (*genericResource)(nil) 48 _ Cloner = (*genericResource)(nil) 49 _ ResourcesLanguageMerger = (*Resources)(nil) 50 _ permalinker = (*genericResource)(nil) 51 ) 52 53 const DefaultResourceType = "unknown" 54 55 var noData = make(map[string]interface{}) 56 57 // Source is an internal template and not meant for use in the templates. It 58 // may change without notice. 59 type Source interface { 60 Publish() error 61 } 62 63 type permalinker interface { 64 relPermalinkFor(target string) string 65 permalinkFor(target string) string 66 relTargetPathsFor(target string) []string 67 relTargetPaths() []string 68 targetPath() string 69 } 70 71 // Cloner is an internal template and not meant for use in the templates. It 72 // may change without notice. 73 type Cloner interface { 74 WithNewBase(base string) Resource 75 } 76 77 // Resource represents a linkable resource, i.e. a content page, image etc. 78 type Resource interface { 79 // Permalink represents the absolute link to this resource. 80 Permalink() string 81 82 // RelPermalink represents the host relative link to this resource. 83 RelPermalink() string 84 85 // ResourceType is the resource type. For most file types, this is the main 86 // part of the MIME type, e.g. "image", "application", "text" etc. 87 // For content pages, this value is "page". 88 ResourceType() string 89 90 // MediaType is this resource's MIME type. 91 MediaType() media.Type 92 93 // Name is the logical name of this resource. This can be set in the front matter 94 // metadata for this resource. If not set, Hugo will assign a value. 95 // This will in most cases be the base filename. 96 // So, for the image "/some/path/sunset.jpg" this will be "sunset.jpg". 97 // The value returned by this method will be used in the GetByPrefix and ByPrefix methods 98 // on Resources. 99 Name() string 100 101 // Title returns the title if set in front matter. For content pages, this will be the expected value. 102 Title() string 103 104 // Resource specific data set by Hugo. 105 // One example would be.Data.Digest for fingerprinted resources. 106 Data() interface{} 107 108 // Params set in front matter for this resource. 109 Params() map[string]interface{} 110 } 111 112 type ResourcesLanguageMerger interface { 113 MergeByLanguage(other Resources) Resources 114 // Needed for integration with the tpl package. 115 MergeByLanguageInterface(other interface{}) (interface{}, error) 116 } 117 118 type translatedResource interface { 119 TranslationKey() string 120 } 121 122 // ContentResource represents a Resource that provides a way to get to its content. 123 // Most Resource types in Hugo implements this interface, including Page. 124 // This should be used with care, as it will read the file content into memory, but it 125 // should be cached as effectively as possible by the implementation. 126 type ContentResource interface { 127 Resource 128 129 // Content returns this resource's content. It will be equivalent to reading the content 130 // that RelPermalink points to in the published folder. 131 // The return type will be contextual, and should be what you would expect: 132 // * Page: template.HTML 133 // * JSON: String 134 // * Etc. 135 Content() (interface{}, error) 136 } 137 138 // OpenReadSeekeCloser allows setting some other way (than reading from a filesystem) 139 // to open or create a ReadSeekCloser. 140 type OpenReadSeekCloser func() (ReadSeekCloser, error) 141 142 // ReadSeekCloserResource is a Resource that supports loading its content. 143 type ReadSeekCloserResource interface { 144 Resource 145 ReadSeekCloser() (ReadSeekCloser, error) 146 } 147 148 // Resources represents a slice of resources, which can be a mix of different types. 149 // I.e. both pages and images etc. 150 type Resources []Resource 151 152 func (r Resources) ByType(tp string) Resources { 153 var filtered Resources 154 155 for _, resource := range r { 156 if resource.ResourceType() == tp { 157 filtered = append(filtered, resource) 158 } 159 } 160 return filtered 161 } 162 163 // GetMatch finds the first Resource matching the given pattern, or nil if none found. 164 // See Match for a more complete explanation about the rules used. 165 func (r Resources) GetMatch(pattern string) Resource { 166 g, err := getGlob(pattern) 167 if err != nil { 168 return nil 169 } 170 171 for _, resource := range r { 172 if g.Match(strings.ToLower(resource.Name())) { 173 return resource 174 } 175 } 176 177 return nil 178 } 179 180 // Match gets all resources matching the given base filename prefix, e.g 181 // "*.png" will match all png files. The "*" does not match path delimiters (/), 182 // so if you organize your resources in sub-folders, you need to be explicit about it, e.g.: 183 // "images/*.png". To match any PNG image anywhere in the bundle you can do "**.png", and 184 // to match all PNG images below the images folder, use "images/**.jpg". 185 // The matching is case insensitive. 186 // Match matches by using the value of Resource.Name, which, by default, is a filename with 187 // path relative to the bundle root with Unix style slashes (/) and no leading slash, e.g. "images/logo.png". 188 // See https://github.com/gobwas/glob for the full rules set. 189 func (r Resources) Match(pattern string) Resources { 190 g, err := getGlob(pattern) 191 if err != nil { 192 return nil 193 } 194 195 var matches Resources 196 for _, resource := range r { 197 if g.Match(strings.ToLower(resource.Name())) { 198 matches = append(matches, resource) 199 } 200 } 201 return matches 202 } 203 204 var ( 205 globCache = make(map[string]glob.Glob) 206 globMu sync.RWMutex 207 ) 208 209 func getGlob(pattern string) (glob.Glob, error) { 210 var g glob.Glob 211 212 globMu.RLock() 213 g, found := globCache[pattern] 214 globMu.RUnlock() 215 if !found { 216 var err error 217 g, err = glob.Compile(strings.ToLower(pattern), '/') 218 if err != nil { 219 return nil, err 220 } 221 222 globMu.Lock() 223 globCache[pattern] = g 224 globMu.Unlock() 225 } 226 227 return g, nil 228 229 } 230 231 // MergeByLanguage adds missing translations in r1 from r2. 232 func (r1 Resources) MergeByLanguage(r2 Resources) Resources { 233 result := append(Resources(nil), r1...) 234 m := make(map[string]bool) 235 for _, r := range r1 { 236 if translated, ok := r.(translatedResource); ok { 237 m[translated.TranslationKey()] = true 238 } 239 } 240 241 for _, r := range r2 { 242 if translated, ok := r.(translatedResource); ok { 243 if _, found := m[translated.TranslationKey()]; !found { 244 result = append(result, r) 245 } 246 } 247 } 248 return result 249 } 250 251 // MergeByLanguageInterface is the generic version of MergeByLanguage. It 252 // is here just so it can be called from the tpl package. 253 func (r1 Resources) MergeByLanguageInterface(in interface{}) (interface{}, error) { 254 r2, ok := in.(Resources) 255 if !ok { 256 return nil, fmt.Errorf("%T cannot be merged by language", in) 257 } 258 return r1.MergeByLanguage(r2), nil 259 } 260 261 type Spec struct { 262 *helpers.PathSpec 263 264 MediaTypes media.Types 265 OutputFormats output.Formats 266 267 Logger *jww.Notepad 268 269 TextTemplates tpl.TemplateParseFinder 270 271 // Holds default filter settings etc. 272 imaging *Imaging 273 274 imageCache *imageCache 275 ResourceCache *ResourceCache 276 277 GenImagePath string 278 GenAssetsPath string 279 } 280 281 func NewSpec(s *helpers.PathSpec, logger *jww.Notepad, outputFormats output.Formats, mimeTypes media.Types) (*Spec, error) { 282 283 imaging, err := decodeImaging(s.Cfg.GetStringMap("imaging")) 284 if err != nil { 285 return nil, err 286 } 287 288 if logger == nil { 289 logger = loggers.NewErrorLogger() 290 } 291 292 genImagePath := filepath.FromSlash("_gen/images") 293 // The transformed assets (CSS etc.) 294 genAssetsPath := filepath.FromSlash("_gen/assets") 295 296 rs := &Spec{PathSpec: s, 297 Logger: logger, 298 GenImagePath: genImagePath, 299 GenAssetsPath: genAssetsPath, 300 imaging: &imaging, 301 MediaTypes: mimeTypes, 302 OutputFormats: outputFormats, 303 imageCache: newImageCache( 304 s, 305 // We're going to write a cache pruning routine later, so make it extremely 306 // unlikely that the user shoots him or herself in the foot 307 // and this is set to a value that represents data he/she 308 // cares about. This should be set in stone once released. 309 genImagePath, 310 )} 311 312 rs.ResourceCache = newResourceCache(rs) 313 314 return rs, nil 315 316 } 317 318 type ResourceSourceDescriptor struct { 319 // TargetPathBuilder is a callback to create target paths's relative to its owner. 320 TargetPathBuilder func(base string) string 321 322 // Need one of these to load the resource content. 323 SourceFile source.File 324 OpenReadSeekCloser OpenReadSeekCloser 325 326 // If OpenReadSeekerCloser is not set, we use this to open the file. 327 SourceFilename string 328 329 // The relative target filename without any language code. 330 RelTargetFilename string 331 332 // Any base path prepeneded to the permalink. 333 // Typically the language code if this resource should be published to its sub-folder. 334 URLBase string 335 336 // Any base paths prepended to the target path. This will also typically be the 337 // language code, but setting it here means that it should not have any effect on 338 // the permalink. 339 // This may be several values. In multihost mode we may publish the same resources to 340 // multiple targets. 341 TargetBasePaths []string 342 343 // Delay publishing until either Permalink or RelPermalink is called. Maybe never. 344 LazyPublish bool 345 } 346 347 func (r ResourceSourceDescriptor) Filename() string { 348 if r.SourceFile != nil { 349 return r.SourceFile.Filename() 350 } 351 return r.SourceFilename 352 } 353 354 func (r *Spec) sourceFs() afero.Fs { 355 return r.PathSpec.BaseFs.Content.Fs 356 } 357 358 func (r *Spec) New(fd ResourceSourceDescriptor) (Resource, error) { 359 return r.newResourceForFs(r.sourceFs(), fd) 360 } 361 362 func (r *Spec) NewForFs(sourceFs afero.Fs, fd ResourceSourceDescriptor) (Resource, error) { 363 return r.newResourceForFs(sourceFs, fd) 364 } 365 366 func (r *Spec) newResourceForFs(sourceFs afero.Fs, fd ResourceSourceDescriptor) (Resource, error) { 367 if fd.OpenReadSeekCloser == nil { 368 if fd.SourceFile != nil && fd.SourceFilename != "" { 369 return nil, errors.New("both SourceFile and AbsSourceFilename provided") 370 } else if fd.SourceFile == nil && fd.SourceFilename == "" { 371 return nil, errors.New("either SourceFile or AbsSourceFilename must be provided") 372 } 373 } 374 375 if fd.RelTargetFilename == "" { 376 fd.RelTargetFilename = fd.Filename() 377 } 378 379 if len(fd.TargetBasePaths) == 0 { 380 // If not set, we publish the same resource to all hosts. 381 fd.TargetBasePaths = r.MultihostTargetBasePaths 382 } 383 384 return r.newResource(sourceFs, fd) 385 } 386 387 func (r *Spec) newResource(sourceFs afero.Fs, fd ResourceSourceDescriptor) (Resource, error) { 388 var fi os.FileInfo 389 var sourceFilename string 390 391 if fd.OpenReadSeekCloser != nil { 392 393 } else if fd.SourceFilename != "" { 394 var err error 395 fi, err = sourceFs.Stat(fd.SourceFilename) 396 if err != nil { 397 return nil, err 398 } 399 sourceFilename = fd.SourceFilename 400 } else { 401 fi = fd.SourceFile.FileInfo() 402 sourceFilename = fd.SourceFile.Filename() 403 } 404 405 if fd.RelTargetFilename == "" { 406 fd.RelTargetFilename = sourceFilename 407 } 408 409 ext := filepath.Ext(fd.RelTargetFilename) 410 mimeType, found := r.MediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")) 411 // TODO(bep) we need to handle these ambigous types better, but in this context 412 // we most likely want the application/xml type. 413 if mimeType.Suffix() == "xml" && mimeType.SubType == "rss" { 414 mimeType, found = r.MediaTypes.GetByType("application/xml") 415 } 416 417 if !found { 418 mimeStr := mime.TypeByExtension(ext) 419 if mimeStr != "" { 420 mimeType, _ = media.FromStringAndExt(mimeStr, ext) 421 } 422 } 423 424 gr := r.newGenericResourceWithBase( 425 sourceFs, 426 fd.LazyPublish, 427 fd.OpenReadSeekCloser, 428 fd.URLBase, 429 fd.TargetBasePaths, 430 fd.TargetPathBuilder, 431 fi, 432 sourceFilename, 433 fd.RelTargetFilename, 434 mimeType) 435 436 if mimeType.MainType == "image" { 437 ext := strings.ToLower(helpers.Ext(sourceFilename)) 438 439 imgFormat, ok := imageFormats[ext] 440 if !ok { 441 // This allows SVG etc. to be used as resources. They will not have the methods of the Image, but 442 // that would not (currently) have worked. 443 return gr, nil 444 } 445 446 if err := gr.initHash(); err != nil { 447 return nil, err 448 } 449 450 return &Image{ 451 format: imgFormat, 452 imaging: r.imaging, 453 genericResource: gr}, nil 454 } 455 return gr, nil 456 457 } 458 459 // TODO(bep) unify 460 func (r *Spec) IsInImageCache(key string) bool { 461 // This is used for cache pruning. We currently only have images, but we could 462 // imagine expanding on this. 463 return r.imageCache.isInCache(key) 464 } 465 466 func (r *Spec) DeleteCacheByPrefix(prefix string) { 467 r.imageCache.deleteByPrefix(prefix) 468 } 469 470 func (r *Spec) ClearCaches() { 471 r.imageCache.clear() 472 r.ResourceCache.clear() 473 } 474 475 func (r *Spec) CacheStats() string { 476 r.imageCache.mu.RLock() 477 defer r.imageCache.mu.RUnlock() 478 479 s := fmt.Sprintf("Cache entries: %d", len(r.imageCache.store)) 480 481 count := 0 482 for k := range r.imageCache.store { 483 if count > 5 { 484 break 485 } 486 s += "\n" + k 487 count++ 488 } 489 490 return s 491 } 492 493 type dirFile struct { 494 // This is the directory component with Unix-style slashes. 495 dir string 496 // This is the file component. 497 file string 498 } 499 500 func (d dirFile) path() string { 501 return path.Join(d.dir, d.file) 502 } 503 504 type resourcePathDescriptor struct { 505 // The relative target directory and filename. 506 relTargetDirFile dirFile 507 508 // Callback used to construct a target path relative to its owner. 509 targetPathBuilder func(rel string) string 510 511 // baseURLDir is the fixed sub-folder for a resource in permalinks. This will typically 512 // be the language code if we publish to the language's sub-folder. 513 baseURLDir string 514 515 // This will normally be the same as above, but this will only apply to publishing 516 // of resources. It may be mulltiple values when in multihost mode. 517 baseTargetPathDirs []string 518 519 // baseOffset is set when the output format's path has a offset, e.g. for AMP. 520 baseOffset string 521 } 522 523 type resourceContent struct { 524 content string 525 contentInit sync.Once 526 } 527 528 type resourceHash struct { 529 hash string 530 hashInit sync.Once 531 } 532 533 type publishOnce struct { 534 publisherInit sync.Once 535 publisherErr error 536 logger *jww.Notepad 537 } 538 539 func (l *publishOnce) publish(s Source) error { 540 l.publisherInit.Do(func() { 541 l.publisherErr = s.Publish() 542 if l.publisherErr != nil { 543 l.logger.ERROR.Printf("failed to publish Resource: %s", l.publisherErr) 544 } 545 }) 546 return l.publisherErr 547 } 548 549 // genericResource represents a generic linkable resource. 550 type genericResource struct { 551 resourcePathDescriptor 552 553 title string 554 name string 555 params map[string]interface{} 556 557 // Absolute filename to the source, including any content folder path. 558 // Note that this is absolute in relation to the filesystem it is stored in. 559 // It can be a base path filesystem, and then this filename will not match 560 // the path to the file on the real filesystem. 561 sourceFilename string 562 563 // Will be set if this resource is backed by something other than a file. 564 openReadSeekerCloser OpenReadSeekCloser 565 566 // A hash of the source content. Is only calculated in caching situations. 567 *resourceHash 568 569 // This may be set to tell us to look in another filesystem for this resource. 570 // We, by default, use the sourceFs filesystem in the spec below. 571 overriddenSourceFs afero.Fs 572 573 spec *Spec 574 575 resourceType string 576 mediaType media.Type 577 578 osFileInfo os.FileInfo 579 580 // We create copies of this struct, so this needs to be a pointer. 581 *resourceContent 582 583 // May be set to signal lazy/delayed publishing. 584 *publishOnce 585 } 586 587 func (l *genericResource) Data() interface{} { 588 return noData 589 } 590 591 func (l *genericResource) Content() (interface{}, error) { 592 if err := l.initContent(); err != nil { 593 return nil, err 594 } 595 596 return l.content, nil 597 } 598 599 func (l *genericResource) ReadSeekCloser() (ReadSeekCloser, error) { 600 if l.openReadSeekerCloser != nil { 601 return l.openReadSeekerCloser() 602 } 603 f, err := l.sourceFs().Open(l.sourceFilename) 604 if err != nil { 605 return nil, err 606 } 607 return f, nil 608 609 } 610 611 func (l *genericResource) MediaType() media.Type { 612 return l.mediaType 613 } 614 615 // Implement the Cloner interface. 616 func (l genericResource) WithNewBase(base string) Resource { 617 l.baseOffset = base 618 l.resourceContent = &resourceContent{} 619 return &l 620 } 621 622 func (l *genericResource) initHash() error { 623 var err error 624 l.hashInit.Do(func() { 625 var hash string 626 var f ReadSeekCloser 627 f, err = l.ReadSeekCloser() 628 if err != nil { 629 err = fmt.Errorf("failed to open source file: %s", err) 630 return 631 } 632 defer f.Close() 633 634 hash, err = helpers.MD5FromFileFast(f) 635 if err != nil { 636 return 637 } 638 l.hash = hash 639 640 }) 641 642 return err 643 } 644 645 func (l *genericResource) initContent() error { 646 var err error 647 l.contentInit.Do(func() { 648 var r ReadSeekCloser 649 r, err = l.ReadSeekCloser() 650 if err != nil { 651 return 652 } 653 defer r.Close() 654 655 var b []byte 656 b, err = ioutil.ReadAll(r) 657 if err != nil { 658 return 659 } 660 661 l.content = string(b) 662 663 }) 664 665 return err 666 } 667 668 func (l *genericResource) sourceFs() afero.Fs { 669 if l.overriddenSourceFs != nil { 670 return l.overriddenSourceFs 671 } 672 return l.spec.sourceFs() 673 } 674 675 func (l *genericResource) publishIfNeeded() { 676 if l.publishOnce != nil { 677 l.publishOnce.publish(l) 678 } 679 } 680 681 func (l *genericResource) Permalink() string { 682 l.publishIfNeeded() 683 return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(l.relTargetDirFile.path()), l.spec.BaseURL.HostURL()) 684 } 685 686 func (l *genericResource) RelPermalink() string { 687 l.publishIfNeeded() 688 return l.relPermalinkFor(l.relTargetDirFile.path()) 689 } 690 691 func (l *genericResource) relPermalinkFor(target string) string { 692 return l.relPermalinkForRel(target) 693 694 } 695 func (l *genericResource) permalinkFor(target string) string { 696 return l.spec.PermalinkForBaseURL(l.relPermalinkForRel(target), l.spec.BaseURL.HostURL()) 697 698 } 699 func (l *genericResource) relTargetPathsFor(target string) []string { 700 return l.relTargetPathsForRel(target) 701 } 702 703 func (l *genericResource) relTargetPaths() []string { 704 return l.relTargetPathsForRel(l.targetPath()) 705 } 706 707 func (l *genericResource) Name() string { 708 return l.name 709 } 710 711 func (l *genericResource) Title() string { 712 return l.title 713 } 714 715 func (l *genericResource) Params() map[string]interface{} { 716 return l.params 717 } 718 719 func (l *genericResource) setTitle(title string) { 720 l.title = title 721 } 722 723 func (l *genericResource) setName(name string) { 724 l.name = name 725 } 726 727 func (l *genericResource) updateParams(params map[string]interface{}) { 728 if l.params == nil { 729 l.params = params 730 return 731 } 732 733 // Sets the params not already set 734 for k, v := range params { 735 if _, found := l.params[k]; !found { 736 l.params[k] = v 737 } 738 } 739 } 740 741 func (l *genericResource) relPermalinkForRel(rel string) string { 742 return l.spec.PathSpec.URLizeFilename(l.relTargetPathForRel(rel, false, true)) 743 } 744 745 func (l *genericResource) relTargetPathsForRel(rel string) []string { 746 if len(l.baseTargetPathDirs) == 0 { 747 return []string{l.relTargetPathForRelAndBasePath(rel, "", false)} 748 } 749 750 var targetPaths = make([]string, len(l.baseTargetPathDirs)) 751 for i, dir := range l.baseTargetPathDirs { 752 targetPaths[i] = l.relTargetPathForRelAndBasePath(rel, dir, false) 753 } 754 return targetPaths 755 } 756 757 func (l *genericResource) relTargetPathForRel(rel string, addBaseTargetPath, isURL bool) string { 758 if addBaseTargetPath && len(l.baseTargetPathDirs) > 1 { 759 panic("multiple baseTargetPathDirs") 760 } 761 var basePath string 762 if addBaseTargetPath && len(l.baseTargetPathDirs) > 0 { 763 basePath = l.baseTargetPathDirs[0] 764 } 765 766 return l.relTargetPathForRelAndBasePath(rel, basePath, isURL) 767 } 768 769 func (l *genericResource) relTargetPathForRelAndBasePath(rel, basePath string, isURL bool) string { 770 if l.targetPathBuilder != nil { 771 rel = l.targetPathBuilder(rel) 772 } 773 774 if isURL && l.baseURLDir != "" { 775 rel = path.Join(l.baseURLDir, rel) 776 } 777 778 if basePath != "" { 779 rel = path.Join(basePath, rel) 780 } 781 782 if l.baseOffset != "" { 783 rel = path.Join(l.baseOffset, rel) 784 } 785 786 if isURL && l.spec.PathSpec.BasePath != "" { 787 rel = path.Join(l.spec.PathSpec.BasePath, rel) 788 } 789 790 if len(rel) == 0 || rel[0] != '/' { 791 rel = "/" + rel 792 } 793 794 return rel 795 } 796 797 func (l *genericResource) ResourceType() string { 798 return l.resourceType 799 } 800 801 func (l *genericResource) String() string { 802 return fmt.Sprintf("Resource(%s: %s)", l.resourceType, l.name) 803 } 804 805 func (l *genericResource) Publish() error { 806 fr, err := l.ReadSeekCloser() 807 if err != nil { 808 return err 809 } 810 defer fr.Close() 811 fw, err := helpers.OpenFilesForWriting(l.spec.BaseFs.PublishFs, l.targetFilenames()...) 812 if err != nil { 813 return err 814 } 815 defer fw.Close() 816 817 _, err = io.Copy(fw, fr) 818 return err 819 } 820 821 // Path is stored with Unix style slashes. 822 func (l *genericResource) targetPath() string { 823 return l.relTargetDirFile.path() 824 } 825 826 func (l *genericResource) targetFilenames() []string { 827 paths := l.relTargetPaths() 828 for i, p := range paths { 829 paths[i] = filepath.Clean(p) 830 } 831 return paths 832 } 833 834 // TODO(bep) clean up below 835 func (r *Spec) newGenericResource(sourceFs afero.Fs, 836 targetPathBuilder func(base string) string, 837 osFileInfo os.FileInfo, 838 sourceFilename, 839 baseFilename string, 840 mediaType media.Type) *genericResource { 841 return r.newGenericResourceWithBase( 842 sourceFs, 843 false, 844 nil, 845 "", 846 nil, 847 targetPathBuilder, 848 osFileInfo, 849 sourceFilename, 850 baseFilename, 851 mediaType, 852 ) 853 854 } 855 856 func (r *Spec) newGenericResourceWithBase( 857 sourceFs afero.Fs, 858 lazyPublish bool, 859 openReadSeekerCloser OpenReadSeekCloser, 860 urlBaseDir string, 861 targetPathBaseDirs []string, 862 targetPathBuilder func(base string) string, 863 osFileInfo os.FileInfo, 864 sourceFilename, 865 baseFilename string, 866 mediaType media.Type) *genericResource { 867 868 // This value is used both to construct URLs and file paths, but start 869 // with a Unix-styled path. 870 baseFilename = helpers.ToSlashTrimLeading(baseFilename) 871 fpath, fname := path.Split(baseFilename) 872 873 var resourceType string 874 if mediaType.MainType == "image" { 875 resourceType = mediaType.MainType 876 } else { 877 resourceType = mediaType.SubType 878 } 879 880 pathDescriptor := resourcePathDescriptor{ 881 baseURLDir: urlBaseDir, 882 baseTargetPathDirs: targetPathBaseDirs, 883 targetPathBuilder: targetPathBuilder, 884 relTargetDirFile: dirFile{dir: fpath, file: fname}, 885 } 886 887 var po *publishOnce 888 if lazyPublish { 889 po = &publishOnce{logger: r.Logger} 890 } 891 892 return &genericResource{ 893 openReadSeekerCloser: openReadSeekerCloser, 894 publishOnce: po, 895 resourcePathDescriptor: pathDescriptor, 896 overriddenSourceFs: sourceFs, 897 osFileInfo: osFileInfo, 898 sourceFilename: sourceFilename, 899 mediaType: mediaType, 900 resourceType: resourceType, 901 spec: r, 902 params: make(map[string]interface{}), 903 name: baseFilename, 904 title: baseFilename, 905 resourceContent: &resourceContent{}, 906 resourceHash: &resourceHash{}, 907 } 908 }