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
+18
-80
@@ -1,99 +1,37 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tailfs provides a filesystem that allows sharing folders between
|
||||
// Tailscale nodes using WebDAV. The actual implementation of the core TailFS
|
||||
// functionality lives in package tailfsimpl. These packages are separated to
|
||||
// allow users of tailfs to refer to the interfaces without having a hard
|
||||
// dependency on tailfs, so that programs which don't actually use tailfs can
|
||||
// avoid its transitive dependencies.
|
||||
package tailfs
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/compositefs"
|
||||
"tailscale.com/tailfs/webdavfs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Remote represents a remote Tailfs node.
|
||||
// Remote represents a remote TailFS node.
|
||||
type Remote struct {
|
||||
Name string
|
||||
URL string
|
||||
Available func() bool
|
||||
}
|
||||
|
||||
// NewFileSystemForLocal starts serving a filesystem for local clients.
|
||||
// Inbound connections must be handed to HandleConn.
|
||||
func NewFileSystemForLocal(logf logger.Logf) *FileSystemForLocal {
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
fs := &FileSystemForLocal{
|
||||
logf: logf,
|
||||
cfs: compositefs.New(compositefs.Options{Logf: logf}),
|
||||
listener: newConnListener(),
|
||||
}
|
||||
fs.startServing()
|
||||
return fs
|
||||
}
|
||||
// FileSystemForLocal is the TailFS filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote TailFS shares on other nodes.
|
||||
type FileSystemForLocal interface {
|
||||
// HandleConn handles connections from local WebDAV clients
|
||||
HandleConn(conn net.Conn, remoteAddr net.Addr) error
|
||||
|
||||
// FileSystemForLocal is the Tailfs filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote Tailfs shares on other nodes.
|
||||
type FileSystemForLocal struct {
|
||||
logf logger.Logf
|
||||
cfs *compositefs.CompositeFileSystem
|
||||
listener *connListener
|
||||
}
|
||||
// SetRemotes sets the complete set of remotes on the given tailnet domain
|
||||
// using a map of name -> url. If transport is specified, that transport
|
||||
// will be used to connect to these remotes.
|
||||
SetRemotes(domain string, remotes []*Remote, transport http.RoundTripper)
|
||||
|
||||
func (s *FileSystemForLocal) startServing() {
|
||||
hs := &http.Server{
|
||||
Handler: &webdav.Handler{
|
||||
FileSystem: s.cfs,
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
err := hs.Serve(s.listener)
|
||||
if err != nil {
|
||||
// TODO(oxtoacart): should we panic or something different here?
|
||||
log.Printf("serve: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// HandleConn handles connections from local WebDAV clients
|
||||
func (s *FileSystemForLocal) HandleConn(conn net.Conn, remoteAddr net.Addr) error {
|
||||
return s.listener.HandleConn(conn, remoteAddr)
|
||||
}
|
||||
|
||||
// SetRemotes sets the complete set of remotes on the given tailnet domain
|
||||
// using a map of name -> url. If transport is specified, that transport
|
||||
// will be used to connect to these remotes.
|
||||
func (s *FileSystemForLocal) SetRemotes(domain string, remotes []*Remote, transport http.RoundTripper) {
|
||||
children := make([]*compositefs.Child, 0, len(remotes))
|
||||
for _, remote := range remotes {
|
||||
opts := webdavfs.Options{
|
||||
URL: remote.URL,
|
||||
Transport: transport,
|
||||
StatCacheTTL: statCacheTTL,
|
||||
Logf: s.logf,
|
||||
}
|
||||
children = append(children, &compositefs.Child{
|
||||
Name: remote.Name,
|
||||
FS: webdavfs.New(opts),
|
||||
Available: remote.Available,
|
||||
})
|
||||
}
|
||||
|
||||
domainChild, found := s.cfs.GetChild(domain)
|
||||
if !found {
|
||||
domainChild = compositefs.New(compositefs.Options{Logf: s.logf})
|
||||
s.cfs.SetChildren(&compositefs.Child{Name: domain, FS: domainChild})
|
||||
}
|
||||
domainChild.(*compositefs.CompositeFileSystem).SetChildren(children...)
|
||||
}
|
||||
|
||||
// Close() stops serving the WebDAV content
|
||||
func (s *FileSystemForLocal) Close() error {
|
||||
s.cfs.Close()
|
||||
return s.listener.Close()
|
||||
// Close() stops serving the WebDAV content
|
||||
Close() error
|
||||
}
|
||||
|
||||
+30
-359
@@ -4,386 +4,57 @@
|
||||
package tailfs
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailfs/compositefs"
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/webdavfs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
disallowShareAs = false
|
||||
// DisallowShareAs forcibly disables sharing as a specific user, only used
|
||||
// for testing.
|
||||
DisallowShareAs = false
|
||||
)
|
||||
|
||||
// AllowShareAs reports whether sharing files as a specific user is allowed.
|
||||
func AllowShareAs() bool {
|
||||
return !disallowShareAs && doAllowShareAs()
|
||||
return !DisallowShareAs && doAllowShareAs()
|
||||
}
|
||||
|
||||
// Share represents a folder that's shared with remote Tailfs nodes.
|
||||
// Share configures a folder to be shared through TailFS.
|
||||
type Share struct {
|
||||
// Name is how this share appears on remote nodes.
|
||||
Name string `json:"name"`
|
||||
|
||||
// Path is the path to the directory on this machine that's being shared.
|
||||
Path string `json:"path"`
|
||||
|
||||
// As is the UNIX or Windows username of the local account used for this
|
||||
// share. File read/write permissions are enforced based on this username.
|
||||
// Can be left blank to use the default value of "whoever is running the
|
||||
// Tailscale GUI".
|
||||
As string `json:"who"`
|
||||
}
|
||||
|
||||
func NewFileSystemForRemote(logf logger.Logf) *FileSystemForRemote {
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
fs := &FileSystemForRemote{
|
||||
logf: logf,
|
||||
lockSystem: webdav.NewMemLS(),
|
||||
fileSystems: make(map[string]webdav.FileSystem),
|
||||
userServers: make(map[string]*userServer),
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
// FileSystemForRemote is the Tailfs filesystem exposed to remote nodes. It
|
||||
// FileSystemForRemote is the TailFS filesystem exposed to remote nodes. It
|
||||
// provides a unified WebDAV interface to local directories that have been
|
||||
// shared.
|
||||
type FileSystemForRemote struct {
|
||||
logf logger.Logf
|
||||
lockSystem webdav.LockSystem
|
||||
type FileSystemForRemote interface {
|
||||
// SetFileServerAddr sets the address of the file server to which we
|
||||
// should proxy. This is used on platforms like Windows and MacOS
|
||||
// sandboxed where we can't spawn user-specific sub-processes and instead
|
||||
// rely on the UI application that's already running as an unprivileged
|
||||
// user to access the filesystem for us.
|
||||
SetFileServerAddr(addr string)
|
||||
|
||||
// mu guards the below values. Acquire a write lock before updating any of
|
||||
// them, acquire a read lock before reading any of them.
|
||||
mu sync.RWMutex
|
||||
fileServerAddr string
|
||||
shares map[string]*Share
|
||||
fileSystems map[string]webdav.FileSystem
|
||||
userServers map[string]*userServer
|
||||
}
|
||||
|
||||
// SetFileServerAddr sets the address of the file server to which we
|
||||
// should proxy. This is used on platforms like Windows and MacOS
|
||||
// sandboxed where we can't spawn user-specific sub-processes and instead
|
||||
// rely on the UI application that's already running as an unprivileged
|
||||
// user to access the filesystem for us.
|
||||
func (s *FileSystemForRemote) SetFileServerAddr(addr string) {
|
||||
s.mu.Lock()
|
||||
s.fileServerAddr = addr
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetShares sets the complete set of shares exposed by this node. If
|
||||
// AllowShareAs() reports true, we will use one subprocess per user to
|
||||
// access the filesystem (see userServer). Otherwise, we will use the file
|
||||
// server configured via SetFileServerAddr.
|
||||
func (s *FileSystemForRemote) SetShares(shares map[string]*Share) {
|
||||
userServers := make(map[string]*userServer)
|
||||
if AllowShareAs() {
|
||||
// set up per-user server
|
||||
for _, share := range shares {
|
||||
p, found := userServers[share.As]
|
||||
if !found {
|
||||
p = &userServer{
|
||||
logf: s.logf,
|
||||
}
|
||||
userServers[share.As] = p
|
||||
}
|
||||
p.shares = append(p.shares, share)
|
||||
}
|
||||
for _, p := range userServers {
|
||||
go p.runLoop()
|
||||
}
|
||||
}
|
||||
|
||||
fileSystems := make(map[string]webdav.FileSystem, len(shares))
|
||||
for _, share := range shares {
|
||||
fileSystems[share.Name] = s.buildWebDAVFS(share)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.shares = shares
|
||||
oldFileSystems := s.fileSystems
|
||||
oldUserServers := s.userServers
|
||||
s.fileSystems = fileSystems
|
||||
s.userServers = userServers
|
||||
s.mu.Unlock()
|
||||
|
||||
s.stopUserServers(oldUserServers)
|
||||
s.closeFileSystems(oldFileSystems)
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) buildWebDAVFS(share *Share) webdav.FileSystem {
|
||||
return webdavfs.New(webdavfs.Options{
|
||||
Logf: s.logf,
|
||||
URL: fmt.Sprintf("http://%v/%v", hex.EncodeToString([]byte(share.Name)), share.Name),
|
||||
Transport: &http.Transport{
|
||||
Dial: func(_, shareAddr string) (net.Conn, error) {
|
||||
shareNameHex, _, err := net.SplitHostPort(shareAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse share address %v: %w", shareAddr, err)
|
||||
}
|
||||
|
||||
// We had to encode the share name in hex to make sure it's a valid hostname
|
||||
shareNameBytes, err := hex.DecodeString(shareNameHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode share name from host %v: %v", shareNameHex, err)
|
||||
}
|
||||
shareName := string(shareNameBytes)
|
||||
|
||||
s.mu.RLock()
|
||||
share, shareFound := s.shares[shareName]
|
||||
userServers := s.userServers
|
||||
fileServerAddr := s.fileServerAddr
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !shareFound {
|
||||
return nil, fmt.Errorf("unknown share %v", shareName)
|
||||
}
|
||||
|
||||
var addr string
|
||||
if !AllowShareAs() {
|
||||
addr = fileServerAddr
|
||||
} else {
|
||||
userServer, found := userServers[share.As]
|
||||
if found {
|
||||
userServer.mu.RLock()
|
||||
addr = userServer.addr
|
||||
userServer.mu.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
if addr == "" {
|
||||
return nil, fmt.Errorf("unable to determine address for share %v", shareName)
|
||||
}
|
||||
|
||||
_, err = netip.ParseAddrPort(addr)
|
||||
if err == nil {
|
||||
// this is a regular network address, dial normally
|
||||
return net.Dial("tcp", addr)
|
||||
}
|
||||
// assume this is a safesocket address
|
||||
return safesocket.Connect(addr)
|
||||
},
|
||||
},
|
||||
StatRoot: true,
|
||||
})
|
||||
}
|
||||
|
||||
// ServeHTTPWithPerms behaves like the similar method from http.Handler but
|
||||
// also accepts a Permissions map that captures the permissions of the
|
||||
// connecting node.
|
||||
func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions Permissions, w http.ResponseWriter, r *http.Request) {
|
||||
isWrite := writeMethods[r.Method]
|
||||
if isWrite {
|
||||
share := shared.CleanAndSplit(r.URL.Path)[0]
|
||||
switch permissions.For(share) {
|
||||
case PermissionNone:
|
||||
// If we have no permissions to this share, treat it as not found
|
||||
// to avoid leaking any information about the share's existence.
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
case PermissionReadOnly:
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
fileSystems := s.fileSystems
|
||||
s.mu.RUnlock()
|
||||
|
||||
children := make([]*compositefs.Child, 0, len(fileSystems))
|
||||
// filter out shares to which the connecting principal has no access
|
||||
for name, fs := range fileSystems {
|
||||
if permissions.For(name) == PermissionNone {
|
||||
continue
|
||||
}
|
||||
|
||||
children = append(children, &compositefs.Child{Name: name, FS: fs})
|
||||
}
|
||||
|
||||
cfs := compositefs.New(
|
||||
compositefs.Options{
|
||||
Logf: s.logf,
|
||||
StatChildren: true,
|
||||
})
|
||||
cfs.SetChildren(children...)
|
||||
h := webdav.Handler{
|
||||
FileSystem: cfs,
|
||||
LockSystem: s.lockSystem,
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) stopUserServers(userServers map[string]*userServer) {
|
||||
for _, server := range userServers {
|
||||
if err := server.Close(); err != nil {
|
||||
s.logf("error closing tailfs user server: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) closeFileSystems(fileSystems map[string]webdav.FileSystem) {
|
||||
for _, fs := range fileSystems {
|
||||
closer, ok := fs.(interface{ Close() error })
|
||||
if ok {
|
||||
if err := closer.Close(); err != nil {
|
||||
s.logf("error closing tailfs filesystem: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close() stops serving the WebDAV content
|
||||
func (s *FileSystemForRemote) Close() error {
|
||||
s.mu.Lock()
|
||||
userServers := s.userServers
|
||||
fileSystems := s.fileSystems
|
||||
s.mu.Unlock()
|
||||
|
||||
s.stopUserServers(userServers)
|
||||
s.closeFileSystems(fileSystems)
|
||||
return nil
|
||||
}
|
||||
|
||||
// userServer runs tailscaled serve-tailfs to serve webdav content for the
|
||||
// given Shares. All Shares are assumed to have the same Share.As, and the
|
||||
// content is served as that Share.As user.
|
||||
type userServer struct {
|
||||
logf logger.Logf
|
||||
shares []*Share
|
||||
|
||||
// mu guards the below values. Acquire a write lock before updating any of
|
||||
// them, acquire a read lock before reading any of them.
|
||||
mu sync.RWMutex
|
||||
cmd *exec.Cmd
|
||||
addr string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (s *userServer) Close() error {
|
||||
s.mu.Lock()
|
||||
cmd := s.cmd
|
||||
s.closed = true
|
||||
s.mu.Unlock()
|
||||
if cmd != nil && cmd.Process != nil {
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
// not running, that's okay
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *userServer) runLoop() {
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
s.logf("can't find executable: %v", err)
|
||||
return
|
||||
}
|
||||
maxSleepTime := 30 * time.Second
|
||||
consecutiveFailures := float64(0)
|
||||
var timeOfLastFailure time.Time
|
||||
for {
|
||||
s.mu.RLock()
|
||||
closed := s.closed
|
||||
s.mu.RUnlock()
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
|
||||
err := s.run(executable)
|
||||
now := time.Now()
|
||||
timeSinceLastFailure := now.Sub(timeOfLastFailure)
|
||||
timeOfLastFailure = now
|
||||
if timeSinceLastFailure < maxSleepTime {
|
||||
consecutiveFailures++
|
||||
} else {
|
||||
consecutiveFailures = 1
|
||||
}
|
||||
sleepTime := time.Duration(math.Pow(2, consecutiveFailures)) * time.Millisecond
|
||||
if sleepTime > maxSleepTime {
|
||||
sleepTime = maxSleepTime
|
||||
}
|
||||
s.logf("user server % v stopped with error %v, will try again in %v", executable, err, sleepTime)
|
||||
time.Sleep(sleepTime)
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs the executable (tailscaled). This function only works on UNIX systems,
|
||||
// but those are the only ones on which we use userServers anyway.
|
||||
func (s *userServer) run(executable string) error {
|
||||
// set up the command
|
||||
args := []string{"serve-tailfs"}
|
||||
for _, s := range s.shares {
|
||||
args = append(args, s.Name, s.Path)
|
||||
}
|
||||
allArgs := []string{"-u", s.shares[0].As, executable}
|
||||
allArgs = append(allArgs, args...)
|
||||
cmd := exec.Command("sudo", allArgs...)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
defer stdout.Close()
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stderr pipe: %w", err)
|
||||
}
|
||||
defer stderr.Close()
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.cmd = cmd
|
||||
s.mu.Unlock()
|
||||
|
||||
// read address
|
||||
stdoutScanner := bufio.NewScanner(stdout)
|
||||
stdoutScanner.Scan()
|
||||
if stdoutScanner.Err() != nil {
|
||||
return fmt.Errorf("read addr: %w", stdoutScanner.Err())
|
||||
}
|
||||
addr := stdoutScanner.Text()
|
||||
// send the rest of stdout and stderr to logger to avoid blocking
|
||||
go func() {
|
||||
for stdoutScanner.Scan() {
|
||||
s.logf("tailscaled serve-tailfs stdout: %v", stdoutScanner.Text())
|
||||
}
|
||||
}()
|
||||
stderrScanner := bufio.NewScanner(stderr)
|
||||
go func() {
|
||||
for stderrScanner.Scan() {
|
||||
s.logf("tailscaled serve-tailfs stderr: %v", stderrScanner.Text())
|
||||
}
|
||||
}()
|
||||
s.mu.Lock()
|
||||
s.addr = strings.TrimSpace(addr)
|
||||
s.mu.Unlock()
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
var writeMethods = map[string]bool{
|
||||
"PUT": true,
|
||||
"POST": true,
|
||||
"COPY": true,
|
||||
"LOCK": true,
|
||||
"UNLOCK": true,
|
||||
"MKCOL": true,
|
||||
"MOVE": true,
|
||||
"PROPPATCH": true,
|
||||
// SetShares sets the complete set of shares exposed by this node. If
|
||||
// AllowShareAs() reports true, we will use one subprocess per user to
|
||||
// access the filesystem (see userServer). Otherwise, we will use the file
|
||||
// server configured via SetFileServerAddr.
|
||||
SetShares(shares map[string]*Share)
|
||||
|
||||
// ServeHTTPWithPerms behaves like the similar method from http.Handler but
|
||||
// also accepts a Permissions map that captures the permissions of the
|
||||
// connecting node.
|
||||
ServeHTTPWithPerms(permissions Permissions, w http.ResponseWriter, r *http.Request)
|
||||
|
||||
// Close() stops serving the WebDAV content
|
||||
Close() error
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tailfs provides a filesystem that allows sharing folders between
|
||||
// Tailscale nodes using WebDAV.
|
||||
package tailfs
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// statCacheTTL causes the local WebDAV proxy to cache file metadata to
|
||||
// avoid excessive network roundtrips. This is similar to the
|
||||
// DirectoryCacheLifetime setting of Windows' built-in SMB client,
|
||||
// see https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-7/ff686200(v=ws.10)
|
||||
statCacheTTL = 10 * time.Second
|
||||
)
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package tailfsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
//go:build windows || darwin
|
||||
|
||||
package tailfs
|
||||
package tailfsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
+1
-1
@@ -15,7 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
)
|
||||
|
||||
// Mkdir implements webdav.Filesystem. The root of this file system is
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
)
|
||||
|
||||
// OpenFile implements interface webdav.Filesystem.
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
)
|
||||
|
||||
// RemoveAll implements webdav.File. The root of this file system is read-only,
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
)
|
||||
|
||||
// Rename implements interface webdav.FileSystem. The root of this file system
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"context"
|
||||
"io/fs"
|
||||
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
)
|
||||
|
||||
// Stat implements webdav.FileSystem.
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package tailfsimpl
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package tailfsimpl
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package tailfsimpl
|
||||
|
||||
import (
|
||||
"net"
|
||||
@@ -9,11 +9,11 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
)
|
||||
|
||||
// FileServer is a standalone WebDAV server that dynamically serves up shares.
|
||||
// It's typically used in a separate process from the actual Tailfs server to
|
||||
// It's typically used in a separate process from the actual TailFS server to
|
||||
// serve up files as an unprivileged user.
|
||||
type FileServer struct {
|
||||
l net.Listener
|
||||
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package tailfsimpl provides an implementation of package tailfs.
|
||||
package tailfsimpl
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/compositefs"
|
||||
"tailscale.com/tailfs/tailfsimpl/webdavfs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// statCacheTTL causes the local WebDAV proxy to cache file metadata to
|
||||
// avoid excessive network roundtrips. This is similar to the
|
||||
// DirectoryCacheLifetime setting of Windows' built-in SMB client,
|
||||
// see https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-7/ff686200(v=ws.10)
|
||||
statCacheTTL = 10 * time.Second
|
||||
)
|
||||
|
||||
// NewFileSystemForLocal starts serving a filesystem for local clients.
|
||||
// Inbound connections must be handed to HandleConn.
|
||||
func NewFileSystemForLocal(logf logger.Logf) *FileSystemForLocal {
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
fs := &FileSystemForLocal{
|
||||
logf: logf,
|
||||
cfs: compositefs.New(compositefs.Options{Logf: logf}),
|
||||
listener: newConnListener(),
|
||||
}
|
||||
fs.startServing()
|
||||
return fs
|
||||
}
|
||||
|
||||
// FileSystemForLocal is the TailFS filesystem exposed to local clients. It
|
||||
// provides a unified WebDAV interface to remote TailFS shares on other nodes.
|
||||
type FileSystemForLocal struct {
|
||||
logf logger.Logf
|
||||
cfs *compositefs.CompositeFileSystem
|
||||
listener *connListener
|
||||
}
|
||||
|
||||
func (s *FileSystemForLocal) startServing() {
|
||||
hs := &http.Server{
|
||||
Handler: &webdav.Handler{
|
||||
FileSystem: s.cfs,
|
||||
LockSystem: webdav.NewMemLS(),
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
err := hs.Serve(s.listener)
|
||||
if err != nil {
|
||||
// TODO(oxtoacart): should we panic or something different here?
|
||||
log.Printf("serve: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// HandleConn handles connections from local WebDAV clients
|
||||
func (s *FileSystemForLocal) HandleConn(conn net.Conn, remoteAddr net.Addr) error {
|
||||
return s.listener.HandleConn(conn, remoteAddr)
|
||||
}
|
||||
|
||||
// SetRemotes sets the complete set of remotes on the given tailnet domain
|
||||
// using a map of name -> url. If transport is specified, that transport
|
||||
// will be used to connect to these remotes.
|
||||
func (s *FileSystemForLocal) SetRemotes(domain string, remotes []*tailfs.Remote, transport http.RoundTripper) {
|
||||
children := make([]*compositefs.Child, 0, len(remotes))
|
||||
for _, remote := range remotes {
|
||||
opts := webdavfs.Options{
|
||||
URL: remote.URL,
|
||||
Transport: transport,
|
||||
StatCacheTTL: statCacheTTL,
|
||||
Logf: s.logf,
|
||||
}
|
||||
children = append(children, &compositefs.Child{
|
||||
Name: remote.Name,
|
||||
FS: webdavfs.New(opts),
|
||||
Available: remote.Available,
|
||||
})
|
||||
}
|
||||
|
||||
domainChild, found := s.cfs.GetChild(domain)
|
||||
if !found {
|
||||
domainChild = compositefs.New(compositefs.Options{Logf: s.logf})
|
||||
s.cfs.SetChildren(&compositefs.Child{Name: domain, FS: domainChild})
|
||||
}
|
||||
domainChild.(*compositefs.CompositeFileSystem).SetChildren(children...)
|
||||
}
|
||||
|
||||
// Close() stops serving the WebDAV content
|
||||
func (s *FileSystemForLocal) Close() error {
|
||||
s.cfs.Close()
|
||||
return s.listener.Close()
|
||||
}
|
||||
@@ -0,0 +1,359 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfsimpl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/compositefs"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/webdavfs"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func NewFileSystemForRemote(logf logger.Logf) *FileSystemForRemote {
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
fs := &FileSystemForRemote{
|
||||
logf: logf,
|
||||
lockSystem: webdav.NewMemLS(),
|
||||
fileSystems: make(map[string]webdav.FileSystem),
|
||||
userServers: make(map[string]*userServer),
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
// FileSystemForRemote implements tailfs.FileSystemForRemote.
|
||||
type FileSystemForRemote struct {
|
||||
logf logger.Logf
|
||||
lockSystem webdav.LockSystem
|
||||
|
||||
// mu guards the below values. Acquire a write lock before updating any of
|
||||
// them, acquire a read lock before reading any of them.
|
||||
mu sync.RWMutex
|
||||
fileServerAddr string
|
||||
shares map[string]*tailfs.Share
|
||||
fileSystems map[string]webdav.FileSystem
|
||||
userServers map[string]*userServer
|
||||
}
|
||||
|
||||
// SetFileServerAddr implements tailfs.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) SetFileServerAddr(addr string) {
|
||||
s.mu.Lock()
|
||||
s.fileServerAddr = addr
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetShares implements tailfs.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) SetShares(shares map[string]*tailfs.Share) {
|
||||
userServers := make(map[string]*userServer)
|
||||
if tailfs.AllowShareAs() {
|
||||
// set up per-user server
|
||||
for _, share := range shares {
|
||||
p, found := userServers[share.As]
|
||||
if !found {
|
||||
p = &userServer{
|
||||
logf: s.logf,
|
||||
}
|
||||
userServers[share.As] = p
|
||||
}
|
||||
p.shares = append(p.shares, share)
|
||||
}
|
||||
for _, p := range userServers {
|
||||
go p.runLoop()
|
||||
}
|
||||
}
|
||||
|
||||
fileSystems := make(map[string]webdav.FileSystem, len(shares))
|
||||
for _, share := range shares {
|
||||
fileSystems[share.Name] = s.buildWebDAVFS(share)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.shares = shares
|
||||
oldFileSystems := s.fileSystems
|
||||
oldUserServers := s.userServers
|
||||
s.fileSystems = fileSystems
|
||||
s.userServers = userServers
|
||||
s.mu.Unlock()
|
||||
|
||||
s.stopUserServers(oldUserServers)
|
||||
s.closeFileSystems(oldFileSystems)
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) buildWebDAVFS(share *tailfs.Share) webdav.FileSystem {
|
||||
return webdavfs.New(webdavfs.Options{
|
||||
Logf: s.logf,
|
||||
URL: fmt.Sprintf("http://%v/%v", hex.EncodeToString([]byte(share.Name)), share.Name),
|
||||
Transport: &http.Transport{
|
||||
Dial: func(_, shareAddr string) (net.Conn, error) {
|
||||
shareNameHex, _, err := net.SplitHostPort(shareAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse share address %v: %w", shareAddr, err)
|
||||
}
|
||||
|
||||
// We had to encode the share name in hex to make sure it's a valid hostname
|
||||
shareNameBytes, err := hex.DecodeString(shareNameHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode share name from host %v: %v", shareNameHex, err)
|
||||
}
|
||||
shareName := string(shareNameBytes)
|
||||
|
||||
s.mu.RLock()
|
||||
share, shareFound := s.shares[shareName]
|
||||
userServers := s.userServers
|
||||
fileServerAddr := s.fileServerAddr
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !shareFound {
|
||||
return nil, fmt.Errorf("unknown share %v", shareName)
|
||||
}
|
||||
|
||||
var addr string
|
||||
if !tailfs.AllowShareAs() {
|
||||
addr = fileServerAddr
|
||||
} else {
|
||||
userServer, found := userServers[share.As]
|
||||
if found {
|
||||
userServer.mu.RLock()
|
||||
addr = userServer.addr
|
||||
userServer.mu.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
if addr == "" {
|
||||
return nil, fmt.Errorf("unable to determine address for share %v", shareName)
|
||||
}
|
||||
|
||||
_, err = netip.ParseAddrPort(addr)
|
||||
if err == nil {
|
||||
// this is a regular network address, dial normally
|
||||
return net.Dial("tcp", addr)
|
||||
}
|
||||
// assume this is a safesocket address
|
||||
return safesocket.Connect(addr)
|
||||
},
|
||||
},
|
||||
StatRoot: true,
|
||||
})
|
||||
}
|
||||
|
||||
// ServeHTTPWithPerms implements tailfs.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions tailfs.Permissions, w http.ResponseWriter, r *http.Request) {
|
||||
isWrite := writeMethods[r.Method]
|
||||
if isWrite {
|
||||
share := shared.CleanAndSplit(r.URL.Path)[0]
|
||||
switch permissions.For(share) {
|
||||
case tailfs.PermissionNone:
|
||||
// If we have no permissions to this share, treat it as not found
|
||||
// to avoid leaking any information about the share's existence.
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
case tailfs.PermissionReadOnly:
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
fileSystems := s.fileSystems
|
||||
s.mu.RUnlock()
|
||||
|
||||
children := make([]*compositefs.Child, 0, len(fileSystems))
|
||||
// filter out shares to which the connecting principal has no access
|
||||
for name, fs := range fileSystems {
|
||||
if permissions.For(name) == tailfs.PermissionNone {
|
||||
continue
|
||||
}
|
||||
|
||||
children = append(children, &compositefs.Child{Name: name, FS: fs})
|
||||
}
|
||||
|
||||
cfs := compositefs.New(
|
||||
compositefs.Options{
|
||||
Logf: s.logf,
|
||||
StatChildren: true,
|
||||
})
|
||||
cfs.SetChildren(children...)
|
||||
h := webdav.Handler{
|
||||
FileSystem: cfs,
|
||||
LockSystem: s.lockSystem,
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) stopUserServers(userServers map[string]*userServer) {
|
||||
for _, server := range userServers {
|
||||
if err := server.Close(); err != nil {
|
||||
s.logf("error closing tailfs user server: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) closeFileSystems(fileSystems map[string]webdav.FileSystem) {
|
||||
for _, fs := range fileSystems {
|
||||
closer, ok := fs.(interface{ Close() error })
|
||||
if ok {
|
||||
if err := closer.Close(); err != nil {
|
||||
s.logf("error closing tailfs filesystem: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close() implements tailfs.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) Close() error {
|
||||
s.mu.Lock()
|
||||
userServers := s.userServers
|
||||
fileSystems := s.fileSystems
|
||||
s.mu.Unlock()
|
||||
|
||||
s.stopUserServers(userServers)
|
||||
s.closeFileSystems(fileSystems)
|
||||
return nil
|
||||
}
|
||||
|
||||
// userServer runs tailscaled serve-tailfs to serve webdav content for the
|
||||
// given Shares. All Shares are assumed to have the same Share.As, and the
|
||||
// content is served as that Share.As user.
|
||||
type userServer struct {
|
||||
logf logger.Logf
|
||||
shares []*tailfs.Share
|
||||
|
||||
// mu guards the below values. Acquire a write lock before updating any of
|
||||
// them, acquire a read lock before reading any of them.
|
||||
mu sync.RWMutex
|
||||
cmd *exec.Cmd
|
||||
addr string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (s *userServer) Close() error {
|
||||
s.mu.Lock()
|
||||
cmd := s.cmd
|
||||
s.closed = true
|
||||
s.mu.Unlock()
|
||||
if cmd != nil && cmd.Process != nil {
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
// not running, that's okay
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *userServer) runLoop() {
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
s.logf("can't find executable: %v", err)
|
||||
return
|
||||
}
|
||||
maxSleepTime := 30 * time.Second
|
||||
consecutiveFailures := float64(0)
|
||||
var timeOfLastFailure time.Time
|
||||
for {
|
||||
s.mu.RLock()
|
||||
closed := s.closed
|
||||
s.mu.RUnlock()
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
|
||||
err := s.run(executable)
|
||||
now := time.Now()
|
||||
timeSinceLastFailure := now.Sub(timeOfLastFailure)
|
||||
timeOfLastFailure = now
|
||||
if timeSinceLastFailure < maxSleepTime {
|
||||
consecutiveFailures++
|
||||
} else {
|
||||
consecutiveFailures = 1
|
||||
}
|
||||
sleepTime := time.Duration(math.Pow(2, consecutiveFailures)) * time.Millisecond
|
||||
if sleepTime > maxSleepTime {
|
||||
sleepTime = maxSleepTime
|
||||
}
|
||||
s.logf("user server % v stopped with error %v, will try again in %v", executable, err, sleepTime)
|
||||
time.Sleep(sleepTime)
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs the executable (tailscaled). This function only works on UNIX systems,
|
||||
// but those are the only ones on which we use userServers anyway.
|
||||
func (s *userServer) run(executable string) error {
|
||||
// set up the command
|
||||
args := []string{"serve-tailfs"}
|
||||
for _, s := range s.shares {
|
||||
args = append(args, s.Name, s.Path)
|
||||
}
|
||||
allArgs := []string{"-u", s.shares[0].As, executable}
|
||||
allArgs = append(allArgs, args...)
|
||||
cmd := exec.Command("sudo", allArgs...)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
defer stdout.Close()
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stderr pipe: %w", err)
|
||||
}
|
||||
defer stderr.Close()
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.cmd = cmd
|
||||
s.mu.Unlock()
|
||||
|
||||
// read address
|
||||
stdoutScanner := bufio.NewScanner(stdout)
|
||||
stdoutScanner.Scan()
|
||||
if stdoutScanner.Err() != nil {
|
||||
return fmt.Errorf("read addr: %w", stdoutScanner.Err())
|
||||
}
|
||||
addr := stdoutScanner.Text()
|
||||
// send the rest of stdout and stderr to logger to avoid blocking
|
||||
go func() {
|
||||
for stdoutScanner.Scan() {
|
||||
s.logf("tailscaled serve-tailfs stdout: %v", stdoutScanner.Text())
|
||||
}
|
||||
}()
|
||||
stderrScanner := bufio.NewScanner(stderr)
|
||||
go func() {
|
||||
for stderrScanner.Scan() {
|
||||
s.logf("tailscaled serve-tailfs stderr: %v", stderrScanner.Text())
|
||||
}
|
||||
}()
|
||||
s.mu.Lock()
|
||||
s.addr = strings.TrimSpace(addr)
|
||||
s.mu.Unlock()
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
var writeMethods = map[string]bool{
|
||||
"PUT": true,
|
||||
"POST": true,
|
||||
"COPY": true,
|
||||
"LOCK": true,
|
||||
"UNLOCK": true,
|
||||
"MKCOL": true,
|
||||
"MOVE": true,
|
||||
"PROPPATCH": true,
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tailfs
|
||||
package tailfsimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -20,8 +20,9 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/webdavfs"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/webdavfs"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -38,10 +39,10 @@ const (
|
||||
func init() {
|
||||
// set AllowShareAs() to false so that we don't try to use sub-processes
|
||||
// for access files on disk.
|
||||
disallowShareAs = true
|
||||
tailfs.DisallowShareAs = true
|
||||
}
|
||||
|
||||
// The tests in this file simulate real-life Tailfs scenarios, but without
|
||||
// The tests in this file simulate real-life TailFS scenarios, but without
|
||||
// going over the Tailscale network stack.
|
||||
func TestDirectoryListing(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
@@ -51,9 +52,9 @@ func TestDirectoryListing(t *testing.T) {
|
||||
s.checkDirList("root directory should contain the one and only domain once a remote has been set", "/", domain)
|
||||
s.checkDirList("domain should contain its only remote", shared.Join(domain), remote1)
|
||||
s.checkDirList("remote with no shares should be empty", shared.Join(domain, remote1))
|
||||
s.addShare(remote1, share11, PermissionReadWrite)
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
s.checkDirList("remote with one share should contain that share", shared.Join(domain, remote1), share11)
|
||||
s.addShare(remote1, share12, PermissionReadOnly)
|
||||
s.addShare(remote1, share12, tailfs.PermissionReadOnly)
|
||||
s.checkDirList("remote with two shares should contain both in lexicographical order", shared.Join(domain, remote1), share12, share11)
|
||||
s.checkDirListIncremental("remote with two shares should contain both in lexicographical order even when reading directory incrementally", shared.Join(domain, remote1), share12, share11)
|
||||
|
||||
@@ -73,12 +74,12 @@ func TestFileManipulation(t *testing.T) {
|
||||
defer s.stop()
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, PermissionReadWrite)
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
s.checkFileStatus(remote1, share11, file111)
|
||||
s.checkFileContents(remote1, share11, file111)
|
||||
|
||||
s.addShare(remote1, share12, PermissionReadOnly)
|
||||
s.addShare(remote1, share12, tailfs.PermissionReadOnly)
|
||||
s.writeFile("writing file to read-only remote should fail", remote1, share12, file111, "hello world", false)
|
||||
|
||||
s.writeFile("writing file to non-existent remote should fail", "non-existent", share11, file111, "hello world", false)
|
||||
@@ -92,7 +93,7 @@ func TestFileOps(t *testing.T) {
|
||||
defer s.stop()
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, PermissionReadWrite)
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
fi, err := s.fs.Stat(context.Background(), pathTo(remote1, share11, file111))
|
||||
if err != nil {
|
||||
@@ -204,7 +205,7 @@ func TestFileRewind(t *testing.T) {
|
||||
defer s.stop()
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, PermissionReadWrite)
|
||||
s.addShare(remote1, share11, tailfs.PermissionReadWrite)
|
||||
|
||||
// Create a file slightly longer than our max rewind buffer of 512
|
||||
fileLength := webdavfs.MaxRewindBuffer + 1
|
||||
@@ -267,7 +268,7 @@ type remote struct {
|
||||
fs *FileSystemForRemote
|
||||
fileServer *FileServer
|
||||
shares map[string]string
|
||||
permissions map[string]Permission
|
||||
permissions map[string]tailfs.Permission
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -343,15 +344,15 @@ func (s *system) addRemote(name string) {
|
||||
fileServer: fileServer,
|
||||
fs: NewFileSystemForRemote(log.Printf),
|
||||
shares: make(map[string]string),
|
||||
permissions: make(map[string]Permission),
|
||||
permissions: make(map[string]tailfs.Permission),
|
||||
}
|
||||
r.fs.SetFileServerAddr(fileServer.Addr())
|
||||
go http.Serve(l, r)
|
||||
s.remotes[name] = r
|
||||
|
||||
remotes := make([]*Remote, 0, len(s.remotes))
|
||||
remotes := make([]*tailfs.Remote, 0, len(s.remotes))
|
||||
for name, r := range s.remotes {
|
||||
remotes = append(remotes, &Remote{
|
||||
remotes = append(remotes, &tailfs.Remote{
|
||||
Name: name,
|
||||
URL: fmt.Sprintf("http://%s", r.l.Addr()),
|
||||
})
|
||||
@@ -359,7 +360,7 @@ func (s *system) addRemote(name string) {
|
||||
s.local.fs.SetRemotes(domain, remotes, &http.Transport{})
|
||||
}
|
||||
|
||||
func (s *system) addShare(remoteName, shareName string, permission Permission) {
|
||||
func (s *system) addShare(remoteName, shareName string, permission tailfs.Permission) {
|
||||
r, ok := s.remotes[remoteName]
|
||||
if !ok {
|
||||
s.t.Fatalf("unknown remote %q", remoteName)
|
||||
@@ -369,9 +370,9 @@ func (s *system) addShare(remoteName, shareName string, permission Permission) {
|
||||
r.shares[shareName] = f
|
||||
r.permissions[shareName] = permission
|
||||
|
||||
shares := make(map[string]*Share, len(r.shares))
|
||||
shares := make(map[string]*tailfs.Share, len(r.shares))
|
||||
for shareName, folder := range r.shares {
|
||||
shares[shareName] = &Share{
|
||||
shares[shareName] = &tailfs.Share{
|
||||
Name: shareName,
|
||||
Path: folder,
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"github.com/tailscale/gowebdav"
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"tailscale.com/tailfs/shared"
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
)
|
||||
|
||||
type writeOnlyFile struct {
|
||||
Reference in New Issue
Block a user