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: 8 additions & 1 deletion cmd/dry_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const (
// Command implementations should keep their validate/plan/execute flow local
// until at least two commands need the same execution driver.
func addDryRunFlag(cmd *cobra.Command) {
cmd.Flags().Bool(dryRunFlagName, false, "Show what would be done without making changes")
cmd.Flags().Bool(dryRunFlagName, false, "Preview intended writes without making changes")
}

func dryRunEnabled(cmd *cobra.Command) (bool, error) {
Expand All @@ -51,3 +51,10 @@ func writeDryRunLine(w io.Writer, verb, path string) error {
_, err := fmt.Fprintf(w, "Would %s %s\n", verb, path)
return err
}

func dryRunDisplayPath(metadata jsonMetadata, fallback string) string {
if metadata.PathDisplay != "" {
return metadata.PathDisplay
}
return fallback
}
9 changes: 9 additions & 0 deletions cmd/dry_run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,12 @@ func TestWriteDryRunLine(t *testing.T) {
t.Fatalf("writeDryRunLine permanent output = %q, want %q", got, want)
}
}

func TestDryRunDisplayPath(t *testing.T) {
if got, want := dryRunDisplayPath(jsonMetadata{PathDisplay: "/Display.txt"}, "/fallback.txt"), "/Display.txt"; got != want {
t.Fatalf("dryRunDisplayPath with display path = %q, want %q", got, want)
}
if got, want := dryRunDisplayPath(jsonMetadata{}, "/fallback.txt"), "/fallback.txt"; got != want {
t.Fatalf("dryRunDisplayPath fallback = %q, want %q", got, want)
}
}
4 changes: 2 additions & 2 deletions cmd/help_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{
"mkdir": {
Args: []jsonCommandArg{commandArg("directory", true, false, "dropbox_path", "Dropbox directory path to create")},
Examples: []jsonCommandExample{{Description: "Create a Dropbox folder", Command: "dbxcli mkdir /Reports"}},
Flags: map[string]jsonCommandFlagMetadata{"parents": {ValueKind: "boolean"}},
Flags: map[string]jsonCommandFlagMetadata{dryRunFlagName: {ValueKind: "boolean"}, "parents": {ValueKind: "boolean"}},
DropboxScopes: []string{"files.content.write"},
Known: true,
},
Expand Down Expand Up @@ -348,7 +348,7 @@ var commandContractRegistry = map[string]jsonCommandContractMetadata{
"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"}},
"mkdir": {Statuses: []string{"created", "existing", jsonStatusPlanned}, Kinds: []string{"folder"}},
"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"}},
Expand Down
2 changes: 1 addition & 1 deletion cmd/json_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1092,7 +1092,7 @@ func jsonCommandSchemas() map[string]jsonGoldenCommandSchema {
"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),
"mkdir": operationSchema("mkdir_input", schemaRef("mkdir_input"), "metadata", []string{mkdirStatusCreated, mkdirStatusExisting, jsonStatusPlanned}, []string{mkdirKindFolder}, nil),
"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),
Expand Down
65 changes: 58 additions & 7 deletions cmd/mkdir.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package cmd

import (
"errors"
"io"
"strings"

"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files"
Expand All @@ -25,6 +26,12 @@ import (
type mkdirInput struct {
Path string `json:"path"`
Parents bool `json:"parents"`
DryRun bool `json:"dry_run,omitempty"`
}

type mkdirOptions struct {
parents bool
dryRun bool
}

const (
Expand All @@ -51,16 +58,28 @@ func mkdir(cmd *cobra.Command, args []string) (err error) {
return
}

arg := files.NewCreateFolderArg(dst)
opts, err := parseMkdirOptions(cmd)
if err != nil {
return err
}

parents, _ := cmd.Flags().GetBool("parents")
if opts.dryRun {
result, err := newPlannedMkdirResult(dst, opts)
if err != nil {
return err
}
return commandOutput(cmd).Render(func(w io.Writer) error {
return writeDryRunLine(w, "create directory", result.displayPath())
}, newJSONCommandOperationOutput(cmd, result.Input, []jsonOperationResult{mkdirOperationResult(result)}, nil))
}

arg := files.NewCreateFolderArg(dst)
dbx := filesNewFunc(config)
created, err := dbx.CreateFolderV2Context(currentContext(), arg)
var metadata *files.FolderMetadata
status := mkdirStatusCreated
if err != nil {
if !parents {
if !opts.parents {
return err
}

Expand Down Expand Up @@ -98,13 +117,30 @@ func mkdir(cmd *cobra.Command, args []string) (err error) {
metadata = created.Metadata
}

result, err := newMkdirResult(status, dst, parents, metadata)
result, err := newMkdirResult(status, dst, opts, metadata)
if err != nil {
return err
}
return renderJSONOperationOutput(cmd, result.Input, []jsonOperationResult{mkdirOperationResult(result)})
}

func parseMkdirOptions(cmd *cobra.Command) (mkdirOptions, error) {
parents, err := cmd.Flags().GetBool("parents")
if err != nil {
return mkdirOptions{}, err
}

dryRun, err := dryRunEnabled(cmd)
if err != nil {
return mkdirOptions{}, err
}

return mkdirOptions{
parents: parents,
dryRun: dryRun,
}, nil
}

func existingFolderMetadata(dbx filesClient, dst string) (*files.FolderMetadata, error) {
metadata, err := dbx.GetMetadataContext(currentContext(), files.NewGetMetadataArg(dst))
if err != nil {
Expand All @@ -117,7 +153,7 @@ func existingFolderMetadata(dbx filesClient, dst string) (*files.FolderMetadata,
return folder, nil
}

func newMkdirResult(status, path string, parents bool, metadata *files.FolderMetadata) (mkdirResult, error) {
func newMkdirResult(status, path string, opts mkdirOptions, metadata *files.FolderMetadata) (mkdirResult, error) {
result, err := jsonMetadataFromDropbox(metadata)
if err != nil {
return mkdirResult{}, err
Expand All @@ -129,14 +165,28 @@ func newMkdirResult(status, path string, parents bool, metadata *files.FolderMet
Kind: mkdirKindFolder,
Input: mkdirInput{
Path: path,
Parents: parents,
Parents: opts.parents,
DryRun: opts.dryRun,
},
Result: result,
}, nil
}

func newPlannedMkdirResult(path string, opts mkdirOptions) (mkdirResult, error) {
return newMkdirResult(mkdirStatusCreated, path, opts, &files.FolderMetadata{
Metadata: files.Metadata{
PathDisplay: path,
PathLower: strings.ToLower(path),
},
})
}

func mkdirOperationResult(result mkdirResult) jsonOperationResult {
return newJSONOperationResult(result.Status, result.Kind, result.Input, result.Result)
return newJSONOperationResult(plannedStatus(result.Input.DryRun, result.Status), result.Kind, result.Input, result.Result)
}

func (r mkdirResult) displayPath() string {
return dryRunDisplayPath(r.Result, r.Input.Path)
}

func createFolderConflictTag(err error) (string, bool) {
Expand Down Expand Up @@ -178,5 +228,6 @@ var mkdirCmd = &cobra.Command{
func init() {
RootCmd.AddCommand(mkdirCmd)
mkdirCmd.Flags().BoolP("parents", "p", false, "No error if existing, create parent directories as needed")
addDryRunFlag(mkdirCmd)
enableStructuredOutput(mkdirCmd)
}
105 changes: 105 additions & 0 deletions cmd/mkdir_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,32 @@ func TestMkdirQuietByDefault(t *testing.T) {
}
}

func TestMkdirDryRunTextOutputSnapshot(t *testing.T) {
cmd, stdout := testMkdirCmd(t)
setMkdirDryRun(t, cmd)

mock := &mockFilesClient{
createFolderV2Fn: func(arg *files.CreateFolderArg) (*files.CreateFolderResult, error) {
t.Fatalf("CreateFolderV2 called during dry-run: %v", arg)
return nil, nil
},
getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) {
t.Fatalf("GetMetadata called during dry-run: %v", arg)
return nil, nil
},
}
stubFilesClient(t, mock)

if err := mkdir(cmd, []string{"/Projects"}); err != nil {
t.Fatalf("mkdir error: %v", err)
}

const want = "Would create directory /Projects\n"
if got := stdout.String(); got != want {
t.Fatalf("stdout = %q, want %q", got, want)
}
}

func TestMkdirJSONOutputsCreatedFolder(t *testing.T) {
cmd, stdout := testMkdirCmd(t)
setMkdirOutputJSON(t, cmd)
Expand Down Expand Up @@ -98,6 +124,76 @@ func TestMkdirJSONOutputsCreatedFolder(t *testing.T) {
}
}

func TestMkdirJSONDryRunOutputsPlannedResult(t *testing.T) {
cmd, stdout := testMkdirCmd(t)
setMkdirOutputJSON(t, cmd)
setMkdirDryRun(t, cmd)

mock := &mockFilesClient{
createFolderV2Fn: func(arg *files.CreateFolderArg) (*files.CreateFolderResult, error) {
t.Fatalf("CreateFolderV2 called during dry-run: %v", arg)
return nil, nil
},
getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) {
t.Fatalf("GetMetadata called during dry-run: %v", arg)
return nil, nil
},
}
stubFilesClient(t, mock)

if err := mkdir(cmd, []string{"/Projects"}); err != nil {
t.Fatalf("mkdir error: %v", err)
}

got := decodeMkdirOutput(t, stdout)
if got.Input.Path != "/Projects" || got.Input.Parents || !got.Input.DryRun {
t.Fatalf("input = %#v, want path /Projects, parents false, dry_run true", got.Input)
}
result := got.Results[0]
if result.Status != jsonStatusPlanned || result.Kind != mkdirKindFolder {
t.Fatalf("status/kind = %s/%s, want planned/folder", result.Status, result.Kind)
}
if result.Input.Path != "/Projects" || result.Input.Parents || !result.Input.DryRun {
t.Fatalf("result input = %#v, want path /Projects, parents false, dry_run true", result.Input)
}
if result.Result.Type != "folder" {
t.Fatalf("result type = %q, want folder", result.Result.Type)
}
if result.Result.PathDisplay != "/Projects" || result.Result.PathLower != "/projects" {
t.Fatalf("path_display/path_lower = %q/%q, want /Projects//projects", result.Result.PathDisplay, result.Result.PathLower)
}
if result.Result.ID != "" {
t.Fatalf("id = %q, want empty for planned metadata", result.Result.ID)
}
}

func TestMkdirJSONDryRunOutputSnapshot(t *testing.T) {
cmd, stdout := testMkdirCmd(t)
setMkdirOutputJSON(t, cmd)
setMkdirDryRun(t, cmd)

mock := &mockFilesClient{
createFolderV2Fn: func(arg *files.CreateFolderArg) (*files.CreateFolderResult, error) {
t.Fatalf("CreateFolderV2 called during dry-run: %v", arg)
return nil, nil
},
getMetadataFn: func(arg *files.GetMetadataArg) (files.IsMetadata, error) {
t.Fatalf("GetMetadata called during dry-run: %v", arg)
return nil, nil
},
}
stubFilesClient(t, mock)

if err := mkdir(cmd, []string{"/Projects"}); err != nil {
t.Fatalf("mkdir error: %v", err)
}

const want = "{\"ok\":true,\"schema_version\":\"1\",\"command\":\"mkdir\",\"input\":{\"path\":\"/Projects\",\"parents\":false,\"dry_run\":true},\"results\":[{\"status\":\"planned\",\"kind\":\"folder\",\"input\":{\"path\":\"/Projects\",\"parents\":false,\"dry_run\":true},\"result\":{\"type\":\"folder\",\"path_display\":\"/Projects\",\"path_lower\":\"/projects\"}}],\"warnings\":[]}\n"
if got := stdout.String(); got != want {
t.Fatalf("stdout = %s, want %s", got, want)
}
}

func TestMkdirJSONParentsReturnsExistingFolderMetadata(t *testing.T) {
cmd, stdout := testMkdirCmd(t)
setMkdirOutputJSON(t, cmd)
Expand Down Expand Up @@ -259,6 +355,7 @@ func testMkdirCmd(t *testing.T) (*cobra.Command, *bytes.Buffer) {
cmd := &cobra.Command{Use: "mkdir"}
cmd.SetOut(&stdout)
cmd.Flags().BoolP("parents", "p", false, "")
addDryRunFlag(cmd)
cmd.Flags().String(outputFlag, "text", "")
return cmd, &stdout
}
Expand All @@ -279,6 +376,14 @@ func setMkdirParents(t *testing.T, cmd *cobra.Command) {
}
}

func setMkdirDryRun(t *testing.T, cmd *cobra.Command) {
t.Helper()

if err := cmd.Flags().Set(dryRunFlagName, "true"); err != nil {
t.Fatal(err)
}
}

func mkdirFolderMetadata(path string) *files.FolderMetadata {
return &files.FolderMetadata{
Metadata: files.Metadata{
Expand Down
5 changes: 1 addition & 4 deletions cmd/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,10 +249,7 @@ func renderRemoveResults(w io.Writer, results []removeResult) error {
}

func (r removeResult) displayPath() string {
if r.Result.PathDisplay != "" {
return r.Result.PathDisplay
}
return r.Input.Path
return dryRunDisplayPath(r.Result, r.Input.Path)
}

func (o removeOptions) allowNonEmptyFolder() bool {
Expand Down
4 changes: 3 additions & 1 deletion cmd/testdata/json_contract/success_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
"type"
],
"mkdir_input": [
"dry_run",
"parents",
"path"
],
Expand Down Expand Up @@ -514,7 +515,8 @@
"result": "metadata",
"statuses": [
"created",
"existing"
"existing",
"planned"
],
"kinds": [
"folder"
Expand Down
3 changes: 2 additions & 1 deletion docs/commands/dbxcli_mkdir.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dbxcli mkdir [flags] <directory>
### Options

```
--dry-run Preview intended writes without making changes
-h, --help help for mkdir
-p, --parents No error if existing, create parent directories as needed
```
Expand All @@ -33,7 +34,7 @@ dbxcli mkdir [flags] <directory>
* Dropbox scopes: `files.content.write`
* Arguments: `directory` (required, dropbox_path)
* Flag metadata: `--output` (values: `json`, `text`)
* Result statuses: `created`, `existing`
* Result statuses: `created`, `existing`, `planned`
* Result kinds: `folder`
* JSON contract: `docs/json-schema/v1/commands.json#/commands/mkdir`
* JSON success schema: `docs/json-schema/v1/commands.schema.json#/$defs/command_mkdir`
Expand Down
2 changes: 1 addition & 1 deletion docs/commands/dbxcli_rm.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ dbxcli rm [flags] <file>
### Options

```
--dry-run Show what would be done without making changes
--dry-run Preview intended writes without making changes
-f, --force Allow removing non-empty folders; same as --recursive
-h, --help help for rm
--permanent Permanently delete instead of moving to Dropbox trash
Expand Down
4 changes: 3 additions & 1 deletion docs/json-schema/v1/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
"type"
],
"mkdir_input": [
"dry_run",
"parents",
"path"
],
Expand Down Expand Up @@ -514,7 +515,8 @@
"result": "metadata",
"statuses": [
"created",
"existing"
"existing",
"planned"
],
"kinds": [
"folder"
Expand Down
Loading
Loading