mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-07 08:44:38 +00:00
cmd/tailscale: add file get options for dealing with existing files
A new flag --conflict=(skip|overwrite|rename) lets users specify what to do when receiving files that match a same-named file in the target directory. Updates #3548 Signed-off-by: David Eger <david.eger@gmail.com>
This commit is contained in:
parent
07f48a7bfe
commit
5be42c0af1
@ -15,6 +15,7 @@ import (
|
|||||||
"mime"
|
"mime"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -286,22 +287,116 @@ func runCpTargets(ctx context.Context, args []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onConflict is a flag.Value for the --conflict flag's three string options.
|
||||||
|
type onConflict string
|
||||||
|
|
||||||
|
const (
|
||||||
|
skipOnExist onConflict = "skip"
|
||||||
|
overwriteExisting onConflict = "overwrite" // Overwrite any existing file at the target location
|
||||||
|
createNumberedFiles onConflict = "rename" // Create an alternately named file in the style of Chrome Downloads
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v *onConflict) String() string { return string(*v) }
|
||||||
|
|
||||||
|
func (v *onConflict) Set(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
*v = skipOnExist
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
*v = onConflict(strings.ToLower(s))
|
||||||
|
if *v != skipOnExist && *v != overwriteExisting && *v != createNumberedFiles {
|
||||||
|
return fmt.Errorf("%q is not one of (skip|overwrite|rename)", s)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var fileGetCmd = &ffcli.Command{
|
var fileGetCmd = &ffcli.Command{
|
||||||
Name: "get",
|
Name: "get",
|
||||||
ShortUsage: "file get [--wait] [--verbose] <target-directory>",
|
ShortUsage: "file get [--wait] [--verbose] [--conflict=(skip|overwrite|rename)] <target-directory>",
|
||||||
ShortHelp: "Move files out of the Tailscale file inbox",
|
ShortHelp: "Move files out of the Tailscale file inbox",
|
||||||
Exec: runFileGet,
|
Exec: runFileGet,
|
||||||
FlagSet: (func() *flag.FlagSet {
|
FlagSet: (func() *flag.FlagSet {
|
||||||
fs := newFlagSet("get")
|
fs := newFlagSet("get")
|
||||||
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
|
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
|
||||||
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
|
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
|
||||||
|
overwrite: overwrite existing file
|
||||||
|
rename: write to a new number-suffixed filename`)
|
||||||
return fs
|
return fs
|
||||||
})(),
|
})(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var getArgs struct {
|
var getArgs = struct {
|
||||||
wait bool
|
wait bool
|
||||||
verbose bool
|
verbose bool
|
||||||
|
conflict onConflict
|
||||||
|
}{conflict: skipOnExist}
|
||||||
|
|
||||||
|
func numberedFileName(dir, name string, i int) string {
|
||||||
|
ext := path.Ext(name)
|
||||||
|
return filepath.Join(dir, fmt.Sprintf("%s (%d)%s",
|
||||||
|
strings.TrimSuffix(name, ext),
|
||||||
|
i, ext))
|
||||||
|
}
|
||||||
|
|
||||||
|
func openFileOrSubstitute(dir, base string, action onConflict) (*os.File, error) {
|
||||||
|
targetFile := filepath.Join(dir, base)
|
||||||
|
f, err := os.OpenFile(targetFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
|
||||||
|
if err == nil {
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
// Something went wrong trying to open targetFile as a new file for writing.
|
||||||
|
switch action {
|
||||||
|
default:
|
||||||
|
// This should not happen.
|
||||||
|
return nil, fmt.Errorf("file issue. how to resolve this conflict? no one knows.")
|
||||||
|
case skipOnExist:
|
||||||
|
if _, statErr := os.Stat(targetFile); statErr == nil {
|
||||||
|
// we can stat a file at that path: so it already exists.
|
||||||
|
return nil, fmt.Errorf("refusing to overwrite file: %w", err)
|
||||||
|
}
|
||||||
|
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
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
if f, err = os.OpenFile(targetFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to overwrite: %w", err)
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
case createNumberedFiles:
|
||||||
|
// It's possible the target directory or filesystem isn't writable by us,
|
||||||
|
// not just that the target file(s) already exists. For now, give up after
|
||||||
|
// a limited number of attempts. In future, maybe distinguish this case
|
||||||
|
// and follow in the style of https://tinyurl.com/chromium100
|
||||||
|
maxAttempts := 100
|
||||||
|
for i := 1; i < maxAttempts; i++ {
|
||||||
|
if f, err = os.OpenFile(numberedFileName(dir, base, i), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644); err == nil {
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("unable to find a name for writing %v, final attempt: %w", targetFile, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func receiveFile(ctx context.Context, wf apitype.WaitingFile, dir string) (targetFile string, size int64, err error) {
|
||||||
|
rc, size, err := tailscale.GetWaitingFile(ctx, wf.Name)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, fmt.Errorf("opening inbox file %q: %w", wf.Name, err)
|
||||||
|
}
|
||||||
|
f, err := openFileOrSubstitute(dir, wf.Name, getArgs.conflict)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(f, rc)
|
||||||
|
rc.Close()
|
||||||
|
if err != nil {
|
||||||
|
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 {
|
func runFileGet(ctx context.Context, args []string) error {
|
||||||
@ -330,47 +425,40 @@ func runFileGet(ctx context.Context, args []string) error {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
if getArgs.verbose {
|
if getArgs.verbose {
|
||||||
log.Printf("waiting for file...")
|
printf("waiting for file...")
|
||||||
}
|
}
|
||||||
if err := waitForFile(ctx); err != nil {
|
if err := waitForFile(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errs []error
|
||||||
deleted := 0
|
deleted := 0
|
||||||
for _, wf := range wfs {
|
for _, wf := range wfs {
|
||||||
rc, size, err := tailscale.GetWaitingFile(ctx, wf.Name)
|
writtenFile, size, err := receiveFile(ctx, wf, dir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("opening inbox file %q: %v", wf.Name, err)
|
errs = append(errs, err)
|
||||||
}
|
continue
|
||||||
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 {
|
if getArgs.verbose {
|
||||||
log.Printf("wrote %v (%d bytes)", wf.Name, size)
|
printf("wrote %v as %v (%d bytes)\n", wf.Name, writtenFile, size)
|
||||||
}
|
}
|
||||||
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
|
if err = tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
|
||||||
return fmt.Errorf("deleting %q from inbox: %v", wf.Name, err)
|
errs = append(errs, fmt.Errorf("deleting %q from inbox: %v", wf.Name, err))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
deleted++
|
deleted++
|
||||||
}
|
}
|
||||||
if getArgs.verbose {
|
if getArgs.verbose {
|
||||||
log.Printf("moved %d files", deleted)
|
printf("moved %d/%d files\n", deleted, len(wfs))
|
||||||
}
|
}
|
||||||
|
if len(errs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
for _, err := range errs[:len(errs)-1] {
|
||||||
|
outln(err)
|
||||||
|
}
|
||||||
|
return errs[len(errs)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func wipeInbox(ctx context.Context) error {
|
func wipeInbox(ctx context.Context) error {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user