URI: 
       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 }