88 "fmt"
99 "io"
1010 "os"
11+ "path/filepath"
1112 "runtime"
1213 "slices"
1314 "strconv"
@@ -250,6 +251,14 @@ func (a *App) mv() *cobra.Command {
250251 }
251252}
252253
254+ const copyDescription = `Copy a file or directory to the target path.
255+
256+ When copying a collection, this command will compare the source and target,
257+ and only copy the missing parts. It can be repeated to keep the target up to date.
258+ In this case, the target collection must end in a slash to avoid ambiguity.
259+ If the source collection ends in a slash, files underneath will be placed directly
260+ in the target collection. Otherwise, a subcollection with the same name will be created.`
261+
253262func (a * App ) cp () * cobra.Command {
254263 var (
255264 skip bool
@@ -259,26 +268,25 @@ func (a *App) cp() *cobra.Command {
259268 examples := []string {
260269 a .name + " cp /path/to/collection1/file.txt /path/to/collection2/file.txt (target should not exist)" ,
261270 a .name + " cp /path/to/collection1/file.txt /path/to/collection2/ (target should not exist)" ,
262- a .name + " cp /path/to/collection1 /path/to/collection2 (target may exist)" ,
263271 a .name + " cp /path/to/collection1 /path/to/collection2/ (target may exist)" ,
272+ a .name + " cp /path/to/collection1/ /path/to/collection2/ (target may exist)" ,
264273 }
265274
266275 cmd := & cobra.Command {
267276 Use : "cp <path> <target path>" ,
268277 Short : "Copy a data object or a collection" ,
278+ Long : copyDescription ,
269279 Args : cobra .ExactArgs (2 ),
270280 Example : strings .Join (examples , "\n " ),
271281 ValidArgsFunction : a .CompleteArgs ,
272282 RunE : func (cmd * cobra.Command , args []string ) error {
273283 src := a .Path (args [0 ])
274- dest := args [1 ]
284+ dest := a . Path ( args [1 ])
275285
276- if strings .HasSuffix (dest , "/" ) {
277- dest += Name (src )
286+ if strings .HasSuffix (args [ 1 ], "/" ) && ! strings . HasSuffix ( args [ 0 ] , "/" ) {
287+ dest = a . Path ( args [ 1 ] + Name (src ) )
278288 }
279289
280- dest = a .Path (dest )
281-
282290 obj , err := a .GetRecord (cmd .Context (), src )
283291 if err != nil {
284292 return err
@@ -292,6 +300,10 @@ func (a *App) cp() *cobra.Command {
292300 SkipTrash : skip ,
293301 }
294302
303+ if ! strings .HasSuffix (args [1 ], "/" ) {
304+ return ErrAmbiguousTarget
305+ }
306+
295307 return a .CopyDir (cmd .Context (), src , dest , opts )
296308 }
297309
@@ -374,23 +386,36 @@ func (a *App) checksum() *cobra.Command {
374386 }
375387}
376388
389+ var ErrAmbiguousTarget = errors .New ("ambiguous command, please specify a target collection or directory with a trailing slash" )
390+
391+ const uploadDescription = `Upload a file or directory to the target path.
392+ This command will compare the source and target, and only upload the missing parts.
393+ It can be repeated to keep the target up to date.
394+
395+ When uploading a directory, the target collection must end in a slash to avoid ambiguity.
396+ If the source directory ends in a slash, files underneath will be placed directly
397+ in the target collection. Otherwise, a subcollection with the same name will be created.`
398+
377399func (a * App ) upload () * cobra.Command {
378400 opts := transfer.Options {
379401 SyncModTime : true ,
380402 MaxQueued : 10000 ,
381403 }
382404
383405 examples := []string {
406+ a .name + " upload /local/file.txt" ,
384407 a .name + " upload /local/file.txt /path/to/collection/file.txt" ,
385408 a .name + " upload /local/file.txt /path/to/collection/" ,
386- a .name + " upload /local/folder /path/to/collection " ,
409+ a .name + " upload /local/folder" ,
387410 a .name + " upload /local/folder /path/to/collection/" ,
411+ a .name + " upload /local/folder/ /path/to/collection/" ,
388412 }
389413
390414 cmd := & cobra.Command {
391415 Use : "upload <local file> [target path]" ,
392416 Aliases : []string {"put" },
393417 Short : "Upload a local file or directory to the destination path" ,
418+ Long : uploadDescription ,
394419 Example : strings .Join (examples , "\n " ),
395420 Args : cobra .RangeArgs (1 , 2 ),
396421 ValidArgsFunction : a .CompleteArgs ,
@@ -399,13 +424,14 @@ func (a *App) upload() *cobra.Command {
399424 args = append (args , a .Workdir + "/" )
400425 }
401426
402- if strings .HasSuffix (args [1 ], "/" ) {
403- args [1 ] += Name (args [0 ])
404- }
405-
427+ source := filepath .Clean (args [0 ])
406428 target := a .Path (args [1 ])
407429
408- fi , err := os .Stat (args [0 ])
430+ if strings .HasSuffix (args [1 ], "/" ) && ! strings .HasSuffix (args [0 ], "/" ) && ! strings .HasSuffix (args [0 ], string (os .PathSeparator )) {
431+ target = a .Path (args [1 ] + filepath .Base (source ))
432+ }
433+
434+ fi , err := os .Stat (source )
409435 if err != nil {
410436 return err
411437 }
@@ -415,10 +441,14 @@ func (a *App) upload() *cobra.Command {
415441 if ! fi .IsDir () {
416442 opts .SyncModTime = false
417443
418- return a .Upload (cmd .Context (), args [ 0 ] , target , opts )
444+ return a .Upload (cmd .Context (), source , target , opts )
419445 }
420446
421- return a .UploadDir (cmd .Context (), args [0 ], target , opts )
447+ if ! strings .HasSuffix (args [1 ], "/" ) {
448+ return ErrAmbiguousTarget
449+ }
450+
451+ return a .UploadDir (cmd .Context (), source , target , opts )
422452 },
423453 }
424454
@@ -432,23 +462,34 @@ func (a *App) upload() *cobra.Command {
432462 return cmd
433463}
434464
465+ const downloadDescription = `Download a data object or a collection to the local path.
466+ This command will compare the source and target, and only download the missing parts.
467+ It can be repeated to keep the target up to date.
468+
469+ When downloading a collection, the target folder must end in a slash to avoid ambiguity.
470+ If the source collection ends in a slash, files underneath will be placed directly
471+ in the target folder. Otherwise, a subfolder with the same name will be created.`
472+
435473func (a * App ) download () * cobra.Command {
436474 opts := transfer.Options {
437475 SyncModTime : true ,
438476 MaxQueued : 10000 ,
439477 }
440478
441479 examples := []string {
480+ a .name + " download /path/to/collection/file.txt" ,
442481 a .name + " download /path/to/collection/file.txt /local/file.txt" ,
443482 a .name + " download /path/to/collection/file.txt /local/folder/" ,
444- a .name + " download /path/to/collection /local/folder " ,
483+ a .name + " download /path/to/collection" ,
445484 a .name + " download /path/to/collection /local/folder/" ,
485+ a .name + " download /path/to/collection/ /local/folder/" ,
446486 }
447487
448488 cmd := & cobra.Command {
449489 Use : "download <path> [local file]" ,
450490 Aliases : []string {"get" },
451491 Short : "Download a data object or a collection to the local path" ,
492+ Long : downloadDescription ,
452493 Example : strings .Join (examples , "\n " ),
453494 Args : cobra .RangeArgs (1 , 2 ),
454495 ValidArgsFunction : a .CompleteArgs ,
@@ -463,10 +504,10 @@ func (a *App) download() *cobra.Command {
463504 }
464505
465506 source := a .Path (args [0 ])
466- target := args [1 ]
507+ target := filepath . Clean ( args [1 ])
467508
468- if strings .HasSuffix (target , "/" ) {
469- target += Name (source )
509+ if ( strings .HasSuffix (args [ 1 ], "/" ) || strings . HasSuffix ( args [ 1 ], string ( os . PathSeparator ))) && ! strings . HasSuffix ( args [ 0 ] , "/" ) {
510+ target = filepath . Join ( target , Name (source ) )
470511 }
471512
472513 record , err := a .GetRecord (cmd .Context (), source )
@@ -482,6 +523,10 @@ func (a *App) download() *cobra.Command {
482523 return a .Download (cmd .Context (), target , source , opts )
483524 }
484525
526+ if ! strings .HasSuffix (args [1 ], "/" ) && ! strings .HasSuffix (args [1 ], string (os .PathSeparator )) {
527+ return ErrAmbiguousTarget
528+ }
529+
485530 return a .DownloadDir (cmd .Context (), target , source , opts )
486531 },
487532 }
0 commit comments