tailfs: clean up naming and package structure

- Restyles tailfs -> tailFS
- Defines interfaces for main TailFS types
- Moves implemenatation of TailFS into tailfsimpl package

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
This commit is contained in:
Percy Wegmann
2024-02-09 11:26:43 -06:00
committed by Percy Wegmann
parent 79b547804b
commit abab0d4197
50 changed files with 753 additions and 683 deletions

View File

@@ -0,0 +1,227 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package compositefs provides a webdav.FileSystem that is composi
package compositefs
import (
"io"
"log"
"os"
"path"
"slices"
"strings"
"sync"
"time"
"github.com/tailscale/xnet/webdav"
"tailscale.com/tailfs/tailfsimpl/shared"
"tailscale.com/tstime"
"tailscale.com/types/logger"
)
// Child is a child filesystem of a CompositeFileSystem
type Child struct {
// Name is the name of the child
Name string
// FS is the child's FileSystem
FS webdav.FileSystem
// Available is a function indicating whether or not the child is currently
// available.
Available func() bool
}
func (c *Child) isAvailable() bool {
if c.Available == nil {
return true
}
return c.Available()
}
// Options specifies options for configuring a CompositeFileSystem.
type Options struct {
// Logf specifies a logging function to use
Logf logger.Logf
// StatChildren, if true, causes the CompositeFileSystem to stat its child
// folders when generating a root directory listing. This gives more
// accurate information but increases latency.
StatChildren bool
// Clock, if specified, determines the current time. If not specified, we
// default to time.Now().
Clock tstime.Clock
}
// New constructs a CompositeFileSystem that logs using the given logf.
func New(opts Options) *CompositeFileSystem {
logf := opts.Logf
if logf == nil {
logf = log.Printf
}
fs := &CompositeFileSystem{
logf: logf,
statChildren: opts.StatChildren,
}
if opts.Clock != nil {
fs.now = opts.Clock.Now
} else {
fs.now = time.Now
}
return fs
}
// CompositeFileSystem is a webdav.FileSystem that is composed of multiple
// child webdav.FileSystems. Each child is identified by a name and appears
// as a folder within the root of the CompositeFileSystem, with the children
// sorted lexicographically by name.
//
// Children in a CompositeFileSystem can only be added or removed via calls to
// the AddChild and RemoveChild methods, they cannot be added via operations
// on the webdav.FileSystem interface like filesystem.Mkdir or filesystem.OpenFile.
// In other words, the root of the CompositeFileSystem acts as read-only, not
// permitting the addition, removal or renaming of folders.
//
// Rename is only supported within a single child. Renaming across children
// is not supported, as it wouldn't be possible to perform it atomically.
type CompositeFileSystem struct {
logf logger.Logf
statChildren bool
now func() time.Time
// childrenMu guards children
childrenMu sync.Mutex
children []*Child
}
// AddChild ads a single child with the given name, replacing any existing
// child with the same name.
func (cfs *CompositeFileSystem) AddChild(child *Child) {
cfs.childrenMu.Lock()
oldIdx, oldChild := cfs.findChildLocked(child.Name)
if oldChild != nil {
// replace old child
cfs.children[oldIdx] = child
} else {
// insert new child
cfs.children = slices.Insert(cfs.children, oldIdx, child)
}
cfs.childrenMu.Unlock()
if oldChild != nil {
if c, ok := oldChild.FS.(io.Closer); ok {
if err := c.Close(); err != nil {
cfs.logf("closing child filesystem %v: %v", child.Name, err)
}
}
}
}
// RemoveChild removes the child with the given name, if it exists.
func (cfs *CompositeFileSystem) RemoveChild(name string) {
cfs.childrenMu.Lock()
oldPos, oldChild := cfs.findChildLocked(name)
if oldChild != nil {
// remove old child
copy(cfs.children[oldPos:], cfs.children[oldPos+1:])
cfs.children = cfs.children[:len(cfs.children)-1]
}
cfs.childrenMu.Unlock()
if oldChild != nil {
closer, ok := oldChild.FS.(io.Closer)
if ok {
err := closer.Close()
if err != nil {
cfs.logf("failed to close child filesystem %v: %v", name, err)
}
}
}
}
// SetChildren replaces the entire existing set of children with the given
// ones.
func (cfs *CompositeFileSystem) SetChildren(children ...*Child) {
slices.SortFunc(children, func(a, b *Child) int {
return strings.Compare(a.Name, b.Name)
})
cfs.childrenMu.Lock()
oldChildren := cfs.children
cfs.children = children
cfs.childrenMu.Unlock()
for _, child := range oldChildren {
closer, ok := child.FS.(io.Closer)
if ok {
_ = closer.Close()
}
}
}
// GetChild returns the child with the given name and a boolean indicating
// whether or not it was found.
func (cfs *CompositeFileSystem) GetChild(name string) (webdav.FileSystem, bool) {
_, child := cfs.findChildLocked(name)
if child == nil {
return nil, false
}
return child.FS, true
}
func (cfs *CompositeFileSystem) findChildLocked(name string) (int, *Child) {
var child *Child
i, found := slices.BinarySearchFunc(cfs.children, name, func(child *Child, name string) int {
return strings.Compare(child.Name, name)
})
if found {
child = cfs.children[i]
}
return i, child
}
// pathInfoFor returns a pathInfo for the given filename. If the filename
// refers to a Child that does not exist within this CompositeFileSystem,
// it will return the error os.ErrNotExist. Even when returning an error,
// it will still return a complete pathInfo.
func (cfs *CompositeFileSystem) pathInfoFor(name string) (pathInfo, error) {
cfs.childrenMu.Lock()
defer cfs.childrenMu.Unlock()
var info pathInfo
pathComponents := shared.CleanAndSplit(name)
_, info.child = cfs.findChildLocked(pathComponents[0])
info.refersToChild = len(pathComponents) == 1
if !info.refersToChild {
info.pathOnChild = path.Join(pathComponents[1:]...)
}
if info.child == nil {
return info, os.ErrNotExist
}
return info, nil
}
// pathInfo provides information about a path
type pathInfo struct {
// child is the Child corresponding to the first component of the path.
child *Child
// refersToChild indicates that that path refers directly to the child
// (i.e. the path has only 1 component).
refersToChild bool
// pathOnChild is the path within the child (i.e. path minus leading component)
// if and only if refersToChild is false.
pathOnChild string
}
func (cfs *CompositeFileSystem) Close() error {
cfs.childrenMu.Lock()
children := cfs.children
cfs.childrenMu.Unlock()
for _, child := range children {
closer, ok := child.FS.(io.Closer)
if ok {
_ = closer.Close()
}
}
return nil
}

