@@ -64,7 +64,8 @@ func parseArgs(args []string, stdout, stderr io.Writer) (*cliOptions, int) {
6464 fs .PrintDefaults ()
6565 }
6666
67- if err := fs .Parse (args ); err != nil {
67+ paths , err := parseInterleaved (fs , args )
68+ if err != nil {
6869 if errors .Is (err , flag .ErrHelp ) {
6970 return nil , exitOK
7071 }
@@ -87,7 +88,7 @@ func parseArgs(args []string, stdout, stderr io.Writer) (*cliOptions, int) {
8788 return nil , exitUsageError
8889 }
8990
90- opts .paths = fs . Args ()
91+ opts .paths = paths
9192 if len (opts .paths ) == 0 {
9293 _ , _ = fmt .Fprintln (stderr , "uploadfun: at least one file or directory argument is required" )
9394 fs .Usage ()
@@ -97,6 +98,27 @@ func parseArgs(args []string, stdout, stderr io.Writer) (*cliOptions, int) {
9798 return opts , exitOK
9899}
99100
101+ // parseInterleaved runs fs.Parse repeatedly so flags and positional
102+ // arguments may appear in any order — the documented invocation puts
103+ // paths before --config, but stdlib flag otherwise stops parsing at the
104+ // first positional. It returns the collected positionals, or the first
105+ // fs.Parse error (including flag.ErrHelp).
106+ func parseInterleaved (fs * flag.FlagSet , args []string ) ([]string , error ) {
107+ var positionals []string
108+ rest := args
109+ for {
110+ if err := fs .Parse (rest ); err != nil {
111+ return nil , err
112+ }
113+ rest = fs .Args ()
114+ if len (rest ) == 0 {
115+ return positionals , nil
116+ }
117+ positionals = append (positionals , rest [0 ])
118+ rest = rest [1 :]
119+ }
120+ }
121+
100122// expandPaths turns the positional file/dir arguments into a flat file
101123// list: directories expand non-recursively, every regular file included,
102124// no extension filtering, subdirectories and hidden/dotfiles silently
@@ -131,9 +153,31 @@ func expandPaths(paths []string) ([]string, error) {
131153 files = append (files , filepath .Join (p , entry .Name ()))
132154 }
133155 }
156+ if err := checkBasenameCollisions (files ); err != nil {
157+ return nil , err
158+ }
134159 return files , nil
135160}
136161
162+ // checkBasenameCollisions rejects inputs that would map to the same remote
163+ // filename. The remote name is a file's basename (see dispatch's
164+ // remoteName), so two inputs sharing a basename — e.g. a/img.jpg and
165+ // b/img.jpg, or the same path passed twice — would, under the default
166+ // delete-first overwrite, have one silently clobber the other. Catch it up
167+ // front rather than reporting success for a file that was overwritten.
168+ func checkBasenameCollisions (files []string ) error {
169+ seen := make (map [string ]string , len (files ))
170+ for _ , f := range files {
171+ base := filepath .Base (f )
172+ if prev , ok := seen [base ]; ok {
173+ return fmt .Errorf (
174+ "inputs %q and %q both upload to remote name %q" , prev , f , base )
175+ }
176+ seen [base ] = f
177+ }
178+ return nil
179+ }
180+
137181func run (ctx context.Context , args []string , stdout , stderr io.Writer ) int {
138182 opts , code := parseArgs (args , stdout , stderr )
139183 if opts == nil {
0 commit comments