diff --git a/cmd/tailscale/cli/file.go b/cmd/tailscale/cli/file.go index f0933ca5a..3197b3774 100644 --- a/cmd/tailscale/cli/file.go +++ b/cmd/tailscale/cli/file.go @@ -27,6 +27,8 @@ import ( "golang.org/x/time/rate" "inet.af/netaddr" "tailscale.com/client/tailscale" + "tailscale.com/client/tailscale/apitype" + "tailscale.com/ipn" ) var fileCmd = &ffcli.Command{ @@ -35,7 +37,7 @@ var fileCmd = &ffcli.Command{ ShortHelp: "Send or receive files", Subcommands: []*ffcli.Command{ fileCpCmd, - // TODO: fileGetCmd, + fileGetCmd, }, Exec: func(context.Context, []string) error { // TODO(bradfitz): is there a better ffcli way to @@ -273,3 +275,140 @@ func runCpTargets(ctx context.Context, args []string) error { } return nil } + +var fileGetCmd = &ffcli.Command{ + Name: "get", + ShortUsage: "file get [--wait] [--verbose] ", + ShortHelp: "Move files out of the Tailscale file inbox", + Exec: runFileGet, + FlagSet: (func() *flag.FlagSet { + fs := flag.NewFlagSet("get", flag.ExitOnError) + fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty") + fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output") + return fs + })(), +} + +var getArgs struct { + wait bool + verbose bool +} + +func runFileGet(ctx context.Context, args []string) error { + if len(args) != 1 { + return errors.New("usage: file get ") + } + log.SetFlags(0) + + dir := args[0] + if dir == "/dev/null" { + return wipeInbox(ctx) + } + + if fi, err := os.Stat(dir); err != nil || !fi.IsDir() { + return fmt.Errorf("%q is not a directory", dir) + } + + var wfs []apitype.WaitingFile + var err error + for { + wfs, err = tailscale.WaitingFiles(ctx) + if err != nil { + return fmt.Errorf("getting WaitingFiles: %v", err) + } + if len(wfs) != 0 || !getArgs.wait { + break + } + if getArgs.verbose { + log.Printf("waiting for file...") + } + if err := waitForFile(ctx); err != nil { + return err + } + } + + deleted := 0 + for _, wf := range wfs { + rc, size, err := tailscale.GetWaitingFile(ctx, wf.Name) + if err != nil { + return fmt.Errorf("opening inbox file %q: %v", wf.Name, err) + } + targetFile := filepath.Join(dir, wf.Name) + of, err := os.OpenFile(targetFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644) + if err != nil { + if _, err := os.Stat(targetFile); err == nil { + return fmt.Errorf("refusing to overwrite %v", targetFile) + } + return err + } + _, err = io.Copy(of, rc) + rc.Close() + if err != nil { + return fmt.Errorf("failed to write %v: %v", targetFile, err) + } + if err := of.Close(); err != nil { + return err + } + if getArgs.verbose { + log.Printf("wrote %v (%d bytes)", wf.Name, size) + } + if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil { + return fmt.Errorf("deleting %q from inbox: %v", wf.Name, err) + } + deleted++ + } + if getArgs.verbose { + log.Printf("moved %d files", deleted) + } + return nil +} + +func wipeInbox(ctx context.Context) error { + if getArgs.wait { + return errors.New("can't use --wait with /dev/null target") + } + wfs, err := tailscale.WaitingFiles(ctx) + if err != nil { + return fmt.Errorf("getting WaitingFiles: %v", err) + } + deleted := 0 + for _, wf := range wfs { + if getArgs.verbose { + log.Printf("deleting %v ...", wf.Name) + } + if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil { + return fmt.Errorf("deleting %q: %v", wf.Name, err) + } + deleted++ + } + if getArgs.verbose { + log.Printf("deleted %d files", deleted) + } + return nil +} + +func waitForFile(ctx context.Context) error { + c, bc, pumpCtx, cancel := connect(ctx) + defer cancel() + fileWaiting := make(chan bool, 1) + bc.SetNotifyCallback(func(n ipn.Notify) { + if n.ErrMessage != nil { + log.Fatal(*n.ErrMessage) + } + if n.FilesWaiting != nil { + select { + case fileWaiting <- true: + default: + } + } + }) + go pump(pumpCtx, bc, c) + select { + case <-fileWaiting: + return nil + case <-pumpCtx.Done(): + return pumpCtx.Err() + case <-ctx.Done(): + return ctx.Err() + } +} diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index c95c3b495..329a2be51 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -15,7 +15,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep rsc.io/goversion/version from tailscale.com/version tailscale.com/atomicfile from tailscale.com/ipn tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli - tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale + tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+ tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale tailscale.com/derp from tailscale.com/derp/derphttp tailscale.com/derp/derphttp from tailscale.com/net/netcheck