rootmapping_fs.go - hugo - [fork] hugo port for 9front
HTML git clone https://git.drkhsh.at/hugo.git
DIR Log
DIR Files
DIR Refs
DIR Submodules
DIR README
DIR LICENSE
---
rootmapping_fs.go (20514B)
---
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 hugofs
15
16 import (
17 "errors"
18 "fmt"
19 iofs "io/fs"
20 "os"
21 "path"
22 "path/filepath"
23 "strings"
24 "sync/atomic"
25
26 "github.com/gohugoio/hugo/common/herrors"
27 "github.com/gohugoio/hugo/common/paths"
28
29 "github.com/bep/overlayfs"
30 "github.com/gohugoio/hugo/hugofs/files"
31 "github.com/gohugoio/hugo/hugofs/glob"
32
33 radix "github.com/armon/go-radix"
34 "github.com/spf13/afero"
35 )
36
37 var filepathSeparator = string(filepath.Separator)
38
39 var _ ReverseLookupProvder = (*RootMappingFs)(nil)
40
41 // NewRootMappingFs creates a new RootMappingFs on top of the provided with
42 // root mappings with some optional metadata about the root.
43 // Note that From represents a virtual root that maps to the actual filename in To.
44 func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) {
45 rootMapToReal := radix.New()
46 realMapToRoot := radix.New()
47 id := fmt.Sprintf("rfs-%d", rootMappingFsCounter.Add(1))
48
49 addMapping := func(key string, rm RootMapping, to *radix.Tree) {
50 var mappings []RootMapping
51 v, found := to.Get(key)
52 if found {
53 // There may be more than one language pointing to the same root.
54 mappings = v.([]RootMapping)
55 }
56 mappings = append(mappings, rm)
57 to.Insert(key, mappings)
58 }
59
60 for _, rm := range rms {
61 (&rm).clean()
62
63 rm.FromBase = files.ResolveComponentFolder(rm.From)
64
65 if len(rm.To) < 2 {
66 panic(fmt.Sprintf("invalid root mapping; from/to: %s/%s", rm.From, rm.To))
67 }
68
69 fi, err := fs.Stat(rm.To)
70 if err != nil {
71 if os.IsNotExist(err) {
72 continue
73 }
74 return nil, err
75 }
76
77 if rm.Meta == nil {
78 rm.Meta = NewFileMeta()
79 }
80
81 if rm.FromBase == "" {
82 panic(" rm.FromBase is empty")
83 }
84
85 rm.Meta.Component = rm.FromBase
86 rm.Meta.Module = rm.Module
87 rm.Meta.ModuleOrdinal = rm.ModuleOrdinal
88 rm.Meta.IsProject = rm.IsProject
89 rm.Meta.BaseDir = rm.ToBase
90
91 if !fi.IsDir() {
92 // We do allow single file mounts.
93 // However, the file system logic will be much simpler with just directories.
94 // So, convert this mount into a directory mount with a renamer,
95 // which will tell the caller if name should be included.
96 dirFrom, nameFrom := filepath.Split(rm.From)
97 dirTo, nameTo := filepath.Split(rm.To)
98 dirFrom, dirTo = strings.TrimSuffix(dirFrom, filepathSeparator), strings.TrimSuffix(dirTo, filepathSeparator)
99 rm.From = dirFrom
100 singleFileMeta := rm.Meta.Copy()
101 singleFileMeta.Name = nameFrom
102 rm.fiSingleFile = NewFileMetaInfo(fi, singleFileMeta)
103 rm.To = dirTo
104
105 rm.Meta.Rename = func(name string, toFrom bool) (string, bool) {
106 if toFrom {
107 if name == nameTo {
108 return nameFrom, true
109 }
110 return "", false
111 }
112
113 if name == nameFrom {
114 return nameTo, true
115 }
116
117 return "", false
118 }
119 nameToFilename := filepathSeparator + nameTo
120
121 rm.Meta.InclusionFilter = rm.Meta.InclusionFilter.Append(glob.NewFilenameFilterForInclusionFunc(
122 func(filename string) bool {
123 return nameToFilename == filename
124 },
125 ))
126
127 // Refresh the FileInfo object.
128 fi, err = fs.Stat(rm.To)
129 if err != nil {
130 if herrors.IsNotExist(err) {
131 continue
132 }
133 return nil, err
134 }
135 }
136
137 // Extract "blog" from "content/blog"
138 rm.path = strings.TrimPrefix(strings.TrimPrefix(rm.From, rm.FromBase), filepathSeparator)
139 rm.Meta.SourceRoot = fi.(MetaProvider).Meta().Filename
140
141 meta := rm.Meta.Copy()
142
143 if !fi.IsDir() {
144 _, name := filepath.Split(rm.From)
145 meta.Name = name
146 }
147
148 rm.fi = NewFileMetaInfo(fi, meta)
149
150 addMapping(filepathSeparator+rm.From, rm, rootMapToReal)
151 rev := rm.To
152 if !strings.HasPrefix(rev, filepathSeparator) {
153 rev = filepathSeparator + rev
154 }
155
156 addMapping(rev, rm, realMapToRoot)
157
158 }
159
160 rfs := &RootMappingFs{
161 id: id,
162 Fs: fs,
163 rootMapToReal: rootMapToReal,
164 realMapToRoot: realMapToRoot,
165 }
166
167 return rfs, nil
168 }
169
170 func newRootMappingFsFromFromTo(
171 baseDir string,
172 fs afero.Fs,
173 fromTo ...string,
174 ) (*RootMappingFs, error) {
175 rms := make([]RootMapping, len(fromTo)/2)
176 for i, j := 0, 0; j < len(fromTo); i, j = i+1, j+2 {
177 rms[i] = RootMapping{
178 From: fromTo[j],
179 To: fromTo[j+1],
180 ToBase: baseDir,
181 }
182 }
183
184 return NewRootMappingFs(fs, rms...)
185 }
186
187 // RootMapping describes a virtual file or directory mount.
188 type RootMapping struct {
189 From string // The virtual mount.
190 FromBase string // The base directory of the virtual mount.
191 To string // The source directory or file.
192 ToBase string // The base of To. May be empty if an absolute path was provided.
193 Module string // The module path/ID.
194 ModuleOrdinal int // The module ordinal starting with 0 which is the project.
195 IsProject bool // Whether this is a mount in the main project.
196 Meta *FileMeta // File metadata (lang etc.)
197
198 fi FileMetaInfo
199 fiSingleFile FileMetaInfo // Also set when this mounts represents a single file with a rename func.
200 path string // The virtual mount point, e.g. "blog".
201 }
202
203 type keyRootMappings struct {
204 key string
205 roots []RootMapping
206 }
207
208 func (rm *RootMapping) clean() {
209 rm.From = strings.Trim(filepath.Clean(rm.From), filepathSeparator)
210 rm.To = filepath.Clean(rm.To)
211 }
212
213 func (r RootMapping) filename(name string) string {
214 if name == "" {
215 return r.To
216 }
217 return filepath.Join(r.To, strings.TrimPrefix(name, r.From))
218 }
219
220 func (r RootMapping) trimFrom(name string) string {
221 if name == "" {
222 return ""
223 }
224 return strings.TrimPrefix(name, r.From)
225 }
226
227 var _ FilesystemUnwrapper = (*RootMappingFs)(nil)
228
229 // A RootMappingFs maps several roots into one. Note that the root of this filesystem
230 // is directories only, and they will be returned in Readdir and Readdirnames
231 // in the order given.
232 type RootMappingFs struct {
233 id string
234 afero.Fs
235 rootMapToReal *radix.Tree
236 realMapToRoot *radix.Tree
237 }
238
239 var rootMappingFsCounter atomic.Int32
240
241 func (fs *RootMappingFs) Mounts(base string) ([]FileMetaInfo, error) {
242 base = filepathSeparator + fs.cleanName(base)
243 roots := fs.getRootsWithPrefix(base)
244
245 if roots == nil {
246 return nil, nil
247 }
248
249 fss := make([]FileMetaInfo, 0, len(roots))
250 for _, r := range roots {
251 if r.fiSingleFile != nil {
252 // A single file mount.
253 fss = append(fss, r.fiSingleFile)
254 continue
255 }
256 bfs := NewBasePathFs(fs.Fs, r.To)
257 fs := bfs
258 if r.Meta.InclusionFilter != nil {
259 fs = newFilenameFilterFs(fs, r.To, r.Meta.InclusionFilter)
260 }
261 fs = decorateDirs(fs, r.Meta)
262 fi, err := fs.Stat("")
263 if err != nil {
264 continue
265 }
266 fss = append(fss, fi.(FileMetaInfo))
267 }
268
269 return fss, nil
270 }
271
272 func (fs *RootMappingFs) Key() string {
273 return fs.id
274 }
275
276 func (fs *RootMappingFs) UnwrapFilesystem() afero.Fs {
277 return fs.Fs
278 }
279
280 // Filter creates a copy of this filesystem with only mappings matching a filter.
281 func (fs RootMappingFs) Filter(f func(m RootMapping) bool) *RootMappingFs {
282 rootMapToReal := radix.New()
283 fs.rootMapToReal.Walk(func(b string, v any) bool {
284 rms := v.([]RootMapping)
285 var nrms []RootMapping
286 for _, rm := range rms {
287 if f(rm) {
288 nrms = append(nrms, rm)
289 }
290 }
291 if len(nrms) != 0 {
292 rootMapToReal.Insert(b, nrms)
293 }
294 return false
295 })
296
297 fs.rootMapToReal = rootMapToReal
298
299 return &fs
300 }
301
302 // Open opens the named file for reading.
303 func (fs *RootMappingFs) Open(name string) (afero.File, error) {
304 fis, err := fs.doStat(name)
305 if err != nil {
306 return nil, err
307 }
308
309 return fs.newUnionFile(fis...)
310 }
311
312 // Stat returns the os.FileInfo structure describing a given file. If there is
313 // an error, it will be of type *os.PathError.
314 // If multiple roots are found, the last one will be used.
315 func (fs *RootMappingFs) Stat(name string) (os.FileInfo, error) {
316 fis, err := fs.doStat(name)
317 if err != nil {
318 return nil, err
319 }
320 return fis[len(fis)-1], nil
321 }
322
323 type ComponentPath struct {
324 Component string
325 Path string
326 Lang string
327 Watch bool
328 }
329
330 func (c ComponentPath) ComponentPathJoined() string {
331 return path.Join(c.Component, c.Path)
332 }
333
334 type ReverseLookupProvder interface {
335 ReverseLookup(filename string) ([]ComponentPath, error)
336 ReverseLookupComponent(component, filename string) ([]ComponentPath, error)
337 }
338
339 // func (fs *RootMappingFs) ReverseStat(filename string) ([]FileMetaInfo, error)
340 func (fs *RootMappingFs) ReverseLookup(filename string) ([]ComponentPath, error) {
341 return fs.ReverseLookupComponent("", filename)
342 }
343
344 func (fs *RootMappingFs) ReverseLookupComponent(component, filename string) ([]ComponentPath, error) {
345 filename = fs.cleanName(filename)
346 key := filepathSeparator + filename
347
348 s, roots := fs.getRootsReverse(key)
349
350 if len(roots) == 0 {
351 return nil, nil
352 }
353
354 var cps []ComponentPath
355
356 base := strings.TrimPrefix(key, s)
357 dir, name := filepath.Split(base)
358
359 for _, first := range roots {
360 if component != "" && first.FromBase != component {
361 continue
362 }
363
364 var filename string
365 if first.Meta.Rename != nil {
366 // Single file mount.
367 if newname, ok := first.Meta.Rename(name, true); ok {
368 filename = filepathSeparator + filepath.Join(first.path, dir, newname)
369 } else {
370 continue
371 }
372 } else {
373 // Now we know that this file _could_ be in this fs.
374 filename = filepathSeparator + filepath.Join(first.path, dir, name)
375 }
376
377 cps = append(cps, ComponentPath{
378 Component: first.FromBase,
379 Path: paths.ToSlashTrimLeading(filename),
380 Lang: first.Meta.Lang,
381 Watch: first.Meta.Watch,
382 })
383 }
384
385 return cps, nil
386 }
387
388 func (fs *RootMappingFs) hasPrefix(prefix string) bool {
389 hasPrefix := false
390 fs.rootMapToReal.WalkPrefix(prefix, func(b string, v any) bool {
391 hasPrefix = true
392 return true
393 })
394
395 return hasPrefix
396 }
397
398 func (fs *RootMappingFs) getRoot(key string) []RootMapping {
399 v, found := fs.rootMapToReal.Get(key)
400 if !found {
401 return nil
402 }
403
404 return v.([]RootMapping)
405 }
406
407 func (fs *RootMappingFs) getRoots(key string) (string, []RootMapping) {
408 tree := fs.rootMapToReal
409 levels := strings.Count(key, filepathSeparator)
410 seen := make(map[RootMapping]bool)
411
412 var roots []RootMapping
413 var s string
414
415 for {
416 var found bool
417 ss, vv, found := tree.LongestPrefix(key)
418
419 if !found || (levels < 2 && ss == key) {
420 break
421 }
422
423 for _, rm := range vv.([]RootMapping) {
424 if !seen[rm] {
425 seen[rm] = true
426 roots = append(roots, rm)
427 }
428 }
429 s = ss
430
431 // We may have more than one root for this key, so walk up.
432 oldKey := key
433 key = filepath.Dir(key)
434 if key == oldKey {
435 break
436 }
437 }
438
439 return s, roots
440 }
441
442 func (fs *RootMappingFs) getRootsReverse(key string) (string, []RootMapping) {
443 tree := fs.realMapToRoot
444 s, v, found := tree.LongestPrefix(key)
445 if !found {
446 return "", nil
447 }
448 return s, v.([]RootMapping)
449 }
450
451 func (fs *RootMappingFs) getRootsWithPrefix(prefix string) []RootMapping {
452 var roots []RootMapping
453 fs.rootMapToReal.WalkPrefix(prefix, func(b string, v any) bool {
454 roots = append(roots, v.([]RootMapping)...)
455 return false
456 })
457
458 return roots
459 }
460
461 func (fs *RootMappingFs) getAncestors(prefix string) []keyRootMappings {
462 var roots []keyRootMappings
463 fs.rootMapToReal.WalkPath(prefix, func(s string, v any) bool {
464 if strings.HasPrefix(prefix, s+filepathSeparator) {
465 roots = append(roots, keyRootMappings{
466 key: s,
467 roots: v.([]RootMapping),
468 })
469 }
470 return false
471 })
472
473 return roots
474 }
475
476 func (fs *RootMappingFs) newUnionFile(fis ...FileMetaInfo) (afero.File, error) {
477 if len(fis) == 1 {
478 return fis[0].Meta().Open()
479 }
480
481 if !fis[0].IsDir() {
482 // Pick the last file mount.
483 return fis[len(fis)-1].Meta().Open()
484 }
485
486 openers := make([]func() (afero.File, error), len(fis))
487 for i := len(fis) - 1; i >= 0; i-- {
488 fi := fis[i]
489 openers[i] = func() (afero.File, error) {
490 meta := fi.Meta()
491 f, err := meta.Open()
492 if err != nil {
493 return nil, err
494 }
495 return &rootMappingDir{DirOnlyOps: f, fs: fs, name: meta.Name, meta: meta}, nil
496 }
497 }
498
499 merge := func(lofi, bofi []iofs.DirEntry) []iofs.DirEntry {
500 // Ignore duplicate directory entries
501 for _, fi1 := range bofi {
502 var found bool
503 for _, fi2 := range lofi {
504 if !fi2.IsDir() {
505 continue
506 }
507 if fi1.Name() == fi2.Name() {
508 found = true
509 break
510 }
511 }
512 if !found {
513 lofi = append(lofi, fi1)
514 }
515 }
516
517 return lofi
518 }
519
520 info := func() (os.FileInfo, error) {
521 return fis[0], nil
522 }
523
524 return overlayfs.OpenDir(merge, info, openers...)
525 }
526
527 func (fs *RootMappingFs) cleanName(name string) string {
528 name = strings.Trim(filepath.Clean(name), filepathSeparator)
529 if name == "." {
530 name = ""
531 }
532 return name
533 }
534
535 func (rfs *RootMappingFs) collectDirEntries(prefix string) ([]iofs.DirEntry, error) {
536 prefix = filepathSeparator + rfs.cleanName(prefix)
537
538 var fis []iofs.DirEntry
539
540 seen := make(map[string]bool) // Prevent duplicate directories
541 level := strings.Count(prefix, filepathSeparator)
542
543 collectDir := func(rm RootMapping, fi FileMetaInfo) error {
544 f, err := fi.Meta().Open()
545 if err != nil {
546 return err
547 }
548 direntries, err := f.(iofs.ReadDirFile).ReadDir(-1)
549 if err != nil {
550 f.Close()
551 return err
552 }
553
554 for _, fi := range direntries {
555
556 meta := fi.(FileMetaInfo).Meta()
557 meta.Merge(rm.Meta)
558
559 if !rm.Meta.InclusionFilter.Match(strings.TrimPrefix(meta.Filename, meta.SourceRoot), fi.IsDir()) {
560 continue
561 }
562
563 if fi.IsDir() {
564 name := fi.Name()
565 if seen[name] {
566 continue
567 }
568 seen[name] = true
569 opener := func() (afero.File, error) {
570 return rfs.Open(filepath.Join(rm.From, name))
571 }
572 fi = newDirNameOnlyFileInfo(name, meta, opener)
573 } else if rm.Meta.Rename != nil {
574 n, ok := rm.Meta.Rename(fi.Name(), true)
575 if !ok {
576 continue
577 }
578 fi.(MetaProvider).Meta().Name = n
579 }
580 fis = append(fis, fi)
581 }
582
583 f.Close()
584
585 return nil
586 }
587
588 // First add any real files/directories.
589 rms := rfs.getRoot(prefix)
590 for _, rm := range rms {
591 if err := collectDir(rm, rm.fi); err != nil {
592 return nil, err
593 }
594 }
595
596 // Next add any file mounts inside the given directory.
597 prefixInside := prefix + filepathSeparator
598 rfs.rootMapToReal.WalkPrefix(prefixInside, func(s string, v any) bool {
599 if (strings.Count(s, filepathSeparator) - level) != 1 {
600 // This directory is not part of the current, but we
601 // need to include the first name part to make it
602 // navigable.
603 path := strings.TrimPrefix(s, prefixInside)
604 parts := strings.Split(path, filepathSeparator)
605 name := parts[0]
606
607 if seen[name] {
608 return false
609 }
610 seen[name] = true
611 opener := func() (afero.File, error) {
612 return rfs.Open(path)
613 }
614
615 fi := newDirNameOnlyFileInfo(name, nil, opener)
616 fis = append(fis, fi)
617
618 return false
619 }
620
621 rms := v.([]RootMapping)
622 for _, rm := range rms {
623 name := filepath.Base(rm.From)
624 if seen[name] {
625 continue
626 }
627 seen[name] = true
628 opener := func() (afero.File, error) {
629 return rfs.Open(rm.From)
630 }
631 fi := newDirNameOnlyFileInfo(name, rm.Meta, opener)
632 fis = append(fis, fi)
633 }
634
635 return false
636 })
637
638 // Finally add any ancestor dirs with files in this directory.
639 ancestors := rfs.getAncestors(prefix)
640 for _, root := range ancestors {
641 subdir := strings.TrimPrefix(prefix, root.key)
642 for _, rm := range root.roots {
643 if rm.fi.IsDir() {
644 fi, err := rm.fi.Meta().JoinStat(subdir)
645 if err == nil {
646 if err := collectDir(rm, fi); err != nil {
647 return nil, err
648 }
649 }
650 }
651 }
652 }
653
654 return fis, nil
655 }
656
657 func (fs *RootMappingFs) doStat(name string) ([]FileMetaInfo, error) {
658 fis, err := fs.doDoStat(name)
659 if err != nil {
660 return nil, err
661 }
662 // Sanity check. Check that all is either file or directories.
663 var isDir, isFile bool
664 for _, fi := range fis {
665 if fi.IsDir() {
666 isDir = true
667 } else {
668 isFile = true
669 }
670 }
671 if isDir && isFile {
672 // For now.
673 return nil, os.ErrNotExist
674 }
675
676 return fis, nil
677 }
678
679 func (fs *RootMappingFs) doDoStat(name string) ([]FileMetaInfo, error) {
680 name = fs.cleanName(name)
681 key := filepathSeparator + name
682
683 roots := fs.getRoot(key)
684
685 if roots == nil {
686 if fs.hasPrefix(key) {
687 // We have directories mounted below this.
688 // Make it look like a directory.
689 return []FileMetaInfo{newDirNameOnlyFileInfo(name, nil, fs.virtualDirOpener(name))}, nil
690 }
691
692 // Find any real directories with this key.
693 _, roots := fs.getRoots(key)
694 if roots == nil {
695 return nil, &os.PathError{Op: "LStat", Path: name, Err: os.ErrNotExist}
696 }
697
698 var err error
699 var fis []FileMetaInfo
700
701 for _, rm := range roots {
702 var fi FileMetaInfo
703 fi, err = fs.statRoot(rm, name)
704 if err == nil {
705 fis = append(fis, fi)
706 }
707 }
708
709 if fis != nil {
710 return fis, nil
711 }
712
713 if err == nil {
714 err = &os.PathError{Op: "LStat", Path: name, Err: err}
715 }
716
717 return nil, err
718 }
719
720 return []FileMetaInfo{newDirNameOnlyFileInfo(name, roots[0].Meta, fs.virtualDirOpener(name))}, nil
721 }
722
723 func (fs *RootMappingFs) statRoot(root RootMapping, filename string) (FileMetaInfo, error) {
724 dir, name := filepath.Split(filename)
725 if root.Meta.Rename != nil {
726 n, ok := root.Meta.Rename(name, false)
727 if !ok {
728 return nil, os.ErrNotExist
729 }
730 filename = filepath.Join(dir, n)
731 }
732
733 if !root.Meta.InclusionFilter.Match(root.trimFrom(filename), true) {
734 return nil, os.ErrNotExist
735 }
736
737 filename = root.filename(filename)
738 fi, err := fs.Fs.Stat(filename)
739 if err != nil {
740 return nil, err
741 }
742
743 var opener func() (afero.File, error)
744 if !fi.IsDir() {
745 // Open the file directly.
746 // Opens the real file directly.
747 opener = func() (afero.File, error) {
748 return fs.Fs.Open(filename)
749 }
750 } else if root.Meta.Rename != nil {
751 // A single file mount where we have mounted the containing directory.
752 n, ok := root.Meta.Rename(fi.Name(), true)
753 if !ok {
754 return nil, os.ErrNotExist
755 }
756 meta := fi.(MetaProvider).Meta()
757 meta.Name = n
758 // Opens the real file directly.
759 opener = func() (afero.File, error) {
760 return fs.Fs.Open(filename)
761 }
762 } else {
763 // Make sure metadata gets applied in ReadDir.
764 opener = fs.realDirOpener(filename, root.Meta)
765 }
766
767 fim := decorateFileInfo(fi, opener, "", root.Meta)
768
769 return fim, nil
770 }
771
772 func (fs *RootMappingFs) virtualDirOpener(name string) func() (afero.File, error) {
773 return func() (afero.File, error) { return &rootMappingDir{name: name, fs: fs}, nil }
774 }
775
776 func (fs *RootMappingFs) realDirOpener(name string, meta *FileMeta) func() (afero.File, error) {
777 return func() (afero.File, error) {
778 f, err := fs.Fs.Open(name)
779 if err != nil {
780 return nil, err
781 }
782 return &rootMappingDir{name: name, meta: meta, fs: fs, DirOnlyOps: f}, nil
783 }
784 }
785
786 var _ iofs.ReadDirFile = (*rootMappingDir)(nil)
787
788 type rootMappingDir struct {
789 *noOpRegularFileOps
790 DirOnlyOps
791 fs *RootMappingFs
792 name string
793 meta *FileMeta
794 }
795
796 func (f *rootMappingDir) Close() error {
797 if f.DirOnlyOps == nil {
798 return nil
799 }
800 return f.DirOnlyOps.Close()
801 }
802
803 func (f *rootMappingDir) Name() string {
804 return f.name
805 }
806
807 func (f *rootMappingDir) ReadDir(count int) ([]iofs.DirEntry, error) {
808 if f.DirOnlyOps != nil {
809 fis, err := f.DirOnlyOps.(iofs.ReadDirFile).ReadDir(count)
810 if err != nil {
811 return nil, err
812 }
813
814 var result []iofs.DirEntry
815 for _, fi := range fis {
816 fim := decorateFileInfo(fi, nil, "", f.meta)
817 meta := fim.Meta()
818 if f.meta.InclusionFilter.Match(strings.TrimPrefix(meta.Filename, meta.SourceRoot), fim.IsDir()) {
819 result = append(result, fim)
820 }
821 }
822 return result, nil
823 }
824
825 return f.fs.collectDirEntries(f.name)
826 }
827
828 // Sentinel error to signal that a file is a directory.
829 var errIsDir = errors.New("isDir")
830
831 func (f *rootMappingDir) Stat() (iofs.FileInfo, error) {
832 return nil, errIsDir
833 }
834
835 func (f *rootMappingDir) Readdir(count int) ([]os.FileInfo, error) {
836 panic("not supported: use ReadDir")
837 }
838
839 // Note that Readdirnames preserves the order of the underlying filesystem(s),
840 // which is usually directory order.
841 func (f *rootMappingDir) Readdirnames(count int) ([]string, error) {
842 dirs, err := f.ReadDir(count)
843 if err != nil {
844 return nil, err
845 }
846 return dirEntriesToNames(dirs), nil
847 }
848
849 func dirEntriesToNames(fis []iofs.DirEntry) []string {
850 names := make([]string, len(fis))
851 for i, d := range fis {
852 names[i] = d.Name()
853 }
854 return names
855 }