@ -318,6 +318,7 @@ var fileGetCmd = &ffcli.Command{
FlagSet : ( func ( ) * flag . FlagSet {
fs := newFlagSet ( "get" )
fs . BoolVar ( & getArgs . wait , "wait" , false , "wait for a file to arrive if inbox is empty" )
fs . BoolVar ( & getArgs . loop , "loop" , false , "run get in a loop, receiving files as they come in" )
fs . BoolVar ( & getArgs . verbose , "verbose" , false , "verbose output" )
fs . Var ( & getArgs . conflict , "conflict" , ` behavior when a conflicting ( same - named ) file already exists in the target directory .
skip : skip conflicting files : leave them in the taildrop inbox and print an error . get any non - conflicting files
@ -329,6 +330,7 @@ var fileGetCmd = &ffcli.Command{
var getArgs = struct {
wait bool
loop bool
verbose bool
conflict onConflict
} { conflict : skipOnExist }
@ -358,7 +360,7 @@ func openFileOrSubstitute(dir, base string, action onConflict) (*os.File, error)
}
return nil , fmt . Errorf ( "failed to write; %w" , err )
case overwriteExisting :
// remove the target file and create it anew so we don't fall for an
// remove the target file and create it anew so we don't fall for an
// attacker who symlinks a known target name to a file he wants changed.
if err = os . Remove ( targetFile ) ; err != nil {
return nil , fmt . Errorf ( "unable to remove target file: %w" , err )
@ -387,54 +389,48 @@ func receiveFile(ctx context.Context, wf apitype.WaitingFile, dir string) (targe
if err != nil {
return "" , 0 , fmt . Errorf ( "opening inbox file %q: %w" , wf . Name , err )
}
defer rc . Close ( )
f , err := openFileOrSubstitute ( dir , wf . Name , getArgs . conflict )
if err != nil {
return "" , 0 , err
}
_ , err = io . Copy ( f , rc )
rc . Close ( )
if err != nil {
f . Close ( )
return "" , 0 , fmt . Errorf ( "failed to write %v: %v" , f . Name ( ) , err )
}
return f . Name ( ) , size , f . Close ( )
}
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 )
}
func runFileGetOneBatch ( ctx context . Context , dir string ) [ ] error {
var wfs [ ] apitype . WaitingFile
var err error
for {
var errs [ ] error
for len ( errs ) == 0 {
wfs , err = tailscale . WaitingFiles ( ctx )
if err != nil {
return fmt . Errorf ( "getting WaitingFiles: %w" , err )
errs = append ( errs , fmt . Errorf ( "getting WaitingFiles: %w" , err ) )
break
}
if len ( wfs ) != 0 || ! getArgs . wait {
if len ( wfs ) != 0 || ! ( getArgs . wait || getArgs . loop ) {
break
}
if getArgs . verbose {
printf ( "waiting for file..." )
}
if err := waitForFile ( ctx ) ; err != nil {
return err
errs = append ( errs , err )
}
}
var errs [ ] error
deleted := 0
for _ , wf := range wfs {
for i , wf := range wfs {
if len ( errs ) > 100 {
// Likely, everything is broken.
// Don't try to receive any more files in this batch.
errs = append ( errs , fmt . Errorf ( "too many errors in runFileGetOneBatch(). %d files unexamined" , len ( wfs ) - i ) )
break
}
writtenFile , size , err := receiveFile ( ctx , wf , dir )
if err != nil {
errs = append ( errs , err )
@ -449,9 +445,51 @@ func runFileGet(ctx context.Context, args []string) error {
}
deleted ++
}
if getArgs . verbose {
if deleted == 0 && len ( wfs ) > 0 {
// persistently stuck files are basically an error
errs = append ( errs , fmt . Errorf ( "moved %d/%d files" , deleted , len ( wfs ) ) )
} else if getArgs . verbose {
printf ( "moved %d/%d files\n" , deleted , len ( wfs ) )
}
return errs
}
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 )
}
if getArgs . loop {
for {
errs := runFileGetOneBatch ( ctx , dir )
for _ , err := range errs {
outln ( err )
}
if len ( errs ) > 0 {
// It's possible whatever caused the error(s) (e.g. conflicting target file,
// full disk, unwritable target directory) will re-occur if we try again so
// let's back off and not busy loop on error.
//
// If we've been invoked as:
// tailscale file get --conflict=skip ~/Downloads
// then any file coming in named the same as one in ~/Downloads will always
// appear as an "error" until the user clears it, but other incoming files
// should be receivable when they arrive, so let's not wait too long to
// check again.
time . Sleep ( 5 * time . Second )
}
}
}
errs := runFileGetOneBatch ( ctx , dir )
if len ( errs ) == 0 {
return nil
}
@ -489,9 +527,10 @@ func waitForFile(ctx context.Context) error {
c , bc , pumpCtx , cancel := connect ( ctx )
defer cancel ( )
fileWaiting := make ( chan bool , 1 )
notifyError := make ( chan error , 1 )
bc . SetNotifyCallback ( func ( n ipn . Notify ) {
if n . ErrMessage != nil {
fatalf ( "Notify.ErrMessage: %v\n ", * n . ErrMessage )
notifyError <- fmt . Errorf ( "Notify.ErrMessage: %v ", * n . ErrMessage )
}
if n . FilesWaiting != nil {
select {
@ -508,5 +547,7 @@ func waitForFile(ctx context.Context) error {
return pumpCtx . Err ( )
case <- ctx . Done ( ) :
return ctx . Err ( )
case err := <- notifyError :
return err
}
}