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