Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ proxy.
* File operations: `ls`, `cp`, `mkdir`, `mv`, `rm`, `put`, and `get`
* Recursive upload and download with `put -r` and `get -r`
* Pipe-friendly transfers with stdin upload and stdout download
* Conflict control with `put --if-exists overwrite|skip|fail` and `cp`/`mv --if-exists fail|skip`
* Conflict control with `put --if-exists overwrite|skip|autorename|fail` and `cp`/`mv --if-exists fail|skip|autorename`
* Shared-link creation, listing, inspection, update, revoke, and download
* Search, file revisions, restore, flexible sorting, and time formatting
* Chunked uploads for large files and paginated listing for large directories
Expand Down
5 changes: 3 additions & 2 deletions cmd/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func cp(cmd *cobra.Command, args []string) error {
cpErrors = append(cpErrors, relocationError)
cpErrorDetails = append(cpErrorDetails, relocationFailureDetails(argument, dst))
} else {
arg.Autorename = opts.ifExists == relocationIfExistsAutorename
result, skipped, err := relocationSkipIfDestinationExists(dbx, arg, opts)
if err != nil {
cpErrors = append(cpErrors, fmt.Errorf("copy %q to %q: %v", arg.FromPath, arg.ToPath, err))
Expand Down Expand Up @@ -97,7 +98,7 @@ func cp(cmd *cobra.Command, args []string) error {
cpErrorDetails = append(cpErrorDetails, relocationFailureDetails(arg.FromPath, arg.ToPath))
continue
}
results = append(results, relocationOperationResult(relocationJSONStatusCopied, result))
results = append(results, relocationOperationResult(relocationSuccessStatus(relocationJSONStatusCopied, arg, result, opts), result))
}
}

Expand Down Expand Up @@ -125,5 +126,5 @@ var cpCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(cpCmd)
enableStructuredOutput(cpCmd)
cpCmd.Flags().String("if-exists", relocationIfExistsFail, "What to do when the destination exists: fail or skip")
cpCmd.Flags().String("if-exists", relocationIfExistsFail, "What to do when the destination exists: fail, skip, or autorename")
}
90 changes: 90 additions & 0 deletions cmd/cp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,96 @@ func TestCpIfExistsFailCallsCopy(t *testing.T) {
}
}

func TestCpIfExistsAutorenameSetsAutorenameFlag(t *testing.T) {
var copied []*files.RelocationArg
stubFilesClient(t, &mockFilesClient{
copyV2Fn: func(arg *files.RelocationArg) (*files.RelocationResult, error) {
copied = append(copied, arg)
metadata := relocationTestFileMetadata("/dest/file (1).txt", 1)
return files.NewRelocationResult(metadata), nil
},
})

var stdout bytes.Buffer
cmd := newRelocationTestCommand(&stdout, nil)
if err := cmd.Flags().Set("if-exists", relocationIfExistsAutorename); err != nil {
t.Fatal(err)
}

if err := cp(cmd, []string{"/src/file.txt", "/dest/file.txt"}); err != nil {
t.Fatalf("cp error: %v", err)
}
if len(copied) != 1 {
t.Fatalf("copied = %d, want 1", len(copied))
}
if !copied[0].Autorename {
t.Fatal("CopyV2 arg Autorename = false, want true")
}
got := decodeRelocationOutput(t, stdout.Bytes())
if len(got.Results) != 1 {
t.Fatalf("results = %d, want 1", len(got.Results))
}
if got.Results[0].Status != relocationJSONStatusAutorenamed {
t.Fatalf("status = %q, want autorenamed", got.Results[0].Status)
}
if got.Results[0].Result.PathDisplay != "/dest/file (1).txt" {
t.Fatalf("path_display = %q, want /dest/file (1).txt", got.Results[0].Result.PathDisplay)
}
}

