mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-27 11:41:14 +00:00
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:
committed by
Percy Wegmann
parent
79b547804b
commit
abab0d4197
227
tailfs/tailfsimpl/compositefs/compositefs.go
Normal file
227
tailfs/tailfsimpl/compositefs/compositefs.go
Normal 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
|
||||
}
|
||||
497
tailfs/tailfsimpl/compositefs/compositefs_test.go
Normal file
497
tailfs/tailfsimpl/compositefs/compositefs_test.go
Normal 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
|
||||
}
|
||||
39
tailfs/tailfsimpl/compositefs/mkdir.go
Normal file
39
tailfs/tailfsimpl/compositefs/mkdir.go
Normal 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)
|
||||
}
|
||||
65
tailfs/tailfsimpl/compositefs/openfile.go
Normal file
65
tailfs/tailfsimpl/compositefs/openfile.go
Normal 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
|
||||
}
|
||||
33
tailfs/tailfsimpl/compositefs/removeall.go
Normal file
33
tailfs/tailfsimpl/compositefs/removeall.go
Normal 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)
|
||||
}
|
||||
49
tailfs/tailfsimpl/compositefs/rename.go
Normal file
49
tailfs/tailfsimpl/compositefs/rename.go
Normal 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)
|
||||
}
|
||||
55
tailfs/tailfsimpl/compositefs/stat.go
Normal file
55
tailfs/tailfsimpl/compositefs/stat.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user