From b4f2c6f7ae4ea673af3aac3279fed754db09b7cb Mon Sep 17 00:00:00 2001 From: Andrey Markelov Date: Sun, 21 Jun 2026 10:13:22 -0700 Subject: [PATCH] Add --access flag to share-link create --- README.md | 3 ++ cmd/share_create_link_test.go | 94 +++++++++++++++++++++++++++++++++++ cmd/share_link_create.go | 44 +++++++++++++++- 3 files changed, 140 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ad67bdc..ddd0180 100644 --- a/README.md +++ b/README.md @@ -229,6 +229,7 @@ All `--sort`, `--reverse`, `--time`, and `--time-format` flags work with both `l ```sh $ 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 --allow-download # create a downloadable shared link $ dbxcli share-link create /file.txt --expires 2026-07-01T00:00:00Z # create an expiring shared link $ dbxcli share-link create /file.txt --remove-expiration # remove expiration when returning an existing link @@ -242,6 +243,8 @@ $ 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 download` writes to the metadata filename when `target` is omitted. Use `-` as the target to write file bytes to stdout, and `--password` for password-protected shared links. New and changed commands should write command results to stdout. Status, progress, warnings, diagnostics, and verbose logs should go to stderr. diff --git a/cmd/share_create_link_test.go b/cmd/share_create_link_test.go index f33bb35..bf980a8 100644 --- a/cmd/share_create_link_test.go +++ b/cmd/share_create_link_test.go @@ -228,6 +228,40 @@ func TestSharedLinkCreateWithAllowDownloadSetsAllowDownload(t *testing.T) { } } +func TestSharedLinkCreateWithAccessSetsAccess(t *testing.T) { + mock := &mockSharedLinkClient{ + createSharedLinkWithSettingsFn: func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) { + if arg.Settings == nil { + t.Fatal("settings = nil, want access settings") + } + if arg.Settings.Access == nil { + t.Fatal("access = nil, want editor") + } + if arg.Settings.Access.Tag != sharing.RequestedLinkAccessLevelEditor { + t.Fatalf("access = %q, want editor", arg.Settings.Access.Tag) + } + return sharedLinkFile("/file.txt", "https://example.com/file"), nil + }, + } + stubSharedLinkClient(t, mock) + + var stdout bytes.Buffer + cmd := &cobra.Command{} + cmd.Flags().String("access", "", "") + cmd.SetOut(&stdout) + if err := cmd.Flags().Set("access", "editor"); err != nil { + t.Fatalf("set access: %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 TestSharedLinkCreateWithInvalidExpiresReturnsError(t *testing.T) { called := false stubSharedLinkClient(t, &mockSharedLinkClient{ @@ -252,6 +286,30 @@ func TestSharedLinkCreateWithInvalidExpiresReturnsError(t *testing.T) { } } +func TestSharedLinkCreateWithInvalidAccessReturnsError(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().String("access", "", "") + if err := cmd.Flags().Set("access", "owner"); err != nil { + t.Fatalf("set access: %v", err) + } + + err := shareLinkCreate(cmd, []string{"/file.txt"}) + if err == nil || !strings.Contains(err.Error(), `invalid --access "owner": use viewer, editor, or max`) { + t.Fatalf("error = %v, want invalid access error", err) + } + if called { + t.Fatal("CreateSharedLinkWithSettings should not be called") + } +} + func TestSharedLinkCreateRejectsExpiresWithRemoveExpiration(t *testing.T) { called := false stubSharedLinkClient(t, &mockSharedLinkClient{ @@ -432,6 +490,39 @@ func TestSharedLinkCreateVerboseReportsExistingLinkOnStderr(t *testing.T) { } } +func TestSharedLinkCreateWithAccessErrorsForExistingLink(t *testing.T) { + existing := sharedLinkFile("/file.txt", "https://example.com/file-old") + mock := &mockSharedLinkClient{ + createSharedLinkWithSettingsFn: func(arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) { + if arg.Settings == nil || arg.Settings.Access == nil || arg.Settings.Access.Tag != sharing.RequestedLinkAccessLevelMax { + t.Fatalf("create settings = %#v, want max access", arg.Settings) + } + return nil, alreadyExistsError(existing) + }, + modifySharedLinkSettingsFn: func(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) { + t.Fatal("ModifySharedLinkSettings should not be called because access cannot be modified") + return nil, nil + }, + } + stubSharedLinkClient(t, mock) + + var stdout bytes.Buffer + cmd := &cobra.Command{} + cmd.Flags().String("access", "", "") + cmd.SetOut(&stdout) + if err := cmd.Flags().Set("access", "max"); err != nil { + t.Fatalf("set access: %v", err) + } + + err := shareLinkCreate(cmd, []string{"/file.txt"}) + if err == nil || !strings.Contains(err.Error(), "cannot apply `--access` because the shared link already exists") { + t.Fatalf("error = %v, want existing link access error", err) + } + if got := stdout.String(); got != "" { + t.Fatalf("stdout = %q, want empty output on error", got) + } +} + func TestSharedLinkCreateWithExpiresUpdatesExistingLink(t *testing.T) { wantExpires := time.Date(2026, 7, 1, 0, 0, 0, 0, time.UTC) existing := sharedLinkFile("/file.txt", "https://example.com/file-old") @@ -672,6 +763,9 @@ func TestShareLinkCreateDoesNotBreakShareListLinkCommand(t *testing.T) { if cmd != shareLinkCreateCmd { t.Fatalf("share-link create resolved to %q", cmd.CommandPath()) } + if shareLinkCreateCmd.Flags().Lookup("access") == nil { + t.Fatal("share-link create should define --access") + } if shareLinkCreateCmd.Flags().Lookup("allow-download") == nil { t.Fatal("share-link create should define --allow-download") } diff --git a/cmd/share_link_create.go b/cmd/share_link_create.go index 5f94aec..8a82ad6 100644 --- a/cmd/share_link_create.go +++ b/cmd/share_link_create.go @@ -21,6 +21,7 @@ import ( "strings" "time" + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/sharing" "github.com/spf13/cobra" ) @@ -29,6 +30,7 @@ type shareLinkCreateOptions struct { expires *time.Time removeExpiration bool allowDownload bool + access *sharing.RequestedLinkAccessLevel } func shareLinkCreate(cmd *cobra.Command, args []string) error { @@ -51,7 +53,7 @@ func shareLinkCreate(cmd *cobra.Command, args []string) error { dbx := newSharedLinkClient(config) arg := sharing.NewCreateSharedLinkWithSettingsArg(path) - if opts.expires != nil || opts.allowDownload { + if opts.hasCreateSettings() { arg.Settings = sharing.NewSharedLinkSettings() applySharedLinkCreateSettings(arg.Settings, opts) } @@ -114,6 +116,14 @@ func parseShareLinkCreateOptions(cmd *cobra.Command) (shareLinkCreateOptions, er opts.allowDownload = allowDownload } + if cmd.Flags().Changed("access") { + access, err := shareLinkAccessFlag(cmd) + if err != nil { + return opts, err + } + opts.access = access + } + if opts.expires != nil && opts.removeExpiration { return opts, errors.New("`--expires` and `--remove-expiration` cannot be used together") } @@ -122,6 +132,9 @@ func parseShareLinkCreateOptions(cmd *cobra.Command) (shareLinkCreateOptions, er } func applyExistingSharedLinkCreateOptions(dbx sharedLinkClient, link sharing.IsSharedLinkMetadata, opts shareLinkCreateOptions) (sharing.IsSharedLinkMetadata, error) { + if opts.access != nil { + return nil, errors.New("cannot apply `--access` because the shared link already exists") + } if opts.expires == nil && !opts.removeExpiration && !opts.allowDownload { return link, nil } @@ -140,6 +153,10 @@ func applyExistingSharedLinkCreateOptions(dbx sharedLinkClient, link sharing.IsS return dbx.ModifySharedLinkSettings(arg) } +func (opts shareLinkCreateOptions) hasCreateSettings() bool { + return opts.expires != nil || opts.allowDownload || opts.access != nil +} + func applySharedLinkCreateSettings(settings *sharing.SharedLinkSettings, opts shareLinkCreateOptions) { if opts.expires != nil { settings.Expires = opts.expires @@ -147,6 +164,9 @@ func applySharedLinkCreateSettings(settings *sharing.SharedLinkSettings, opts sh if opts.allowDownload { settings.AllowDownload = true } + if opts.access != nil { + settings.Access = opts.access + } } func existingSharedLink(dbx sharedLinkClient, path string, err error) (sharing.IsSharedLinkMetadata, error) { @@ -273,6 +293,27 @@ func shareLinkExpiresFlag(cmd *cobra.Command) (*time.Time, error) { return &parsed, nil } +func shareLinkAccessFlag(cmd *cobra.Command) (*sharing.RequestedLinkAccessLevel, error) { + value, err := cmd.Flags().GetString("access") + if err != nil { + return nil, err + } + switch value { + case sharing.RequestedLinkAccessLevelViewer: + return requestedLinkAccessLevel(sharing.RequestedLinkAccessLevelViewer), nil + case sharing.RequestedLinkAccessLevelEditor: + return requestedLinkAccessLevel(sharing.RequestedLinkAccessLevelEditor), nil + case sharing.RequestedLinkAccessLevelMax: + return requestedLinkAccessLevel(sharing.RequestedLinkAccessLevelMax), nil + default: + return nil, fmt.Errorf("invalid --access %q: use viewer, editor, or max", value) + } +} + +func requestedLinkAccessLevel(tag string) *sharing.RequestedLinkAccessLevel { + return &sharing.RequestedLinkAccessLevel{Tagged: dropbox.Tagged{Tag: tag}} +} + var shareLinkCreateCmd = &cobra.Command{ Use: "create ", Short: "Create a shared link", @@ -280,6 +321,7 @@ var shareLinkCreateCmd = &cobra.Command{ } func init() { + shareLinkCreateCmd.Flags().String("access", "", "Set shared link access level: viewer, editor, or max") shareLinkCreateCmd.Flags().Bool("allow-download", false, "Allow downloads from the shared link") 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")