View File

@@ -0,0 +1,497 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package compositefs
import (
"context"
"errors"
"io/fs"
"net"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/tailscale/xnet/webdav"
"tailscale.com/tailfs/tailfsimpl/shared"
"tailscale.com/tstest"
)
func TestStat(t *testing.T) {
cfs, dir1, _, clock, close := createFileSystem(t, nil)
defer close()
tests := []struct {
label string
name string
expected fs.FileInfo
err error
}{
{
label: "root folder",
name: "/",
expected: &shared.StaticFileInfo{
Named: "/",
Sized: 0,
ModdedTime: clock.Now(),
Dir: true,
},
},
{
label: "remote1",
name: "/remote1",
expected: &shared.StaticFileInfo{
Named: "/remote1",
Sized: 0,
ModdedTime: clock.Now(),
Dir: true,
},
},
{
label: "remote2",
name: "/remote2",
expected: &shared.StaticFileInfo{
Named: "/remote2",
Sized: 0,
ModdedTime: clock.Now(),
Dir: true,
},
},
{
label: "non-existent remote",
name: "/remote3",
err: os.ErrNotExist,
},
{
label: "file on remote1",
name: "/remote1/file1.txt",
expected: &shared.StaticFileInfo{
Named: "/remote1/file1.txt",
Sized: stat(t, filepath.Join(dir1, "file1.txt")).Size(),
ModdedTime: stat(t, filepath.Join(dir1, "file1.txt")).ModTime(),
Dir: false,
},
},
}
ctx := context.Background()
for _, test := range tests {
t.Run(test.label, func(t *testing.T) {
fi, err := cfs.Stat(ctx, test.name)
if test.err != nil {
if err == nil || !errors.Is(err, test.err) {
t.Errorf("expected error: %v got: %v", test.err, err)
}
} else {
if err != nil {
t.Errorf("unable to stat file: %v", err)
} else {
infosEqual(t, test.expected, fi)
}
}
})
}
}
func TestStatWithStatChildren(t *testing.T) {
cfs, dir1, dir2, _, close := createFileSystem(t, &Options{StatChildren: true})
defer close()
tests := []struct {
label string
name string
expected fs.FileInfo
}{
{
label: "root folder",
name: "/",
expected: &shared.StaticFileInfo{
Named: "/",
Sized: 0,
ModdedTime: stat(t, dir2).ModTime(), // ModTime should be greatest modtime of children
Dir: true,
},
},
{
label: "remote1",
name: "/remote1",
expected: &shared.StaticFileInfo{
Named: "/remote1",
Sized: stat(t, dir1).Size(),
ModdedTime: stat(t, dir1).ModTime(),
Dir: true,
},
},
{
label: "remote2",
name: "/remote2",
expected: &shared.StaticFileInfo{
Named: "/remote2",
Sized: stat(t, dir2).Size(),
ModdedTime: stat(t, dir2).ModTime(),
Dir: true,
},
},
}
ctx := context.Background()
for _, test := range tests {
t.Run(test.label, func(t *testing.T) {
fi, err := cfs.Stat(ctx, test.name)
if err != nil {
t.Errorf("unable to stat file: %v", err)
} else {
infosEqual(t, test.expected, fi)
}
})
}
}
func TestMkdir(t *testing.T) {
fs, _, _, _, close := createFileSystem(t, nil)
defer close()
tests := []struct {
label string
name string
perm os.FileMode
err error
}{
{
label: "attempt to create root folder",
name: "/",
},
{
label: "attempt to create remote",
name: "/remote1",
},
{
label: "attempt to create non-existent remote",
name: "/remote3",
err: os.ErrPermission,
},
{
label: "attempt to create file on non-existent remote",
name: "/remote3/somefile.txt",
err: os.ErrNotExist,
},
{
label: "success",
name: "/remote1/newfile.txt",
perm: 0772,
},
}
ctx := context.Background()
for _, test := range tests {
t.Run(test.label, func(t *testing.T) {
err := fs.Mkdir(ctx, test.name, test.perm)
if test.err != nil {
if err == nil || !errors.Is(err, test.err) {
t.Errorf("expected error: %v got: %v", test.err, err)
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
} else {
fi, err := fs.Stat(ctx, test.name)
if err != nil {
t.Errorf("unable to stat file: %v", err)
} else {
if fi.Name() != test.name {
t.Errorf("expected name: %v got: %v", test.name, fi.Name())
}
if !fi.IsDir() {
t.Error("expected directory")
}
}
}
}
})
}
}
func TestRemoveAll(t *testing.T) {
fs, _, _, _, close := createFileSystem(t, nil)
defer close()
tests := []struct {
label string
name string
err error
}{
{
label: "attempt to remove root folder",
name: "/",
err: os.ErrPermission,
},
{
label: "attempt to remove remote",
name: "/remote1",
err: os.ErrPermission,
},
{
label: "attempt to remove non-existent remote",
name: "/remote3",
err: os.ErrPermission,
},
{
label: "attempt to remove file on non-existent remote",
name: "/remote3/somefile.txt",
err: os.ErrNotExist,
},
{
label: "remove non-existent file",
name: "/remote1/nonexistent.txt",
},
{
label: "remove existing file",
name: "/remote1/dir1",
},
}
ctx := context.Background()
for _, test := range tests {
t.Run(test.label, func(t *testing.T) {
err := fs.RemoveAll(ctx, test.name)
if test.err != nil {
if err == nil || !errors.Is(err, test.err) {
t.Errorf("expected error: %v got: %v", test.err, err)
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
} else {
_, err := fs.Stat(ctx, test.name)
if !os.IsNotExist(err) {
t.Errorf("expected dir to be gone: %v", err)
}
}
}
})
}
}
func TestRename(t *testing.T) {
fs, _, _, _, close := createFileSystem(t, nil)
defer close()
tests := []struct {
label string
oldName string
newName string
err error
expectedNewInfo *shared.StaticFileInfo
}{
{
label: "attempt to move root folder",
oldName: "/",
newName: "/remote2/copy.txt",
err: os.ErrPermission,
},
{
label: "attempt to move to root folder",
oldName: "/remote1/file1.txt",
newName: "/",
err: os.ErrPermission,
},
{
label: "attempt to move to remote",
oldName: "/remote1/file1.txt",
newName: "/remote2",
err: os.ErrPermission,
},
{
label: "attempt to move to non-existent remote",
oldName: "/remote1/file1.txt",
newName: "/remote3",
err: os.ErrPermission,
},
{
label: "attempt to move file from non-existent remote",
oldName: "/remote3/file1.txt",
newName: "/remote1/file1.txt",
err: os.ErrNotExist,
},
{
label: "attempt to move file to a non-existent remote",
oldName: "/remote2/file2.txt",
newName: "/remote3/file2.txt",
err: os.ErrNotExist,
},
{
label: "attempt to move file across remotes",
oldName: "/remote1/file1.txt",
newName: "/remote2/file1.txt",
err: os.ErrPermission,
},
{
label: "attempt to move remote itself",
oldName: "/remote1",
newName: "/remote2",
err: os.ErrPermission,
},
{
label: "attempt to move to a remote",
oldName: "/remote1/file2.txt",
newName: "/remote2",
err: os.ErrPermission,
},
{
label: "move file within remote",
oldName: "/remote2/file2.txt",
newName: "/remote2/file3.txt",
expectedNewInfo: &shared.StaticFileInfo{
Named: "/remote2/file3.txt",
Sized: 5,
Dir: false,
},
},
}
ctx := context.Background()
for _, test := range tests {
t.Run(test.label, func(t *testing.T) {
err := fs.Rename(ctx, test.oldName, test.newName)
if test.err != nil {
if err == nil || test.err.Error() != err.Error() {
t.Errorf("expected error: %v got: %v", test.err, err)
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
} else {
fi, err := fs.Stat(ctx, test.newName)
if err != nil {
t.Errorf("unexpected error: %v", err)
} else {
// Override modTime to avoid having to compare it
test.expectedNewInfo.ModdedTime = fi.ModTime()
infosEqual(t, test.expectedNewInfo, fi)
}
}
}
})
}
}
func createFileSystem(t *testing.T, opts *Options) (webdav.FileSystem, string, string, *tstest.Clock, func()) {
l1, dir1 := startRemote(t)
l2, dir2 := startRemote(t)
// Make some files, use perms 0666 as lowest common denominator that works
// on both UNIX and Windows.
err := os.WriteFile(filepath.Join(dir1, "file1.txt"), []byte("12345"), 0666)
if err != nil {
t.Fatal(err)
}
err = os.WriteFile(filepath.Join(dir2, "file2.txt"), []byte("54321"), 0666)
if err != nil {
t.Fatal(err)
}
// make some directories
err = os.Mkdir(filepath.Join(dir1, "dir1"), 0666)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir(filepath.Join(dir2, "dir2"), 0666)
if err != nil {
t.Fatal(err)
}
if opts == nil {
opts = &Options{}
}
if opts.Logf == nil {
opts.Logf = t.Logf
}
clock := tstest.NewClock(tstest.ClockOpts{Start: time.Now()})
opts.Clock = clock
fs := New(*opts)
fs.AddChild(&Child{Name: "remote4", FS: &closeableFS{webdav.Dir(dir2)}})
fs.SetChildren(&Child{Name: "remote2", FS: webdav.Dir(dir2)},
&Child{Name: "remote3", FS: &closeableFS{webdav.Dir(dir2)}},
)
fs.AddChild(&Child{Name: "remote1", FS: webdav.Dir(dir1)})
fs.RemoveChild("remote3")
child, ok := fs.GetChild("remote1")
if !ok || child == nil {
t.Fatal("unable to GetChild(remote1)")
}
child, ok = fs.GetChild("remote2")
if !ok || child == nil {
t.Fatal("unable to GetChild(remote2)")
}
child, ok = fs.GetChild("remote3")
if ok || child != nil {
t.Fatal("should have been able to GetChild(remote3)")
}
child, ok = fs.GetChild("remote4")
if ok || child != nil {
t.Fatal("should have been able to GetChild(remote4)")
}
return fs, dir1, dir2, clock, func() {
defer l1.Close()
defer os.RemoveAll(dir1)
defer l2.Close()
defer os.RemoveAll(dir2)
}
}
func stat(t *testing.T, path string) fs.FileInfo {
fi, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
return fi
}
func startRemote(t *testing.T) (net.Listener, string) {
dir := t.TempDir()
l, err := net.Listen("tcp", "127.0.0.1:")
if err != nil {
t.Fatal(err)
}
h := &webdav.Handler{
FileSystem: webdav.Dir(dir),
LockSystem: webdav.NewMemLS(),
}
s := &http.Server{Handler: h}
go s.Serve(l)
return l, dir
}
func infosEqual(t *testing.T, expected, actual fs.FileInfo) {
t.Helper()
if expected.Name() != actual.Name() {
t.Errorf("expected name: %v got: %v", expected.Name(), actual.Name())
}
if expected.Size() != actual.Size() {
t.Errorf("expected Size: %v got: %v", expected.Size(), actual.Size())
}
if !expected.ModTime().Truncate(time.Second).UTC().Equal(actual.ModTime().Truncate(time.Second).UTC()) {
t.Errorf("expected ModTime: %v got: %v", expected.ModTime(), actual.ModTime())
}
if expected.IsDir() != actual.IsDir() {
t.Errorf("expected IsDir: %v got: %v", expected.IsDir(), actual.IsDir())
}
}
// closeableFS is a webdav.FileSystem that implements io.Closer()
type closeableFS struct {
webdav.FileSystem
}
func (cfs *closeableFS) Close() error {
return nil
}

