|
|
|
@ -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] <target-directory>",
|
|
|
|
|
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 <target-directory>")
|
|
|
|
|
}
|
|
|
|
|
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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|