func TestCpIfExistsAutorenameNoConflictKeepsCopiedStatus(t *testing.T) {
stubFilesClient(t, &mockFilesClient{
copyV2Fn: func(arg *files.RelocationArg) (*files.RelocationResult, error) {
return files.NewRelocationResult(relocationTestFileMetadata(arg.ToPath, 1)), nil
},
})

var stdout bytes.Buffer
cmd := newRelocationTestCommand(&stdout, nil)
if err := cmd.Flags().Set("if-exists", relocationIfExistsAutorename); err != nil {
t.Fatal(err)
}

if err := cp(cmd, []string{"/src/file.txt", "/dest/file.txt"}); err != nil {
t.Fatalf("cp error: %v", err)
}
got := decodeRelocationOutput(t, stdout.Bytes())
if len(got.Results) != 1 {
t.Fatalf("results = %d, want 1", len(got.Results))
}
if got.Results[0].Status != relocationJSONStatusCopied {
t.Fatalf("status = %q, want copied", got.Results[0].Status)
}
}

func TestCpIfExistsAutorenameCaseOnlyPathKeepsCopiedStatus(t *testing.T) {
stubFilesClient(t, &mockFilesClient{
copyV2Fn: func(arg *files.RelocationArg) (*files.RelocationResult, error) {
return files.NewRelocationResult(relocationTestFileMetadata("/Dest/File.txt", 1)), nil
},
})

var stdout bytes.Buffer
cmd := newRelocationTestCommand(&stdout, nil)
if err := cmd.Flags().Set("if-exists", relocationIfExistsAutorename); err != nil {
t.Fatal(err)
}

if err := cp(cmd, []string{"/src/file.txt", "/dest/file.txt"}); err != nil {
t.Fatalf("cp error: %v", err)
}
got := decodeRelocationOutput(t, stdout.Bytes())
if len(got.Results) != 1 {
t.Fatalf("results = %d, want 1", len(got.Results))
}
if got.Results[0].Status != relocationJSONStatusCopied {
t.Fatalf("status = %q, want copied", got.Results[0].Status)
}
if got.Results[0].Result.PathDisplay != "/Dest/File.txt" {
t.Fatalf("path_display = %q, want /Dest/File.txt", got.Results[0].Result.PathDisplay)
}
}

