feature/taildrop: do not use m.opts.Dir for Android (#16316)
In Android, we are prompting the user to select a Taildrop directory when they first receive a Taildrop: we block writes on Taildrop dir selection. This means that we cannot use Dir inside managerOptions, since the http request would not get the new Taildrop extension. This PR removes, in the Android case, the reliance on m.opts.Dir, and instead has FileOps hold the correct directory. This expands FileOps to be the Taildrop interface for all file system operations. Updates tailscale/corp#29211 Signed-off-by: kari-ts <kari@tailscale.com> restore tstest
This commit is contained in:
@@ -4,123 +4,64 @@
|
||||
package taildrop
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
// nopWriteCloser is a no-op io.WriteCloser wrapping a bytes.Buffer.
|
||||
type nopWriteCloser struct{ *bytes.Buffer }
|
||||
|
||||
func (nwc nopWriteCloser) Close() error { return nil }
|
||||
|
||||
// mockFileOps implements just enough of the FileOps interface for SAF tests.
|
||||
type mockFileOps struct {
|
||||
writes *bytes.Buffer
|
||||
renameOK bool
|
||||
}
|
||||
|
||||
func (m *mockFileOps) OpenFileWriter(name string) (io.WriteCloser, string, error) {
|
||||
m.writes = new(bytes.Buffer)
|
||||
return nopWriteCloser{m.writes}, "uri://" + name + ".partial", nil
|
||||
}
|
||||
|
||||
func (m *mockFileOps) RenamePartialFile(partialPath, dir, finalName string) (string, error) {
|
||||
if !m.renameOK {
|
||||
m.renameOK = true
|
||||
return "uri://" + finalName, nil
|
||||
}
|
||||
return "", io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
func TestPutFile(t *testing.T) {
|
||||
const content = "hello, world"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mode PutMode
|
||||
setup func(t *testing.T) (*manager, string, *mockFileOps)
|
||||
wantFile string
|
||||
name string
|
||||
directFileMode bool
|
||||
}{
|
||||
{
|
||||
name: "PutModeDirect",
|
||||
mode: PutModeDirect,
|
||||
setup: func(t *testing.T) (*manager, string, *mockFileOps) {
|
||||
dir := t.TempDir()
|
||||
opts := managerOptions{
|
||||
Logf: t.Logf,
|
||||
Clock: tstime.DefaultClock{},
|
||||
State: nil,
|
||||
Dir: dir,
|
||||
Mode: PutModeDirect,
|
||||
DirectFileMode: true,
|
||||
SendFileNotify: func() {},
|
||||
}
|
||||
mgr := opts.New()
|
||||
return mgr, dir, nil
|
||||
},
|
||||
wantFile: "file.txt",
|
||||
},
|
||||
{
|
||||
name: "PutModeAndroidSAF",
|
||||
mode: PutModeAndroidSAF,
|
||||
setup: func(t *testing.T) (*manager, string, *mockFileOps) {
|
||||
// SAF still needs a non-empty Dir to pass the guard.
|
||||
dir := t.TempDir()
|
||||
mops := &mockFileOps{}
|
||||
opts := managerOptions{
|
||||
Logf: t.Logf,
|
||||
Clock: tstime.DefaultClock{},
|
||||
State: nil,
|
||||
Dir: dir,
|
||||
Mode: PutModeAndroidSAF,
|
||||
FileOps: mops,
|
||||
DirectFileMode: true,
|
||||
SendFileNotify: func() {},
|
||||
}
|
||||
mgr := opts.New()
|
||||
return mgr, dir, mops
|
||||
},
|
||||
wantFile: "file.txt",
|
||||
},
|
||||
{"DirectFileMode", true},
|
||||
{"NonDirectFileMode", false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mgr, dir, mops := tc.setup(t)
|
||||
id := clientID(fmt.Sprint(0))
|
||||
reader := bytes.NewReader([]byte(content))
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
mgr := managerOptions{
|
||||
Logf: t.Logf,
|
||||
Clock: tstime.DefaultClock{},
|
||||
State: nil,
|
||||
fileOps: must.Get(newFileOps(dir)),
|
||||
DirectFileMode: tt.directFileMode,
|
||||
SendFileNotify: func() {},
|
||||
}.New()
|
||||
|
||||
n, err := mgr.PutFile(id, "file.txt", reader, 0, int64(len(content)))
|
||||
id := clientID("0")
|
||||
n, err := mgr.PutFile(id, "file.txt", strings.NewReader(content), 0, int64(len(content)))
|
||||
if err != nil {
|
||||
t.Fatalf("PutFile(%s) error: %v", tc.name, err)
|
||||
t.Fatalf("PutFile error: %v", err)
|
||||
}
|
||||
if n != int64(len(content)) {
|
||||
t.Errorf("wrote %d bytes; want %d", n, len(content))
|
||||
}
|
||||
|
||||
switch tc.mode {
|
||||
case PutModeDirect:
|
||||
path := filepath.Join(dir, tc.wantFile)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile error: %v", err)
|
||||
}
|
||||
if got := string(data); got != content {
|
||||
t.Errorf("file contents = %q; want %q", got, content)
|
||||
}
|
||||
path := filepath.Join(dir, "file.txt")
|
||||
|
||||
case PutModeAndroidSAF:
|
||||
if mops.writes == nil {
|
||||
t.Fatal("SAF writer was never created")
|
||||
}
|
||||
if got := mops.writes.String(); got != content {
|
||||
t.Errorf("SAF writes = %q; want %q", got, content)
|
||||
got, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile %q: %v", path, err)
|
||||
}
|
||||
if string(got) != content {
|
||||
t.Errorf("file contents = %q; want %q", string(got), content)
|
||||
}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if strings.Contains(entry.Name(), ".partial") {
|
||||
t.Errorf("unexpected partial file left behind: %s", entry.Name())
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user