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
4 changes: 2 additions & 2 deletions cmd/help_manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ var commandManifestRegistry = map[string]jsonCommandManifestMetadata{
"share-link create": {
Args: []jsonCommandArg{commandArg("path", true, false, "dropbox_path", "Dropbox path to share")},
Examples: []jsonCommandExample{{Description: "Create a shared link", Command: "dbxcli share-link create /Reports/report.pdf"}},
Flags: mergeCommandFlagMetadata(mergeCommandFlagMetadata(sharedLinkSettingsFlagMetadata, sharedLinkPasswordFlagMetadata), map[string]jsonCommandFlagMetadata{"access": {EnumValues: []string{"viewer", "editor", "max"}, ValueKind: "enum"}}),
Flags: mergeCommandFlagMetadata(mergeCommandFlagMetadata(sharedLinkSettingsFlagMetadata, sharedLinkPasswordFlagMetadata), map[string]jsonCommandFlagMetadata{dryRunFlagName: {ValueKind: "boolean"}, "access": {EnumValues: []string{"viewer", "editor", "max"}, ValueKind: "enum"}}),
DropboxScopes: []string{"sharing.write", "sharing.read"},
Known: true,
},
Expand Down Expand Up @@ -360,7 +360,7 @@ var commandContractRegistry = map[string]jsonCommandContractMetadata{
"search": {Statuses: []string{"found"}, Kinds: []string{"deleted", "file", "folder"}},
"share list folder": {Statuses: []string{"listed"}, Kinds: []string{"shared_folder"}},
"share list link": {Statuses: []string{"listed"}, Kinds: []string{"file", "folder", "link"}, Warnings: []string{jsonWarningCodeDeprecatedCommand}},
"share-link create": {Statuses: []string{"created", "existing"}, Kinds: []string{"file", "folder", "link"}},
"share-link create": {Statuses: []string{"created", "existing", jsonStatusPlanned}, Kinds: []string{"file", "folder", "link"}},
"share-link download": {Statuses: []string{"downloaded"}, Kinds: []string{"file", "folder", "link"}},
"share-link info": {Statuses: []string{"found"}, Kinds: []string{"file", "folder", "link"}},
"share-link list": {Statuses: []string{"listed"}, Kinds: []string{"file", "folder", "link"}},
Expand Down
7 changes: 4 additions & 3 deletions cmd/json_contract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -760,8 +760,8 @@ func jsonGoldenSuccessOutputExamples() map[string]jsonOperationOutput {
"share list link": newJSONOperationOutput(shareLinkListInput{Path: "/Reports/old.pdf", DirectOnly: true}, []jsonOperationResult{
shareLinkJSONOperationResult(shareLinkJSONStatusListed, sharedLink),
}, []jsonWarning{{Code: jsonWarningCodeDeprecatedCommand, Message: "use `dbxcli share-link list` instead"}}),
"share-link create": newJSONOperationOutput(shareLinkCreateInput{Path: "/Reports/old.pdf", Access: "max", Audience: "public", Expires: "2026-07-01T00:00:00Z", RemoveExpiration: false, AllowDownload: true, DisallowDownload: false, Password: true}, []jsonOperationResult{
shareLinkJSONOperationResult(shareLinkJSONStatusCreated, sharedLink),
"share-link create": newJSONOperationOutput(shareLinkCreateInput{Path: "/Reports/old.pdf", Access: "max", Audience: "public", Expires: "2026-07-01T00:00:00Z", RemoveExpiration: false, AllowDownload: true, DisallowDownload: false, Password: true, DryRun: false}, []jsonOperationResult{
shareLinkCreateOperationResult(shareLinkJSONStatusCreated, sharedLink, shareLinkCreateOptions{dryRun: false}),
}, nil),
"share-link download": newJSONOperationOutput(shareLinkDownloadInput{URL: sharedLink.URL, Target: "old.pdf", Path: "/old.pdf", Recursive: false, Password: true}, []jsonOperationResult{
newJSONOperationResult(shareLinkJSONStatusDownloaded, sharedLink.Type, nil, shareLinkDownloadResult{Target: "old.pdf", Link: sharedLink}),
Expand Down Expand Up @@ -1063,6 +1063,7 @@ func jsonContractDefinitions() map[string][]string {
"search_input": jsonFieldNames[searchInput](),
"share_folder": jsonFieldNames[shareFolderJSONMetadata](),
"share_link_create_input": jsonFieldNames[shareLinkCreateInput](),
"share_link_create_result_input": jsonFieldNames[shareLinkCreateResultInput](),
"share_link_download_input": jsonFieldNames[shareLinkDownloadInput](),
"share_link_download_result": jsonFieldNames[shareLinkDownloadResult](),
"share_link_info_input": jsonFieldNames[shareLinkInfoInput](),
Expand Down Expand Up @@ -1103,7 +1104,7 @@ func jsonCommandSchemas() map[string]jsonGoldenCommandSchema {
"search": operationSchema("search_input", schemaRef("empty"), "metadata", []string{searchJSONStatusFound}, metadataKinds(), nil),
"share list folder": operationSchema("empty", schemaRef("empty"), "share_folder", []string{shareFolderJSONStatusListed}, []string{shareFolderJSONKindFolder}, nil),
"share list link": operationSchema("share_link_list_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusListed}, shareLinkKinds(), []string{jsonWarningCodeDeprecatedCommand}),
"share-link create": operationSchema("share_link_create_input", schemaRef("empty"), "share_link_metadata", []string{shareLinkJSONStatusCreated, shareLinkJSONStatusExisting}, shareLinkKinds(), nil),
"share-link create": operationSchema("share_link_create_input", schemaRef("share_link_create_result_input"), "share_link_metadata", []string{shareLinkJSONStatusCreated, shareLinkJSONStatusExisting, jsonStatusPlanned}, shareLinkKinds(), nil),
"share-link download": operationSchema(
"share_link_download_input",
schemaRef("empty"),
Expand Down
62 changes: 62 additions & 0 deletions cmd/share_create_link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,49 @@ func TestSharedLinkCreatePrintsURLAndUsesDefaultSettings(t *testing.T) {
}
}

func TestSharedLinkCreateDryRunDoesNotCallAPI(t *testing.T) {
stubSharedLinkClient(t, &mockSharedLinkClient{
createSharedLinkWithSettingsFn: func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) {
t.Fatalf("CreateSharedLinkWithSettings called during dry-run: %v", arg)
return nil, nil
},
createSharedLinkWithRawSettingsFn: func(path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) {
t.Fatalf("CreateSharedLinkWithRawSettings called during dry-run: %s %#v", path, settings)
return nil, nil
},
listSharedLinksFn: func(arg *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error) {
t.Fatalf("ListSharedLinks called during dry-run: %v", arg)
return nil, nil
},
modifySharedLinkSettingsFn: func(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) {
t.Fatalf("ModifySharedLinkSettings called during dry-run: %v", arg)
return nil, nil
},
modifySharedLinkSettingsRawFn: func(url string, settings *rawSharedLinkSettings, removeExpiration bool) error {
t.Fatalf("ModifySharedLinkSettingsRaw called during dry-run: %s %#v removeExpiration=%t", url, settings, removeExpiration)
return nil
},
})

var stdout bytes.Buffer
cmd := newShareLinkCreateTestCommand(&stdout)
if err := cmd.Flags().Set("audience", "team"); err != nil {
t.Fatalf("set audience: %v", err)
}
if err := cmd.Flags().Set(dryRunFlagName, "true"); err != nil {
t.Fatal(err)
}

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

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

func TestSharedLinkCreateWithExpiresSetsExpiration(t *testing.T) {
wantExpires := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
mock := &mockSharedLinkClient{
Expand Down Expand Up @@ -1283,6 +1326,9 @@ func TestShareLinkCreateDoesNotBreakShareListLinkCommand(t *testing.T) {
if shareLinkCreateCmd.Flags().Lookup("password-file") == nil {
t.Fatal("share-link create should define --password-file")
}
if shareLinkCreateCmd.Flags().Lookup(dryRunFlagName) == nil {
t.Fatalf("share-link create should define --%s", dryRunFlagName)
}

cmd, _, err = RootCmd.Find([]string{"share", "list", "link"})
if err != nil {
Expand All @@ -1299,6 +1345,22 @@ func TestShareLinkCreateDoesNotBreakShareListLinkCommand(t *testing.T) {
}
}

func newShareLinkCreateTestCommand(stdout *bytes.Buffer) *cobra.Command {
cmd := &cobra.Command{}
cmd.Flags().String("access", "", "")
cmd.Flags().String("audience", "", "")
cmd.Flags().Bool("allow-download", false, "")
cmd.Flags().Bool("disallow-download", false, "")
cmd.Flags().String("expires", "", "")
cmd.Flags().Bool("remove-expiration", false, "")
addSharedLinkPasswordFlags(cmd)
addDryRunFlag(cmd)
if stdout != nil {
cmd.SetOut(stdout)
}
return cmd
}

func TestShareLinkListListsAllLinks(t *testing.T) {
var listArg *sharing.ListSharedLinksArg
stubSharedLinkClient(t, &mockSharedLinkClient{
Expand Down
46 changes: 45 additions & 1 deletion cmd/share_link_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"io"
"strings"
"time"

"github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox"
Expand All @@ -33,6 +34,7 @@ type shareLinkCreateOptions struct {
access *sharing.RequestedLinkAccessLevel
audience *sharing.LinkAudience
password sharedLinkPasswordOptions
dryRun bool
}

type shareLinkCreateInput struct {
Expand All @@ -44,6 +46,11 @@ type shareLinkCreateInput struct {
AllowDownload bool `json:"allow_download,omitempty"`
DisallowDownload bool `json:"disallow_download,omitempty"`
Password bool `json:"password,omitempty"`
DryRun bool `json:"dry_run,omitempty"`
}

type shareLinkCreateResultInput struct {
DryRun bool `json:"dry_run,omitempty"`
}

func shareLinkCreate(cmd *cobra.Command, args []string) error {
Expand All @@ -64,6 +71,10 @@ func shareLinkCreate(cmd *cobra.Command, args []string) error {
return err
}

if opts.dryRun {
return renderShareLinkCreateDryRunOutput(cmd, path, opts)
}

dbx := newSharedLinkClient(config)
link, err := createSharedLink(dbx, path, opts)
usedExisting := false
Expand Down Expand Up @@ -107,7 +118,7 @@ func shareLinkCreate(cmd *cobra.Command, args []string) error {
}, newJSONCommandOperationOutput(
cmd,
newShareLinkCreateInput(path, opts),
[]jsonOperationResult{shareLinkJSONOperationResult(status, result)},
[]jsonOperationResult{shareLinkCreateOperationResult(status, result, opts)},
nil,
))
}
Expand All @@ -119,6 +130,7 @@ func newShareLinkCreateInput(path string, opts shareLinkCreateOptions) shareLink
AllowDownload: opts.allowDownload,
DisallowDownload: opts.disallowDownload,
Password: opts.password.set,
DryRun: opts.dryRun,
}
if opts.access != nil {
input.Access = opts.access.Tag
Expand Down Expand Up @@ -201,6 +213,13 @@ func parseShareLinkCreateOptions(cmd *cobra.Command) (shareLinkCreateOptions, er
return opts, err
}
opts.password = password
if cmd.Flags().Lookup(dryRunFlagName) != nil {
dryRun, err := dryRunEnabled(cmd)
if err != nil {
return opts, err
}
opts.dryRun = dryRun
}

if opts.expires != nil && opts.removeExpiration {
return opts, invalidArgumentsErrorWithDetails("`--expires` and `--remove-expiration` cannot be used together", flagsErrorDetails("expires", "remove-expiration"))
Expand All @@ -212,6 +231,30 @@ func parseShareLinkCreateOptions(cmd *cobra.Command) (shareLinkCreateOptions, er
return opts, nil
}

func shareLinkCreateOperationResult(status string, metadata shareLinkJSONMetadata, opts shareLinkCreateOptions) jsonOperationResult {
return newJSONOperationResult(plannedStatus(opts.dryRun, status), metadata.Type, shareLinkCreateResultInput{DryRun: opts.dryRun}, metadata)
}

func plannedShareLinkCreateMetadata(path string) shareLinkJSONMetadata {
return shareLinkJSONMetadata{
Type: shareLinkJSONKindLink,
URL: "",
PathLower: strings.ToLower(path),
}
}

func renderShareLinkCreateDryRunOutput(cmd *cobra.Command, path string, opts shareLinkCreateOptions) error {
result := plannedShareLinkCreateMetadata(path)
return commandOutput(cmd).Render(func(w io.Writer) error {
return writeDryRunLine(w, "create shared link", path)
}, newJSONCommandOperationOutput(
cmd,
newShareLinkCreateInput(path, opts),
[]jsonOperationResult{shareLinkCreateOperationResult(shareLinkJSONStatusCreated, result, opts)},
nil,
))
}

func applyExistingSharedLinkCreateOptions(dbx sharedLinkClient, link sharing.IsSharedLinkMetadata, opts shareLinkCreateOptions) (sharing.IsSharedLinkMetadata, error) {
if opts.access != nil {
return nil, invalidArgumentsErrorWithDetails("cannot apply `--access` because the shared link already exists", flagErrorDetails("access"))
Expand Down Expand Up @@ -477,6 +520,7 @@ func init() {
shareLinkCreateCmd.Flags().String("expires", "", "Set shared link expiration time as an RFC3339 timestamp")
shareLinkCreateCmd.Flags().Bool("remove-expiration", false, "Remove expiration when returning an existing shared link")
addSharedLinkPasswordFlags(shareLinkCreateCmd)
addDryRunFlag(shareLinkCreateCmd)
shareLinkCmd.AddCommand(shareLinkCreateCmd)
enableStructuredOutput(shareLinkCreateCmd)
}
65 changes: 65 additions & 0 deletions cmd/share_link_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,71 @@ func TestShareLinkCreateJSONReportsExistingAsStatus(t *testing.T) {
assertNoTopLevelJSONField(t, stdout.Bytes(), "existing")
}

func TestShareLinkCreateJSONDryRunOutputsPlannedLink(t *testing.T) {
stubSharedLinkClient(t, &mockSharedLinkClient{
createSharedLinkWithSettingsFn: func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) {
t.Fatalf("CreateSharedLinkWithSettings called during dry-run: %v", arg)
return nil, nil
},
createSharedLinkWithRawSettingsFn: func(path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) {
t.Fatalf("CreateSharedLinkWithRawSettings called during dry-run: %s %#v", path, settings)
return nil, nil
},
listSharedLinksFn: func(arg *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error) {
t.Fatalf("ListSharedLinks called during dry-run: %v", arg)
return nil, nil
},
modifySharedLinkSettingsFn: func(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) {
t.Fatalf("ModifySharedLinkSettings called during dry-run: %v", arg)
return nil, nil
},
modifySharedLinkSettingsRawFn: func(url string, settings *rawSharedLinkSettings, removeExpiration bool) error {
t.Fatalf("ModifySharedLinkSettingsRaw called during dry-run: %s %#v removeExpiration=%t", url, settings, removeExpiration)
return nil
},
})

var stdout bytes.Buffer
cmd := newShareLinkCreateTestCommand(&stdout)
setShareLinkOutputJSON(t, cmd)
if err := cmd.Flags().Set("audience", "team"); err != nil {
t.Fatalf("set audience: %v", err)
}
if err := cmd.Flags().Set(dryRunFlagName, "true"); err != nil {
t.Fatal(err)
}

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

var got struct {
Input shareLinkCreateInput `json:"input"`
Results []struct {
Status string `json:"status"`
Kind string `json:"kind"`
Input shareLinkCreateResultInput `json:"input"`
Result shareLinkJSONMetadata `json:"result"`
} `json:"results"`
}
if err := json.Unmarshal(stdout.Bytes(), &got); err != nil {
t.Fatalf("decode output: %v\n%s", err, stdout.String())
}
if got.Input.Path != "/Reports/Old.pdf" || got.Input.Audience != "team" || !got.Input.DryRun {
t.Fatalf("input = %+v, want planned create input", got.Input)
}
if len(got.Results) != 1 {
t.Fatalf("results len = %d, want 1", len(got.Results))
}
result := got.Results[0]
if result.Status != jsonStatusPlanned || result.Kind != shareLinkJSONKindLink || !result.Input.DryRun {
t.Fatalf("result = %+v, want planned link create", result)
}
if result.Result.Type != shareLinkJSONKindLink || result.Result.PathLower != "/reports/old.pdf" {
t.Fatalf("result metadata = %+v, want planned link metadata", result.Result)
}
}

func TestShareLinkListJSONOutputsResultsAndInput(t *testing.T) {
stubSharedLinkClient(t, &mockSharedLinkClient{
listSharedLinksFn: func(arg *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error) {
Expand Down
9 changes: 7 additions & 2 deletions cmd/testdata/json_contract/success_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,15 @@
"allow_download",
"audience",
"disallow_download",
"dry_run",
"expires",
"password",
"path",
"remove_expiration"
],
"share_link_create_result_input": [
"dry_run"
],
"share_link_download_input": [
"password",
"path",
Expand Down Expand Up @@ -676,11 +680,12 @@
"top_level": "operation_output",
"result_wrapper": "operation_result",
"input": "share_link_create_input",
"result_input": "empty",
"result_input": "share_link_create_result_input",
"result": "share_link_metadata",
"statuses": [
"created",
"existing"
"existing",
"planned"
],
"kinds": [
"file",
Expand Down
3 changes: 2 additions & 1 deletion docs/commands/dbxcli_share-link_create.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ dbxcli share-link create <path> [flags]
--allow-download Allow downloads from the shared link
--audience string Set shared link audience: public, team, members, or no-one
--disallow-download Disallow downloads from the shared link
--dry-run Preview intended writes without making changes
--expires string Set shared link expiration time as an RFC3339 timestamp
-h, --help help for create
--password string Password for password-protected shared links
Expand All @@ -57,7 +58,7 @@ dbxcli share-link create <path> [flags]
* Dropbox scopes: `sharing.read`, `sharing.write`
* Arguments: `path` (required, dropbox_path)
* Flag metadata: `--access` (values: `editor`, `max`, `viewer`), `--allow-download` (conflicts: `disallow-download`), `--audience` (values: `members`, `no-one`, `public`, `team`), `--disallow-download` (conflicts: `allow-download`), `--expires` (conflicts: `remove-expiration`), `--output` (values: `json`, `text`), `--password` (conflicts: `password-file`, `password-prompt`; sensitive), `--password-file` (conflicts: `password`, `password-prompt`), `--password-prompt` (conflicts: `password`, `password-file`; may prompt), `--remove-expiration` (conflicts: `expires`)
* Result statuses: `created`, `existing`
* Result statuses: `created`, `existing`, `planned`
* Result kinds: `file`, `folder`, `link`
* JSON contract: `docs/json-schema/v1/commands.json#/commands/share-link create`
* JSON success schema: `docs/json-schema/v1/commands.schema.json#/$defs/command_share_2dlink_20create`
Expand Down
9 changes: 7 additions & 2 deletions docs/json-schema/v1/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,11 +267,15 @@
"allow_download",
"audience",
"disallow_download",
"dry_run",
"expires",
"password",
"path",
"remove_expiration"
],
"share_link_create_result_input": [
"dry_run"
],
"share_link_download_input": [
"password",
"path",
Expand Down Expand Up @@ -676,11 +680,12 @@
"top_level": "operation_output",
"result_wrapper": "operation_result",
"input": "share_link_create_input",
"result_input": "empty",
"result_input": "share_link_create_result_input",
"result": "share_link_metadata",
"statuses": [
"created",
"existing"
"existing",
"planned"
],
"kinds": [
"file",
Expand Down
Loading
Loading