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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ dbxcli put --help --output=json
Stable JSON error codes and process exit codes are documented in
[Automation and JSON output](https://github.com/dropbox/dbxcli/blob/master/docs/automation.md).

## JSON contract stability

`--output=json` uses schema v1 success and error envelopes. Schema v1 keeps
top-level fields, stable error codes, and result status meanings stable within
the v1 contract; minor releases may add fields, commands, warnings, and error
details. Use JSON help for machine-readable command manifests, and use the
[JSON schema v1 docs](https://github.com/dropbox/dbxcli/blob/master/docs/json-schema/v1/README.md)
for schemas, command contracts, and examples.

## Common workflows

Upload a file:
Expand Down
2 changes: 1 addition & 1 deletion cmd/add-member.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func addMember(cmd *cobra.Command, args []string) (err error) {
arg := team.NewMembersAddArg([]*team.MemberAddArg{member})
res, err := dbx.MembersAdd(arg)
if err != nil {
return err
return withJSONErrorDetails(err, operationErrorDetails("team_add_member"), emailErrorDetails(email))
}
input := teamMemberAddInput{
Email: email,
Expand Down
7 changes: 6 additions & 1 deletion cmd/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func cp(cmd *cobra.Command, args []string) error {
}

var cpErrors []error
var cpErrorDetails []map[string]any
var relocationArgs []*files.RelocationArg
var results []jsonOperationResult
collectResults := commandOutputFormat(cmd) == output.FormatJSON
Expand All @@ -56,10 +57,12 @@ func cp(cmd *cobra.Command, args []string) error {
if err != nil {
relocationError := fmt.Errorf("Error validating copy for %s to %s: %v", argument, dst, err)
cpErrors = append(cpErrors, relocationError)
cpErrorDetails = append(cpErrorDetails, relocationFailureDetails(argument, dst))
} else {
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))
cpErrorDetails = append(cpErrorDetails, relocationFailureDetails(arg.FromPath, arg.ToPath))
continue
}
if skipped {
Expand All @@ -83,13 +86,15 @@ func cp(cmd *cobra.Command, args []string) error {
}
copyError := fmt.Errorf("copy %q to %q: %v", arg.FromPath, arg.ToPath, err)
cpErrors = append(cpErrors, copyError)
cpErrorDetails = append(cpErrorDetails, relocationFailureDetails(arg.FromPath, arg.ToPath))
continue
}
if collectResults {
result, err := newRelocationResult(arg, res)
if err != nil {
copyError := fmt.Errorf("copy %q to %q: %v", arg.FromPath, arg.ToPath, err)
cpErrors = append(cpErrors, copyError)
cpErrorDetails = append(cpErrorDetails, relocationFailureDetails(arg.FromPath, arg.ToPath))
continue
}
results = append(results, relocationOperationResult(relocationJSONStatusCopied, result))
Expand All @@ -100,7 +105,7 @@ func cp(cmd *cobra.Command, args []string) error {
for _, cpError := range cpErrors {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%v\n", cpError)
}
return fmt.Errorf("cp: %d error(s)", len(cpErrors))
return relocationAggregateError("cp", "copy", len(cpErrors), cpErrorDetails)
}

if !collectResults {
Expand Down
4 changes: 4 additions & 0 deletions cmd/cp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,10 @@ func TestCpJSONErrorUsesCommandStderr(t *testing.T) {
if !strings.Contains(stderr.String(), `copy "/src/file.txt" to "/dest/file.txt": path/malformed_path/`) {
t.Fatalf("stderr = %q, want copy API error", stderr.String())
}
details := jsonErrorDetails(err)
if details["operation"] != "copy" || details["from_path"] != "/src/file.txt" || details["to_path"] != "/dest/file.txt" {
t.Fatalf("details = %#v, want copy from/to paths", details)
}
}

func TestCpCommandDefinesIfExistsFlag(t *testing.T) {
Expand Down
28 changes: 14 additions & 14 deletions cmd/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func get(cmd *cobra.Command, args []string) (err error) {

if dst == "-" {
if commandOutputFormat(cmd) == output.FormatJSON {
return invalidArgumentsErrorWithDetails("`get --output=json` cannot be used with stdout target `-`", mergeJSONErrorDetails(argumentErrorDetails("dst"), flagErrorDetails("output")))
return invalidArgumentsErrorWithDetails("`get --output=json` cannot be used with stdout target `-`", mergeJSONErrorDetails(operationErrorDetails("download"), argumentErrorDetails("dst"), flagErrorDetails("output")))
}
return getStdout(cmd, src, recursive)
}
Expand All @@ -93,15 +93,15 @@ func get(cmd *cobra.Command, args []string) (err error) {
meta, err := dbx.GetMetadata(files.NewGetMetadataArg(src))
if err != nil {
if recursive {
return fmt.Errorf("get metadata for %s: %v", src, err)
return withJSONErrorDetails(fmt.Errorf("get metadata for %s: %v", src, err), operationErrorDetails("download"), pathErrorDetails(src))
}
// For non-recursive, fall through to download (will fail with proper error)
if f, statErr := os.Stat(dst); statErr == nil && f.IsDir() {
dst = filepath.Join(dst, path.Base(src))
}
result, err := downloadFileWithResult(dbx, src, dst, opts)
if err != nil {
return err
return withJSONErrorDetails(err, operationErrorDetails("download"), pathErrorDetails(src), relocationErrorDetails(src, dst))
}
return renderGetResults(cmd, getCommandInput{
Source: src,
Expand All @@ -113,17 +113,17 @@ func get(cmd *cobra.Command, args []string) (err error) {

if _, ok := meta.(*files.FolderMetadata); ok {
if !recursive {
return invalidArgumentsErrorfWithDetails("%s is a folder (use --recursive to download folders)", pathErrorDetails(src), src)
return invalidArgumentsErrorfWithDetails("%s is a folder (use --recursive to download folders)", mergeJSONErrorDetails(operationErrorDetails("download"), pathErrorDetails(src)), src)
}
if f, statErr := os.Stat(dst); statErr == nil && f.IsDir() {
dst = filepath.Join(dst, path.Base(src))
}
if commandOutputFormat(cmd) == output.FormatText {
return getRecursive(dbx, src, dst)
return withJSONErrorDetails(getRecursive(dbx, src, dst), operationErrorDetails("download"), pathErrorDetails(src), relocationErrorDetails(src, dst))
}
results, err := getRecursiveWithResults(dbx, src, dst, meta, opts)
if err != nil {
return err
return withJSONErrorDetails(err, operationErrorDetails("download"), pathErrorDetails(src), relocationErrorDetails(src, dst))
}
return renderGetResults(cmd, getCommandInput{
Source: src,
Expand All @@ -139,7 +139,7 @@ func get(cmd *cobra.Command, args []string) (err error) {

result, err := downloadFileWithResult(dbx, src, dst, opts)
if err != nil {
return err
return withJSONErrorDetails(err, operationErrorDetails("download"), pathErrorDetails(src), relocationErrorDetails(src, dst))
}
return renderGetResults(cmd, getCommandInput{
Source: src,
Expand Down Expand Up @@ -199,19 +199,19 @@ func getOperationResults(results []getResult) []jsonOperationResult {

func getStdout(cmd *cobra.Command, src string, recursive bool) error {
if recursive {
return invalidArgumentsErrorWithDetails("`get -` cannot be used with --recursive", flagErrorDetails("recursive"))
return invalidArgumentsErrorWithDetails("`get -` cannot be used with --recursive", mergeJSONErrorDetails(operationErrorDetails("download"), flagErrorDetails("recursive")))
}

dbx := filesNewFunc(config)

meta, err := dbx.GetMetadata(files.NewGetMetadataArg(src))
if err == nil {
if _, ok := meta.(*files.FolderMetadata); ok {
return invalidArgumentsErrorfWithDetails("%s is a folder; cannot download folder to stdout", pathErrorDetails(src), src)
return invalidArgumentsErrorfWithDetails("%s is a folder; cannot download folder to stdout", mergeJSONErrorDetails(operationErrorDetails("download"), pathErrorDetails(src)), src)
}
}

return downloadToStdout(dbx, src, cmd.OutOrStdout())
return withJSONErrorDetails(downloadToStdout(dbx, src, cmd.OutOrStdout()), operationErrorDetails("download"), pathErrorDetails(src))
}

func getWithClient(dbx files.Client, args []string) (err error) {
Expand All @@ -232,7 +232,7 @@ func getWithClient(dbx files.Client, args []string) (err error) {
dst = filepath.Join(dst, path.Base(src))
}

return downloadFile(dbx, src, dst)
return withJSONErrorDetails(downloadFile(dbx, src, dst), operationErrorDetails("download"), pathErrorDetails(src), relocationErrorDetails(src, dst))
}

func getRecursive(dbx files.Client, src, dst string) error {
Expand All @@ -250,7 +250,7 @@ func getRecursiveInternal(dbx files.Client, src, dst string, rootMeta files.IsMe

res, err := dbx.ListFolder(arg)
if err != nil {
return nil, fmt.Errorf("list folder %s: %v", src, err)
return nil, withJSONErrorDetails(fmt.Errorf("list folder %s: %v", src, err), operationErrorDetails("download"), pathErrorDetails(src))
}

var entries []files.IsMetadata
Expand All @@ -259,7 +259,7 @@ func getRecursiveInternal(dbx files.Client, src, dst string, rootMeta files.IsMe
cont := files.NewListFolderContinueArg(res.Cursor)
res, err = dbx.ListFolderContinue(cont)
if err != nil {
return nil, fmt.Errorf("list folder continue: %v", err)
return nil, withJSONErrorDetails(fmt.Errorf("list folder continue: %v", err), operationErrorDetails("download"), pathErrorDetails(src))
}
entries = append(entries, res.Entries...)
}
Expand Down Expand Up @@ -335,7 +335,7 @@ func getRecursiveInternal(dbx files.Client, src, dst string, rootMeta files.IsMe
for _, e := range downloadErrors {
fmt.Fprintf(getErrorOutput(opts), "Error: %v\n", e)
}
return nil, fmt.Errorf("get: %d error(s)", len(downloadErrors))
return nil, commandFailedErrorfWithDetails("get: %d error(s)", mergeJSONErrorDetails(operationErrorDetails("download"), pathErrorDetails(src), relocationErrorDetails(src, dst)), len(downloadErrors))
}

return results, nil
Expand Down
4 changes: 4 additions & 0 deletions cmd/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,10 @@ func TestGetJSONRecursiveErrorEmitsNoSuccessJSON(t *testing.T) {
if err == nil {
t.Fatal("expected recursive error")
}
details := jsonErrorDetails(err)
if details["operation"] != "download" || details["path"] != "/remote" {
t.Fatalf("details = %#v, want download operation and source path", details)
}
if stdout.Len() != 0 {
t.Fatalf("stdout = %q, want empty on recursive error", stdout.String())
}
Expand Down
Loading
Loading