View File

@@ -0,0 +1,39 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package compositefs
import (
"context"
"os"
"tailscale.com/tailfs/tailfsimpl/shared"
)
// Mkdir implements webdav.Filesystem. The root of this file system is
// read-only, so any attempts to make directories within the root will fail
// with os.ErrPermission. Attempts to make directories within one of the child
// filesystems will be handled by the respective child.
func (cfs *CompositeFileSystem) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
if shared.IsRoot(name) {
// root directory already exists, consider this okay
return nil
}
pathInfo, err := cfs.pathInfoFor(name)
if pathInfo.refersToChild {
// children can't be made
if pathInfo.child != nil {
// since child already exists, consider this okay
return nil
}
// since child doesn't exist, return permission error
return os.ErrPermission
}
if err != nil {
return err
}
return pathInfo.child.FS.Mkdir(ctx, pathInfo.pathOnChild, perm)
}

View File

@@ -0,0 +1,65 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package compositefs
import (
"context"
"io/fs"
"os"
"github.com/tailscale/xnet/webdav"
"tailscale.com/tailfs/tailfsimpl/shared"
)
// OpenFile implements interface webdav.Filesystem.
func (cfs *CompositeFileSystem) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
if !shared.IsRoot(name) {
pathInfo, err := cfs.pathInfoFor(name)
if err != nil {
return nil, err
}
if pathInfo.refersToChild {
// this is the child itself, ask it to open its root
return pathInfo.child.FS.OpenFile(ctx, "/", flag, perm)
}
return pathInfo.child.FS.OpenFile(ctx, pathInfo.pathOnChild, flag, perm)
}
// the root directory contains one directory for each child
di, err := cfs.Stat(ctx, name)
if err != nil {
return nil, err
}
return &shared.DirFile{
Info: di,
LoadChildren: func() ([]fs.FileInfo, error) {
cfs.childrenMu.Lock()
children := cfs.children
cfs.childrenMu.Unlock()
childInfos := make([]fs.FileInfo, 0, len(cfs.children))
for _, c := range children {
if c.isAvailable() {
var childInfo fs.FileInfo
if cfs.statChildren {
fi, err := c.FS.Stat(ctx, "/")
if err != nil {
return nil, err
}
// we use the full name
childInfo = shared.RenamedFileInfo(ctx, c.Name, fi)
} else {
// always use now() as the modified time to bust caches
childInfo = shared.ReadOnlyDirInfo(c.Name, cfs.now())
}
childInfos = append(childInfos, childInfo)
}
}
return childInfos, nil
},
}, nil
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package compositefs
import (
"context"
"os"
"tailscale.com/tailfs/tailfsimpl/shared"
)
// RemoveAll implements webdav.File. The root of this file system is read-only,
// so attempting to call RemoveAll on the root will fail with os.ErrPermission.
// RemoveAll within a child will be handled by the respective child.
func (cfs *CompositeFileSystem) RemoveAll(ctx context.Context, name string) error {
if shared.IsRoot(name) {
// root directory is read-only
return os.ErrPermission
}
pathInfo, err := cfs.pathInfoFor(name)
if pathInfo.refersToChild {
// children can't be removed
return os.ErrPermission
}
if err != nil {
return err
}
return pathInfo.child.FS.RemoveAll(ctx, pathInfo.pathOnChild)
}

