ipn,cmd/tailscale,client/tailscale: add support for renaming TailFS shares

- Updates API to support renaming TailFS shares.
- Adds a CLI rename subcommand for renaming a share.
- Renames the CLI subcommand 'add' to 'set' to make it clear that
  this is an add or update.
- Adds a unit test for TailFS in ipnlocal

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
This commit is contained in:
Percy Wegmann
2024-03-08 10:43:32 -06:00
committed by Percy Wegmann
parent 6c160e6321
commit e496451928
6 changed files with 411 additions and 38 deletions
+85 -11
View File
@@ -6,7 +6,9 @@ package ipnlocal
import (
"errors"
"fmt"
"os"
"regexp"
"slices"
"strings"
"tailscale.com/ipn"
@@ -24,7 +26,8 @@ const (
var (
shareNameRegex = regexp.MustCompile(`^[a-z0-9_\(\) ]+$`)
errInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
ErrTailFSNotEnabled = errors.New("TailFS not enabled")
ErrInvalidShareName = errors.New("Share names may only contain the letters a-z, underscore _, parentheses (), or spaces")
)
// TailFSSharingEnabled reports whether sharing to remote nodes via tailfs is
@@ -59,18 +62,18 @@ func (b *LocalBackend) tailFSAccessEnabledLocked() bool {
func (b *LocalBackend) TailFSSetFileServerAddr(addr string) error {
fs, ok := b.sys.TailFSForRemote.GetOK()
if !ok {
return errors.New("tailfs not enabled")
return ErrTailFSNotEnabled
}
fs.SetFileServerAddr(addr)
return nil
}
// TailFSAddShare adds the given share if no share with that name exists, or
// replaces the existing share if one with the same name already exists.
// To avoid potential incompatibilities across file systems, share names are
// TailFSSetShare adds the given share if no share with that name exists, or
// replaces the existing share if one with the same name already exists. To
// avoid potential incompatibilities across file systems, share names are
// limited to alphanumeric characters and the underscore _.
func (b *LocalBackend) TailFSAddShare(share *tailfs.Share) error {
func (b *LocalBackend) TailFSSetShare(share *tailfs.Share) error {
var err error
share.Name, err = normalizeShareName(share.Name)
if err != nil {
@@ -78,7 +81,7 @@ func (b *LocalBackend) TailFSAddShare(share *tailfs.Share) error {
}
b.mu.Lock()
shares, err := b.tailFSAddShareLocked(share)
shares, err := b.tailFSSetShareLocked(share)
b.mu.Unlock()
if err != nil {
return err
@@ -99,18 +102,18 @@ func normalizeShareName(name string) (string, error) {
name = strings.TrimSpace(name)
if !shareNameRegex.MatchString(name) {
return "", errInvalidShareName
return "", ErrInvalidShareName
}
return name, nil
}
func (b *LocalBackend) tailFSAddShareLocked(share *tailfs.Share) (views.SliceView[*tailfs.Share, tailfs.ShareView], error) {
func (b *LocalBackend) tailFSSetShareLocked(share *tailfs.Share) (views.SliceView[*tailfs.Share, tailfs.ShareView], error) {
existingShares := b.pm.prefs.TailFSShares()
fs, ok := b.sys.TailFSForRemote.GetOK()
if !ok {
return existingShares, errors.New("tailfs not enabled")
return existingShares, ErrTailFSNotEnabled
}
addedShare := false
@@ -139,6 +142,70 @@ func (b *LocalBackend) tailFSAddShareLocked(share *tailfs.Share) (views.SliceVie
return b.pm.prefs.TailFSShares(), nil
}
// TailFSRenameShare renames the share at old name to new name. To avoid
// potential incompatibilities across file systems, the new share name is
// limited to alphanumeric characters and the underscore _.
// Any of the following will result in an error.
// - no share found under old name
// - new share name contains disallowed characters
// - share already exists under new name
func (b *LocalBackend) TailFSRenameShare(oldName, newName string) error {
var err error
newName, err = normalizeShareName(newName)
if err != nil {
return err
}
b.mu.Lock()
shares, err := b.tailFSRenameShareLocked(oldName, newName)
b.mu.Unlock()
if err != nil {
return err
}
b.tailFSNotifyShares(shares)
return nil
}
func (b *LocalBackend) tailFSRenameShareLocked(oldName, newName string) (views.SliceView[*tailfs.Share, tailfs.ShareView], error) {
existingShares := b.pm.prefs.TailFSShares()
fs, ok := b.sys.TailFSForRemote.GetOK()
if !ok {
return existingShares, ErrTailFSNotEnabled
}
found := false
var shares []*tailfs.Share
for i := 0; i < existingShares.Len(); i++ {
existing := existingShares.At(i)
if existing.Name() == newName {
return existingShares, os.ErrExist
}
if existing.Name() == oldName {
share := existing.AsStruct()
share.Name = newName
shares = append(shares, share)
found = true
} else {
shares = append(shares, existing.AsStruct())
}
}
if !found {
return existingShares, os.ErrNotExist
}
slices.SortFunc(shares, tailfs.CompareShares)
err := b.tailFSSetSharesLocked(shares)
if err != nil {
return existingShares, err
}
fs.SetShares(shares)
return b.pm.prefs.TailFSShares(), nil
}
// TailFSRemoveShare removes the named share. Share names are forced to
// lowercase.
func (b *LocalBackend) TailFSRemoveShare(name string) error {
@@ -166,17 +233,24 @@ func (b *LocalBackend) tailFSRemoveShareLocked(name string) (views.SliceView[*ta
fs, ok := b.sys.TailFSForRemote.GetOK()
if !ok {
return existingShares, errors.New("tailfs not enabled")
return existingShares, ErrTailFSNotEnabled
}
found := false
var shares []*tailfs.Share
for i := 0; i < existingShares.Len(); i++ {
existing := existingShares.At(i)
if existing.Name() != name {
shares = append(shares, existing.AsStruct())
} else {
found = true
}
}
if !found {
return existingShares, os.ErrNotExist
}
err := b.tailFSSetSharesLocked(shares)
if err != nil {
return existingShares, err