github.com/rabbouni145/gg@v0.47.1/hugolib/filesystems/basefs.go (about) 1 // Copyright 2018 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 // Package filesystems provides the fine grained file systems used by Hugo. These 15 // are typically virtual filesystems that are composites of project and theme content. 16 package filesystems 17 18 import ( 19 "errors" 20 "io" 21 "os" 22 "path/filepath" 23 "strings" 24 25 "github.com/gohugoio/hugo/config" 26 27 "github.com/gohugoio/hugo/hugofs" 28 29 "fmt" 30 31 "github.com/gohugoio/hugo/hugolib/paths" 32 "github.com/gohugoio/hugo/langs" 33 "github.com/spf13/afero" 34 ) 35 36 // When we create a virtual filesystem with data and i18n bundles for the project and the themes, 37 // this is the name of the project's virtual root. It got it's funky name to make sure 38 // (or very unlikely) that it collides with a theme name. 39 const projectVirtualFolder = "__h__project" 40 41 var filePathSeparator = string(filepath.Separator) 42 43 // BaseFs contains the core base filesystems used by Hugo. The name "base" is used 44 // to underline that even if they can be composites, they all have a base path set to a specific 45 // resource folder, e.g "/my-project/content". So, no absolute filenames needed. 46 type BaseFs struct { 47 48 // SourceFilesystems contains the different source file systems. 49 *SourceFilesystems 50 51 // The filesystem used to publish the rendered site. 52 // This usually maps to /my-project/public. 53 PublishFs afero.Fs 54 55 themeFs afero.Fs 56 57 // TODO(bep) improve the "theme interaction" 58 AbsThemeDirs []string 59 } 60 61 // RelContentDir tries to create a path relative to the content root from 62 // the given filename. The return value is the path and language code. 63 func (b *BaseFs) RelContentDir(filename string) string { 64 for _, dirname := range b.SourceFilesystems.Content.Dirnames { 65 if strings.HasPrefix(filename, dirname) { 66 rel := strings.TrimPrefix(filename, dirname) 67 return strings.TrimPrefix(rel, filePathSeparator) 68 } 69 } 70 // Either not a content dir or already relative. 71 return filename 72 } 73 74 // SourceFilesystems contains the different source file systems. These can be 75 // composite file systems (theme and project etc.), and they have all root 76 // set to the source type the provides: data, i18n, static, layouts. 77 type SourceFilesystems struct { 78 Content *SourceFilesystem 79 Data *SourceFilesystem 80 I18n *SourceFilesystem 81 Layouts *SourceFilesystem 82 Archetypes *SourceFilesystem 83 Assets *SourceFilesystem 84 Resources *SourceFilesystem 85 86 // This is a unified read-only view of the project's and themes' workdir. 87 Work *SourceFilesystem 88 89 // When in multihost we have one static filesystem per language. The sync 90 // static files is currently done outside of the Hugo build (where there is 91 // a concept of a site per language). 92 // When in non-multihost mode there will be one entry in this map with a blank key. 93 Static map[string]*SourceFilesystem 94 } 95 96 // A SourceFilesystem holds the filesystem for a given source type in Hugo (data, 97 // i18n, layouts, static) and additional metadata to be able to use that filesystem 98 // in server mode. 99 type SourceFilesystem struct { 100 // This is a virtual composite filesystem. It expects path relative to a context. 101 Fs afero.Fs 102 103 // This is the base source filesystem. In real Hugo, this will be the OS filesystem. 104 // Use this if you need to resolve items in Dirnames below. 105 SourceFs afero.Fs 106 107 // Dirnames is absolute filenames to the directories in this filesystem. 108 Dirnames []string 109 110 // When syncing a source folder to the target (e.g. /public), this may 111 // be set to publish into a subfolder. This is used for static syncing 112 // in multihost mode. 113 PublishFolder string 114 } 115 116 // ContentStaticAssetFs will create a new composite filesystem from the content, 117 // static, and asset filesystems. The site language is needed to pick the correct static filesystem. 118 // The order is content, static and then assets. 119 // TODO(bep) check usage 120 func (s SourceFilesystems) ContentStaticAssetFs(lang string) afero.Fs { 121 staticFs := s.StaticFs(lang) 122 123 base := afero.NewCopyOnWriteFs(s.Assets.Fs, staticFs) 124 return afero.NewCopyOnWriteFs(base, s.Content.Fs) 125 126 } 127 128 // StaticFs returns the static filesystem for the given language. 129 // This can be a composite filesystem. 130 func (s SourceFilesystems) StaticFs(lang string) afero.Fs { 131 var staticFs afero.Fs = hugofs.NoOpFs 132 133 if fs, ok := s.Static[lang]; ok { 134 staticFs = fs.Fs 135 } else if fs, ok := s.Static[""]; ok { 136 staticFs = fs.Fs 137 } 138 139 return staticFs 140 } 141 142 // StatResource looks for a resource in these filesystems in order: static, assets and finally content. 143 // If found in any of them, it returns FileInfo and the relevant filesystem. 144 // Any non os.IsNotExist error will be returned. 145 // An os.IsNotExist error wil be returned only if all filesystems return such an error. 146 // Note that if we only wanted to find the file, we could create a composite Afero fs, 147 // but we also need to know which filesystem root it lives in. 148 func (s SourceFilesystems) StatResource(lang, filename string) (fi os.FileInfo, fs afero.Fs, err error) { 149 for _, fsToCheck := range []afero.Fs{s.StaticFs(lang), s.Assets.Fs, s.Content.Fs} { 150 fs = fsToCheck 151 fi, err = fs.Stat(filename) 152 if err == nil || !os.IsNotExist(err) { 153 return 154 } 155 } 156 // Not found. 157 return 158 } 159 160 // IsStatic returns true if the given filename is a member of one of the static 161 // filesystems. 162 func (s SourceFilesystems) IsStatic(filename string) bool { 163 for _, staticFs := range s.Static { 164 if staticFs.Contains(filename) { 165 return true 166 } 167 } 168 return false 169 } 170 171 // IsContent returns true if the given filename is a member of the content filesystem. 172 func (s SourceFilesystems) IsContent(filename string) bool { 173 return s.Content.Contains(filename) 174 } 175 176 // IsLayout returns true if the given filename is a member of the layouts filesystem. 177 func (s SourceFilesystems) IsLayout(filename string) bool { 178 return s.Layouts.Contains(filename) 179 } 180 181 // IsData returns true if the given filename is a member of the data filesystem. 182 func (s SourceFilesystems) IsData(filename string) bool { 183 return s.Data.Contains(filename) 184 } 185 186 // IsAsset returns true if the given filename is a member of the data filesystem. 187 func (s SourceFilesystems) IsAsset(filename string) bool { 188 return s.Assets.Contains(filename) 189 } 190 191 // IsI18n returns true if the given filename is a member of the i18n filesystem. 192 func (s SourceFilesystems) IsI18n(filename string) bool { 193 return s.I18n.Contains(filename) 194 } 195 196 // MakeStaticPathRelative makes an absolute static filename into a relative one. 197 // It will return an empty string if the filename is not a member of a static filesystem. 198 func (s SourceFilesystems) MakeStaticPathRelative(filename string) string { 199 for _, staticFs := range s.Static { 200 rel := staticFs.MakePathRelative(filename) 201 if rel != "" { 202 return rel 203 } 204 } 205 return "" 206 } 207 208 // MakePathRelative creates a relative path from the given filename. 209 // It will return an empty string if the filename is not a member of this filesystem. 210 func (d *SourceFilesystem) MakePathRelative(filename string) string { 211 for _, currentPath := range d.Dirnames { 212 if strings.HasPrefix(filename, currentPath) { 213 return strings.TrimPrefix(filename, currentPath) 214 } 215 } 216 return "" 217 } 218 219 func (d *SourceFilesystem) RealFilename(rel string) string { 220 fi, err := d.Fs.Stat(rel) 221 if err != nil { 222 return rel 223 } 224 if realfi, ok := fi.(hugofs.RealFilenameInfo); ok { 225 return realfi.RealFilename() 226 } 227 228 return rel 229 } 230 231 // Contains returns whether the given filename is a member of the current filesystem. 232 func (d *SourceFilesystem) Contains(filename string) bool { 233 for _, dir := range d.Dirnames { 234 if strings.HasPrefix(filename, dir) { 235 return true 236 } 237 } 238 return false 239 } 240 241 // RealDirs gets a list of absolute paths to directories starting from the given 242 // path. 243 func (d *SourceFilesystem) RealDirs(from string) []string { 244 var dirnames []string 245 for _, dir := range d.Dirnames { 246 dirname := filepath.Join(dir, from) 247 if _, err := d.SourceFs.Stat(dirname); err == nil { 248 dirnames = append(dirnames, dirname) 249 } 250 } 251 return dirnames 252 } 253 254 // WithBaseFs allows reuse of some potentially expensive to create parts that remain 255 // the same across sites/languages. 256 func WithBaseFs(b *BaseFs) func(*BaseFs) error { 257 return func(bb *BaseFs) error { 258 bb.themeFs = b.themeFs 259 bb.AbsThemeDirs = b.AbsThemeDirs 260 return nil 261 } 262 } 263 264 func newRealBase(base afero.Fs) afero.Fs { 265 return hugofs.NewBasePathRealFilenameFs(base.(*afero.BasePathFs)) 266 267 } 268 269 // NewBase builds the filesystems used by Hugo given the paths and options provided.NewBase 270 func NewBase(p *paths.Paths, options ...func(*BaseFs) error) (*BaseFs, error) { 271 fs := p.Fs 272 273 publishFs := afero.NewBasePathFs(fs.Destination, p.AbsPublishDir) 274 275 contentFs, absContentDirs, err := createContentFs(fs.Source, p.WorkingDir, p.DefaultContentLanguage, p.Languages) 276 if err != nil { 277 return nil, err 278 } 279 280 // Make sure we don't have any overlapping content dirs. That will never work. 281 for i, d1 := range absContentDirs { 282 for j, d2 := range absContentDirs { 283 if i == j { 284 continue 285 } 286 if strings.HasPrefix(d1, d2) || strings.HasPrefix(d2, d1) { 287 return nil, fmt.Errorf("found overlapping content dirs (%q and %q)", d1, d2) 288 } 289 } 290 } 291 292 b := &BaseFs{ 293 PublishFs: publishFs, 294 } 295 296 for _, opt := range options { 297 if err := opt(b); err != nil { 298 return nil, err 299 } 300 } 301 302 builder := newSourceFilesystemsBuilder(p, b) 303 sourceFilesystems, err := builder.Build() 304 if err != nil { 305 return nil, err 306 } 307 308 sourceFilesystems.Content = &SourceFilesystem{ 309 SourceFs: fs.Source, 310 Fs: contentFs, 311 Dirnames: absContentDirs, 312 } 313 314 b.SourceFilesystems = sourceFilesystems 315 b.themeFs = builder.themeFs 316 b.AbsThemeDirs = builder.absThemeDirs 317 318 return b, nil 319 } 320 321 type sourceFilesystemsBuilder struct { 322 p *paths.Paths 323 result *SourceFilesystems 324 themeFs afero.Fs 325 hasTheme bool 326 absThemeDirs []string 327 } 328 329 func newSourceFilesystemsBuilder(p *paths.Paths, b *BaseFs) *sourceFilesystemsBuilder { 330 return &sourceFilesystemsBuilder{p: p, themeFs: b.themeFs, absThemeDirs: b.AbsThemeDirs, result: &SourceFilesystems{}} 331 } 332 333 func (b *sourceFilesystemsBuilder) Build() (*SourceFilesystems, error) { 334 if b.themeFs == nil && b.p.ThemeSet() { 335 themeFs, absThemeDirs, err := createThemesOverlayFs(b.p) 336 if err != nil { 337 return nil, err 338 } 339 if themeFs == nil { 340 panic("createThemesFs returned nil") 341 } 342 b.themeFs = themeFs 343 b.absThemeDirs = absThemeDirs 344 345 } 346 347 b.hasTheme = len(b.absThemeDirs) > 0 348 349 sfs, err := b.createRootMappingFs("dataDir", "data") 350 if err != nil { 351 return nil, err 352 } 353 b.result.Data = sfs 354 355 sfs, err = b.createRootMappingFs("i18nDir", "i18n") 356 if err != nil { 357 return nil, err 358 } 359 b.result.I18n = sfs 360 361 sfs, err = b.createFs(false, true, "layoutDir", "layouts") 362 if err != nil { 363 return nil, err 364 } 365 b.result.Layouts = sfs 366 367 sfs, err = b.createFs(false, true, "archetypeDir", "archetypes") 368 if err != nil { 369 return nil, err 370 } 371 b.result.Archetypes = sfs 372 373 sfs, err = b.createFs(false, true, "assetDir", "assets") 374 if err != nil { 375 return nil, err 376 } 377 b.result.Assets = sfs 378 379 sfs, err = b.createFs(true, false, "resourceDir", "resources") 380 if err != nil { 381 return nil, err 382 } 383 384 b.result.Resources = sfs 385 386 err = b.createStaticFs() 387 388 sfs, err = b.createFs(false, true, "", "") 389 if err != nil { 390 return nil, err 391 } 392 b.result.Work = sfs 393 394 err = b.createStaticFs() 395 if err != nil { 396 return nil, err 397 } 398 399 return b.result, nil 400 } 401 402 func (b *sourceFilesystemsBuilder) createFs( 403 mkdir bool, 404 readOnly bool, 405 dirKey, themeFolder string) (*SourceFilesystem, error) { 406 s := &SourceFilesystem{ 407 SourceFs: b.p.Fs.Source, 408 } 409 410 if themeFolder == "" { 411 themeFolder = filePathSeparator 412 } 413 414 var dir string 415 if dirKey != "" { 416 dir = b.p.Cfg.GetString(dirKey) 417 if dir == "" { 418 return s, fmt.Errorf("config %q not set", dirKey) 419 } 420 } 421 422 var fs afero.Fs 423 424 absDir := b.p.AbsPathify(dir) 425 existsInSource := b.existsInSource(absDir) 426 if !existsInSource && mkdir { 427 // We really need this directory. Make it. 428 if err := b.p.Fs.Source.MkdirAll(absDir, 0777); err == nil { 429 existsInSource = true 430 } 431 } 432 if existsInSource { 433 fs = newRealBase(afero.NewBasePathFs(b.p.Fs.Source, absDir)) 434 s.Dirnames = []string{absDir} 435 } 436 437 if b.hasTheme { 438 themeFolderFs := newRealBase(afero.NewBasePathFs(b.themeFs, themeFolder)) 439 if fs == nil { 440 fs = themeFolderFs 441 } else { 442 fs = afero.NewCopyOnWriteFs(themeFolderFs, fs) 443 } 444 445 for _, absThemeDir := range b.absThemeDirs { 446 absThemeFolderDir := filepath.Join(absThemeDir, themeFolder) 447 if b.existsInSource(absThemeFolderDir) { 448 s.Dirnames = append(s.Dirnames, absThemeFolderDir) 449 } 450 } 451 } 452 453 if fs == nil { 454 s.Fs = hugofs.NoOpFs 455 } else if readOnly { 456 s.Fs = afero.NewReadOnlyFs(fs) 457 } else { 458 s.Fs = fs 459 } 460 461 return s, nil 462 } 463 464 // Used for data, i18n -- we cannot use overlay filsesystems for those, but we need 465 // to keep a strict order. 466 func (b *sourceFilesystemsBuilder) createRootMappingFs(dirKey, themeFolder string) (*SourceFilesystem, error) { 467 s := &SourceFilesystem{ 468 SourceFs: b.p.Fs.Source, 469 } 470 471 projectDir := b.p.Cfg.GetString(dirKey) 472 if projectDir == "" { 473 return nil, fmt.Errorf("config %q not set", dirKey) 474 } 475 476 var fromTo []string 477 to := b.p.AbsPathify(projectDir) 478 479 if b.existsInSource(to) { 480 s.Dirnames = []string{to} 481 fromTo = []string{projectVirtualFolder, to} 482 } 483 484 for _, theme := range b.p.AllThemes { 485 to := b.p.AbsPathify(filepath.Join(b.p.ThemesDir, theme.Name, themeFolder)) 486 if b.existsInSource(to) { 487 s.Dirnames = append(s.Dirnames, to) 488 from := theme 489 fromTo = append(fromTo, from.Name, to) 490 } 491 } 492 493 if len(fromTo) == 0 { 494 s.Fs = hugofs.NoOpFs 495 return s, nil 496 } 497 498 fs, err := hugofs.NewRootMappingFs(b.p.Fs.Source, fromTo...) 499 if err != nil { 500 return nil, err 501 } 502 503 s.Fs = afero.NewReadOnlyFs(fs) 504 505 return s, nil 506 } 507 508 func (b *sourceFilesystemsBuilder) existsInSource(abspath string) bool { 509 exists, _ := afero.Exists(b.p.Fs.Source, abspath) 510 return exists 511 } 512 513 func (b *sourceFilesystemsBuilder) createStaticFs() error { 514 isMultihost := b.p.Cfg.GetBool("multihost") 515 ms := make(map[string]*SourceFilesystem) 516 b.result.Static = ms 517 518 if isMultihost { 519 for _, l := range b.p.Languages { 520 s := &SourceFilesystem{ 521 SourceFs: b.p.Fs.Source, 522 PublishFolder: l.Lang} 523 staticDirs := removeDuplicatesKeepRight(getStaticDirs(l)) 524 if len(staticDirs) == 0 { 525 continue 526 } 527 528 for _, dir := range staticDirs { 529 absDir := b.p.AbsPathify(dir) 530 if !b.existsInSource(absDir) { 531 continue 532 } 533 534 s.Dirnames = append(s.Dirnames, absDir) 535 } 536 537 fs, err := createOverlayFs(b.p.Fs.Source, s.Dirnames) 538 if err != nil { 539 return err 540 } 541 542 if b.hasTheme { 543 themeFolder := "static" 544 fs = afero.NewCopyOnWriteFs(newRealBase(afero.NewBasePathFs(b.themeFs, themeFolder)), fs) 545 for _, absThemeDir := range b.absThemeDirs { 546 s.Dirnames = append(s.Dirnames, filepath.Join(absThemeDir, themeFolder)) 547 } 548 } 549 550 s.Fs = fs 551 ms[l.Lang] = s 552 553 } 554 555 return nil 556 } 557 558 s := &SourceFilesystem{ 559 SourceFs: b.p.Fs.Source, 560 } 561 562 var staticDirs []string 563 564 for _, l := range b.p.Languages { 565 staticDirs = append(staticDirs, getStaticDirs(l)...) 566 } 567 568 staticDirs = removeDuplicatesKeepRight(staticDirs) 569 if len(staticDirs) == 0 { 570 return nil 571 } 572 573 for _, dir := range staticDirs { 574 absDir := b.p.AbsPathify(dir) 575 if !b.existsInSource(absDir) { 576 continue 577 } 578 s.Dirnames = append(s.Dirnames, absDir) 579 } 580 581 fs, err := createOverlayFs(b.p.Fs.Source, s.Dirnames) 582 if err != nil { 583 return err 584 } 585 586 if b.hasTheme { 587 themeFolder := "static" 588 fs = afero.NewCopyOnWriteFs(newRealBase(afero.NewBasePathFs(b.themeFs, themeFolder)), fs) 589 for _, absThemeDir := range b.absThemeDirs { 590 s.Dirnames = append(s.Dirnames, filepath.Join(absThemeDir, themeFolder)) 591 } 592 } 593 594 s.Fs = fs 595 ms[""] = s 596 597 return nil 598 } 599 600 func getStaticDirs(cfg config.Provider) []string { 601 var staticDirs []string 602 for i := -1; i <= 10; i++ { 603 staticDirs = append(staticDirs, getStringOrStringSlice(cfg, "staticDir", i)...) 604 } 605 return staticDirs 606 } 607 608 func getStringOrStringSlice(cfg config.Provider, key string, id int) []string { 609 610 if id >= 0 { 611 key = fmt.Sprintf("%s%d", key, id) 612 } 613 614 return config.GetStringSlicePreserveString(cfg, key) 615 616 } 617 618 func createContentFs(fs afero.Fs, 619 workingDir, 620 defaultContentLanguage string, 621 languages langs.Languages) (afero.Fs, []string, error) { 622 623 var contentLanguages langs.Languages 624 var contentDirSeen = make(map[string]bool) 625 languageSet := make(map[string]bool) 626 627 // The default content language needs to be first. 628 for _, language := range languages { 629 if language.Lang == defaultContentLanguage { 630 contentLanguages = append(contentLanguages, language) 631 contentDirSeen[language.ContentDir] = true 632 } 633 languageSet[language.Lang] = true 634 } 635 636 for _, language := range languages { 637 if contentDirSeen[language.ContentDir] { 638 continue 639 } 640 if language.ContentDir == "" { 641 language.ContentDir = defaultContentLanguage 642 } 643 contentDirSeen[language.ContentDir] = true 644 contentLanguages = append(contentLanguages, language) 645 646 } 647 648 var absContentDirs []string 649 650 fs, err := createContentOverlayFs(fs, workingDir, contentLanguages, languageSet, &absContentDirs) 651 return fs, absContentDirs, err 652 653 } 654 655 func createContentOverlayFs(source afero.Fs, 656 workingDir string, 657 languages langs.Languages, 658 languageSet map[string]bool, 659 absContentDirs *[]string) (afero.Fs, error) { 660 if len(languages) == 0 { 661 return source, nil 662 } 663 664 language := languages[0] 665 666 contentDir := language.ContentDir 667 if contentDir == "" { 668 panic("missing contentDir") 669 } 670 671 absContentDir := paths.AbsPathify(workingDir, language.ContentDir) 672 if !strings.HasSuffix(absContentDir, paths.FilePathSeparator) { 673 absContentDir += paths.FilePathSeparator 674 } 675 676 // If root, remove the second '/' 677 if absContentDir == "//" { 678 absContentDir = paths.FilePathSeparator 679 } 680 681 if len(absContentDir) < 6 { 682 return nil, fmt.Errorf("invalid content dir %q: Path is too short", absContentDir) 683 } 684 685 *absContentDirs = append(*absContentDirs, absContentDir) 686 687 overlay := hugofs.NewLanguageFs(language.Lang, languageSet, afero.NewBasePathFs(source, absContentDir)) 688 if len(languages) == 1 { 689 return overlay, nil 690 } 691 692 base, err := createContentOverlayFs(source, workingDir, languages[1:], languageSet, absContentDirs) 693 if err != nil { 694 return nil, err 695 } 696 697 return hugofs.NewLanguageCompositeFs(base, overlay), nil 698 699 } 700 701 func createThemesOverlayFs(p *paths.Paths) (afero.Fs, []string, error) { 702 703 themes := p.AllThemes 704 705 if len(themes) == 0 { 706 panic("AllThemes not set") 707 } 708 709 themesDir := p.AbsPathify(p.ThemesDir) 710 if themesDir == "" { 711 return nil, nil, errors.New("no themes dir set") 712 } 713 714 absPaths := make([]string, len(themes)) 715 716 // The themes are ordered from left to right. We need to revert it to get the 717 // overlay logic below working as expected. 718 for i := 0; i < len(themes); i++ { 719 absPaths[i] = filepath.Join(themesDir, themes[len(themes)-1-i].Name) 720 } 721 722 fs, err := createOverlayFs(p.Fs.Source, absPaths) 723 724 return fs, absPaths, err 725 726 } 727 728 func createOverlayFs(source afero.Fs, absPaths []string) (afero.Fs, error) { 729 if len(absPaths) == 0 { 730 return hugofs.NoOpFs, nil 731 } 732 733 if len(absPaths) == 1 { 734 return afero.NewReadOnlyFs(newRealBase(afero.NewBasePathFs(source, absPaths[0]))), nil 735 } 736 737 base := afero.NewReadOnlyFs(newRealBase(afero.NewBasePathFs(source, absPaths[0]))) 738 overlay, err := createOverlayFs(source, absPaths[1:]) 739 if err != nil { 740 return nil, err 741 } 742 743 return afero.NewCopyOnWriteFs(base, overlay), nil 744 } 745 746 func removeDuplicatesKeepRight(in []string) []string { 747 seen := make(map[string]bool) 748 var out []string 749 for i := len(in) - 1; i >= 0; i-- { 750 v := in[i] 751 if seen[v] { 752 continue 753 } 754 out = append([]string{v}, out...) 755 seen[v] = true 756 } 757 758 return out 759 } 760 761 func printFs(fs afero.Fs, path string, w io.Writer) { 762 if fs == nil { 763 return 764 } 765 afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error { 766 if info != nil && !info.IsDir() { 767 s := path 768 if lang, ok := info.(hugofs.LanguageAnnouncer); ok { 769 s = s + "\tLANG: " + lang.Lang() 770 } 771 if fp, ok := info.(hugofs.FilePather); ok { 772 s = s + "\tRF: " + fp.Filename() + "\tBP: " + fp.BaseDir() 773 } 774 fmt.Fprintln(w, " ", s) 775 } 776 return nil 777 }) 778 }