func TestCpIfExistsSkipExistingDestinationDoesNotCopy(t *testing.T) {
var copied bool
stubFilesClient(t, &mockFilesClient{
Expand Down
10 changes: 5 additions & 5 deletions cmd/help_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,12 +277,12 @@ func TestJSONHelpManifestV1MachineFields(t *testing.T) {
}
assertJSONHelpArg(t, put.Args, "source", true, "local_path", true)
assertJSONHelpArg(t, put.Args, "target", false, "dropbox_path", false)
assertStringSliceEqual(t, "put --if-exists enum", jsonHelpFlagByName(t, put.Flags, "if-exists").EnumValues, []string{"overwrite", "skip", "fail"})
assertStringSliceEqual(t, "put --if-exists enum", jsonHelpFlagByName(t, put.Flags, "if-exists").EnumValues, []string{"overwrite", "skip", "fail", "autorename"})
if !put.StdinStdout.ReadsStdin || put.StdinStdout.WritesBinaryStdout {
t.Fatalf("put stdin_stdout = %+v, want stdin only", put.StdinStdout)
}
assertStringSliceEqual(t, "put warning codes", put.WarningCodes, []string{jsonWarningCodeSkippedSymlink})
assertStringSliceEqual(t, "put result statuses", put.ResultStatuses, []string{"created", "existing", "skipped", "uploaded"})
assertStringSliceEqual(t, "put result statuses", put.ResultStatuses, []string{"autorenamed", "created", "existing", "skipped", "uploaded"})
assertStringSliceEqual(t, "put result kinds", put.ResultKinds, []string{"file", "folder"})
assertStringSliceEqual(t, "put scopes", put.DropboxScopes, []string{"files.content.write", "files.metadata.read"})
if put.ScopeAccuracy != commandManifestScopeAccuracyBestEffort {
Expand All @@ -308,7 +308,7 @@ func TestJSONHelpManifestV1MachineFields(t *testing.T) {
}
assertJSONHelpInputProperty(t, put.InputSchema, "target", "string", "arg", "target", "dropbox_path")
ifExists := assertJSONHelpInputProperty(t, put.InputSchema, "if_exists", "string", "flag", "if-exists", "enum")
assertStringSliceEqual(t, "put input_schema if_exists enum", ifExists.Enum, []string{"fail", "overwrite", "skip"})
assertStringSliceEqual(t, "put input_schema if_exists enum", ifExists.Enum, []string{"autorename", "fail", "overwrite", "skip"})
if ifExists.Default != "overwrite" {
t.Fatalf("put if_exists default = %#v, want overwrite", ifExists.Default)
}
Expand Down Expand Up @@ -337,7 +337,7 @@ func TestJSONHelpManifestV1SelectedCommandMetadata(t *testing.T) {
if !jsonHelpArgByName(t, cp.Args, "source").Variadic {
t.Fatal("cp source variadic = false, want true")
}
assertStringSliceEqual(t, "cp --if-exists enum", jsonHelpFlagByName(t, cp.Flags, "if-exists").EnumValues, []string{"fail", "skip"})
assertStringSliceEqual(t, "cp --if-exists enum", jsonHelpFlagByName(t, cp.Flags, "if-exists").EnumValues, []string{"fail", "skip", "autorename"})
assertStringSliceEqual(t, "cp input_schema required", cp.InputSchema.Required, []string{"source", "target"})
source := assertJSONHelpInputProperty(t, cp.InputSchema, "source", "array", "arg", "source", "dropbox_path")
if source.Items == nil || source.Items.Type != "string" {
Expand All @@ -348,7 +348,7 @@ func TestJSONHelpManifestV1SelectedCommandMetadata(t *testing.T) {
}
assertJSONHelpInputProperty(t, cp.InputSchema, "target", "string", "arg", "target", "dropbox_path")
cpIfExists := assertJSONHelpInputProperty(t, cp.InputSchema, "if_exists", "string", "flag", "if-exists", "enum")
assertStringSliceEqual(t, "cp input_schema if_exists enum", cpIfExists.Enum, []string{"fail", "skip"})
assertStringSliceEqual(t, "cp input_schema if_exists enum", cpIfExists.Enum, []string{"autorename", "fail", "skip"})

create := jsonCommandManifestFor(shareLinkCreateCmd)
assertStringSliceEqual(t, "share-link create audience enum", jsonHelpFlagByName(t, create.Flags, "audience").EnumValues, []string{"public", "team", "members", "no-one"})
Expand Down
12 changes: 6 additions & 6 deletions cmd/help_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{
commandArg("target", true, false, "dropbox_path", "Dropbox destination path"),
},
Examples: []jsonCommandExample{{Description: "Copy a Dropbox file", Command: "dbxcli cp /from.txt /to.txt"}},
Flags: map[string]jsonCommandFlagMetadata{"if-exists": {EnumValues: []string{"fail", "skip"}, ValueKind: "enum"}},
Flags: map[string]jsonCommandFlagMetadata{"if-exists": {EnumValues: []string{"fail", "skip", "autorename"}, ValueKind: "enum"}},
DropboxScopes: []string{"files.content.write", "files.metadata.read"},
Known: true,
},
Expand Down Expand Up @@ -174,7 +174,7 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{
commandArg("target", true, false, "dropbox_path", "Dropbox destination path"),
},
Examples: []jsonCommandExample{{Description: "Move a Dropbox file", Command: "dbxcli mv /from.txt /to.txt"}},
Flags: map[string]jsonCommandFlagMetadata{"if-exists": {EnumValues: []string{"fail", "skip"}, ValueKind: "enum"}},
Flags: map[string]jsonCommandFlagMetadata{"if-exists": {EnumValues: []string{"fail", "skip", "autorename"}, ValueKind: "enum"}},
DropboxScopes: []string{"files.content.write", "files.metadata.read"},
Known: true,
},
Expand All @@ -190,7 +190,7 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{
Flags: map[string]jsonCommandFlagMetadata{
"chunksize": {ValueKind: "bytes"},
"debug": {ValueKind: "boolean"},
"if-exists": {EnumValues: []string{"overwrite", "skip", "fail"}, ValueKind: "enum"},
"if-exists": {EnumValues: []string{"overwrite", "skip", "fail", "autorename"}, ValueKind: "enum"},
"recursive": {ValueKind: "boolean"},
"workers": {ValueKind: "integer"},
},
Expand Down Expand Up @@ -341,15 +341,15 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{

var commandContractRegistry = map[string]jsonCommandContractMetadata{
"account": {Statuses: []string{"found"}, Kinds: []string{"account"}},
"cp": {Statuses: []string{"copied", "skipped"}, Kinds: []string{"deleted", "file", "folder"}},
"cp": {Statuses: []string{"autorenamed", "copied", "skipped"}, Kinds: []string{"deleted", "file", "folder"}},
"du": {Statuses: []string{"reported"}, Kinds: []string{"space_usage"}},
"get": {Statuses: []string{"created", "downloaded", "existing"}, Kinds: []string{"file", "folder"}},
"help": {Statuses: []string{"described"}, Kinds: []string{"command"}},
"logout": {Statuses: []string{"already_logged_out", "logged_out"}, Kinds: []string{"auth"}, Warnings: []string{jsonWarningCodeTokenRevokeFailed}},
"ls": {Statuses: []string{"listed"}, Kinds: []string{"deleted", "file", "folder"}},
"mkdir": {Statuses: []string{"created", "existing"}, Kinds: []string{"folder"}},
"mv": {Statuses: []string{"moved", "skipped"}, Kinds: []string{"deleted", "file", "folder"}},
"put": {Statuses: []string{"created", "existing", "skipped", "uploaded"}, Kinds: []string{"file", "folder"}, Warnings: []string{jsonWarningCodeSkippedSymlink}},
"mv": {Statuses: []string{"autorenamed", "moved", "skipped"}, Kinds: []string{"deleted", "file", "folder"}},
"put": {Statuses: []string{"autorenamed", "created", "existing", "skipped", "uploaded"}, Kinds: []string{"file", "folder"}, Warnings: []string{jsonWarningCodeSkippedSymlink}},
"restore": {Statuses: []string{"restored"}, Kinds: []string{"file"}},
"revs": {Statuses: []string{"revision"}, Kinds: []string{"file"}},
"rm": {Statuses: []string{"deleted", "permanently_deleted"}, Kinds: []string{"deleted", "file", "folder"}},
Expand Down
8 changes: 4 additions & 4 deletions cmd/json_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -841,7 +841,7 @@ func jsonGoldenErrorOutputExamples() map[string]jsonErrorResponse {
),
"invalid_arguments": newJSONErrorResponse(
jsonErrorExampleCommand("put"),
invalidArgumentsErrorfWithDetails("invalid --if-exists %q (use overwrite, skip, or fail)", flagValueErrorDetails("if-exists", "replace"), "replace"),
invalidArgumentsErrorfWithDetails("invalid --if-exists %q (use overwrite, skip, autorename, or fail)", flagValueErrorDetails("if-exists", "replace"), "replace"),
),
"not_found": newJSONErrorResponse(
jsonErrorExampleCommand("get"),
Expand Down Expand Up @@ -1086,15 +1086,15 @@ func jsonContractDefinitions() map[string][]string {
func jsonCommandSchemas() map[string]jsonGoldenCommandSchema {
return map[string]jsonGoldenCommandSchema{
"account": operationSchema("account_input", schemaRef("account_input"), "account", []string{accountJSONStatusFound}, []string{accountKindAccount}, nil),
"cp": operationSchema("empty", schemaRef("relocation_input"), "metadata", []string{relocationJSONStatusCopied, relocationJSONStatusSkipped}, metadataKinds(), nil),
"cp": operationSchema("empty", schemaRef("relocation_input"), "metadata", []string{relocationJSONStatusAutorenamed, relocationJSONStatusCopied, relocationJSONStatusSkipped}, metadataKinds(), nil),
"du": operationSchema("empty", schemaRef("empty"), "du_output", []string{duJSONStatusReported}, []string{duKindSpaceUsage}, nil),
"get": operationSchema("get_input", schemaRef("get_result_input"), "metadata", []string{getStatusCreated, getStatusDownloaded, getStatusExisting}, []string{getKindFile, getKindFolder}, nil),
"help": operationSchema("help_input", schemaRef("empty"), "command_manifest", []string{jsonHelpStatusDescribed}, []string{jsonHelpKindCommand}, nil),
"ls": operationSchema("ls_input", schemaRef("empty"), "metadata", []string{lsJSONStatusListed}, metadataKinds(), nil),
"logout": operationSchema("empty", schemaRef("empty"), "logout_result", []string{logoutStatusAlreadyLoggedOut, logoutStatusLoggedOut}, []string{logoutKindAuth}, []string{jsonWarningCodeTokenRevokeFailed}),
"mkdir": operationSchema("mkdir_input", schemaRef("mkdir_input"), "metadata", []string{mkdirStatusCreated, mkdirStatusExisting}, []string{mkdirKindFolder}, nil),
"mv": operationSchema("empty", schemaRef("relocation_input"), "metadata", []string{relocationJSONStatusMoved, relocationJSONStatusSkipped}, metadataKinds(), nil),
"put": operationSchema("put_input", schemaRef("put_result_input"), "metadata", []string{putStatusCreated, putStatusExisting, putStatusSkipped, putStatusUploaded}, []string{putKindFile, putKindFolder}, []string{jsonWarningCodeSkippedSymlink}),
"mv": operationSchema("empty", schemaRef("relocation_input"), "metadata", []string{relocationJSONStatusAutorenamed, relocationJSONStatusMoved, relocationJSONStatusSkipped}, metadataKinds(), nil),
"put": operationSchema("put_input", schemaRef("put_result_input"), "metadata", []string{putStatusAutorenamed, putStatusCreated, putStatusExisting, putStatusSkipped, putStatusUploaded}, []string{putKindFile, putKindFolder}, []string{jsonWarningCodeSkippedSymlink}),
"restore": operationSchema("restore_input", schemaRef("restore_input"), "metadata", []string{restoreStatusRestored}, []string{restoreKindFile}, nil),
"revs": operationSchema("revs_input", schemaRef("empty"), "metadata", []string{revsJSONStatusRevision}, []string{"file"}, nil),
"rm": operationSchema("empty", schemaRef("remove_input"), "metadata", []string{removeJSONStatusDeleted, removeJSONStatusPermanentlyDeleted}, metadataKinds(), nil),
Expand Down
16 changes: 16 additions & 0 deletions cmd/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,25 @@

package cmd

import "strings"

func metadataDisplayPath(inputPath, metadataPath string) string {
if metadataPath != "" {
return metadataPath
}
return inputPath
}

func sameDropboxPath(a string, b string) bool {
return strings.EqualFold(cleanDropboxPath(a), cleanDropboxPath(b))
}

func sameDropboxMetadataPath(pathDisplay, pathLower, requested string) bool {
if pathLower != "" {
return sameDropboxPath(pathLower, requested)
}
if pathDisplay != "" {
return sameDropboxPath(pathDisplay, requested)
}
return true
}
5 changes: 3 additions & 2 deletions cmd/mv.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func mv(cmd *cobra.Command, args []string) error {
mvErrors = append(mvErrors, fmt.Errorf("Error validating move for %s to %s: %v", argument, dst, err))
mvErrorDetails = append(mvErrorDetails, relocationFailureDetails(argument, dst))
} else {
arg.Autorename = opts.ifExists == relocationIfExistsAutorename
result, skipped, err := relocationSkipIfDestinationExists(dbx, arg, opts)
if err != nil {
mvErrors = append(mvErrors, fmt.Errorf("move %q to %q: %v", arg.FromPath, arg.ToPath, err))
Expand Down Expand Up @@ -96,7 +97,7 @@ func mv(cmd *cobra.Command, args []string) error {
mvErrorDetails = append(mvErrorDetails, relocationFailureDetails(arg.FromPath, arg.ToPath))
continue
}
results = append(results, relocationOperationResult(relocationJSONStatusMoved, result))
results = append(results, relocationOperationResult(relocationSuccessStatus(relocationJSONStatusMoved, arg, result, opts), result))
}
}

Expand All @@ -123,5 +124,5 @@ var mvCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(mvCmd)
enableStructuredOutput(mvCmd)
mvCmd.Flags().String("if-exists", relocationIfExistsFail, "What to do when the destination exists: fail or skip")
mvCmd.Flags().String("if-exists", relocationIfExistsFail, "What to do when the destination exists: fail, skip, or autorename")
}
Loading
Loading