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
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -232,27 +232,33 @@ $ dbxcli share-link create /file.txt # create or return an existing shared link
$ dbxcli share-link create /file.txt --access viewer # create a link with requested access
$ dbxcli share-link create /file.txt --audience team # create a link with requested audience
$ dbxcli share-link create /file.txt --allow-download # create a downloadable shared link
$ dbxcli share-link create /file.txt --disallow-download # create a shared link with downloads disabled
$ dbxcli share-link create /file.txt --expires 2026-07-01T00:00:00Z # create an expiring shared link
$ dbxcli share-link create /file.txt --password-prompt # create a password-protected shared link
$ dbxcli share-link create /file.txt --remove-expiration # remove expiration when returning an existing link
$ dbxcli share-link download <url> [target] # download a shared-link file
$ dbxcli share-link download <url> [target] --recursive # download a folder shared link
$ dbxcli share-link info <url> # display shared link information
$ dbxcli share-link info <url> --path /nested/file.txt # display information for a path inside the shared link
$ dbxcli share-link list # list existing shared links
$ dbxcli share-link list /file.txt # list direct shared links for a path
$ dbxcli share-link revoke <url> # revoke a shared link
$ dbxcli share-link update <url> --allow-download # update shared link settings
$ dbxcli share-link update <url> --disallow-download # disable downloads from a shared link
$ dbxcli share-link update <url> --audience public # update shared link audience
$ dbxcli share-link update <url> --expires 2026-07-01T00:00:00Z # update shared link expiration
$ dbxcli share-link update <url> --remove-expiration # remove shared link expiration
$ dbxcli share-link update <url> --password-prompt # set or change a shared link password
$ dbxcli share-link update <url> --remove-password # remove a shared link password
$ dbxcli share list link # deprecated compatibility command
$ dbxcli share list folder # list shared folders
```

`share-link create --access` supports `viewer`, `editor`, and `max`. Dropbox does not support changing access for an existing shared link, so `--access` fails clearly if the link already exists.

`share-link create --audience` and `share-link update --audience` support `public`, `team`, `members`, and `no-one`. Dropbox team and folder policies can still resolve the effective audience differently.

`share-link create`, `share-link update`, and `share-link download` support `--password <value>`, `--password-prompt`, and `--password-file <path>` for password-protected links. Use `--password-prompt` for interactive use so the password is not echoed.
`share-link create`, `share-link update`, `share-link info`, and `share-link download` support `--password <value>`, `--password-prompt`, and `--password-file <path>` for password-protected links. Use `--password-prompt` for interactive use so the password is not echoed.

`share-link download` writes to the metadata filename when `target` is omitted. Use `-` as the target to write file bytes to stdout. Folder shared links require `--recursive` and cannot be written to stdout.

Expand Down
227 changes: 214 additions & 13 deletions cmd/share_create_link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ import (
)

type mockSharedLinkClient struct {
createSharedLinkWithSettingsFn func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error)
getSharedLinkFileFn func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error)
getSharedLinkMetadataFn func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error)
listSharedLinksFn func(arg *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error)
modifySharedLinkSettingsFn func(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error)
removeSharedLinkPasswordFn func(url string) error
revokeSharedLinkFn func(arg *sharing.RevokeSharedLinkArg) error
createSharedLinkWithSettingsFn func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error)
createSharedLinkWithRawSettingsFn func(path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error)
getSharedLinkFileFn func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error)
getSharedLinkMetadataFn func(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error)
listSharedLinksFn func(arg *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error)
modifySharedLinkSettingsFn func(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error)
revokeSharedLinkFn func(arg *sharing.RevokeSharedLinkArg) error
modifySharedLinkSettingsRawFn func(url string, settings *rawSharedLinkSettings, removeExpiration bool) error
}

func (m *mockSharedLinkClient) CreateSharedLinkWithSettings(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) {
Expand All @@ -45,6 +46,13 @@ func (m *mockSharedLinkClient) CreateSharedLinkWithSettings(arg *sharing.CreateS
return nil, nil
}

func (m *mockSharedLinkClient) CreateSharedLinkWithRawSettings(path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) {
if m.createSharedLinkWithRawSettingsFn != nil {
return m.createSharedLinkWithRawSettingsFn(path, settings)
}
return nil, nil
}

func (m *mockSharedLinkClient) GetSharedLinkFile(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) {
if m.getSharedLinkFileFn != nil {
return m.getSharedLinkFileFn(arg)
Expand Down Expand Up @@ -73,16 +81,16 @@ func (m *mockSharedLinkClient) ModifySharedLinkSettings(arg *sharing.ModifyShare
return nil, nil
}

func (m *mockSharedLinkClient) RemoveSharedLinkPassword(url string) error {
if m.removeSharedLinkPasswordFn != nil {
return m.removeSharedLinkPasswordFn(url)
func (m *mockSharedLinkClient) RevokeSharedLink(arg *sharing.RevokeSharedLinkArg) error {
if m.revokeSharedLinkFn != nil {
return m.revokeSharedLinkFn(arg)
}
return nil
}

func (m *mockSharedLinkClient) RevokeSharedLink(arg *sharing.RevokeSharedLinkArg) error {
if m.revokeSharedLinkFn != nil {
return m.revokeSharedLinkFn(arg)
func (m *mockSharedLinkClient) ModifySharedLinkSettingsRaw(url string, settings *rawSharedLinkSettings, removeExpiration bool) error {
if m.modifySharedLinkSettingsRawFn != nil {
return m.modifySharedLinkSettingsRawFn(url, settings, removeExpiration)
}
return nil
}
Expand Down Expand Up @@ -236,6 +244,137 @@ func TestSharedLinkCreateWithAllowDownloadSetsAllowDownload(t *testing.T) {
}
}

func TestSharedLinkCreateWithDisallowDownloadUpdatesNewLink(t *testing.T) {
mock := &mockSharedLinkClient{
createSharedLinkWithSettingsFn: func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) {
t.Fatal("CreateSharedLinkWithSettings should not be called for disallow-download")
return nil, nil
},
createSharedLinkWithRawSettingsFn: func(path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) {
if path != "/file.txt" {
t.Fatalf("path = %q, want /file.txt", path)
}
if settings == nil || settings.AllowDownload == nil {
t.Fatalf("settings = %#v, want allow_download setting", settings)
}
if *settings.AllowDownload {
t.Fatal("allow_download = true, want false")
}
return sharedLinkFile("/file.txt", "https://example.com/file"), nil
},
}
stubSharedLinkClient(t, mock)

var stdout bytes.Buffer
cmd := &cobra.Command{}
cmd.Flags().Bool("disallow-download", false, "")
cmd.SetOut(&stdout)
if err := cmd.Flags().Set("disallow-download", "true"); err != nil {
t.Fatalf("set disallow-download: %v", err)
}

if err := shareLinkCreate(cmd, []string{"/file.txt"}); err != nil {
t.Fatalf("shareLinkCreate error: %v", err)
}
if got := stdout.String(); got != "https://example.com/file\n" {
t.Fatalf("stdout = %q, want URL only", got)
}
}

func TestSharedLinkCreateWithDisallowDownloadCombinesRawCreateSettings(t *testing.T) {
expires := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC)
mock := &mockSharedLinkClient{
createSharedLinkWithSettingsFn: func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) {
t.Fatal("CreateSharedLinkWithSettings should not be called for disallow-download")
return nil, nil
},
createSharedLinkWithRawSettingsFn: func(path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) {
if path != "/file.txt" {
t.Fatalf("path = %q, want /file.txt", path)
}
if settings == nil {
t.Fatal("settings = nil, want raw settings")
}
if settings.AllowDownload == nil || *settings.AllowDownload {
t.Fatalf("allow_download = %v, want false", settings.AllowDownload)
}
if settings.Expires == nil || !settings.Expires.Equal(expires) {
t.Fatalf("expires = %v, want %v", settings.Expires, expires)
}
if settings.Access == nil || settings.Access.Tag != sharing.RequestedLinkAccessLevelViewer {
t.Fatalf("access = %#v, want viewer", settings.Access)
}
if settings.Audience == nil || settings.Audience.Tag != sharing.LinkAudienceTeam {
t.Fatalf("audience = %#v, want team", settings.Audience)
}
if settings.RequirePassword == nil || !*settings.RequirePassword || settings.LinkPassword != "secret" {
t.Fatalf("password settings = require:%v value:%q, want password settings", settings.RequirePassword, settings.LinkPassword)
}
return sharedLinkFile("/file.txt", "https://example.com/file"), nil
},
}
stubSharedLinkClient(t, mock)

var stdout bytes.Buffer
cmd := &cobra.Command{}
cmd.Flags().Bool("disallow-download", false, "")
cmd.Flags().String("expires", "", "")
cmd.Flags().String("access", "", "")
cmd.Flags().String("audience", "", "")
addSharedLinkPasswordFlags(cmd)
cmd.SetOut(&stdout)
if err := cmd.Flags().Set("disallow-download", "true"); err != nil {
t.Fatalf("set disallow-download: %v", err)
}
if err := cmd.Flags().Set("expires", expires.Format(time.RFC3339)); err != nil {
t.Fatalf("set expires: %v", err)
}
if err := cmd.Flags().Set("access", "viewer"); err != nil {
t.Fatalf("set access: %v", err)
}
if err := cmd.Flags().Set("audience", "team"); err != nil {
t.Fatalf("set audience: %v", err)
}
if err := cmd.Flags().Set("password", "secret"); err != nil {
t.Fatalf("set password: %v", err)
}

if err := shareLinkCreate(cmd, []string{"/file.txt"}); err != nil {
t.Fatalf("shareLinkCreate error: %v", err)
}
if got := stdout.String(); got != "https://example.com/file\n" {
t.Fatalf("stdout = %q, want URL only", got)
}
}

func TestSharedLinkCreateRejectsAllowAndDisallowDownload(t *testing.T) {
called := false
stubSharedLinkClient(t, &mockSharedLinkClient{
createSharedLinkWithSettingsFn: func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) {
called = true
return nil, nil
},
})

cmd := &cobra.Command{}
cmd.Flags().Bool("allow-download", false, "")
cmd.Flags().Bool("disallow-download", false, "")
if err := cmd.Flags().Set("allow-download", "true"); err != nil {
t.Fatalf("set allow-download: %v", err)
}
if err := cmd.Flags().Set("disallow-download", "true"); err != nil {
t.Fatalf("set disallow-download: %v", err)
}

err := shareLinkCreate(cmd, []string{"/file.txt"})
if err == nil || !strings.Contains(err.Error(), "cannot be used together") {
t.Fatalf("error = %v, want mutual exclusion error", err)
}
if called {
t.Fatal("CreateSharedLinkWithSettings should not be called")
}
}

func TestSharedLinkCreateWithPasswordSetsPassword(t *testing.T) {
mock := &mockSharedLinkClient{
createSharedLinkWithSettingsFn: func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) {
Expand Down Expand Up @@ -864,6 +1003,65 @@ func TestSharedLinkCreateWithAllowDownloadUpdatesExistingLink(t *testing.T) {
}
}

func TestSharedLinkCreateWithDisallowDownloadUpdatesExistingLink(t *testing.T) {
existing := sharedLinkFile("/file.txt", "https://example.com/file-old")
var rawURL string
mock := &mockSharedLinkClient{
createSharedLinkWithSettingsFn: func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) {
t.Fatal("CreateSharedLinkWithSettings should not be called for disallow-download")
return nil, nil
},
createSharedLinkWithRawSettingsFn: func(path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) {
if path != "/file.txt" {
t.Fatalf("path = %q, want /file.txt", path)
}
if settings == nil || settings.AllowDownload == nil {
t.Fatalf("create settings = %#v, want allow_download setting", settings)
}
if *settings.AllowDownload {
t.Fatal("create allow_download = true, want false")
}
return nil, alreadyExistsError(existing)
},
modifySharedLinkSettingsFn: func(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) {
t.Fatal("ModifySharedLinkSettings should not be called for disallow-download only")
return nil, nil
},
modifySharedLinkSettingsRawFn: func(url string, settings *rawSharedLinkSettings, removeExpiration bool) error {
rawURL = url
if removeExpiration {
t.Fatal("remove expiration = true, want false")
}
if settings == nil || settings.AllowDownload == nil {
t.Fatalf("settings = %#v, want allow_download setting", settings)
}
if *settings.AllowDownload {
t.Fatal("allow_download = true, want false")
}
return nil
},
}
stubSharedLinkClient(t, mock)

var stdout bytes.Buffer
cmd := &cobra.Command{}
cmd.Flags().Bool("disallow-download", false, "")
cmd.SetOut(&stdout)
if err := cmd.Flags().Set("disallow-download", "true"); err != nil {
t.Fatalf("set disallow-download: %v", err)
}

if err := shareLinkCreate(cmd, []string{"/file.txt"}); err != nil {
t.Fatalf("shareLinkCreate error: %v", err)
}
if rawURL != "https://example.com/file-old" {
t.Fatalf("raw modify URL = %q, want existing URL", rawURL)
}
if got := stdout.String(); got != "https://example.com/file-old\n" {
t.Fatalf("stdout = %q, want existing URL", got)
}
}

func TestSharedLinkCreateWithPasswordUpdatesExistingLink(t *testing.T) {
existing := sharedLinkFile("/file.txt", "https://example.com/file-old")
mock := &mockSharedLinkClient{
Expand Down Expand Up @@ -1029,6 +1227,9 @@ func TestShareLinkCreateDoesNotBreakShareListLinkCommand(t *testing.T) {
if shareLinkCreateCmd.Flags().Lookup("allow-download") == nil {
t.Fatal("share-link create should define --allow-download")
}
if shareLinkCreateCmd.Flags().Lookup("disallow-download") == nil {
t.Fatal("share-link create should define --disallow-download")
}
if shareLinkCreateCmd.Flags().Lookup("expires") == nil {
t.Fatal("share-link create should define --expires")
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/share_link.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ import (

type sharedLinkClient interface {
CreateSharedLinkWithSettings(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error)
CreateSharedLinkWithRawSettings(path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error)
GetSharedLinkFile(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error)
GetSharedLinkMetadata(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error)
ListSharedLinks(arg *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error)
ModifySharedLinkSettings(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error)
RemoveSharedLinkPassword(url string) error
RevokeSharedLink(arg *sharing.RevokeSharedLinkArg) error
ModifySharedLinkSettingsRaw(url string, settings *rawSharedLinkSettings, removeExpiration bool) error
}

type sdkSharedLinkClient struct {
Expand Down
Loading