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