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