github.com/gohugoio/hugo@v0.88.1/hugolib/hugo_sites.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 hugolib 15 16 import ( 17 "context" 18 "io" 19 "path/filepath" 20 "sort" 21 "strings" 22 "sync" 23 "sync/atomic" 24 25 "github.com/fsnotify/fsnotify" 26 27 "github.com/gohugoio/hugo/identity" 28 29 radix "github.com/armon/go-radix" 30 31 "github.com/gohugoio/hugo/output" 32 "github.com/gohugoio/hugo/parser/metadecoders" 33 34 "github.com/gohugoio/hugo/common/para" 35 "github.com/gohugoio/hugo/hugofs" 36 "github.com/pkg/errors" 37 38 "github.com/gohugoio/hugo/source" 39 40 "github.com/bep/gitmap" 41 "github.com/gohugoio/hugo/config" 42 43 "github.com/gohugoio/hugo/publisher" 44 45 "github.com/gohugoio/hugo/common/herrors" 46 "github.com/gohugoio/hugo/common/loggers" 47 "github.com/gohugoio/hugo/deps" 48 "github.com/gohugoio/hugo/helpers" 49 "github.com/gohugoio/hugo/langs" 50 "github.com/gohugoio/hugo/lazy" 51 52 "github.com/gohugoio/hugo/langs/i18n" 53 "github.com/gohugoio/hugo/resources/page" 54 "github.com/gohugoio/hugo/resources/page/pagemeta" 55 "github.com/gohugoio/hugo/tpl" 56 "github.com/gohugoio/hugo/tpl/tplimpl" 57 ) 58 59 // HugoSites represents the sites to build. Each site represents a language. 60 type HugoSites struct { 61 Sites []*Site 62 63 multilingual *Multilingual 64 65 // Multihost is set if multilingual and baseURL set on the language level. 66 multihost bool 67 68 // If this is running in the dev server. 69 running bool 70 71 // Serializes rebuilds when server is running. 72 runningMu sync.Mutex 73 74 // Render output formats for all sites. 75 renderFormats output.Formats 76 77 *deps.Deps 78 79 gitInfo *gitInfo 80 81 // As loaded from the /data dirs 82 data map[string]interface{} 83 84 contentInit sync.Once 85 content *pageMaps 86 87 // Keeps track of bundle directories and symlinks to enable partial rebuilding. 88 ContentChanges *contentChangeMap 89 90 // File change events with filename stored in this map will be skipped. 91 skipRebuildForFilenamesMu sync.Mutex 92 skipRebuildForFilenames map[string]bool 93 94 init *hugoSitesInit 95 96 workers *para.Workers 97 numWorkers int 98 99 *fatalErrorHandler 100 *testCounters 101 } 102 103 // ShouldSkipFileChangeEvent allows skipping filesystem event early before 104 // the build is started. 105 func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool { 106 h.skipRebuildForFilenamesMu.Lock() 107 defer h.skipRebuildForFilenamesMu.Unlock() 108 return h.skipRebuildForFilenames[ev.Name] 109 } 110 111 func (h *HugoSites) getContentMaps() *pageMaps { 112 h.contentInit.Do(func() { 113 h.content = newPageMaps(h) 114 }) 115 return h.content 116 } 117 118 // Only used in tests. 119 type testCounters struct { 120 contentRenderCounter uint64 121 } 122 123 func (h *testCounters) IncrContentRender() { 124 if h == nil { 125 return 126 } 127 atomic.AddUint64(&h.contentRenderCounter, 1) 128 } 129 130 type fatalErrorHandler struct { 131 mu sync.Mutex 132 133 h *HugoSites 134 135 err error 136 137 done bool 138 donec chan bool // will be closed when done 139 } 140 141 // FatalError error is used in some rare situations where it does not make sense to 142 // continue processing, to abort as soon as possible and log the error. 143 func (f *fatalErrorHandler) FatalError(err error) { 144 f.mu.Lock() 145 defer f.mu.Unlock() 146 if !f.done { 147 f.done = true 148 close(f.donec) 149 } 150 f.err = err 151 } 152 153 func (f *fatalErrorHandler) getErr() error { 154 f.mu.Lock() 155 defer f.mu.Unlock() 156 return f.err 157 } 158 159 func (f *fatalErrorHandler) Done() <-chan bool { 160 return f.donec 161 } 162 163 type hugoSitesInit struct { 164 // Loads the data from all of the /data folders. 165 data *lazy.Init 166 167 // Performs late initialization (before render) of the templates. 168 layouts *lazy.Init 169 170 // Loads the Git info for all the pages if enabled. 171 gitInfo *lazy.Init 172 173 // Maps page translations. 174 translations *lazy.Init 175 } 176 177 func (h *hugoSitesInit) Reset() { 178 h.data.Reset() 179 h.layouts.Reset() 180 h.gitInfo.Reset() 181 h.translations.Reset() 182 } 183 184 func (h *HugoSites) Data() map[string]interface{} { 185 if _, err := h.init.data.Do(); err != nil { 186 h.SendError(errors.Wrap(err, "failed to load data")) 187 return nil 188 } 189 return h.data 190 } 191 192 func (h *HugoSites) gitInfoForPage(p page.Page) (*gitmap.GitInfo, error) { 193 if _, err := h.init.gitInfo.Do(); err != nil { 194 return nil, err 195 } 196 197 if h.gitInfo == nil { 198 return nil, nil 199 } 200 201 return h.gitInfo.forPage(p), nil 202 } 203 204 func (h *HugoSites) siteInfos() page.Sites { 205 infos := make(page.Sites, len(h.Sites)) 206 for i, site := range h.Sites { 207 infos[i] = site.Info 208 } 209 return infos 210 } 211 212 func (h *HugoSites) pickOneAndLogTheRest(errors []error) error { 213 if len(errors) == 0 { 214 return nil 215 } 216 217 var i int 218 219 for j, err := range errors { 220 // If this is in server mode, we want to return an error to the client 221 // with a file context, if possible. 222 if herrors.UnwrapErrorWithFileContext(err) != nil { 223 i = j 224 break 225 } 226 } 227 228 // Log the rest, but add a threshold to avoid flooding the log. 229 const errLogThreshold = 5 230 231 for j, err := range errors { 232 if j == i || err == nil { 233 continue 234 } 235 236 if j >= errLogThreshold { 237 break 238 } 239 240 h.Log.Errorln(err) 241 } 242 243 return errors[i] 244 } 245 246 func (h *HugoSites) IsMultihost() bool { 247 return h != nil && h.multihost 248 } 249 250 // TODO(bep) consolidate 251 func (h *HugoSites) LanguageSet() map[string]int { 252 set := make(map[string]int) 253 for i, s := range h.Sites { 254 set[s.language.Lang] = i 255 } 256 return set 257 } 258 259 func (h *HugoSites) NumLogErrors() int { 260 if h == nil { 261 return 0 262 } 263 return int(h.Log.LogCounters().ErrorCounter.Count()) 264 } 265 266 func (h *HugoSites) PrintProcessingStats(w io.Writer) { 267 stats := make([]*helpers.ProcessingStats, len(h.Sites)) 268 for i := 0; i < len(h.Sites); i++ { 269 stats[i] = h.Sites[i].PathSpec.ProcessingStats 270 } 271 helpers.ProcessingStatsTable(w, stats...) 272 } 273 274 // GetContentPage finds a Page with content given the absolute filename. 275 // Returns nil if none found. 276 func (h *HugoSites) GetContentPage(filename string) page.Page { 277 var p page.Page 278 279 h.getContentMaps().walkBundles(func(b *contentNode) bool { 280 if b.p == nil || b.fi == nil { 281 return false 282 } 283 284 if b.fi.Meta().Filename == filename { 285 p = b.p 286 return true 287 } 288 289 return false 290 }) 291 292 return p 293 } 294 295 // NewHugoSites creates a new collection of sites given the input sites, building 296 // a language configuration based on those. 297 func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) { 298 if cfg.Language != nil { 299 return nil, errors.New("Cannot provide Language in Cfg when sites are provided") 300 } 301 302 // Return error at the end. Make the caller decide if it's fatal or not. 303 var initErr error 304 305 langConfig, err := newMultiLingualFromSites(cfg.Cfg, sites...) 306 if err != nil { 307 return nil, errors.Wrap(err, "failed to create language config") 308 } 309 310 var contentChangeTracker *contentChangeMap 311 312 numWorkers := config.GetNumWorkerMultiplier() 313 if numWorkers > len(sites) { 314 numWorkers = len(sites) 315 } 316 var workers *para.Workers 317 if numWorkers > 1 { 318 workers = para.New(numWorkers) 319 } 320 321 h := &HugoSites{ 322 running: cfg.Running, 323 multilingual: langConfig, 324 multihost: cfg.Cfg.GetBool("multihost"), 325 Sites: sites, 326 workers: workers, 327 numWorkers: numWorkers, 328 skipRebuildForFilenames: make(map[string]bool), 329 init: &hugoSitesInit{ 330 data: lazy.New(), 331 layouts: lazy.New(), 332 gitInfo: lazy.New(), 333 translations: lazy.New(), 334 }, 335 } 336 337 h.fatalErrorHandler = &fatalErrorHandler{ 338 h: h, 339 donec: make(chan bool), 340 } 341 342 h.init.data.Add(func() (interface{}, error) { 343 err := h.loadData(h.PathSpec.BaseFs.Data.Dirs) 344 if err != nil { 345 return nil, errors.Wrap(err, "failed to load data") 346 } 347 return nil, nil 348 }) 349 350 h.init.layouts.Add(func() (interface{}, error) { 351 for _, s := range h.Sites { 352 if err := s.Tmpl().(tpl.TemplateManager).MarkReady(); err != nil { 353 return nil, err 354 } 355 } 356 return nil, nil 357 }) 358 359 h.init.translations.Add(func() (interface{}, error) { 360 if len(h.Sites) > 1 { 361 allTranslations := pagesToTranslationsMap(h.Sites) 362 assignTranslationsToPages(allTranslations, h.Sites) 363 } 364 365 return nil, nil 366 }) 367 368 h.init.gitInfo.Add(func() (interface{}, error) { 369 err := h.loadGitInfo() 370 if err != nil { 371 return nil, errors.Wrap(err, "failed to load Git info") 372 } 373 return nil, nil 374 }) 375 376 for _, s := range sites { 377 s.h = h 378 } 379 380 var l configLoader 381 if err := l.applyDeps(cfg, sites...); err != nil { 382 initErr = errors.Wrap(err, "add site dependencies") 383 } 384 385 h.Deps = sites[0].Deps 386 387 // Only needed in server mode. 388 // TODO(bep) clean up the running vs watching terms 389 if cfg.Running { 390 contentChangeTracker = &contentChangeMap{ 391 pathSpec: h.PathSpec, 392 symContent: make(map[string]map[string]bool), 393 leafBundles: radix.New(), 394 branchBundles: make(map[string]bool), 395 } 396 h.ContentChanges = contentChangeTracker 397 } 398 399 return h, initErr 400 } 401 402 func (h *HugoSites) loadGitInfo() error { 403 if h.Cfg.GetBool("enableGitInfo") { 404 gi, err := newGitInfo(h.Cfg) 405 if err != nil { 406 h.Log.Errorln("Failed to read Git log:", err) 407 } else { 408 h.gitInfo = gi 409 } 410 } 411 return nil 412 } 413 414 func (l configLoader) applyDeps(cfg deps.DepsCfg, sites ...*Site) error { 415 if cfg.TemplateProvider == nil { 416 cfg.TemplateProvider = tplimpl.DefaultTemplateProvider 417 } 418 419 if cfg.TranslationProvider == nil { 420 cfg.TranslationProvider = i18n.NewTranslationProvider() 421 } 422 423 var ( 424 d *deps.Deps 425 err error 426 ) 427 428 for _, s := range sites { 429 if s.Deps != nil { 430 continue 431 } 432 433 onCreated := func(d *deps.Deps) error { 434 s.Deps = d 435 436 // Set up the main publishing chain. 437 pub, err := publisher.NewDestinationPublisher( 438 d.ResourceSpec, 439 s.outputFormatsConfig, 440 s.mediaTypesConfig, 441 ) 442 if err != nil { 443 return err 444 } 445 s.publisher = pub 446 447 if err := s.initializeSiteInfo(); err != nil { 448 return err 449 } 450 451 d.Site = s.Info 452 453 siteConfig, err := l.loadSiteConfig(s.language) 454 if err != nil { 455 return errors.Wrap(err, "load site config") 456 } 457 s.siteConfigConfig = siteConfig 458 459 pm := &pageMap{ 460 contentMap: newContentMap(contentMapConfig{ 461 lang: s.Lang(), 462 taxonomyConfig: s.siteCfg.taxonomiesConfig.Values(), 463 taxonomyDisabled: !s.isEnabled(page.KindTerm), 464 taxonomyTermDisabled: !s.isEnabled(page.KindTaxonomy), 465 pageDisabled: !s.isEnabled(page.KindPage), 466 }), 467 s: s, 468 } 469 470 s.PageCollections = newPageCollections(pm) 471 472 s.siteRefLinker, err = newSiteRefLinker(s.language, s) 473 return err 474 } 475 476 cfg.Language = s.language 477 cfg.MediaTypes = s.mediaTypesConfig 478 cfg.OutputFormats = s.outputFormatsConfig 479 480 if d == nil { 481 cfg.WithTemplate = s.withSiteTemplates(cfg.WithTemplate) 482 483 var err error 484 d, err = deps.New(cfg) 485 if err != nil { 486 return errors.Wrap(err, "create deps") 487 } 488 489 d.OutputFormatsConfig = s.outputFormatsConfig 490 491 if err := onCreated(d); err != nil { 492 return errors.Wrap(err, "on created") 493 } 494 495 if err = d.LoadResources(); err != nil { 496 return errors.Wrap(err, "load resources") 497 } 498 499 } else { 500 d, err = d.ForLanguage(cfg, onCreated) 501 if err != nil { 502 return err 503 } 504 d.OutputFormatsConfig = s.outputFormatsConfig 505 } 506 } 507 508 return nil 509 } 510 511 // NewHugoSites creates HugoSites from the given config. 512 func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { 513 if cfg.Logger == nil { 514 cfg.Logger = loggers.NewErrorLogger() 515 } 516 sites, err := createSitesFromConfig(cfg) 517 if err != nil { 518 return nil, errors.Wrap(err, "from config") 519 } 520 return newHugoSites(cfg, sites...) 521 } 522 523 func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateManager) error) func(templ tpl.TemplateManager) error { 524 return func(templ tpl.TemplateManager) error { 525 for _, wt := range withTemplates { 526 if wt == nil { 527 continue 528 } 529 if err := wt(templ); err != nil { 530 return err 531 } 532 } 533 534 return nil 535 } 536 } 537 538 func createSitesFromConfig(cfg deps.DepsCfg) ([]*Site, error) { 539 var sites []*Site 540 541 languages := getLanguages(cfg.Cfg) 542 543 for _, lang := range languages { 544 if lang.Disabled { 545 continue 546 } 547 var s *Site 548 var err error 549 cfg.Language = lang 550 s, err = newSite(cfg) 551 552 if err != nil { 553 return nil, err 554 } 555 556 sites = append(sites, s) 557 } 558 559 return sites, nil 560 } 561 562 // Reset resets the sites and template caches etc., making it ready for a full rebuild. 563 func (h *HugoSites) reset(config *BuildCfg) { 564 if config.ResetState { 565 for i, s := range h.Sites { 566 h.Sites[i] = s.reset() 567 if r, ok := s.Fs.Destination.(hugofs.Reseter); ok { 568 r.Reset() 569 } 570 } 571 } 572 573 h.fatalErrorHandler = &fatalErrorHandler{ 574 h: h, 575 donec: make(chan bool), 576 } 577 578 h.init.Reset() 579 } 580 581 // resetLogs resets the log counters etc. Used to do a new build on the same sites. 582 func (h *HugoSites) resetLogs() { 583 h.Log.Reset() 584 loggers.GlobalErrorCounter.Reset() 585 for _, s := range h.Sites { 586 s.Deps.Log.Reset() 587 s.Deps.LogDistinct.Reset() 588 } 589 } 590 591 func (h *HugoSites) withSite(fn func(s *Site) error) error { 592 if h.workers == nil { 593 for _, s := range h.Sites { 594 if err := fn(s); err != nil { 595 return err 596 } 597 } 598 return nil 599 } 600 601 g, _ := h.workers.Start(context.Background()) 602 for _, s := range h.Sites { 603 s := s 604 g.Run(func() error { 605 return fn(s) 606 }) 607 } 608 return g.Wait() 609 } 610 611 func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error { 612 oldLangs, _ := h.Cfg.Get("languagesSorted").(langs.Languages) 613 614 l := configLoader{cfg: h.Cfg} 615 if err := l.loadLanguageSettings(oldLangs); err != nil { 616 return err 617 } 618 619 depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: l.cfg} 620 621 sites, err := createSitesFromConfig(depsCfg) 622 if err != nil { 623 return err 624 } 625 626 langConfig, err := newMultiLingualFromSites(depsCfg.Cfg, sites...) 627 if err != nil { 628 return err 629 } 630 631 h.Sites = sites 632 633 for _, s := range sites { 634 s.h = h 635 } 636 637 var cl configLoader 638 if err := cl.applyDeps(depsCfg, sites...); err != nil { 639 return err 640 } 641 642 h.Deps = sites[0].Deps 643 644 h.multilingual = langConfig 645 h.multihost = h.Deps.Cfg.GetBool("multihost") 646 647 return nil 648 } 649 650 func (h *HugoSites) toSiteInfos() []*SiteInfo { 651 infos := make([]*SiteInfo, len(h.Sites)) 652 for i, s := range h.Sites { 653 infos[i] = s.Info 654 } 655 return infos 656 } 657 658 // BuildCfg holds build options used to, as an example, skip the render step. 659 type BuildCfg struct { 660 // Reset site state before build. Use to force full rebuilds. 661 ResetState bool 662 // If set, we re-create the sites from the given configuration before a build. 663 // This is needed if new languages are added. 664 NewConfig config.Provider 665 // Skip rendering. Useful for testing. 666 SkipRender bool 667 // Use this to indicate what changed (for rebuilds). 668 whatChanged *whatChanged 669 670 // This is a partial re-render of some selected pages. This means 671 // we should skip most of the processing. 672 PartialReRender bool 673 674 // Set in server mode when the last build failed for some reason. 675 ErrRecovery bool 676 677 // Recently visited URLs. This is used for partial re-rendering. 678 RecentlyVisited map[string]bool 679 680 testCounters *testCounters 681 } 682 683 // shouldRender is used in the Fast Render Mode to determine if we need to re-render 684 // a Page: If it is recently visited (the home pages will always be in this set) or changed. 685 // Note that a page does not have to have a content page / file. 686 // For regular builds, this will allways return true. 687 // TODO(bep) rename/work this. 688 func (cfg *BuildCfg) shouldRender(p *pageState) bool { 689 if p.forceRender { 690 return true 691 } 692 693 if len(cfg.RecentlyVisited) == 0 { 694 return true 695 } 696 697 if cfg.RecentlyVisited[p.RelPermalink()] { 698 return true 699 } 700 701 if cfg.whatChanged != nil && !p.File().IsZero() { 702 return cfg.whatChanged.files[p.File().Filename()] 703 } 704 705 return false 706 } 707 708 func (h *HugoSites) renderCrossSitesSitemap() error { 709 if !h.multilingual.enabled() || h.IsMultihost() { 710 return nil 711 } 712 713 sitemapEnabled := false 714 for _, s := range h.Sites { 715 if s.isEnabled(kindSitemap) { 716 sitemapEnabled = true 717 break 718 } 719 } 720 721 if !sitemapEnabled { 722 return nil 723 } 724 725 s := h.Sites[0] 726 727 templ := s.lookupLayouts("sitemapindex.xml", "_default/sitemapindex.xml", "_internal/_default/sitemapindex.xml") 728 729 return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemapindex", 730 s.siteCfg.sitemap.Filename, h.toSiteInfos(), templ) 731 } 732 733 func (h *HugoSites) renderCrossSitesRobotsTXT() error { 734 if h.multihost { 735 return nil 736 } 737 if !h.Cfg.GetBool("enableRobotsTXT") { 738 return nil 739 } 740 741 s := h.Sites[0] 742 743 p, err := newPageStandalone(&pageMeta{ 744 s: s, 745 kind: kindRobotsTXT, 746 urlPaths: pagemeta.URLPath{ 747 URL: "robots.txt", 748 }, 749 }, 750 output.RobotsTxtFormat) 751 if err != nil { 752 return err 753 } 754 755 if !p.render { 756 return nil 757 } 758 759 templ := s.lookupLayouts("robots.txt", "_default/robots.txt", "_internal/_default/robots.txt") 760 761 return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "Robots Txt", "robots.txt", p, templ) 762 } 763 764 func (h *HugoSites) removePageByFilename(filename string) { 765 h.getContentMaps().withMaps(func(m *pageMap) error { 766 m.deleteBundleMatching(func(b *contentNode) bool { 767 if b.p == nil { 768 return false 769 } 770 771 if b.fi == nil { 772 return false 773 } 774 775 return b.fi.Meta().Filename == filename 776 }) 777 return nil 778 }) 779 } 780 781 func (h *HugoSites) createPageCollections() error { 782 allPages := newLazyPagesFactory(func() page.Pages { 783 var pages page.Pages 784 for _, s := range h.Sites { 785 pages = append(pages, s.Pages()...) 786 } 787 788 page.SortByDefault(pages) 789 790 return pages 791 }) 792 793 allRegularPages := newLazyPagesFactory(func() page.Pages { 794 return h.findPagesByKindIn(page.KindPage, allPages.get()) 795 }) 796 797 for _, s := range h.Sites { 798 s.PageCollections.allPages = allPages 799 s.PageCollections.allRegularPages = allRegularPages 800 } 801 802 return nil 803 } 804 805 func (s *Site) preparePagesForRender(isRenderingSite bool, idx int) error { 806 var err error 807 s.pageMap.withEveryBundlePage(func(p *pageState) bool { 808 if err = p.initOutputFormat(isRenderingSite, idx); err != nil { 809 return true 810 } 811 return false 812 }) 813 return nil 814 } 815 816 // Pages returns all pages for all sites. 817 func (h *HugoSites) Pages() page.Pages { 818 return h.Sites[0].AllPages() 819 } 820 821 func (h *HugoSites) loadData(fis []hugofs.FileMetaInfo) (err error) { 822 spec := source.NewSourceSpec(h.PathSpec, nil) 823 824 h.data = make(map[string]interface{}) 825 for _, fi := range fis { 826 fileSystem := spec.NewFilesystemFromFileMetaInfo(fi) 827 files, err := fileSystem.Files() 828 if err != nil { 829 return err 830 } 831 for _, r := range files { 832 if err := h.handleDataFile(r); err != nil { 833 return err 834 } 835 } 836 } 837 838 return 839 } 840 841 func (h *HugoSites) handleDataFile(r source.File) error { 842 var current map[string]interface{} 843 844 f, err := r.FileInfo().Meta().Open() 845 if err != nil { 846 return errors.Wrapf(err, "data: failed to open %q:", r.LogicalName()) 847 } 848 defer f.Close() 849 850 // Crawl in data tree to insert data 851 current = h.data 852 keyParts := strings.Split(r.Dir(), helpers.FilePathSeparator) 853 854 for _, key := range keyParts { 855 if key != "" { 856 if _, ok := current[key]; !ok { 857 current[key] = make(map[string]interface{}) 858 } 859 current = current[key].(map[string]interface{}) 860 } 861 } 862 863 data, err := h.readData(r) 864 if err != nil { 865 return h.errWithFileContext(err, r) 866 } 867 868 if data == nil { 869 return nil 870 } 871 872 // filepath.Walk walks the files in lexical order, '/' comes before '.' 873 higherPrecedentData := current[r.BaseFileName()] 874 875 switch data.(type) { 876 case nil: 877 case map[string]interface{}: 878 879 switch higherPrecedentData.(type) { 880 case nil: 881 current[r.BaseFileName()] = data 882 case map[string]interface{}: 883 // merge maps: insert entries from data for keys that 884 // don't already exist in higherPrecedentData 885 higherPrecedentMap := higherPrecedentData.(map[string]interface{}) 886 for key, value := range data.(map[string]interface{}) { 887 if _, exists := higherPrecedentMap[key]; exists { 888 // this warning could happen if 889 // 1. A theme uses the same key; the main data folder wins 890 // 2. A sub folder uses the same key: the sub folder wins 891 // TODO(bep) figure out a way to detect 2) above and make that a WARN 892 h.Log.Infof("Data for key '%s' in path '%s' is overridden by higher precedence data already in the data tree", key, r.Path()) 893 } else { 894 higherPrecedentMap[key] = value 895 } 896 } 897 default: 898 // can't merge: higherPrecedentData is not a map 899 h.Log.Warnf("The %T data from '%s' overridden by "+ 900 "higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData) 901 } 902 903 case []interface{}: 904 if higherPrecedentData == nil { 905 current[r.BaseFileName()] = data 906 } else { 907 // we don't merge array data 908 h.Log.Warnf("The %T data from '%s' overridden by "+ 909 "higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData) 910 } 911 912 default: 913 h.Log.Errorf("unexpected data type %T in file %s", data, r.LogicalName()) 914 } 915 916 return nil 917 } 918 919 func (h *HugoSites) errWithFileContext(err error, f source.File) error { 920 fim, ok := f.FileInfo().(hugofs.FileMetaInfo) 921 if !ok { 922 return err 923 } 924 925 realFilename := fim.Meta().Filename 926 927 err, _ = herrors.WithFileContextForFile( 928 err, 929 realFilename, 930 realFilename, 931 h.SourceSpec.Fs.Source, 932 herrors.SimpleLineMatcher) 933 934 return err 935 } 936 937 func (h *HugoSites) readData(f source.File) (interface{}, error) { 938 file, err := f.FileInfo().Meta().Open() 939 if err != nil { 940 return nil, errors.Wrap(err, "readData: failed to open data file") 941 } 942 defer file.Close() 943 content := helpers.ReaderToBytes(file) 944 945 format := metadecoders.FormatFromString(f.Extension()) 946 return metadecoders.Default.Unmarshal(content, format) 947 } 948 949 func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Pages { 950 return h.Sites[0].findPagesByKindIn(kind, inPages) 951 } 952 953 func (h *HugoSites) resetPageState() { 954 h.getContentMaps().walkBundles(func(n *contentNode) bool { 955 if n.p == nil { 956 return false 957 } 958 p := n.p 959 for _, po := range p.pageOutputs { 960 if po.cp == nil { 961 continue 962 } 963 po.cp.Reset() 964 } 965 966 return false 967 }) 968 } 969 970 func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) { 971 h.getContentMaps().walkBundles(func(n *contentNode) bool { 972 if n.p == nil { 973 return false 974 } 975 p := n.p 976 OUTPUTS: 977 for _, po := range p.pageOutputs { 978 if po.cp == nil { 979 continue 980 } 981 for id := range idset { 982 if po.cp.dependencyTracker.Search(id) != nil { 983 po.cp.Reset() 984 continue OUTPUTS 985 } 986 } 987 } 988 989 if p.shortcodeState == nil { 990 return false 991 } 992 993 for _, s := range p.shortcodeState.shortcodes { 994 for _, templ := range s.templs { 995 sid := templ.(identity.Manager) 996 for id := range idset { 997 if sid.Search(id) != nil { 998 for _, po := range p.pageOutputs { 999 if po.cp != nil { 1000 po.cp.Reset() 1001 } 1002 } 1003 return false 1004 } 1005 } 1006 } 1007 } 1008 return false 1009 }) 1010 } 1011 1012 // Used in partial reloading to determine if the change is in a bundle. 1013 type contentChangeMap struct { 1014 mu sync.RWMutex 1015 1016 // Holds directories with leaf bundles. 1017 leafBundles *radix.Tree 1018 1019 // Holds directories with branch bundles. 1020 branchBundles map[string]bool 1021 1022 pathSpec *helpers.PathSpec 1023 1024 // Hugo supports symlinked content (both directories and files). This 1025 // can lead to situations where the same file can be referenced from several 1026 // locations in /content -- which is really cool, but also means we have to 1027 // go an extra mile to handle changes. 1028 // This map is only used in watch mode. 1029 // It maps either file to files or the real dir to a set of content directories 1030 // where it is in use. 1031 symContentMu sync.Mutex 1032 symContent map[string]map[string]bool 1033 } 1034 1035 func (m *contentChangeMap) add(dirname string, tp bundleDirType) { 1036 m.mu.Lock() 1037 if !strings.HasSuffix(dirname, helpers.FilePathSeparator) { 1038 dirname += helpers.FilePathSeparator 1039 } 1040 switch tp { 1041 case bundleBranch: 1042 m.branchBundles[dirname] = true 1043 case bundleLeaf: 1044 m.leafBundles.Insert(dirname, true) 1045 default: 1046 m.mu.Unlock() 1047 panic("invalid bundle type") 1048 } 1049 m.mu.Unlock() 1050 } 1051 1052 func (m *contentChangeMap) resolveAndRemove(filename string) (string, bundleDirType) { 1053 m.mu.RLock() 1054 defer m.mu.RUnlock() 1055 1056 // Bundles share resources, so we need to start from the virtual root. 1057 relFilename := m.pathSpec.RelContentDir(filename) 1058 dir, name := filepath.Split(relFilename) 1059 if !strings.HasSuffix(dir, helpers.FilePathSeparator) { 1060 dir += helpers.FilePathSeparator 1061 } 1062 1063 if _, found := m.branchBundles[dir]; found { 1064 delete(m.branchBundles, dir) 1065 return dir, bundleBranch 1066 } 1067 1068 if key, _, found := m.leafBundles.LongestPrefix(dir); found { 1069 m.leafBundles.Delete(key) 1070 dir = string(key) 1071 return dir, bundleLeaf 1072 } 1073 1074 fileTp, isContent := classifyBundledFile(name) 1075 if isContent && fileTp != bundleNot { 1076 // A new bundle. 1077 return dir, fileTp 1078 } 1079 1080 return dir, bundleNot 1081 } 1082 1083 func (m *contentChangeMap) addSymbolicLinkMapping(fim hugofs.FileMetaInfo) { 1084 meta := fim.Meta() 1085 if !meta.IsSymlink { 1086 return 1087 } 1088 m.symContentMu.Lock() 1089 1090 from, to := meta.Filename, meta.OriginalFilename 1091 if fim.IsDir() { 1092 if !strings.HasSuffix(from, helpers.FilePathSeparator) { 1093 from += helpers.FilePathSeparator 1094 } 1095 } 1096 1097 mm, found := m.symContent[from] 1098 1099 if !found { 1100 mm = make(map[string]bool) 1101 m.symContent[from] = mm 1102 } 1103 mm[to] = true 1104 m.symContentMu.Unlock() 1105 } 1106 1107 func (m *contentChangeMap) GetSymbolicLinkMappings(dir string) []string { 1108 mm, found := m.symContent[dir] 1109 if !found { 1110 return nil 1111 } 1112 dirs := make([]string, len(mm)) 1113 i := 0 1114 for dir := range mm { 1115 dirs[i] = dir 1116 i++ 1117 } 1118 1119 sort.Strings(dirs) 1120 1121 return dirs 1122 }