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