mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-26 11:35:35 +00:00
50fb8b9123
Instead of modeling remote WebDAV servers as actual webdav.FS instances, we now just proxy traffic to them. This not only simplifies the code, but it also allows WebDAV locking to work correctly by making sure locks are handled by the servers that need to (i.e. the ones actually serving the files). Updates tailscale/corp#16827 Signed-off-by: Percy Wegmann <percy@tailscale.com>
349 lines
6.9 KiB
Go
349 lines
6.9 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package dirfs
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/tailscale/xnet/webdav"
|
|
"tailscale.com/tailfs/tailfsimpl/shared"
|
|
"tailscale.com/tstest"
|
|
)
|
|
|
|
func TestStat(t *testing.T) {
|
|
cfs, _, _, clock := createFileSystem(t)
|
|
|
|
tests := []struct {
|
|
label string
|
|
name string
|
|
expected fs.FileInfo
|
|
err error
|
|
}{
|
|
{
|
|
label: "root folder",
|
|
name: "",
|
|
expected: &shared.StaticFileInfo{
|
|
Named: "",
|
|
Sized: 0,
|
|
Moded: 0555,
|
|
ModdedTime: clock.Now(),
|
|
Dir: true,
|
|
},
|
|
},
|
|
{
|
|
label: "static root folder",
|
|
name: "/domain",
|
|
expected: &shared.StaticFileInfo{
|
|
Named: "domain",
|
|
Sized: 0,
|
|
Moded: 0555,
|
|
ModdedTime: clock.Now(),
|
|
Dir: true,
|
|
},
|
|
},
|
|
{
|
|
label: "remote1",
|
|
name: "/domain/remote1",
|
|
expected: &shared.StaticFileInfo{
|
|
Named: "remote1",
|
|
Sized: 0,
|
|
Moded: 0555,
|
|
ModdedTime: clock.Now(),
|
|
Dir: true,
|
|
},
|
|
},
|
|
{
|
|
label: "remote2",
|
|
name: "/domain/remote2",
|
|
expected: &shared.StaticFileInfo{
|
|
Named: "remote2",
|
|
Sized: 0,
|
|
Moded: 0555,
|
|
ModdedTime: clock.Now(),
|
|
Dir: true,
|
|
},
|
|
},
|
|
{
|
|
label: "non-existent remote",
|
|
name: "remote3",
|
|
err: os.ErrNotExist,
|
|
},
|
|
}
|
|
|
|
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 !errors.Is(err, test.err) {
|
|
t.Errorf("got %v, want %v", err, test.err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unable to stat file: %v", err)
|
|
} else {
|
|
infosEqual(t, test.expected, fi)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListDir(t *testing.T) {
|
|
cfs, _, _, clock := createFileSystem(t)
|
|
|
|
tests := []struct {
|
|
label string
|
|
name string
|
|
expected []fs.FileInfo
|
|
err error
|
|
}{
|
|
{
|
|
label: "root folder",
|
|
name: "",
|
|
expected: []fs.FileInfo{
|
|
&shared.StaticFileInfo{
|
|
Named: "domain",
|
|
Sized: 0,
|
|
Moded: 0555,
|
|
ModdedTime: clock.Now(),
|
|
Dir: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
label: "static root folder",
|
|
name: "/domain",
|
|
expected: []fs.FileInfo{
|
|
&shared.StaticFileInfo{
|
|
Named: "remote1",
|
|
Sized: 0,
|
|
Moded: 0555,
|
|
ModdedTime: clock.Now(),
|
|
Dir: true,
|
|
},
|
|
&shared.StaticFileInfo{
|
|
Named: "remote2",
|
|
Sized: 0,
|
|
Moded: 0555,
|
|
ModdedTime: clock.Now(),
|
|
Dir: true,
|
|
},
|
|
&shared.StaticFileInfo{
|
|
Named: "remote4",
|
|
Sized: 0,
|
|
Moded: 0555,
|
|
ModdedTime: clock.Now(),
|
|
Dir: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
for _, test := range tests {
|
|
t.Run(test.label, func(t *testing.T) {
|
|
var infos []fs.FileInfo
|
|
file, err := cfs.OpenFile(ctx, test.name, os.O_RDONLY, 0)
|
|
if err == nil {
|
|
defer file.Close()
|
|
infos, err = file.Readdir(0)
|
|
}
|
|
if test.err != nil {
|
|
if !errors.Is(err, test.err) {
|
|
t.Errorf("got %v, want %v", err, test.err)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("unable to stat file: %v", err)
|
|
} else {
|
|
if len(infos) != len(test.expected) {
|
|
t.Errorf("wrong number of file infos, want %d, got %d", len(test.expected), len(infos))
|
|
} else {
|
|
for i, expected := range test.expected {
|
|
infosEqual(t, expected, infos[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMkdir(t *testing.T) {
|
|
fs, _, _, _ := createFileSystem(t)
|
|
|
|
tests := []struct {
|
|
label string
|
|
name string
|
|
perm os.FileMode
|
|
err error
|
|
}{
|
|
{
|
|
label: "attempt to create root folder",
|
|
name: "/",
|
|
},
|
|
{
|
|
label: "attempt to create static root folder",
|
|
name: "/domain",
|
|
},
|
|
{
|
|
label: "attempt to create remote",
|
|
name: "/domain/remote1",
|
|
},
|
|
{
|
|
label: "attempt to create non-existent remote",
|
|
name: "/domain/remote3",
|
|
err: os.ErrPermission,
|
|
},
|
|
}
|
|
|
|
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 !errors.Is(err, test.err) {
|
|
t.Errorf("got %v, want %v", err, test.err)
|
|
}
|
|
} else if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemoveAll(t *testing.T) {
|
|
fs, _, _, _ := createFileSystem(t)
|
|
|
|
tests := []struct {
|
|
label string
|
|
name string
|
|
err error
|
|
}{
|
|
{
|
|
label: "attempt to remove root folder",
|
|
name: "/",
|
|
err: os.ErrPermission,
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
for _, test := range tests {
|
|
t.Run(test.label, func(t *testing.T) {
|
|
err := fs.RemoveAll(ctx, test.name)
|
|
if !errors.Is(err, test.err) {
|
|
t.Errorf("got %v, want %v", err, test.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRename(t *testing.T) {
|
|
fs, _, _, _ := createFileSystem(t)
|
|
|
|
tests := []struct {
|
|
label string
|
|
oldName string
|
|
newName string
|
|
err error
|
|
}{
|
|
{
|
|
label: "attempt to move root folder",
|
|
oldName: "/",
|
|
newName: "/domain/remote2/copy.txt",
|
|
err: os.ErrPermission,
|
|
},
|
|
}
|
|
|
|
ctx := context.Background()
|
|
for _, test := range tests {
|
|
t.Run(test.label, func(t *testing.T) {
|
|
err := fs.Rename(ctx, test.oldName, test.newName)
|
|
if !errors.Is(err, test.err) {
|
|
t.Errorf("got %v, want: %v", err, test.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func createFileSystem(t *testing.T) (webdav.FileSystem, string, string, *tstest.Clock) {
|
|
s1, dir1 := startRemote(t)
|
|
s2, 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)
|
|
}
|
|
|
|
clock := tstest.NewClock(tstest.ClockOpts{Start: time.Now()})
|
|
fs := &FS{
|
|
Clock: clock,
|
|
StaticRoot: "domain",
|
|
Children: []*Child{
|
|
{Name: "remote1"},
|
|
{Name: "remote2"},
|
|
{Name: "remote4"},
|
|
},
|
|
}
|
|
|
|
t.Cleanup(func() {
|
|
defer s1.Close()
|
|
defer os.RemoveAll(dir1)
|
|
defer s2.Close()
|
|
defer os.RemoveAll(dir2)
|
|
})
|
|
|
|
return fs, dir1, dir2, clock
|
|
}
|
|
|
|
func startRemote(t *testing.T) (*httptest.Server, string) {
|
|
dir := t.TempDir()
|
|
|
|
h := &webdav.Handler{
|
|
FileSystem: webdav.Dir(dir),
|
|
LockSystem: webdav.NewMemLS(),
|
|
}
|
|
|
|
s := httptest.NewServer(h)
|
|
t.Cleanup(s.Close)
|
|
return s, dir
|
|
}
|
|
|
|
func infosEqual(t *testing.T, expected, actual fs.FileInfo) {
|
|
t.Helper()
|
|
sfi, ok := actual.(*shared.StaticFileInfo)
|
|
if ok {
|
|
// zero out BirthedTime because we don't want to compare that
|
|
sfi.BirthedTime = time.Time{}
|
|
}
|
|
if diff := cmp.Diff(actual, expected); diff != "" {
|
|
t.Errorf("Wrong file info (-got, +want):\n%s", diff)
|
|
}
|
|
}
|