github.com/gohugoio/hugo@v0.88.1/hugolib/filesystems/basefs.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 filesystems provides the fine grained file systems used by Hugo. These 15 // are typically virtual filesystems that are composites of project and theme content. 16 package filesystems 17 18 import ( 19 "fmt" 20 "io" 21 "os" 22 "path" 23 "path/filepath" 24 "strings" 25 "sync" 26 27 "github.com/gohugoio/hugo/common/loggers" 28 29 "github.com/gohugoio/hugo/hugofs/files" 30 31 "github.com/pkg/errors" 32 33 "github.com/gohugoio/hugo/modules" 34 35 "github.com/gohugoio/hugo/hugofs" 36 37 "github.com/gohugoio/hugo/hugolib/paths" 38 "github.com/spf13/afero" 39 ) 40 41 var filePathSeparator = string(filepath.Separator) 42 43 // BaseFs contains the core base filesystems used by Hugo. The name "base" is used 44 // to underline that even if they can be composites, they all have a base path set to a specific 45 // resource folder, e.g "/my-project/content". So, no absolute filenames needed. 46 type BaseFs struct { 47 48 // SourceFilesystems contains the different source file systems. 49 *SourceFilesystems 50 51 // The project source. 52 SourceFs afero.Fs 53 54 // The filesystem used to publish the rendered site. 55 // This usually maps to /my-project/public. 56 PublishFs afero.Fs 57 58 theBigFs *filesystemsCollector 59 } 60 61 // TODO(bep) we can get regular files in here and that is fine, but 62 // we need to clean up the naming. 63 func (fs *BaseFs) WatchDirs() []hugofs.FileMetaInfo { 64 var dirs []hugofs.FileMetaInfo 65 for _, dir := range fs.AllDirs() { 66 if dir.Meta().Watch { 67 dirs = append(dirs, dir) 68 } 69 } 70 return dirs 71 } 72 73 func (fs *BaseFs) AllDirs() []hugofs.FileMetaInfo { 74 var dirs []hugofs.FileMetaInfo 75 for _, dirSet := range [][]hugofs.FileMetaInfo{ 76 fs.Archetypes.Dirs, 77 fs.I18n.Dirs, 78 fs.Data.Dirs, 79 fs.Content.Dirs, 80 fs.Assets.Dirs, 81 fs.Layouts.Dirs, 82 // fs.Resources.Dirs, 83 fs.StaticDirs, 84 } { 85 dirs = append(dirs, dirSet...) 86 } 87 88 return dirs 89 } 90 91 // RelContentDir tries to create a path relative to the content root from 92 // the given filename. The return value is the path and language code. 93 func (b *BaseFs) RelContentDir(filename string) string { 94 for _, dir := range b.SourceFilesystems.Content.Dirs { 95 dirname := dir.Meta().Filename 96 if strings.HasPrefix(filename, dirname) { 97 rel := path.Join(dir.Meta().Path, strings.TrimPrefix(filename, dirname)) 98 return strings.TrimPrefix(rel, filePathSeparator) 99 } 100 } 101 // Either not a content dir or already relative. 102 return filename 103 } 104 105 // ResolveJSConfigFile resolves the JS-related config file to a absolute 106 // filename. One example of such would be postcss.config.js. 107 func (fs *BaseFs) ResolveJSConfigFile(name string) string { 108 // First look in assets/_jsconfig 109 fi, err := fs.Assets.Fs.Stat(filepath.Join(files.FolderJSConfig, name)) 110 if err == nil { 111 return fi.(hugofs.FileMetaInfo).Meta().Filename 112 } 113 // Fall back to the work dir. 114 fi, err = fs.Work.Stat(name) 115 if err == nil { 116 return fi.(hugofs.FileMetaInfo).Meta().Filename 117 } 118 119 return "" 120 } 121 122 // SourceFilesystems contains the different source file systems. These can be 123 // composite file systems (theme and project etc.), and they have all root 124 // set to the source type the provides: data, i18n, static, layouts. 125 type SourceFilesystems struct { 126 Content *SourceFilesystem 127 Data *SourceFilesystem 128 I18n *SourceFilesystem 129 Layouts *SourceFilesystem 130 Archetypes *SourceFilesystem 131 Assets *SourceFilesystem 132 133 // Writable filesystem on top the project's resources directory, 134 // with any sub module's resource fs layered below. 135 ResourcesCache afero.Fs 136 137 // The project folder. 138 Work afero.Fs 139 140 // When in multihost we have one static filesystem per language. The sync 141 // static files is currently done outside of the Hugo build (where there is 142 // a concept of a site per language). 143 // When in non-multihost mode there will be one entry in this map with a blank key. 144 Static map[string]*SourceFilesystem 145 146 // All the /static dirs (including themes/modules). 147 StaticDirs []hugofs.FileMetaInfo 148 } 149 150 // FileSystems returns the FileSystems relevant for the change detection 151 // in server mode. 152 // Note: This does currently not return any static fs. 153 func (s *SourceFilesystems) FileSystems() []*SourceFilesystem { 154 return []*SourceFilesystem{ 155 s.Content, 156 s.Data, 157 s.I18n, 158 s.Layouts, 159 s.Archetypes, 160 // TODO(bep) static 161 } 162 } 163 164 // A SourceFilesystem holds the filesystem for a given source type in Hugo (data, 165 // i18n, layouts, static) and additional metadata to be able to use that filesystem 166 // in server mode. 167 type SourceFilesystem struct { 168 // Name matches one in files.ComponentFolders 169 Name string 170 171 // This is a virtual composite filesystem. It expects path relative to a context. 172 Fs afero.Fs 173 174 // This filesystem as separate root directories, starting from project and down 175 // to the themes/modules. 176 Dirs []hugofs.FileMetaInfo 177 178 // When syncing a source folder to the target (e.g. /public), this may 179 // be set to publish into a subfolder. This is used for static syncing 180 // in multihost mode. 181 PublishFolder string 182 } 183 184 // ContentStaticAssetFs will create a new composite filesystem from the content, 185 // static, and asset filesystems. The site language is needed to pick the correct static filesystem. 186 // The order is content, static and then assets. 187 // TODO(bep) check usage 188 func (s SourceFilesystems) ContentStaticAssetFs(lang string) afero.Fs { 189 staticFs := s.StaticFs(lang) 190 191 base := afero.NewCopyOnWriteFs(s.Assets.Fs, staticFs) 192 return afero.NewCopyOnWriteFs(base, s.Content.Fs) 193 } 194 195 // StaticFs returns the static filesystem for the given language. 196 // This can be a composite filesystem. 197 func (s SourceFilesystems) StaticFs(lang string) afero.Fs { 198 var staticFs afero.Fs = hugofs.NoOpFs 199 200 if fs, ok := s.Static[lang]; ok { 201 staticFs = fs.Fs 202 } else if fs, ok := s.Static[""]; ok { 203 staticFs = fs.Fs 204 } 205 206 return staticFs 207 } 208 209 // StatResource looks for a resource in these filesystems in order: static, assets and finally content. 210 // If found in any of them, it returns FileInfo and the relevant filesystem. 211 // Any non os.IsNotExist error will be returned. 212 // An os.IsNotExist error wil be returned only if all filesystems return such an error. 213 // Note that if we only wanted to find the file, we could create a composite Afero fs, 214 // but we also need to know which filesystem root it lives in. 215 func (s SourceFilesystems) StatResource(lang, filename string) (fi os.FileInfo, fs afero.Fs, err error) { 216 for _, fsToCheck := range []afero.Fs{s.StaticFs(lang), s.Assets.Fs, s.Content.Fs} { 217 fs = fsToCheck 218 fi, err = fs.Stat(filename) 219 if err == nil || !os.IsNotExist(err) { 220 return 221 } 222 } 223 // Not found. 224 return 225 } 226 227 // IsStatic returns true if the given filename is a member of one of the static 228 // filesystems. 229 func (s SourceFilesystems) IsStatic(filename string) bool { 230 for _, staticFs := range s.Static { 231 if staticFs.Contains(filename) { 232 return true 233 } 234 } 235 return false 236 } 237 238 // IsContent returns true if the given filename is a member of the content filesystem. 239 func (s SourceFilesystems) IsContent(filename string) bool { 240 return s.Content.Contains(filename) 241 } 242 243 // IsLayout returns true if the given filename is a member of the layouts filesystem. 244 func (s SourceFilesystems) IsLayout(filename string) bool { 245 return s.Layouts.Contains(filename) 246 } 247 248 // IsData returns true if the given filename is a member of the data filesystem. 249 func (s SourceFilesystems) IsData(filename string) bool { 250 return s.Data.Contains(filename) 251 } 252 253 // IsAsset returns true if the given filename is a member of the asset filesystem. 254 func (s SourceFilesystems) IsAsset(filename string) bool { 255 return s.Assets.Contains(filename) 256 } 257 258 // IsI18n returns true if the given filename is a member of the i18n filesystem. 259 func (s SourceFilesystems) IsI18n(filename string) bool { 260 return s.I18n.Contains(filename) 261 } 262 263 // MakeStaticPathRelative makes an absolute static filename into a relative one. 264 // It will return an empty string if the filename is not a member of a static filesystem. 265 func (s SourceFilesystems) MakeStaticPathRelative(filename string) string { 266 for _, staticFs := range s.Static { 267 rel, _ := staticFs.MakePathRelative(filename) 268 if rel != "" { 269 return rel 270 } 271 } 272 return "" 273 } 274 275 // MakePathRelative creates a relative path from the given filename. 276 func (d *SourceFilesystem) MakePathRelative(filename string) (string, bool) { 277 for _, dir := range d.Dirs { 278 meta := dir.(hugofs.FileMetaInfo).Meta() 279 currentPath := meta.Filename 280 281 if strings.HasPrefix(filename, currentPath) { 282 rel := strings.TrimPrefix(filename, currentPath) 283 if mp := meta.Path; mp != "" { 284 rel = filepath.Join(mp, rel) 285 } 286 return strings.TrimPrefix(rel, filePathSeparator), true 287 } 288 } 289 return "", false 290 } 291 292 func (d *SourceFilesystem) RealFilename(rel string) string { 293 fi, err := d.Fs.Stat(rel) 294 if err != nil { 295 return rel 296 } 297 if realfi, ok := fi.(hugofs.FileMetaInfo); ok { 298 return realfi.Meta().Filename 299 } 300 301 return rel 302 } 303 304 // Contains returns whether the given filename is a member of the current filesystem. 305 func (d *SourceFilesystem) Contains(filename string) bool { 306 for _, dir := range d.Dirs { 307 if strings.HasPrefix(filename, dir.Meta().Filename) { 308 return true 309 } 310 } 311 return false 312 } 313 314 // Path returns the mount relative path to the given filename if it is a member of 315 // of the current filesystem, an empty string if not. 316 func (d *SourceFilesystem) Path(filename string) string { 317 for _, dir := range d.Dirs { 318 meta := dir.Meta() 319 if strings.HasPrefix(filename, meta.Filename) { 320 p := strings.TrimPrefix(strings.TrimPrefix(filename, meta.Filename), filePathSeparator) 321 if mountRoot := meta.MountRoot; mountRoot != "" { 322 return filepath.Join(mountRoot, p) 323 } 324 return p 325 } 326 } 327 return "" 328 } 329 330 // RealDirs gets a list of absolute paths to directories starting from the given 331 // path. 332 func (d *SourceFilesystem) RealDirs(from string) []string { 333 var dirnames []string 334 for _, dir := range d.Dirs { 335 meta := dir.Meta() 336 dirname := filepath.Join(meta.Filename, from) 337 _, err := meta.Fs.Stat(from) 338 339 if err == nil { 340 dirnames = append(dirnames, dirname) 341 } 342 } 343 return dirnames 344 } 345 346 // WithBaseFs allows reuse of some potentially expensive to create parts that remain 347 // the same across sites/languages. 348 func WithBaseFs(b *BaseFs) func(*BaseFs) error { 349 return func(bb *BaseFs) error { 350 bb.theBigFs = b.theBigFs 351 bb.SourceFilesystems = b.SourceFilesystems 352 return nil 353 } 354 } 355 356 // NewBase builds the filesystems used by Hugo given the paths and options provided.NewBase 357 func NewBase(p *paths.Paths, logger loggers.Logger, options ...func(*BaseFs) error) (*BaseFs, error) { 358 fs := p.Fs 359 if logger == nil { 360 logger = loggers.NewWarningLogger() 361 } 362 363 publishFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Destination, p.AbsPublishDir)) 364 sourceFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Source, p.WorkingDir)) 365 366 b := &BaseFs{ 367 SourceFs: sourceFs, 368 PublishFs: publishFs, 369 } 370 371 for _, opt := range options { 372 if err := opt(b); err != nil { 373 return nil, err 374 } 375 } 376 377 if b.theBigFs != nil && b.SourceFilesystems != nil { 378 return b, nil 379 } 380 381 builder := newSourceFilesystemsBuilder(p, logger, b) 382 sourceFilesystems, err := builder.Build() 383 if err != nil { 384 return nil, errors.Wrap(err, "build filesystems") 385 } 386 387 b.SourceFilesystems = sourceFilesystems 388 b.theBigFs = builder.theBigFs 389 390 return b, nil 391 } 392 393 type sourceFilesystemsBuilder struct { 394 logger loggers.Logger 395 p *paths.Paths 396 sourceFs afero.Fs 397 result *SourceFilesystems 398 theBigFs *filesystemsCollector 399 } 400 401 func newSourceFilesystemsBuilder(p *paths.Paths, logger loggers.Logger, b *BaseFs) *sourceFilesystemsBuilder { 402 sourceFs := hugofs.NewBaseFileDecorator(p.Fs.Source) 403 return &sourceFilesystemsBuilder{p: p, logger: logger, sourceFs: sourceFs, theBigFs: b.theBigFs, result: &SourceFilesystems{}} 404 } 405 406 func (b *sourceFilesystemsBuilder) newSourceFilesystem(name string, fs afero.Fs, dirs []hugofs.FileMetaInfo) *SourceFilesystem { 407 return &SourceFilesystem{ 408 Name: name, 409 Fs: fs, 410 Dirs: dirs, 411 } 412 } 413 414 func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { 415 if b.theBigFs == nil { 416 417 theBigFs, err := b.createMainOverlayFs(b.p) 418 if err != nil { 419 return nil, errors.Wrap(err, "create main fs") 420 } 421 422 b.theBigFs = theBigFs 423 } 424 425 createView := func(componentID string) *SourceFilesystem { 426 if b.theBigFs == nil || b.theBigFs.overlayMounts == nil { 427 return b.newSourceFilesystem(componentID, hugofs.NoOpFs, nil) 428 } 429 430 dirs := b.theBigFs.overlayDirs[componentID] 431 432 return b.newSourceFilesystem(componentID, afero.NewBasePathFs(b.theBigFs.overlayMounts, componentID), dirs) 433 } 434 435 b.theBigFs.finalizeDirs() 436 437 b.result.Archetypes = createView(files.ComponentFolderArchetypes) 438 b.result.Layouts = createView(files.ComponentFolderLayouts) 439 b.result.Assets = createView(files.ComponentFolderAssets) 440 b.result.ResourcesCache = b.theBigFs.overlayResources 441 442 // Data, i18n and content cannot use the overlay fs 443 dataDirs := b.theBigFs.overlayDirs[files.ComponentFolderData] 444 dataFs, err := hugofs.NewSliceFs(dataDirs...) 445 if err != nil { 446 return nil, err 447 } 448 449 b.result.Data = b.newSourceFilesystem(files.ComponentFolderData, dataFs, dataDirs) 450 451 i18nDirs := b.theBigFs.overlayDirs[files.ComponentFolderI18n] 452 i18nFs, err := hugofs.NewSliceFs(i18nDirs...) 453 if err != nil { 454 return nil, err 455 } 456 b.result.I18n = b.newSourceFilesystem(files.ComponentFolderI18n, i18nFs, i18nDirs) 457 458 contentDirs := b.theBigFs.overlayDirs[files.ComponentFolderContent] 459 contentBfs := afero.NewBasePathFs(b.theBigFs.overlayMountsContent, files.ComponentFolderContent) 460 461 contentFs, err := hugofs.NewLanguageFs(b.p.LanguagesDefaultFirst.AsOrdinalSet(), contentBfs) 462 if err != nil { 463 return nil, errors.Wrap(err, "create content filesystem") 464 } 465 466 b.result.Content = b.newSourceFilesystem(files.ComponentFolderContent, contentFs, contentDirs) 467 468 b.result.Work = afero.NewReadOnlyFs(b.theBigFs.overlayFull) 469 470 // Create static filesystem(s) 471 ms := make(map[string]*SourceFilesystem) 472 b.result.Static = ms 473 b.result.StaticDirs = b.theBigFs.overlayDirs[files.ComponentFolderStatic] 474 475 if b.theBigFs.staticPerLanguage != nil { 476 // Multihost mode 477 for k, v := range b.theBigFs.staticPerLanguage { 478 sfs := b.newSourceFilesystem(files.ComponentFolderStatic, v, b.result.StaticDirs) 479 sfs.PublishFolder = k 480 ms[k] = sfs 481 } 482 } else { 483 bfs := afero.NewBasePathFs(b.theBigFs.overlayMountsStatic, files.ComponentFolderStatic) 484 ms[""] = b.newSourceFilesystem(files.ComponentFolderStatic, bfs, b.result.StaticDirs) 485 } 486 487 return b.result, nil 488 } 489 490 func (b *sourceFilesystemsBuilder) createMainOverlayFs(p *paths.Paths) (*filesystemsCollector, error) { 491 var staticFsMap map[string]afero.Fs 492 if b.p.Cfg.GetBool("multihost") { 493 staticFsMap = make(map[string]afero.Fs) 494 } 495 496 collector := &filesystemsCollector{ 497 sourceProject: b.sourceFs, 498 sourceModules: hugofs.NewNoSymlinkFs(b.sourceFs, b.logger, false), 499 overlayDirs: make(map[string][]hugofs.FileMetaInfo), 500 staticPerLanguage: staticFsMap, 501 } 502 503 mods := p.AllModules 504 505 if len(mods) == 0 { 506 return collector, nil 507 } 508 509 modsReversed := make([]mountsDescriptor, len(mods)) 510 511 // The theme components are ordered from left to right. 512 // We need to revert it to get the 513 // overlay logic below working as expected, with the project on top. 514 j := 0 515 for i := len(mods) - 1; i >= 0; i-- { 516 mod := mods[i] 517 dir := mod.Dir() 518 519 isMainProject := mod.Owner() == nil 520 modsReversed[j] = mountsDescriptor{ 521 Module: mod, 522 dir: dir, 523 isMainProject: isMainProject, 524 } 525 j++ 526 } 527 528 err := b.createOverlayFs(collector, modsReversed) 529 530 return collector, err 531 } 532 533 func (b *sourceFilesystemsBuilder) isContentMount(mnt modules.Mount) bool { 534 return strings.HasPrefix(mnt.Target, files.ComponentFolderContent) 535 } 536 537 func (b *sourceFilesystemsBuilder) isStaticMount(mnt modules.Mount) bool { 538 return strings.HasPrefix(mnt.Target, files.ComponentFolderStatic) 539 } 540 541 func (b *sourceFilesystemsBuilder) createModFs( 542 collector *filesystemsCollector, 543 md mountsDescriptor) error { 544 var ( 545 fromTo []hugofs.RootMapping 546 fromToContent []hugofs.RootMapping 547 fromToStatic []hugofs.RootMapping 548 ) 549 550 absPathify := func(path string) (string, string) { 551 if filepath.IsAbs(path) { 552 return "", path 553 } 554 return md.dir, paths.AbsPathify(md.dir, path) 555 } 556 557 for _, mount := range md.Mounts() { 558 559 mountWeight := 1 560 if md.isMainProject { 561 mountWeight++ 562 } 563 564 base, filename := absPathify(mount.Source) 565 566 rm := hugofs.RootMapping{ 567 From: mount.Target, 568 To: filename, 569 ToBasedir: base, 570 Module: md.Module.Path(), 571 Meta: &hugofs.FileMeta{ 572 Watch: md.Watch(), 573 Weight: mountWeight, 574 Classifier: files.ContentClassContent, 575 }, 576 } 577 578 isContentMount := b.isContentMount(mount) 579 580 lang := mount.Lang 581 if lang == "" && isContentMount { 582 lang = b.p.DefaultContentLanguage 583 } 584 585 rm.Meta.Lang = lang 586 587 if isContentMount { 588 fromToContent = append(fromToContent, rm) 589 } else if b.isStaticMount(mount) { 590 fromToStatic = append(fromToStatic, rm) 591 } else { 592 fromTo = append(fromTo, rm) 593 } 594 } 595 596 modBase := collector.sourceProject 597 if !md.isMainProject { 598 modBase = collector.sourceModules 599 } 600 sourceStatic := hugofs.NewNoSymlinkFs(modBase, b.logger, true) 601 602 rmfs, err := hugofs.NewRootMappingFs(modBase, fromTo...) 603 if err != nil { 604 return err 605 } 606 rmfsContent, err := hugofs.NewRootMappingFs(modBase, fromToContent...) 607 if err != nil { 608 return err 609 } 610 rmfsStatic, err := hugofs.NewRootMappingFs(sourceStatic, fromToStatic...) 611 if err != nil { 612 return err 613 } 614 615 // We need to keep the ordered list of directories for watching and 616 // some special merge operations (data, i18n). 617 collector.addDirs(rmfs) 618 collector.addDirs(rmfsContent) 619 collector.addDirs(rmfsStatic) 620 621 if collector.staticPerLanguage != nil { 622 for _, l := range b.p.Languages { 623 lang := l.Lang 624 625 lfs := rmfsStatic.Filter(func(rm hugofs.RootMapping) bool { 626 rlang := rm.Meta.Lang 627 return rlang == "" || rlang == lang 628 }) 629 630 bfs := afero.NewBasePathFs(lfs, files.ComponentFolderStatic) 631 632 sfs, found := collector.staticPerLanguage[lang] 633 if found { 634 collector.staticPerLanguage[lang] = afero.NewCopyOnWriteFs(sfs, bfs) 635 } else { 636 collector.staticPerLanguage[lang] = bfs 637 } 638 } 639 } 640 641 getResourcesDir := func() string { 642 if md.isMainProject { 643 return b.p.AbsResourcesDir 644 } 645 _, filename := absPathify(files.FolderResources) 646 return filename 647 } 648 649 if collector.overlayMounts == nil { 650 collector.overlayMounts = rmfs 651 collector.overlayMountsContent = rmfsContent 652 collector.overlayMountsStatic = rmfsStatic 653 collector.overlayFull = afero.NewBasePathFs(modBase, md.dir) 654 collector.overlayResources = afero.NewBasePathFs(modBase, getResourcesDir()) 655 } else { 656 657 collector.overlayMounts = afero.NewCopyOnWriteFs(collector.overlayMounts, rmfs) 658 collector.overlayMountsContent = hugofs.NewLanguageCompositeFs(collector.overlayMountsContent, rmfsContent) 659 collector.overlayMountsStatic = hugofs.NewLanguageCompositeFs(collector.overlayMountsStatic, rmfsStatic) 660 collector.overlayFull = afero.NewCopyOnWriteFs(collector.overlayFull, afero.NewBasePathFs(modBase, md.dir)) 661 collector.overlayResources = afero.NewCopyOnWriteFs(collector.overlayResources, afero.NewBasePathFs(modBase, getResourcesDir())) 662 } 663 664 return nil 665 } 666 667 func printFs(fs afero.Fs, path string, w io.Writer) { 668 if fs == nil { 669 return 670 } 671 afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error { 672 if err != nil { 673 return err 674 } 675 if info.IsDir() { 676 return nil 677 } 678 var filename string 679 if fim, ok := info.(hugofs.FileMetaInfo); ok { 680 filename = fim.Meta().Filename 681 } 682 fmt.Fprintf(w, " %q %q\n", path, filename) 683 return nil 684 }) 685 } 686 687 type filesystemsCollector struct { 688 sourceProject afero.Fs // Source for project folders 689 sourceModules afero.Fs // Source for modules/themes 690 691 overlayMounts afero.Fs 692 overlayMountsContent afero.Fs 693 overlayMountsStatic afero.Fs 694 overlayFull afero.Fs 695 overlayResources afero.Fs 696 697 // Maps component type (layouts, static, content etc.) an ordered list of 698 // directories representing the overlay filesystems above. 699 overlayDirs map[string][]hugofs.FileMetaInfo 700 701 // Set if in multihost mode 702 staticPerLanguage map[string]afero.Fs 703 704 finalizerInit sync.Once 705 } 706 707 func (c *filesystemsCollector) addDirs(rfs *hugofs.RootMappingFs) { 708 for _, componentFolder := range files.ComponentFolders { 709 c.addDir(rfs, componentFolder) 710 } 711 } 712 713 func (c *filesystemsCollector) addDir(rfs *hugofs.RootMappingFs, componentFolder string) { 714 dirs, err := rfs.Dirs(componentFolder) 715 716 if err == nil { 717 c.overlayDirs[componentFolder] = append(c.overlayDirs[componentFolder], dirs...) 718 } 719 } 720 721 func (c *filesystemsCollector) finalizeDirs() { 722 c.finalizerInit.Do(func() { 723 // Order the directories from top to bottom (project, theme a, theme ...). 724 for _, dirs := range c.overlayDirs { 725 c.reverseFis(dirs) 726 } 727 }) 728 } 729 730 func (c *filesystemsCollector) reverseFis(fis []hugofs.FileMetaInfo) { 731 for i := len(fis)/2 - 1; i >= 0; i-- { 732 opp := len(fis) - 1 - i 733 fis[i], fis[opp] = fis[opp], fis[i] 734 } 735 } 736 737 type mountsDescriptor struct { 738 modules.Module 739 dir string 740 isMainProject bool 741 } 742 743 func (b *sourceFilesystemsBuilder) createOverlayFs(collector *filesystemsCollector, mounts []mountsDescriptor) error { 744 if len(mounts) == 0 { 745 return nil 746 } 747 748 err := b.createModFs(collector, mounts[0]) 749 if err != nil { 750 return err 751 } 752 753 if len(mounts) == 1 { 754 return nil 755 } 756 757 return b.createOverlayFs(collector, mounts[1:]) 758 }