tailscale/tailfs/compositefs/compositefs.go
Percy Wegmann 993acf4475 tailfs: initial implementation
Add a WebDAV-based folder sharing mechanism that is exposed to local clients at
100.100.100.100:8080 and to remote peers via a new peerapi endpoint at
/v0/tailfs.

Add the ability to manage folder sharing via the new 'share' CLI sub-command.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-09 09:13:51 -06:00

228 lines
6.1 KiB
Go

// 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/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
}