mirror of https://github.com/tailscale/tailscale/
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
228 lines
6.1 KiB
Go
228 lines
6.1 KiB
Go
10 months ago
|
// 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
|
||
|
}
|