View File

@@ -0,0 +1,49 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package compositefs
import (
"context"
"os"
"tailscale.com/tailfs/tailfsimpl/shared"
)
// Rename implements interface webdav.FileSystem. The root of this file system
// is read-only, so any attempt to rename a child within the root of this
// filesystem will fail with os.ErrPermission. Renaming across children is not
// supported and will fail with os.ErrPermission. Renaming within a child will
// be handled by the respective child.
func (cfs *CompositeFileSystem) Rename(ctx context.Context, oldName, newName string) error {
if shared.IsRoot(oldName) || shared.IsRoot(newName) {
// root directory is read-only
return os.ErrPermission
}
oldPathInfo, err := cfs.pathInfoFor(oldName)
if oldPathInfo.refersToChild {
// children themselves are read-only
return os.ErrPermission
}
if err != nil {
return err
}
newPathInfo, err := cfs.pathInfoFor(newName)
if newPathInfo.refersToChild {
// children themselves are read-only
return os.ErrPermission
}
if err != nil {
return err
}
if oldPathInfo.child != newPathInfo.child {
// moving a file across children is not permitted
return os.ErrPermission
}
// file is moving within the same child, let the child handle it
return oldPathInfo.child.FS.Rename(ctx, oldPathInfo.pathOnChild, newPathInfo.pathOnChild)
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package compositefs
import (
"context"
"io/fs"
"tailscale.com/tailfs/tailfsimpl/shared"
)
// Stat implements webdav.FileSystem.
func (cfs *CompositeFileSystem) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
if shared.IsRoot(name) {
// Root is a directory
// always use now() as the modified time to bust caches
fi := shared.ReadOnlyDirInfo(name, cfs.now())
if cfs.statChildren {
// update last modified time based on children
cfs.childrenMu.Lock()
children := cfs.children
cfs.childrenMu.Unlock()
for i, child := range children {
childInfo, err := child.FS.Stat(ctx, "/")
if err != nil {
return nil, err
}
if i == 0 || childInfo.ModTime().After(fi.ModTime()) {
fi.ModdedTime = childInfo.ModTime()
}
}
}
return fi, nil
}
pathInfo, err := cfs.pathInfoFor(name)
if err != nil {
return nil, err
}
if pathInfo.refersToChild && !cfs.statChildren {
// Return a read-only FileInfo for this child.
// Always use now() as the modified time to bust caches.
return shared.ReadOnlyDirInfo(name, cfs.now()), nil
}
fi, err := pathInfo.child.FS.Stat(ctx, pathInfo.pathOnChild)
if err != nil {
return nil, err
}
// we use the full name, which is different than what the child sees
return shared.RenamedFileInfo(ctx, name, fi), nil
}