Skip to content

Commit 739b34f

Browse files
committed
[#15] Enforce trailing slash for upload/download/copy
Enforce users to specify a target collection or directory with a trailing slash, to avoid ambiguity about the expected behaviour. If calling e.g. the command "iron upload folder/subfolder target", for a folder containing folder/subfolder/file1.txt, a user would expect target/subfolder/file1.txt to exist after the copy, but earlier versions of iron would create target/file1.txt. We cannot suddenly change the behaviour of "iron upload folder/subfolder target" to rectify this. That is why we now simply return an error if such a command is given. To copy a folder properly, a user needs either to call "iron upload folder/subfolder target/" to create target/subfolder/file1.txt, or "iron upload folder/subfolder/ target/" to copy the contents of folder/subfolder into target. Signed-off-by: Peter Verraedt <peter@verraedt.be>
1 parent 2b30cc8 commit 739b34f

File tree

1 file changed

+63
-18
lines changed

1 file changed

+63
-18
lines changed

cmd/iron/cli/commands.go

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
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+
253262
func (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+
377399
func (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+
435473
func (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

Comments
 (0)