diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index eb34737e..c94acf19 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -6,6 +6,9 @@ on: paths: - README.md +permissions: + contents: read + jobs: deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index af0f8d0c..2be05709 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - "v*" +permissions: + contents: read + jobs: release: runs-on: ubuntu-latest diff --git a/cmd/account.go b/cmd/account.go index 3af160c6..d9da5d70 100644 --- a/cmd/account.go +++ b/cmd/account.go @@ -149,7 +149,7 @@ func account(cmd *cobra.Command, args []string) error { if len(args) == 0 { // If no arguments are provided get the current user's account - res, err := dbx.GetCurrentAccount() + res, err := dbx.GetCurrentAccountContext(currentContext()) if err != nil { return err } @@ -161,7 +161,7 @@ func account(cmd *cobra.Command, args []string) error { // Otherwise look up an account with the provided ID arg := users.NewGetAccountArg(args[0]) - res, err := dbx.GetAccount(arg) + res, err := dbx.GetAccountContext(currentContext(), arg) if err != nil { return err } diff --git a/cmd/add-member.go b/cmd/add-member.go index 6269bd1e..91c97d74 100644 --- a/cmd/add-member.go +++ b/cmd/add-member.go @@ -35,7 +35,7 @@ func addMember(cmd *cobra.Command, args []string) (err error) { member.MemberGivenName = firstName member.MemberSurname = lastName arg := team.NewMembersAddArg([]*team.MemberAddArg{member}) - res, err := dbx.MembersAdd(arg) + res, err := dbx.MembersAddContext(currentContext(), arg) if err != nil { return withJSONErrorDetails(err, operationErrorDetails("team_add_member"), emailErrorDetails(email)) } diff --git a/cmd/auth.go b/cmd/auth.go index 07526e7d..05ef5244 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -377,7 +377,7 @@ func requestAccessCredential(tokType string, domain string) (storedCredential, e if err != nil { return storedCredential{}, err } - token, err := exchangeAuthorizationCode(context.Background(), conf, code, verifier) + token, err := exchangeAuthorizationCode(currentContext(), conf, code, verifier) if err != nil { return storedCredential{}, authExchangeFailedErrorfWithDetails("exchange authorization code: %w", map[string]any{ "token_type": authTokenTypeName(tokType), @@ -407,7 +407,7 @@ func refreshStoredCredential(tokType string, domain string, credential storedCre }) } - token, err := refreshOAuthToken(context.Background(), oauthConfigWithAppKey(appKey, domain), credential.oauthToken()) + token, err := refreshOAuthToken(currentContext(), oauthConfigWithAppKey(appKey, domain), credential.oauthToken()) if err != nil { return storedCredential{}, err } diff --git a/cmd/cp.go b/cmd/cp.go index 2c62555c..294d4c5b 100644 --- a/cmd/cp.go +++ b/cmd/cp.go @@ -76,7 +76,7 @@ func cp(cmd *cobra.Command, args []string) error { } for _, arg := range relocationArgs { - res, err := dbx.CopyV2(arg) + res, err := dbx.CopyV2Context(currentContext(), arg) if err != nil { if result, skipped := relocationSkipAfterDestinationConflict(dbx, arg, err, opts); skipped { if collectResults { diff --git a/cmd/cp_test.go b/cmd/cp_test.go index 9183d418..ff8aa4c6 100644 --- a/cmd/cp_test.go +++ b/cmd/cp_test.go @@ -17,11 +17,11 @@ import ( "github.com/spf13/cobra" ) -func stubFilesClient(t *testing.T, client files.Client) { +func stubFilesClient(t *testing.T, client filesClient) { t.Helper() origNew := filesNewFunc - filesNewFunc = func(_ dropbox.Config) files.Client { return client } + filesNewFunc = func(_ dropbox.Config) filesClient { return client } t.Cleanup(func() { filesNewFunc = origNew }) } diff --git a/cmd/dropbox_path.go b/cmd/dropbox_path.go index 5a7a2a40..8d86cdc1 100644 --- a/cmd/dropbox_path.go +++ b/cmd/dropbox_path.go @@ -37,12 +37,12 @@ func relocationDestination(source, destination string, destinationIsFolder bool) return destination } -func isRemoteFolder(dbx files.Client, dst string) bool { +func isRemoteFolder(dbx filesClient, dst string) bool { p, err := validatePath(dst) if err != nil { return false } - meta, err := dbx.GetMetadata(files.NewGetMetadataArg(p)) + meta, err := dbx.GetMetadataContext(currentContext(), files.NewGetMetadataArg(p)) if err != nil { return false } diff --git a/cmd/du.go b/cmd/du.go index dcc5f117..7d72f21b 100644 --- a/cmd/du.go +++ b/cmd/du.go @@ -46,7 +46,7 @@ const ( func du(cmd *cobra.Command, args []string) (err error) { dbx := usersNewFunc(config) - usage, err := dbx.GetSpaceUsage() + usage, err := dbx.GetSpaceUsageContext(currentContext()) if err != nil { return } diff --git a/cmd/files_client.go b/cmd/files_client.go new file mode 100644 index 00000000..2979d314 --- /dev/null +++ b/cmd/files_client.go @@ -0,0 +1,33 @@ +package cmd + +import ( + "context" + "io" + + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox" + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files" +) + +type filesClient interface { + CopyV2Context(context.Context, *files.RelocationArg) (*files.RelocationResult, error) + CreateFolderV2Context(context.Context, *files.CreateFolderArg) (*files.CreateFolderResult, error) + DeleteV2Context(context.Context, *files.DeleteArg) (*files.DeleteResult, error) + DownloadContext(context.Context, *files.DownloadArg) (*files.FileMetadata, io.ReadCloser, error) + GetMetadataContext(context.Context, *files.GetMetadataArg) (files.IsMetadata, error) + ListFolderContext(context.Context, *files.ListFolderArg) (*files.ListFolderResult, error) + ListFolderContinueContext(context.Context, *files.ListFolderContinueArg) (*files.ListFolderResult, error) + ListRevisionsContext(context.Context, *files.ListRevisionsArg) (*files.ListRevisionsResult, error) + MoveV2Context(context.Context, *files.RelocationArg) (*files.RelocationResult, error) + PermanentlyDeleteContext(context.Context, *files.DeleteArg) error + RestoreContext(context.Context, *files.RestoreArg) (*files.FileMetadata, error) + SearchV2Context(context.Context, *files.SearchV2Arg) (*files.SearchV2Result, error) + SearchContinueV2Context(context.Context, *files.SearchV2ContinueArg) (*files.SearchV2Result, error) + UploadContext(context.Context, *files.UploadArg, io.Reader) (*files.FileMetadata, error) + UploadSessionAppendV2Context(context.Context, *files.UploadSessionAppendArg, io.Reader) error + UploadSessionFinishContext(context.Context, *files.UploadSessionFinishArg, io.Reader) (*files.FileMetadata, error) + UploadSessionStartContext(context.Context, *files.UploadSessionStartArg, io.Reader) (*files.UploadSessionStartResult, error) +} + +var filesNewFunc = func(cfg dropbox.Config) filesClient { + return files.NewContext(cfg) +} diff --git a/cmd/get.go b/cmd/get.go index f0dd5b92..68121a2a 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -90,7 +90,7 @@ func get(cmd *cobra.Command, args []string) (err error) { dbx := filesNewFunc(config) - meta, err := dbx.GetMetadata(files.NewGetMetadataArg(src)) + meta, err := dbx.GetMetadataContext(currentContext(), files.NewGetMetadataArg(src)) if err != nil { if recursive { return withJSONErrorDetails(fmt.Errorf("get metadata for %s: %v", src, err), operationErrorDetails("download"), pathErrorDetails(src)) @@ -204,7 +204,7 @@ func getStdout(cmd *cobra.Command, src string, recursive bool) error { dbx := filesNewFunc(config) - meta, err := dbx.GetMetadata(files.NewGetMetadataArg(src)) + meta, err := dbx.GetMetadataContext(currentContext(), files.NewGetMetadataArg(src)) if err == nil { if _, ok := meta.(*files.FolderMetadata); ok { return invalidArgumentsErrorfWithDetails("%s is a folder; cannot download folder to stdout", mergeJSONErrorDetails(operationErrorDetails("download"), pathErrorDetails(src)), src) @@ -214,7 +214,7 @@ func getStdout(cmd *cobra.Command, src string, recursive bool) error { return withJSONErrorDetails(downloadToStdout(dbx, src, cmd.OutOrStdout()), operationErrorDetails("download"), pathErrorDetails(src)) } -func getWithClient(dbx files.Client, args []string) (err error) { +func getWithClient(dbx filesClient, args []string) (err error) { if len(args) == 0 || len(args) > 2 { return invalidArgumentsErrorWithDetails("`get` requires `src` and/or `dst` arguments", argumentsErrorDetails("src", "dst")) } @@ -235,20 +235,20 @@ func getWithClient(dbx files.Client, args []string) (err error) { return withJSONErrorDetails(downloadFile(dbx, src, dst), operationErrorDetails("download"), pathErrorDetails(src), relocationErrorDetails(src, dst)) } -func getRecursive(dbx files.Client, src, dst string) error { +func getRecursive(dbx filesClient, src, dst string) error { _, err := getRecursiveInternal(dbx, src, dst, nil, getOptions{}, false) return err } -func getRecursiveWithResults(dbx files.Client, src, dst string, rootMeta files.IsMetadata, opts getOptions) ([]getResult, error) { +func getRecursiveWithResults(dbx filesClient, src, dst string, rootMeta files.IsMetadata, opts getOptions) ([]getResult, error) { return getRecursiveInternal(dbx, src, dst, rootMeta, opts, true) } -func getRecursiveInternal(dbx files.Client, src, dst string, rootMeta files.IsMetadata, opts getOptions, collectResults bool) ([]getResult, error) { +func getRecursiveInternal(dbx filesClient, src, dst string, rootMeta files.IsMetadata, opts getOptions, collectResults bool) ([]getResult, error) { arg := files.NewListFolderArg(src) arg.Recursive = true - res, err := dbx.ListFolder(arg) + res, err := dbx.ListFolderContext(currentContext(), arg) if err != nil { return nil, withJSONErrorDetails(fmt.Errorf("list folder %s: %v", src, err), operationErrorDetails("download"), pathErrorDetails(src)) } @@ -257,7 +257,7 @@ func getRecursiveInternal(dbx files.Client, src, dst string, rootMeta files.IsMe entries = append(entries, res.Entries...) for res.HasMore { cont := files.NewListFolderContinueArg(res.Cursor) - res, err = dbx.ListFolderContinue(cont) + res, err = dbx.ListFolderContinueContext(currentContext(), cont) if err != nil { return nil, withJSONErrorDetails(fmt.Errorf("list folder continue: %v", err), operationErrorDetails("download"), pathErrorDetails(src)) } @@ -369,12 +369,12 @@ func relativeTo(base, full string) (string, error) { return rel, nil } -func downloadFile(dbx files.Client, src string, dst string) error { +func downloadFile(dbx filesClient, src string, dst string) error { _, err := downloadFileWithMetadata(dbx, src, dst, os.Stderr) return err } -func downloadFileWithResult(dbx files.Client, src string, dst string, opts getOptions) (getResult, error) { +func downloadFileWithResult(dbx filesClient, src string, dst string, opts getOptions) (getResult, error) { metadata, err := downloadFileWithMetadata(dbx, src, dst, getErrorOutput(opts)) if err != nil { return getResult{}, err @@ -382,7 +382,7 @@ func downloadFileWithResult(dbx files.Client, src string, dst string, opts getOp return newGetResult(getStatusDownloaded, getKindFile, src, dst, metadata) } -func downloadFileWithMetadata(dbx files.Client, src string, dst string, errOut io.Writer) (*files.FileMetadata, error) { +func downloadFileWithMetadata(dbx filesClient, src string, dst string, errOut io.Writer) (*files.FileMetadata, error) { arg := files.NewDownloadArg(src) var metadata *files.FileMetadata @@ -434,8 +434,8 @@ func downloadDestinationPath(dst string) (string, error) { return "", fmt.Errorf("too many symlinks resolving %s", dst) } -func downloadFileOnce(dbx files.Client, arg *files.DownloadArg, dst string, errOut io.Writer) (*files.FileMetadata, error) { - res, contents, err := dbx.Download(arg) +func downloadFileOnce(dbx filesClient, arg *files.DownloadArg, dst string, errOut io.Writer) (*files.FileMetadata, error) { + res, contents, err := dbx.DownloadContext(currentContext(), arg) if err != nil { return nil, err } diff --git a/cmd/info.go b/cmd/info.go index db34797c..9424ca0f 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -25,7 +25,7 @@ import ( func info(cmd *cobra.Command, args []string) (err error) { dbx := teamNewFunc(config) - res, err := dbx.GetInfo() + res, err := dbx.GetInfoContext(currentContext()) if err != nil { return err } diff --git a/cmd/json_contract_test.go b/cmd/json_contract_test.go index 182c2a5d..c009bf94 100644 --- a/cmd/json_contract_test.go +++ b/cmd/json_contract_test.go @@ -181,10 +181,10 @@ func TestAccountAuthContractOmitsSensitiveFields(t *testing.T) { t.Fatal(err) } - output := string(encoded) + payload := string(encoded) for _, forbidden := range []string{"access_token", "refresh_token", "app_key", "auth.json", ".config"} { - if strings.Contains(output, forbidden) { - t.Fatalf("account JSON contains %q: %s", forbidden, output) + if strings.Contains(payload, forbidden) { + t.Fatalf("account JSON contains %q: %s", forbidden, payload) } } } diff --git a/cmd/list-groups.go b/cmd/list-groups.go index 31ce0a60..2daf833c 100644 --- a/cmd/list-groups.go +++ b/cmd/list-groups.go @@ -42,7 +42,7 @@ func listGroups(cmd *cobra.Command, args []string) (err error) { func listTeamGroups(dbx teamClient, arg *team.GroupsListArg) ([]*team_common.GroupSummary, error) { var groups []*team_common.GroupSummary - res, err := dbx.GroupsList(arg) + res, err := dbx.GroupsListContext(currentContext(), arg) if err != nil { return nil, err } @@ -52,7 +52,7 @@ func listTeamGroups(dbx teamClient, arg *team.GroupsListArg) ([]*team_common.Gro if res.Cursor == "" { return nil, errors.New("team group list has more results but no cursor") } - res, err = dbx.GroupsListContinue(team.NewGroupsListContinueArg(res.Cursor)) + res, err = dbx.GroupsListContinueContext(currentContext(), team.NewGroupsListContinueArg(res.Cursor)) if err != nil { return nil, err } diff --git a/cmd/list-members.go b/cmd/list-members.go index 699ef759..c3621c5b 100644 --- a/cmd/list-members.go +++ b/cmd/list-members.go @@ -41,7 +41,7 @@ func listMembers(cmd *cobra.Command, args []string) (err error) { func listTeamMembers(dbx teamClient, arg *team.MembersListArg) ([]*team.TeamMemberInfo, error) { var members []*team.TeamMemberInfo - res, err := dbx.MembersList(arg) + res, err := dbx.MembersListContext(currentContext(), arg) if err != nil { return nil, err } @@ -51,7 +51,7 @@ func listTeamMembers(dbx teamClient, arg *team.MembersListArg) ([]*team.TeamMemb if res.Cursor == "" { return nil, errors.New("team member list has more results but no cursor") } - res, err = dbx.MembersListContinue(team.NewMembersListContinueArg(res.Cursor)) + res, err = dbx.MembersListContinueContext(currentContext(), team.NewMembersListContinueArg(res.Cursor)) if err != nil { return nil, err } diff --git a/cmd/login.go b/cmd/login.go index 62b1025a..5f6f7f9f 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -78,6 +78,9 @@ By default, login stores credentials for regular Dropbox user commands. Use "team-access" for --as-member commands or "team-manage" for team commands.`, Args: cobra.MaximumNArgs(1), PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := initCommandContext(cmd); err != nil { + return err + } return validateOutputFormat(cmd) }, RunE: login, diff --git a/cmd/logout.go b/cmd/logout.go index 3ae145cf..6ed4bd69 100644 --- a/cmd/logout.go +++ b/cmd/logout.go @@ -47,8 +47,8 @@ var revokeAccessToken = func(domain string, token string) error { HeaderGenerator: nil, URLGenerator: nil, } - client := auth.New(cfg) - return client.TokenRevoke() + client := auth.NewContext(cfg) + return client.TokenRevokeContext(currentContext()) } // Command logout revokes all saved API tokens and deletes auth.json. @@ -141,6 +141,9 @@ credentials. If DBXCLI_ACCESS_TOKEN is set, unset it before running logout; environment-provided tokens are not saved locally and cannot be removed by dbxcli.`, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if err := initCommandContext(cmd); err != nil { + return err + } return validateOutputFormat(cmd) }, RunE: logout, diff --git a/cmd/ls.go b/cmd/ls.go index 395e7943..26b88543 100644 --- a/cmd/ls.go +++ b/cmd/ls.go @@ -46,12 +46,12 @@ type lsInput struct { const lsJSONStatusListed = "listed" // Sends a get_metadata request for a given path and returns the response -func getFileMetadata(c files.Client, path string) (files.IsMetadata, error) { +func getFileMetadata(c filesClient, path string) (files.IsMetadata, error) { arg := files.NewGetMetadataArg(path) arg.IncludeDeleted = true - res, err := c.GetMetadata(arg) + res, err := c.GetMetadataContext(currentContext(), arg) if err != nil { return nil, err } @@ -148,7 +148,7 @@ func ls(cmd *cobra.Command, args []string) (err error) { } } - res, err := dbx.ListFolder(arg) + res, err := dbx.ListFolderContext(currentContext(), arg) if err != nil { if !isListFolderNotFolderError(err) { @@ -184,12 +184,12 @@ func metadataLimitReached(entries []files.IsMetadata, limit uint64) bool { return limit > 0 && uint64(len(entries)) >= limit } -func finalizeLsEntries(dbx files.Client, entries []files.IsMetadata, onlyDeleted bool, opts listOptions) ([]files.IsMetadata, error) { +func finalizeLsEntries(dbx filesClient, entries []files.IsMetadata, onlyDeleted bool, opts listOptions) ([]files.IsMetadata, error) { sortEntries(entries, opts) return prepareLsEntries(dbx, entries, onlyDeleted) } -func collectAndPrepareLsEntries(dbx files.Client, res *files.ListFolderResult, limit uint64, onlyDeleted bool, opts listOptions) ([]files.IsMetadata, error) { +func collectAndPrepareLsEntries(dbx filesClient, res *files.ListFolderResult, limit uint64, onlyDeleted bool, opts listOptions) ([]files.IsMetadata, error) { if onlyDeleted && limit > 0 { entries, err := collectOnlyDeletedEntriesWithLimit(dbx, res, limit) if err != nil { @@ -203,7 +203,7 @@ func collectAndPrepareLsEntries(dbx files.Client, res *files.ListFolderResult, l entries = appendMetadataWithLimit(entries, res.Entries, limit) for res.HasMore && !metadataLimitReached(entries, limit) { var err error - res, err = dbx.ListFolderContinue(files.NewListFolderContinueArg(res.Cursor)) + res, err = dbx.ListFolderContinueContext(currentContext(), files.NewListFolderContinueArg(res.Cursor)) if err != nil { return nil, err } @@ -213,7 +213,7 @@ func collectAndPrepareLsEntries(dbx files.Client, res *files.ListFolderResult, l return finalizeLsEntries(dbx, entries, onlyDeleted, opts) } -func collectOnlyDeletedEntriesWithLimit(dbx files.Client, res *files.ListFolderResult, limit uint64) ([]files.IsMetadata, error) { +func collectOnlyDeletedEntriesWithLimit(dbx filesClient, res *files.ListFolderResult, limit uint64) ([]files.IsMetadata, error) { var entries []files.IsMetadata for { filtered, err := prepareLsEntries(dbx, res.Entries, true) @@ -225,7 +225,7 @@ func collectOnlyDeletedEntriesWithLimit(dbx files.Client, res *files.ListFolderR return entries, nil } - res, err = dbx.ListFolderContinue(files.NewListFolderContinueArg(res.Cursor)) + res, err = dbx.ListFolderContinueContext(currentContext(), files.NewListFolderContinueArg(res.Cursor)) if err != nil { return nil, err } @@ -238,13 +238,13 @@ func lsRecursive(cmd *cobra.Command) bool { return recursive || recurse } -func prepareLsEntries(dbx files.Client, entries []files.IsMetadata, onlyDeleted bool) ([]files.IsMetadata, error) { +func prepareLsEntries(dbx filesClient, entries []files.IsMetadata, onlyDeleted bool) ([]files.IsMetadata, error) { var filtered []files.IsMetadata for _, entry := range entries { deletedItem, isDeleted := entry.(*files.DeletedMetadata) if isDeleted { revisionArg := files.NewListRevisionsArg(deletedItem.PathLower) - res, err := dbx.ListRevisions(revisionArg) + res, err := dbx.ListRevisionsContext(currentContext(), revisionArg) if err != nil { if isListRevisionsNotFileError(err) { // Don't treat a "not_file" error as fatal; recover by sending a diff --git a/cmd/mkdir.go b/cmd/mkdir.go index 3466b118..90773080 100644 --- a/cmd/mkdir.go +++ b/cmd/mkdir.go @@ -56,7 +56,7 @@ func mkdir(cmd *cobra.Command, args []string) (err error) { parents, _ := cmd.Flags().GetBool("parents") dbx := filesNewFunc(config) - created, err := dbx.CreateFolderV2(arg) + created, err := dbx.CreateFolderV2Context(currentContext(), arg) var metadata *files.FolderMetadata status := mkdirStatusCreated if err != nil { @@ -105,8 +105,8 @@ func mkdir(cmd *cobra.Command, args []string) (err error) { return renderJSONOperationOutput(cmd, result.Input, []jsonOperationResult{mkdirOperationResult(result)}) } -func existingFolderMetadata(dbx files.Client, dst string) (*files.FolderMetadata, error) { - metadata, err := dbx.GetMetadata(files.NewGetMetadataArg(dst)) +func existingFolderMetadata(dbx filesClient, dst string) (*files.FolderMetadata, error) { + metadata, err := dbx.GetMetadataContext(currentContext(), files.NewGetMetadataArg(dst)) if err != nil { return nil, err } diff --git a/cmd/mock_test.go b/cmd/mock_test.go index b07f622f..96dea74e 100644 --- a/cmd/mock_test.go +++ b/cmd/mock_test.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "io" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/async" @@ -35,6 +36,10 @@ func (m *mockFilesClient) Download(arg *files.DownloadArg) (*files.FileMetadata, return nil, nil, nil } +func (m *mockFilesClient) DownloadContext(ctx context.Context, arg *files.DownloadArg) (*files.FileMetadata, io.ReadCloser, error) { + return m.Download(arg) +} + func (m *mockFilesClient) Upload(arg *files.UploadArg, content io.Reader) (*files.FileMetadata, error) { if m.uploadFn != nil { return m.uploadFn(arg, content) @@ -42,6 +47,10 @@ func (m *mockFilesClient) Upload(arg *files.UploadArg, content io.Reader) (*file return nil, nil } +func (m *mockFilesClient) UploadContext(ctx context.Context, arg *files.UploadArg, content io.Reader) (*files.FileMetadata, error) { + return m.Upload(arg, content) +} + func (m *mockFilesClient) UploadSessionStart(arg *files.UploadSessionStartArg, content io.Reader) (*files.UploadSessionStartResult, error) { if m.uploadSessionStartFn != nil { return m.uploadSessionStartFn(arg, content) @@ -49,6 +58,10 @@ func (m *mockFilesClient) UploadSessionStart(arg *files.UploadSessionStartArg, c return nil, nil } +func (m *mockFilesClient) UploadSessionStartContext(ctx context.Context, arg *files.UploadSessionStartArg, content io.Reader) (*files.UploadSessionStartResult, error) { + return m.UploadSessionStart(arg, content) +} + func (m *mockFilesClient) UploadSessionAppendV2(arg *files.UploadSessionAppendArg, content io.Reader) error { if m.uploadSessionAppendV2Fn != nil { return m.uploadSessionAppendV2Fn(arg, content) @@ -56,6 +69,10 @@ func (m *mockFilesClient) UploadSessionAppendV2(arg *files.UploadSessionAppendAr return nil } +func (m *mockFilesClient) UploadSessionAppendV2Context(ctx context.Context, arg *files.UploadSessionAppendArg, content io.Reader) error { + return m.UploadSessionAppendV2(arg, content) +} + func (m *mockFilesClient) UploadSessionFinish(arg *files.UploadSessionFinishArg, content io.Reader) (*files.FileMetadata, error) { if m.uploadSessionFinishFn != nil { return m.uploadSessionFinishFn(arg, content) @@ -63,6 +80,10 @@ func (m *mockFilesClient) UploadSessionFinish(arg *files.UploadSessionFinishArg, return nil, nil } +func (m *mockFilesClient) UploadSessionFinishContext(ctx context.Context, arg *files.UploadSessionFinishArg, content io.Reader) (*files.FileMetadata, error) { + return m.UploadSessionFinish(arg, content) +} + // Stubs for the rest of the interface func (m *mockFilesClient) AlphaGetMetadata(arg *files.AlphaGetMetadataArg) (files.IsMetadata, error) { return nil, nil @@ -76,6 +97,10 @@ func (m *mockFilesClient) CopyV2(arg *files.RelocationArg) (*files.RelocationRes } return nil, nil } + +func (m *mockFilesClient) CopyV2Context(ctx context.Context, arg *files.RelocationArg) (*files.RelocationResult, error) { + return m.CopyV2(arg) +} func (m *mockFilesClient) Copy(arg *files.RelocationArg) (files.IsMetadata, error) { return nil, nil } @@ -103,6 +128,10 @@ func (m *mockFilesClient) CreateFolderV2(arg *files.CreateFolderArg) (*files.Cre } return nil, nil } + +func (m *mockFilesClient) CreateFolderV2Context(ctx context.Context, arg *files.CreateFolderArg) (*files.CreateFolderResult, error) { + return m.CreateFolderV2(arg) +} func (m *mockFilesClient) CreateFolder(arg *files.CreateFolderArg) (*files.FolderMetadata, error) { return nil, nil } @@ -118,6 +147,10 @@ func (m *mockFilesClient) DeleteV2(arg *files.DeleteArg) (*files.DeleteResult, e } return nil, nil } + +func (m *mockFilesClient) DeleteV2Context(ctx context.Context, arg *files.DeleteArg) (*files.DeleteResult, error) { + return m.DeleteV2(arg) +} func (m *mockFilesClient) Delete(arg *files.DeleteArg) (files.IsMetadata, error) { return nil, nil } func (m *mockFilesClient) DeleteBatch(arg *files.DeleteBatchArg) (*files.DeleteBatchLaunch, error) { return nil, nil @@ -140,6 +173,10 @@ func (m *mockFilesClient) GetMetadata(arg *files.GetMetadataArg) (files.IsMetada } return nil, nil } + +func (m *mockFilesClient) GetMetadataContext(ctx context.Context, arg *files.GetMetadataArg) (files.IsMetadata, error) { + return m.GetMetadata(arg) +} func (m *mockFilesClient) GetPreview(arg *files.PreviewArg) (*files.FileMetadata, io.ReadCloser, error) { return nil, nil, nil } @@ -164,12 +201,20 @@ func (m *mockFilesClient) ListFolder(arg *files.ListFolderArg) (*files.ListFolde } return nil, nil } + +func (m *mockFilesClient) ListFolderContext(ctx context.Context, arg *files.ListFolderArg) (*files.ListFolderResult, error) { + return m.ListFolder(arg) +} func (m *mockFilesClient) ListFolderContinue(arg *files.ListFolderContinueArg) (*files.ListFolderResult, error) { if m.listFolderContinueFn != nil { return m.listFolderContinueFn(arg) } return nil, nil } + +func (m *mockFilesClient) ListFolderContinueContext(ctx context.Context, arg *files.ListFolderContinueArg) (*files.ListFolderResult, error) { + return m.ListFolderContinue(arg) +} func (m *mockFilesClient) ListFolderGetLatestCursor(arg *files.ListFolderArg) (*files.ListFolderGetLatestCursorResult, error) { return nil, nil } @@ -182,6 +227,10 @@ func (m *mockFilesClient) ListRevisions(arg *files.ListRevisionsArg) (*files.Lis } return nil, nil } + +func (m *mockFilesClient) ListRevisionsContext(ctx context.Context, arg *files.ListRevisionsArg) (*files.ListRevisionsResult, error) { + return m.ListRevisions(arg) +} func (m *mockFilesClient) LockFileBatch(arg *files.LockFileBatchArg) (*files.LockFileBatchResult, error) { return nil, nil } @@ -191,6 +240,10 @@ func (m *mockFilesClient) MoveV2(arg *files.RelocationArg) (*files.RelocationRes } return nil, nil } + +func (m *mockFilesClient) MoveV2Context(ctx context.Context, arg *files.RelocationArg) (*files.RelocationResult, error) { + return m.MoveV2(arg) +} func (m *mockFilesClient) Move(arg *files.RelocationArg) (files.IsMetadata, error) { return nil, nil } @@ -218,6 +271,10 @@ func (m *mockFilesClient) PermanentlyDelete(arg *files.DeleteArg) error { } return nil } + +func (m *mockFilesClient) PermanentlyDeleteContext(ctx context.Context, arg *files.DeleteArg) error { + return m.PermanentlyDelete(arg) +} func (m *mockFilesClient) PropertiesAdd(arg *file_properties.AddPropertiesArg) error { return nil } @@ -242,6 +299,10 @@ func (m *mockFilesClient) Restore(arg *files.RestoreArg) (*files.FileMetadata, e } return nil, nil } + +func (m *mockFilesClient) RestoreContext(ctx context.Context, arg *files.RestoreArg) (*files.FileMetadata, error) { + return m.Restore(arg) +} func (m *mockFilesClient) SaveUrl(arg *files.SaveUrlArg) (*files.SaveUrlResult, error) { return nil, nil } @@ -257,12 +318,20 @@ func (m *mockFilesClient) SearchV2(arg *files.SearchV2Arg) (*files.SearchV2Resul } return nil, nil } + +func (m *mockFilesClient) SearchV2Context(ctx context.Context, arg *files.SearchV2Arg) (*files.SearchV2Result, error) { + return m.SearchV2(arg) +} func (m *mockFilesClient) SearchContinueV2(arg *files.SearchV2ContinueArg) (*files.SearchV2Result, error) { if m.searchContinueV2Fn != nil { return m.searchContinueV2Fn(arg) } return nil, nil } + +func (m *mockFilesClient) SearchContinueV2Context(ctx context.Context, arg *files.SearchV2ContinueArg) (*files.SearchV2Result, error) { + return m.SearchContinueV2(arg) +} func (m *mockFilesClient) TagsAdd(arg *files.AddTagArg) error { return nil } func (m *mockFilesClient) TagsGet(arg *files.GetTagsArg) (*files.GetTagsResult, error) { return nil, nil diff --git a/cmd/mv.go b/cmd/mv.go index 07cc7dd0..b9b68831 100644 --- a/cmd/mv.go +++ b/cmd/mv.go @@ -75,7 +75,7 @@ func mv(cmd *cobra.Command, args []string) error { } for _, arg := range relocationArgs { - res, err := dbx.MoveV2(arg) + res, err := dbx.MoveV2Context(currentContext(), arg) if err != nil { if result, skipped := relocationSkipAfterDestinationConflict(dbx, arg, err, opts); skipped { if collectResults { diff --git a/cmd/pipe_test.go b/cmd/pipe_test.go index 4f8696ae..99418804 100644 --- a/cmd/pipe_test.go +++ b/cmd/pipe_test.go @@ -382,7 +382,7 @@ func TestPutLocalDashFile(t *testing.T) { }, } origNew := filesNewFunc - filesNewFunc = func(_ dropbox.Config) files.Client { return testClient } + filesNewFunc = func(_ dropbox.Config) filesClient { return testClient } defer func() { filesNewFunc = origNew }() cmd := testPutCmd() diff --git a/cmd/put.go b/cmd/put.go index 49dfc783..27dcba57 100644 --- a/cmd/put.go +++ b/cmd/put.go @@ -48,19 +48,15 @@ const ( putIfExistsFail = "fail" ) -var filesNewFunc = func(cfg dropbox.Config) files.Client { - return files.New(cfg) -} - type uploadChunk struct { data []byte offset uint64 close bool } -func uploadOneChunk(dbx files.Client, args *files.UploadSessionAppendArg, data []byte) error { +func uploadOneChunk(dbx filesClient, args *files.UploadSessionAppendArg, data []byte) error { return retryWithBackoff(func() error { - err := dbx.UploadSessionAppendV2(args, bytes.NewReader(data)) + err := dbx.UploadSessionAppendV2Context(currentContext(), args, bytes.NewReader(data)) if uploadChunkAlreadyAccepted(err, args.Cursor.Offset+uint64(len(data))) { return nil } @@ -93,20 +89,20 @@ func uploadProgressReader(r io.Reader, size int64, errOut io.Writer) *ioprogress } } -func uploadSingleShot(dbx files.Client, r io.ReadSeeker, uploadArg *files.UploadArg, size int64, errOut io.Writer) (*files.FileMetadata, error) { +func uploadSingleShot(dbx filesClient, r io.ReadSeeker, uploadArg *files.UploadArg, size int64, errOut io.Writer) (*files.FileMetadata, error) { var metadata *files.FileMetadata err := retryWithBackoff(func() error { if _, err := r.Seek(0, io.SeekStart); err != nil { return err } var err error - metadata, err = dbx.Upload(uploadArg, uploadProgressReader(r, size, errOut)) + metadata, err = dbx.UploadContext(currentContext(), uploadArg, uploadProgressReader(r, size, errOut)) return err }) return metadata, err } -func uploadChunked(dbx files.Client, r io.Reader, commitInfo *files.CommitInfo, sizeTotal int64, workers int, chunkSize int64, debug bool) (metadata *files.FileMetadata, err error) { +func uploadChunked(dbx filesClient, r io.Reader, commitInfo *files.CommitInfo, sizeTotal int64, workers int, chunkSize int64, debug bool) (metadata *files.FileMetadata, err error) { t0 := time.Now() startArgs := files.NewUploadSessionStartArg() startArgs.SessionType = &files.UploadSessionType{} @@ -114,7 +110,7 @@ func uploadChunked(dbx files.Client, r io.Reader, commitInfo *files.CommitInfo, var res *files.UploadSessionStartResult err = retryWithBackoff(func() error { var e error - res, e = dbx.UploadSessionStart(startArgs, nil) + res, e = dbx.UploadSessionStartContext(currentContext(), startArgs, nil) return e }) if err != nil { @@ -193,7 +189,7 @@ func uploadChunked(dbx files.Client, r io.Reader, commitInfo *files.CommitInfo, finishArgs := files.NewUploadSessionFinishArg(cursor, commitInfo) err = retryWithBackoff(func() error { var e error - metadata, e = dbx.UploadSessionFinish(finishArgs, nil) + metadata, e = dbx.UploadSessionFinishContext(currentContext(), finishArgs, nil) return e }) if debug { @@ -431,11 +427,11 @@ func reportStdinCleanupFailure(opts putOptions, tmpPath string, err error) { putOutput(opts).Status("error: failed to remove temp file %s: %v; sensitive stdin data may remain on disk", tmpPath, err) } -func resolveDestination(dbx files.Client, src, dst string, dstIsDir bool) string { +func resolveDestination(dbx filesClient, src, dst string, dstIsDir bool) string { if dstIsDir { return path.Join("/", dst, filepath.Base(src)) } - meta, err := dbx.GetMetadata(files.NewGetMetadataArg(dst)) + meta, err := dbx.GetMetadataContext(currentContext(), files.NewGetMetadataArg(dst)) if err != nil { return dst } @@ -576,7 +572,7 @@ func writeModeForIfExists(ifExists string) string { return files.WriteModeAdd } -func checkPutStdinDestination(dbx files.Client, dst string, ifExists string) (putDestinationAction, files.IsMetadata, error) { +func checkPutStdinDestination(dbx filesClient, dst string, ifExists string) (putDestinationAction, files.IsMetadata, error) { ifExists, err := normalizePutIfExists(ifExists) if err != nil { return putDestinationUpload, nil, err @@ -598,7 +594,7 @@ func checkPutStdinDestination(dbx files.Client, dst string, ifExists string) (pu return actionForExistingDestination(dst, ifExists, meta) } -func checkPutDestination(dbx files.Client, dst string, ifExists string) (putDestinationAction, files.IsMetadata, error) { +func checkPutDestination(dbx filesClient, dst string, ifExists string) (putDestinationAction, files.IsMetadata, error) { ifExists, err := normalizePutIfExists(ifExists) if err != nil { return putDestinationUpload, nil, err @@ -631,8 +627,8 @@ func actionForExistingDestination(dst string, ifExists string, metadata files.Is } } -func getDestinationMetadata(dbx files.Client, dst string) (files.IsMetadata, bool, error) { - meta, err := dbx.GetMetadata(files.NewGetMetadataArg(dst)) +func getDestinationMetadata(dbx filesClient, dst string) (files.IsMetadata, bool, error) { + meta, err := dbx.GetMetadataContext(currentContext(), files.NewGetMetadataArg(dst)) if err != nil { if isGetMetadataNotFoundError(err) { return nil, false, nil @@ -851,17 +847,17 @@ func putRecursiveInternal(src, dst string, opts putOptions, collectResults bool) return results, warnings, nil } -func putDirectory(dbx files.Client, dst string) error { - _, err := dbx.CreateFolderV2(files.NewCreateFolderArg(dst)) +func putDirectory(dbx filesClient, dst string) error { + _, err := dbx.CreateFolderV2Context(currentContext(), files.NewCreateFolderArg(dst)) if err == nil { return nil } return putDirectoryConflictError(dst, err) } -func putDirectoryWithResult(dbx files.Client, src, dst string) (putResult, error) { +func putDirectoryWithResult(dbx filesClient, src, dst string) (putResult, error) { arg := files.NewCreateFolderArg(dst) - created, err := dbx.CreateFolderV2(arg) + created, err := dbx.CreateFolderV2Context(currentContext(), arg) if err != nil { if conflictErr := putDirectoryConflictError(dst, err); conflictErr != nil { return putResult{}, conflictErr diff --git a/cmd/put_test.go b/cmd/put_test.go index c19c95f8..9980122c 100644 --- a/cmd/put_test.go +++ b/cmd/put_test.go @@ -450,7 +450,7 @@ func TestPutFileDestinationTrailingSlash(t *testing.T) { }, } origNew := filesNewFunc - filesNewFunc = func(_ dropbox.Config) files.Client { return testClient } + filesNewFunc = func(_ dropbox.Config) filesClient { return testClient } defer func() { filesNewFunc = origNew }() err := put(testPutCmd(), []string{tmpFile, "/folder/"}) @@ -507,7 +507,7 @@ func TestPutRecursive_WalksDirectoryStructure(t *testing.T) { }, } origNew := filesNewFunc - filesNewFunc = func(_ dropbox.Config) files.Client { return testClient } + filesNewFunc = func(_ dropbox.Config) filesClient { return testClient } defer func() { filesNewFunc = origNew }() opts := putOptions{chunkSize: 1 << 24, workers: 4} @@ -563,7 +563,7 @@ func TestPutRecursive_CreatesEmptyDirectories(t *testing.T) { }, } origNew := filesNewFunc - filesNewFunc = func(_ dropbox.Config) files.Client { return testClient } + filesNewFunc = func(_ dropbox.Config) filesClient { return testClient } defer func() { filesNewFunc = origNew }() opts := putOptions{chunkSize: 1 << 24, workers: 4} @@ -611,7 +611,7 @@ func TestPutRecursive_CreatesEmptyRootDirectory(t *testing.T) { }, } origNew := filesNewFunc - filesNewFunc = func(_ dropbox.Config) files.Client { return testClient } + filesNewFunc = func(_ dropbox.Config) filesClient { return testClient } defer func() { filesNewFunc = origNew }() opts := putOptions{chunkSize: 1 << 24, workers: 4} @@ -649,7 +649,7 @@ func TestPutRecursive_SkipsSymlinks(t *testing.T) { }, } origNew := filesNewFunc - filesNewFunc = func(_ dropbox.Config) files.Client { return testClient } + filesNewFunc = func(_ dropbox.Config) filesClient { return testClient } defer func() { filesNewFunc = origNew }() opts := putOptions{chunkSize: 1 << 24, workers: 4} diff --git a/cmd/relocation_if_exists.go b/cmd/relocation_if_exists.go index 6b2a85a1..e0b58e99 100644 --- a/cmd/relocation_if_exists.go +++ b/cmd/relocation_if_exists.go @@ -46,14 +46,14 @@ func normalizeRelocationIfExists(ifExists string) (string, error) { } } -func relocationSkipIfDestinationExists(dbx files.Client, arg *files.RelocationArg, opts relocationOptions) (relocationResult, bool, error) { +func relocationSkipIfDestinationExists(dbx filesClient, arg *files.RelocationArg, opts relocationOptions) (relocationResult, bool, error) { if opts.ifExists != relocationIfExistsSkip { return relocationResult{}, false, nil } return relocationSkippedResult(dbx, arg) } -func relocationSkipAfterDestinationConflict(dbx files.Client, arg *files.RelocationArg, err error, opts relocationOptions) (relocationResult, bool) { +func relocationSkipAfterDestinationConflict(dbx filesClient, arg *files.RelocationArg, err error, opts relocationOptions) (relocationResult, bool) { if opts.ifExists != relocationIfExistsSkip || !isRelocationDestinationConflict(err) { return relocationResult{}, false } @@ -65,7 +65,7 @@ func relocationSkipAfterDestinationConflict(dbx files.Client, arg *files.Relocat return result, true } -func relocationSkippedResult(dbx files.Client, arg *files.RelocationArg) (relocationResult, bool, error) { +func relocationSkippedResult(dbx filesClient, arg *files.RelocationArg) (relocationResult, bool, error) { metadata, exists, err := getDestinationMetadata(dbx, arg.ToPath) if err != nil || !exists { return relocationResult{}, false, err diff --git a/cmd/remove-member.go b/cmd/remove-member.go index f5f0fb16..8b8b7aef 100644 --- a/cmd/remove-member.go +++ b/cmd/remove-member.go @@ -33,7 +33,7 @@ func removeMember(cmd *cobra.Command, args []string) (err error) { selector := &team.UserSelectorArg{Email: email} selector.Tag = "email" arg := team.NewMembersRemoveArg(selector) - res, err := dbx.MembersRemove(arg) + res, err := dbx.MembersRemoveContext(currentContext(), arg) if err != nil { return withJSONErrorDetails(err, operationErrorDetails("team_remove_member"), emailErrorDetails(email)) } diff --git a/cmd/restore.go b/cmd/restore.go index 54d4a733..09a34127 100644 --- a/cmd/restore.go +++ b/cmd/restore.go @@ -55,7 +55,7 @@ func restore(cmd *cobra.Command, args []string) (err error) { arg := files.NewRestoreArg(path, rev) dbx := filesNewFunc(config) - metadata, err := dbx.Restore(arg) + metadata, err := dbx.RestoreContext(currentContext(), arg) if err != nil { return withJSONErrorDetails(err, restoreErrorDetails(path, rev)) } diff --git a/cmd/retry.go b/cmd/retry.go index 3e89996c..0b145906 100644 --- a/cmd/retry.go +++ b/cmd/retry.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "errors" "io" "net" @@ -17,7 +18,17 @@ const ( maxBackoff = 30 * time.Second ) -var retrySleep = time.Sleep +var retrySleep = func(ctx context.Context, delay time.Duration) error { + timer := time.NewTimer(delay) + defer timer.Stop() + + select { + case <-timer.C: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} func isTransientError(err error) bool { if err == nil { @@ -91,14 +102,24 @@ func retryDelay(err error, backoff time.Duration) (time.Duration, bool) { } func retryWithBackoff(fn func() error) error { + return retryWithBackoffContext(currentContext(), fn) +} + +func retryWithBackoffContext(ctx context.Context, fn func() error) error { backoff := initialBackoff var lastErr error for attempt := 0; attempt <= maxRetries; attempt++ { + if err := ctx.Err(); err != nil { + return err + } lastErr = fn() if lastErr == nil { return nil } + if errors.Is(lastErr, context.Canceled) || errors.Is(lastErr, context.DeadlineExceeded) { + return lastErr + } if !isTransientError(lastErr) { return lastErr } @@ -106,7 +127,9 @@ func retryWithBackoff(fn func() error) error { break } delay, isRateLimit := retryDelay(lastErr, backoff) - retrySleep(delay) + if err := retrySleep(ctx, delay); err != nil { + return err + } if !isRateLimit { backoff *= 2 if backoff > maxBackoff { diff --git a/cmd/retry_test.go b/cmd/retry_test.go index 269eac8c..a02a504b 100644 --- a/cmd/retry_test.go +++ b/cmd/retry_test.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "errors" "io" "net" @@ -17,8 +18,9 @@ func stubRetrySleep(t *testing.T) *[]time.Duration { var delays []time.Duration origRetrySleep := retrySleep - retrySleep = func(delay time.Duration) { + retrySleep = func(ctx context.Context, delay time.Duration) error { delays = append(delays, delay) + return nil } t.Cleanup(func() { retrySleep = origRetrySleep @@ -246,3 +248,28 @@ func TestRetryWithBackoff_ExhaustsRetries(t *testing.T) { } } } + +func TestRetryWithBackoffContextStopsDuringSleep(t *testing.T) { + origRetrySleep := retrySleep + t.Cleanup(func() { + retrySleep = origRetrySleep + }) + + ctx, cancel := context.WithCancel(context.Background()) + calls := 0 + retrySleep = func(ctx context.Context, delay time.Duration) error { + cancel() + return ctx.Err() + } + + err := retryWithBackoffContext(ctx, func() error { + calls++ + return auth.ServerError{APIError: dropbox.APIError{ErrorSummary: "500"}} + }) + if !errors.Is(err, context.Canceled) { + t.Fatalf("error = %v, want context.Canceled", err) + } + if calls != 1 { + t.Fatalf("calls = %d, want 1", calls) + } +} diff --git a/cmd/revs.go b/cmd/revs.go index cb7568b8..6e2bca51 100644 --- a/cmd/revs.go +++ b/cmd/revs.go @@ -56,7 +56,7 @@ func revs(cmd *cobra.Command, args []string) (err error) { } dbx := filesNewFunc(config) - res, err := dbx.ListRevisions(arg) + res, err := dbx.ListRevisionsContext(currentContext(), arg) if err != nil { return } diff --git a/cmd/rm.go b/cmd/rm.go index 68c1d9a9..119c633b 100644 --- a/cmd/rm.go +++ b/cmd/rm.go @@ -123,7 +123,7 @@ func parseRemoveOptions(cmd *cobra.Command) (removeOptions, error) { }, nil } -func validateRemoveTargets(dbx files.Client, args []string, opts removeOptions) ([]removeTarget, error) { +func validateRemoveTargets(dbx filesClient, args []string, opts removeOptions) ([]removeTarget, error) { var targets []removeTarget // Validate remove paths before executing removal @@ -140,7 +140,7 @@ func validateRemoveTargets(dbx files.Client, args []string, opts removeOptions) if _, ok := pathMetaData.(*files.FileMetadata); !ok && !opts.allowNonEmptyFolder() { folderArg := files.NewListFolderArg(path) - res, err := dbx.ListFolder(folderArg) + res, err := dbx.ListFolderContext(currentContext(), folderArg) if err != nil { return nil, withJSONErrorDetails(err, operationErrorDetails(removeOperation(opts)), pathErrorDetails(path)) } @@ -154,7 +154,7 @@ func validateRemoveTargets(dbx files.Client, args []string, opts removeOptions) return targets, nil } -func removeTargets(dbx files.Client, targets []removeTarget, opts removeOptions) ([]removeResult, error) { +func removeTargets(dbx filesClient, targets []removeTarget, opts removeOptions) ([]removeResult, error) { results := make([]removeResult, 0, len(targets)) for _, target := range targets { @@ -162,11 +162,11 @@ func removeTargets(dbx files.Client, targets []removeTarget, opts removeOptions) metadata := target.metadata if opts.permanent { - if err := dbx.PermanentlyDelete(arg); err != nil { + if err := dbx.PermanentlyDeleteContext(currentContext(), arg); err != nil { return nil, withJSONErrorDetails(err, operationErrorDetails(removeOperation(opts)), pathErrorDetails(target.path)) } } else { - res, err := dbx.DeleteV2(arg) + res, err := dbx.DeleteV2Context(currentContext(), arg) if err != nil { return nil, withJSONErrorDetails(err, operationErrorDetails(removeOperation(opts)), pathErrorDetails(target.path)) } diff --git a/cmd/root.go b/cmd/root.go index 941b83e0..64f83bde 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -15,15 +15,18 @@ package cmd import ( + "context" "fmt" "os" "strings" + "time" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/common" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/files" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/users" "github.com/spf13/cobra" + "github.com/spf13/pflag" ) const ( @@ -51,6 +54,62 @@ var ( ) var config dropbox.Config +var commandContext context.Context = context.Background() +var commandContextCancel context.CancelFunc + +func currentContext() context.Context { + if commandContext == nil { + return context.Background() + } + return commandContext +} + +func initCommandContext(cmd *cobra.Command) error { + finishCommandContext() + + ctx := cmd.Context() + if ctx == nil { + ctx = context.Background() + } + + timeout, err := commandTimeout(cmd) + if err != nil { + return err + } + if timeout < 0 { + return invalidArgumentsErrorWithDetails("`--timeout` must be greater than or equal to 0", flagErrorDetails("timeout")) + } + if timeout > 0 { + ctx, commandContextCancel = context.WithTimeout(ctx, timeout) + cmd.SetContext(ctx) + } + commandContext = ctx + return nil +} + +func commandTimeout(cmd *cobra.Command) (time.Duration, error) { + for _, flags := range []interface { + Lookup(string) *pflag.Flag + GetDuration(string) (time.Duration, error) + }{ + cmd.Flags(), + cmd.InheritedFlags(), + cmd.PersistentFlags(), + } { + if flags.Lookup("timeout") != nil { + return flags.GetDuration("timeout") + } + } + return 0, nil +} + +func finishCommandContext() { + if commandContextCancel != nil { + commandContextCancel() + commandContextCancel = nil + } + commandContext = context.Background() +} func commandSkipsAuth(cmd *cobra.Command) bool { for c := cmd; c != nil; c = c.Parent() { @@ -147,6 +206,10 @@ func makeDropboxConfig(token string, verbose bool, asMember string, domain strin func initDbx(cmd *cobra.Command, args []string) (err error) { currentAuthContext = nil + if err := initCommandContext(cmd); err != nil { + return err + } + if commandIsJSONHelp(cmd) { return nil } @@ -196,7 +259,7 @@ func withRootNamespace(cfg dropbox.Config, tokType string) dropbox.Config { return cfg } - account, err := usersNewFunc(cfg).GetCurrentAccount() + account, err := usersNewFunc(cfg).GetCurrentAccountContext(currentContext()) if err != nil { cfg.LogInfo("Warning: could not auto-detect root namespace (%v); team folders may not be accessible", err) return cfg @@ -252,6 +315,7 @@ func Execute() { restoreDeprecatedCommands = temporarilyClearDeprecatedCommands(RootCmd) } defer restoreDeprecatedCommands() + defer finishCommandContext() cmd, err := RootCmd.ExecuteC() if err != nil { @@ -269,6 +333,7 @@ func loadOAuthCredentialsFromEnv() { func init() { RootCmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose logging") RootCmd.PersistentFlags().String(outputFlag, "text", "Output format: text, json") + RootCmd.PersistentFlags().Duration("timeout", 0, "Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h)") RootCmd.PersistentFlags().String("as-member", "", "Member ID to perform action as") // This flag should only be used for testing. Marked hidden so it doesn't clutter usage etc. RootCmd.PersistentFlags().String("domain", "", "Override default Dropbox domain, useful for testing") diff --git a/cmd/root_test.go b/cmd/root_test.go index 81dc186a..b7603305 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -283,6 +283,37 @@ func TestInitDbxSkipsAuthForLocalCommands(t *testing.T) { } } +func TestInitCommandContextUsesTimeout(t *testing.T) { + t.Cleanup(finishCommandContext) + + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Duration("timeout", 50*time.Millisecond, "") + + if err := initCommandContext(cmd); err != nil { + t.Fatalf("initCommandContext error: %v", err) + } + + deadline, ok := currentContext().Deadline() + if !ok { + t.Fatal("currentContext deadline missing") + } + if remaining := time.Until(deadline); remaining <= 0 || remaining > time.Second { + t.Fatalf("deadline remaining = %v, want positive timeout near 50ms", remaining) + } +} + +func TestInitCommandContextRejectsNegativeTimeout(t *testing.T) { + t.Cleanup(finishCommandContext) + + cmd := &cobra.Command{Use: "test"} + cmd.Flags().Duration("timeout", -time.Second, "") + + err := initCommandContext(cmd) + if err == nil || !strings.Contains(err.Error(), "`--timeout` must be greater than or equal to 0") { + t.Fatalf("error = %v, want negative timeout error", err) + } +} + func TestInitDbxValidatesOutputBeforeAuth(t *testing.T) { t.Setenv(envAccessToken, "") t.Setenv(envAuthFile, filepath.Join(t.TempDir(), "missing-auth.json")) diff --git a/cmd/search.go b/cmd/search.go index 38dede98..75760406 100644 --- a/cmd/search.go +++ b/cmd/search.go @@ -71,7 +71,7 @@ func search(cmd *cobra.Command, args []string) (err error) { arg := newSearchV2Arg(args[0], scope, opts) dbx := filesNewFunc(config) - res, err := dbx.SearchV2(arg) + res, err := dbx.SearchV2Context(currentContext(), arg) if err != nil { return err } @@ -81,7 +81,7 @@ func search(cmd *cobra.Command, args []string) (err error) { for res.HasMore && !searchLimitReached(entries, opts.limit) { contArg := files.NewSearchV2ContinueArg(res.Cursor) - res, err = dbx.SearchContinueV2(contArg) + res, err = dbx.SearchContinueV2Context(currentContext(), contArg) if err != nil { return err } diff --git a/cmd/search_test.go b/cmd/search_test.go index 64b44504..c1df5f4c 100644 --- a/cmd/search_test.go +++ b/cmd/search_test.go @@ -22,7 +22,7 @@ func TestSearchArgValidation(t *testing.T) { func TestSearchPathScopeValidation(t *testing.T) { err := search(searchCmd, []string{"query", "no-slash"}) if err == nil { - t.Error("expected error for path-scope without leading slash") + t.Fatal("expected error for path-scope without leading slash") } details := jsonErrorDetails(err) if details["argument"] != "path-scope" || details["path"] != "no-slash" { diff --git a/cmd/share-list-folders.go b/cmd/share-list-folders.go index 5bc2b02e..53f16f8e 100644 --- a/cmd/share-list-folders.go +++ b/cmd/share-list-folders.go @@ -15,6 +15,7 @@ package cmd import ( + "context" "fmt" "io" "time" @@ -25,8 +26,8 @@ import ( ) type sharedFolderClient interface { - ListFolders(*sharing.ListFoldersArgs) (*sharing.ListFoldersResult, error) - ListFoldersContinue(*sharing.ListFoldersContinueArg) (*sharing.ListFoldersResult, error) + ListFoldersContext(context.Context, *sharing.ListFoldersArgs) (*sharing.ListFoldersResult, error) + ListFoldersContinueContext(context.Context, *sharing.ListFoldersContinueArg) (*sharing.ListFoldersResult, error) } type shareFolderListInput struct{} @@ -53,7 +54,7 @@ const ( ) var newSharedFolderClient = func(cfg dropbox.Config) sharedFolderClient { - return sharing.New(cfg) + return sharing.NewContext(cfg) } func shareListFolders(cmd *cobra.Command, args []string) (err error) { @@ -79,7 +80,7 @@ func shareListFolders(cmd *cobra.Command, args []string) (err error) { func listSharedFolders(dbx sharedFolderClient, arg *sharing.ListFoldersArgs) ([]*sharing.SharedFolderMetadata, error) { var entries []*sharing.SharedFolderMetadata - res, err := dbx.ListFolders(arg) + res, err := dbx.ListFoldersContext(currentContext(), arg) if err != nil { return nil, err } @@ -88,7 +89,7 @@ func listSharedFolders(dbx sharedFolderClient, arg *sharing.ListFoldersArgs) ([] for len(res.Cursor) > 0 { continueArg := sharing.NewListFoldersContinueArg(res.Cursor) - res, err = dbx.ListFoldersContinue(continueArg) + res, err = dbx.ListFoldersContinueContext(currentContext(), continueArg) if err != nil { return nil, err } diff --git a/cmd/share-list-links.go b/cmd/share-list-links.go index e0239653..460fefcd 100644 --- a/cmd/share-list-links.go +++ b/cmd/share-list-links.go @@ -94,7 +94,7 @@ func shareLinkListErrorDetails(path string) map[string]any { func listSharedLinks(dbx sharedLinkClient, arg *sharing.ListSharedLinksArg) ([]sharing.IsSharedLinkMetadata, error) { var links []sharing.IsSharedLinkMetadata for { - res, err := dbx.ListSharedLinks(arg) + res, err := dbx.ListSharedLinksContext(currentContext(), arg) if err != nil { return nil, err } diff --git a/cmd/share_create_link_test.go b/cmd/share_create_link_test.go index 186b8ea6..eeb4f0d5 100644 --- a/cmd/share_create_link_test.go +++ b/cmd/share_create_link_test.go @@ -16,6 +16,7 @@ package cmd import ( "bytes" + "context" "errors" "fmt" "io" @@ -47,6 +48,10 @@ func (m *mockSharedLinkClient) CreateSharedLinkWithSettings(arg *sharing.CreateS return nil, nil } +func (m *mockSharedLinkClient) CreateSharedLinkWithSettingsContext(ctx context.Context, arg *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) { + return m.CreateSharedLinkWithSettings(arg) +} + func (m *mockSharedLinkClient) CreateSharedLinkWithRawSettings(path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) { if m.createSharedLinkWithRawSettingsFn != nil { return m.createSharedLinkWithRawSettingsFn(path, settings) @@ -54,6 +59,10 @@ func (m *mockSharedLinkClient) CreateSharedLinkWithRawSettings(path string, sett return nil, nil } +func (m *mockSharedLinkClient) CreateSharedLinkWithRawSettingsContext(ctx context.Context, path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) { + return m.CreateSharedLinkWithRawSettings(path, settings) +} + func (m *mockSharedLinkClient) GetSharedLinkFile(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { if m.getSharedLinkFileFn != nil { return m.getSharedLinkFileFn(arg) @@ -61,6 +70,10 @@ func (m *mockSharedLinkClient) GetSharedLinkFile(arg *sharing.GetSharedLinkMetad return nil, nil, nil } +func (m *mockSharedLinkClient) GetSharedLinkFileContext(ctx context.Context, arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) { + return m.GetSharedLinkFile(arg) +} + func (m *mockSharedLinkClient) GetSharedLinkMetadata(arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error) { if m.getSharedLinkMetadataFn != nil { return m.getSharedLinkMetadataFn(arg) @@ -68,6 +81,10 @@ func (m *mockSharedLinkClient) GetSharedLinkMetadata(arg *sharing.GetSharedLinkM return nil, nil } +func (m *mockSharedLinkClient) GetSharedLinkMetadataContext(ctx context.Context, arg *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error) { + return m.GetSharedLinkMetadata(arg) +} + func (m *mockSharedLinkClient) ListSharedLinks(arg *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error) { if m.listSharedLinksFn != nil { return m.listSharedLinksFn(arg) @@ -75,6 +92,10 @@ func (m *mockSharedLinkClient) ListSharedLinks(arg *sharing.ListSharedLinksArg) return &sharing.ListSharedLinksResult{}, nil } +func (m *mockSharedLinkClient) ListSharedLinksContext(ctx context.Context, arg *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error) { + return m.ListSharedLinks(arg) +} + func (m *mockSharedLinkClient) ModifySharedLinkSettings(arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) { if m.modifySharedLinkSettingsFn != nil { return m.modifySharedLinkSettingsFn(arg) @@ -82,6 +103,10 @@ func (m *mockSharedLinkClient) ModifySharedLinkSettings(arg *sharing.ModifyShare return nil, nil } +func (m *mockSharedLinkClient) ModifySharedLinkSettingsContext(ctx context.Context, arg *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) { + return m.ModifySharedLinkSettings(arg) +} + func (m *mockSharedLinkClient) RevokeSharedLink(arg *sharing.RevokeSharedLinkArg) error { if m.revokeSharedLinkFn != nil { return m.revokeSharedLinkFn(arg) @@ -89,6 +114,10 @@ func (m *mockSharedLinkClient) RevokeSharedLink(arg *sharing.RevokeSharedLinkArg return nil } +func (m *mockSharedLinkClient) RevokeSharedLinkContext(ctx context.Context, arg *sharing.RevokeSharedLinkArg) error { + return m.RevokeSharedLink(arg) +} + func (m *mockSharedLinkClient) ModifySharedLinkSettingsRaw(url string, settings *rawSharedLinkSettings, removeExpiration bool) error { if m.modifySharedLinkSettingsRawFn != nil { return m.modifySharedLinkSettingsRawFn(url, settings, removeExpiration) @@ -96,6 +125,10 @@ func (m *mockSharedLinkClient) ModifySharedLinkSettingsRaw(url string, settings return nil } +func (m *mockSharedLinkClient) ModifySharedLinkSettingsRawContext(ctx context.Context, url string, settings *rawSharedLinkSettings, removeExpiration bool) error { + return m.ModifySharedLinkSettingsRaw(url, settings, removeExpiration) +} + func stubSharedLinkClient(t *testing.T, client sharedLinkClient) { t.Helper() diff --git a/cmd/share_link.go b/cmd/share_link.go index 90f3c897..a8c8fd3d 100644 --- a/cmd/share_link.go +++ b/cmd/share_link.go @@ -15,6 +15,7 @@ package cmd import ( + "context" "io" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox" @@ -23,25 +24,25 @@ 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) - RevokeSharedLink(arg *sharing.RevokeSharedLinkArg) error - ModifySharedLinkSettingsRaw(url string, settings *rawSharedLinkSettings, removeExpiration bool) error + CreateSharedLinkWithSettingsContext(context.Context, *sharing.CreateSharedLinkWithSettingsArg) (sharing.IsSharedLinkMetadata, error) + CreateSharedLinkWithRawSettingsContext(context.Context, string, *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) + GetSharedLinkFileContext(context.Context, *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, io.ReadCloser, error) + GetSharedLinkMetadataContext(context.Context, *sharing.GetSharedLinkMetadataArg) (sharing.IsSharedLinkMetadata, error) + ListSharedLinksContext(context.Context, *sharing.ListSharedLinksArg) (*sharing.ListSharedLinksResult, error) + ModifySharedLinkSettingsContext(context.Context, *sharing.ModifySharedLinkSettingsArgs) (sharing.IsSharedLinkMetadata, error) + RevokeSharedLinkContext(context.Context, *sharing.RevokeSharedLinkArg) error + ModifySharedLinkSettingsRawContext(context.Context, string, *rawSharedLinkSettings, bool) error } type sdkSharedLinkClient struct { - sharing.Client + sharing.ContextClient cfg dropbox.Config } var newSharedLinkClient = func(cfg dropbox.Config) sharedLinkClient { return &sdkSharedLinkClient{ - Client: sharing.New(cfg), - cfg: cfg, + ContextClient: sharing.NewContext(cfg), + cfg: cfg, } } diff --git a/cmd/share_link_create.go b/cmd/share_link_create.go index 62b8d740..5b94ab97 100644 --- a/cmd/share_link_create.go +++ b/cmd/share_link_create.go @@ -135,7 +135,7 @@ func newShareLinkCreateInput(path string, opts shareLinkCreateOptions) shareLink func createSharedLink(dbx sharedLinkClient, path string, opts shareLinkCreateOptions) (sharing.IsSharedLinkMetadata, error) { if opts.disallowDownload { - return dbx.CreateSharedLinkWithRawSettings(path, rawSharedLinkSettingsFromCreateOptions(opts)) + return dbx.CreateSharedLinkWithRawSettingsContext(currentContext(), path, rawSharedLinkSettingsFromCreateOptions(opts)) } arg := sharing.NewCreateSharedLinkWithSettingsArg(path) @@ -143,7 +143,7 @@ func createSharedLink(dbx sharedLinkClient, path string, opts shareLinkCreateOpt arg.Settings = sharing.NewSharedLinkSettings() applySharedLinkCreateSettings(arg.Settings, opts) } - return dbx.CreateSharedLinkWithSettings(arg) + return dbx.CreateSharedLinkWithSettingsContext(currentContext(), arg) } func parseShareLinkCreateOptions(cmd *cobra.Command) (shareLinkCreateOptions, error) { @@ -227,7 +227,7 @@ func applyExistingSharedLinkCreateOptions(dbx sharedLinkClient, link sharing.IsS } if opts.disallowDownload { - if err := dbx.ModifySharedLinkSettingsRaw(url, rawSharedLinkSettingsFromCreateOptions(opts), opts.removeExpiration); err != nil { + if err := dbx.ModifySharedLinkSettingsRawContext(currentContext(), url, rawSharedLinkSettingsFromCreateOptions(opts), opts.removeExpiration); err != nil { return nil, withJSONErrorDetails(err, operationErrorDetails("share_link_create"), urlErrorDetails(url)) } return link, nil @@ -240,7 +240,7 @@ func applyExistingSharedLinkCreateOptions(dbx sharedLinkClient, link sharing.IsS arg := sharing.NewModifySharedLinkSettingsArgs(url, settings) arg.RemoveExpiration = opts.removeExpiration - updated, err := dbx.ModifySharedLinkSettings(arg) + updated, err := dbx.ModifySharedLinkSettingsContext(currentContext(), arg) if err != nil { return nil, withJSONErrorDetails(err, operationErrorDetails("share_link_create"), urlErrorDetails(url)) } @@ -335,7 +335,7 @@ func findExistingSharedLink(dbx sharedLinkClient, requestedPath string) (sharing var firstDirect sharing.IsSharedLinkMetadata for { - res, err := dbx.ListSharedLinks(arg) + res, err := dbx.ListSharedLinksContext(currentContext(), arg) if err != nil { return nil, withJSONErrorDetails(err, operationErrorDetails("share_link_create"), pathErrorDetails(requestedPath)) } diff --git a/cmd/share_link_download.go b/cmd/share_link_download.go index 83bf34a8..69f0effd 100644 --- a/cmd/share_link_download.go +++ b/cmd/share_link_download.go @@ -89,7 +89,7 @@ func shareLinkDownload(cmd *cobra.Command, args []string) error { return nil } - link, err := dbx.GetSharedLinkMetadata(arg) + link, err := dbx.GetSharedLinkMetadataContext(currentContext(), arg) if err != nil { return withJSONErrorDetails(err, urlErrorDetails(url), operationErrorDetails("share_link_download")) } @@ -209,7 +209,7 @@ func sharedLinkFolderDownloadTarget(target string, link *sharing.FolderLinkMetad return target, nil } -func downloadSharedLinkFolder(filesDbx files.Client, dbx sharedLinkClient, arg *sharing.GetSharedLinkMetadataArg, rootName, dst string, errOut io.Writer) error { +func downloadSharedLinkFolder(filesDbx filesClient, dbx sharedLinkClient, arg *sharing.GetSharedLinkMetadataArg, rootName, dst string, errOut io.Writer) error { if errOut == nil { errOut = io.Discard } @@ -289,12 +289,12 @@ func downloadSharedLinkFolder(filesDbx files.Client, dbx sharedLinkClient, arg * return nil } -func listSharedLinkFolderEntries(dbx files.Client, arg *sharing.GetSharedLinkMetadataArg, relFolder string) ([]files.IsMetadata, error) { +func listSharedLinkFolderEntries(dbx filesClient, arg *sharing.GetSharedLinkMetadataArg, relFolder string) ([]files.IsMetadata, error) { listArg := files.NewListFolderArg(sharedLinkAPIPath(relFolder)) listArg.SharedLink = files.NewSharedLink(arg.Url) listArg.SharedLink.Password = arg.LinkPassword - res, err := dbx.ListFolder(listArg) + res, err := dbx.ListFolderContext(currentContext(), listArg) if err != nil { return nil, fmt.Errorf("list shared link folder %q: %v", relFolder, err) } @@ -305,7 +305,7 @@ func listSharedLinkFolderEntries(dbx files.Client, arg *sharing.GetSharedLinkMet return entries, errors.New("list shared link folder has more results but no cursor") } cont := files.NewListFolderContinueArg(res.Cursor) - res, err = dbx.ListFolderContinue(cont) + res, err = dbx.ListFolderContinueContext(currentContext(), cont) if err != nil { return entries, fmt.Errorf("list shared link folder continue: %v", err) } @@ -321,7 +321,7 @@ func downloadSharedLinkRelativeFile(dbx sharedLinkClient, baseArg *sharing.GetSh arg.LinkPassword = baseArg.LinkPassword return retryWithBackoff(func() error { - link, contents, err := dbx.GetSharedLinkFile(arg) + link, contents, err := dbx.GetSharedLinkFileContext(currentContext(), arg) if err != nil { return err } @@ -378,7 +378,7 @@ func downloadSharedLinkToFile(dbx sharedLinkClient, arg *sharing.GetSharedLinkMe var dst string var downloaded sharing.IsSharedLinkMetadata err := retryWithBackoff(func() error { - link, contents, err := dbx.GetSharedLinkFile(arg) + link, contents, err := dbx.GetSharedLinkFileContext(currentContext(), arg) if err != nil { return err } @@ -407,7 +407,7 @@ func downloadSharedLinkToStdout(dbx sharedLinkClient, arg *sharing.GetSharedLink return partialStdoutError(bytesWritten) } - _, contents, err := dbx.GetSharedLinkFile(arg) + _, contents, err := dbx.GetSharedLinkFileContext(currentContext(), arg) if err != nil { return err } diff --git a/cmd/share_link_info.go b/cmd/share_link_info.go index 1784b517..010b5a1d 100644 --- a/cmd/share_link_info.go +++ b/cmd/share_link_info.go @@ -61,7 +61,7 @@ func shareLinkInfo(cmd *cobra.Command, args []string) error { arg.LinkPassword = opts.password.password } - link, err := dbx.GetSharedLinkMetadata(arg) + link, err := dbx.GetSharedLinkMetadataContext(currentContext(), arg) if err != nil { return withJSONErrorDetails(err, urlErrorDetails(url), operationErrorDetails("share_link_info")) } diff --git a/cmd/share_link_raw.go b/cmd/share_link_raw.go index 6be3c068..94907593 100644 --- a/cmd/share_link_raw.go +++ b/cmd/share_link_raw.go @@ -15,6 +15,7 @@ package cmd import ( + "context" "encoding/json" "fmt" "io" @@ -54,7 +55,7 @@ func rawSharedLinkExpires(value *time.Time) *dropbox.DBXTime { return &t } -func (dbx *sdkSharedLinkClient) CreateSharedLinkWithRawSettings(path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) { +func (dbx *sdkSharedLinkClient) CreateSharedLinkWithRawSettingsContext(ctx context.Context, path string, settings *rawSharedLinkSettings) (sharing.IsSharedLinkMetadata, error) { arg := &rawCreateSharedLinkWithSettingsArg{ Path: path, Settings: settings, @@ -68,7 +69,7 @@ func (dbx *sdkSharedLinkClient) CreateSharedLinkWithRawSettings(path string, set Arg: arg, } - resp, respBody, err := executeSharingRawRequest(dbx.cfg, req, parseCreateSharedLinkWithSettingsError) + resp, respBody, err := executeSharingRawRequest(ctx, dbx.cfg, req, parseCreateSharedLinkWithSettingsError) if err != nil { return nil, err } @@ -79,7 +80,7 @@ func (dbx *sdkSharedLinkClient) CreateSharedLinkWithRawSettings(path string, set return parseSharedLinkMetadata(resp) } -func (dbx *sdkSharedLinkClient) ModifySharedLinkSettingsRaw(url string, settings *rawSharedLinkSettings, removeExpiration bool) error { +func (dbx *sdkSharedLinkClient) ModifySharedLinkSettingsRawContext(ctx context.Context, url string, settings *rawSharedLinkSettings, removeExpiration bool) error { arg := &rawModifySharedLinkSettingsArgs{ URL: url, Settings: settings, @@ -94,7 +95,7 @@ func (dbx *sdkSharedLinkClient) ModifySharedLinkSettingsRaw(url string, settings Arg: arg, } - _, respBody, err := executeSharingRawRequest(dbx.cfg, req, parseModifySharedLinkSettingsError) + _, respBody, err := executeSharingRawRequest(ctx, dbx.cfg, req, parseModifySharedLinkSettingsError) if err != nil { return err } @@ -105,9 +106,9 @@ func (dbx *sdkSharedLinkClient) ModifySharedLinkSettingsRaw(url string, settings return nil } -func executeSharingRawRequest(cfg dropbox.Config, req dropbox.Request, parseError func(error) error) ([]byte, io.ReadCloser, error) { - ctx := dropbox.NewContext(cfg) - resp, respBody, err := (&ctx).Execute(req, nil) +func executeSharingRawRequest(ctx context.Context, cfg dropbox.Config, req dropbox.Request, parseError func(error) error) ([]byte, io.ReadCloser, error) { + dbx := dropbox.NewContext(cfg) + resp, respBody, err := (&dbx).ExecuteContext(ctx, req, nil) if err != nil { return nil, nil, parseError(err) } diff --git a/cmd/share_link_raw_test.go b/cmd/share_link_raw_test.go index 442cebff..1f3a98cf 100644 --- a/cmd/share_link_raw_test.go +++ b/cmd/share_link_raw_test.go @@ -76,7 +76,7 @@ func TestModifySharedLinkSettingsRawSendsRequirePasswordFalse(t *testing.T) { } requirePassword := false - if err := dbx.ModifySharedLinkSettingsRaw("https://example.com/link", &rawSharedLinkSettings{ + if err := dbx.ModifySharedLinkSettingsRawContext(currentContext(), "https://example.com/link", &rawSharedLinkSettings{ RequirePassword: &requirePassword, }, false); err != nil { t.Fatalf("ModifySharedLinkSettingsRaw error: %v", err) @@ -123,7 +123,7 @@ func TestCreateSharedLinkWithRawSettingsSendsAllowDownloadFalse(t *testing.T) { } allowDownload := false - link, err := dbx.CreateSharedLinkWithRawSettings("/file.txt", &rawSharedLinkSettings{ + link, err := dbx.CreateSharedLinkWithRawSettingsContext(currentContext(), "/file.txt", &rawSharedLinkSettings{ AllowDownload: &allowDownload, }) if err != nil { @@ -175,7 +175,7 @@ func TestModifySharedLinkSettingsRawSendsAllowDownloadFalse(t *testing.T) { } allowDownload := false - if err := dbx.ModifySharedLinkSettingsRaw("https://example.com/link", &rawSharedLinkSettings{ + if err := dbx.ModifySharedLinkSettingsRawContext(currentContext(), "https://example.com/link", &rawSharedLinkSettings{ AllowDownload: &allowDownload, }, false); err != nil { t.Fatalf("ModifySharedLinkSettingsRaw error: %v", err) diff --git a/cmd/share_link_revoke.go b/cmd/share_link_revoke.go index 1cecd26b..0c5d4075 100644 --- a/cmd/share_link_revoke.go +++ b/cmd/share_link_revoke.go @@ -61,7 +61,7 @@ func shareLinkRevoke(cmd *cobra.Command, args []string) error { dbx := newSharedLinkClient(config) arg := sharing.NewRevokeSharedLinkArg(url) - if err := dbx.RevokeSharedLink(arg); err != nil { + if err := dbx.RevokeSharedLinkContext(currentContext(), arg); err != nil { return withJSONErrorDetails(err, urlErrorDetails(url), operationErrorDetails("share_link_revoke")) } @@ -142,7 +142,7 @@ func revokeSharedLinksForPath(cmd *cobra.Command, path string) ([]shareLinkRevok if !ok { return nil, withJSONErrorDetails(errors.New("found unknown shared link type"), operationErrorDetails("share_link_revoke"), pathErrorDetails(path)) } - if err := dbx.RevokeSharedLink(sharing.NewRevokeSharedLinkArg(url)); err != nil { + if err := dbx.RevokeSharedLinkContext(currentContext(), sharing.NewRevokeSharedLinkArg(url)); err != nil { return nil, withJSONErrorDetails(fmt.Errorf("revoke shared link %s: %w", url, err), urlErrorDetails(url), operationErrorDetails("share_link_revoke")) } revoked = append(revoked, shareLinkRevokeResult{ diff --git a/cmd/share_link_update.go b/cmd/share_link_update.go index 88a2b753..5b6e40cb 100644 --- a/cmd/share_link_update.go +++ b/cmd/share_link_update.go @@ -62,7 +62,7 @@ func shareLinkUpdate(cmd *cobra.Command, args []string) error { dbx := newSharedLinkClient(config) if opts.usesRawSettings() { - if err := dbx.ModifySharedLinkSettingsRaw(url, rawSharedLinkSettingsFromUpdateOptions(opts), opts.removeExpiration); err != nil { + if err := dbx.ModifySharedLinkSettingsRawContext(currentContext(), url, rawSharedLinkSettingsFromUpdateOptions(opts), opts.removeExpiration); err != nil { return withJSONErrorDetails(err, urlErrorDetails(url), operationErrorDetails("share_link_update")) } return renderShareLinkUpdateOutput(cmd, dbx, url, opts, nil) @@ -88,7 +88,7 @@ func shareLinkUpdate(cmd *cobra.Command, args []string) error { arg.RemoveExpiration = opts.removeExpiration if opts.hasSDKSettings() { - link, err := dbx.ModifySharedLinkSettings(arg) + link, err := dbx.ModifySharedLinkSettingsContext(currentContext(), arg) if err != nil { return withJSONErrorDetails(err, urlErrorDetails(url), operationErrorDetails("share_link_update")) } @@ -111,7 +111,7 @@ func renderShareLinkUpdateOutput(cmd *cobra.Command, dbx sharedLinkClient, url s arg.LinkPassword = opts.password.password } var err error - link, err = dbx.GetSharedLinkMetadata(arg) + link, err = dbx.GetSharedLinkMetadataContext(currentContext(), arg) if err != nil { return withJSONErrorDetails(err, urlErrorDetails(url), operationErrorDetails("share_link_update")) } diff --git a/cmd/share_list_folders_test.go b/cmd/share_list_folders_test.go index 57ae54c7..bd8389ac 100644 --- a/cmd/share_list_folders_test.go +++ b/cmd/share_list_folders_test.go @@ -2,6 +2,7 @@ package cmd import ( "bytes" + "context" "errors" "strings" "testing" @@ -24,6 +25,10 @@ func (m *mockSharedFolderClient) ListFolders(arg *sharing.ListFoldersArgs) (*sha return sharing.NewListFoldersResult(nil), nil } +func (m *mockSharedFolderClient) ListFoldersContext(ctx context.Context, arg *sharing.ListFoldersArgs) (*sharing.ListFoldersResult, error) { + return m.ListFolders(arg) +} + func (m *mockSharedFolderClient) ListFoldersContinue(arg *sharing.ListFoldersContinueArg) (*sharing.ListFoldersResult, error) { if m.listFoldersContinueFn != nil { return m.listFoldersContinueFn(arg) @@ -31,6 +36,10 @@ func (m *mockSharedFolderClient) ListFoldersContinue(arg *sharing.ListFoldersCon return sharing.NewListFoldersResult(nil), nil } +func (m *mockSharedFolderClient) ListFoldersContinueContext(ctx context.Context, arg *sharing.ListFoldersContinueArg) (*sharing.ListFoldersResult, error) { + return m.ListFoldersContinue(arg) +} + func TestShareListFoldersTextUsesCommandOutput(t *testing.T) { stubSharedFolderClient(t, &mockSharedFolderClient{ listFoldersFn: func(arg *sharing.ListFoldersArgs) (*sharing.ListFoldersResult, error) { diff --git a/cmd/stdout.go b/cmd/stdout.go index dbfa575e..1183ad45 100644 --- a/cmd/stdout.go +++ b/cmd/stdout.go @@ -28,7 +28,7 @@ func (e partialTransferError) JSONErrorDetails() map[string]any { return map[string]any{"bytes_written": e.bytesWritten} } -func downloadToStdout(dbx files.Client, src string, w io.Writer) error { +func downloadToStdout(dbx filesClient, src string, w io.Writer) error { ignoreBrokenPipeSignal() arg := files.NewDownloadArg(src) @@ -39,7 +39,7 @@ func downloadToStdout(dbx files.Client, src string, w io.Writer) error { return partialStdoutError(bytesWritten) } - _, contents, err := dbx.Download(arg) + _, contents, err := dbx.DownloadContext(currentContext(), arg) if err != nil { return err } diff --git a/cmd/team_json.go b/cmd/team_json.go index 4a261448..7e488495 100644 --- a/cmd/team_json.go +++ b/cmd/team_json.go @@ -15,6 +15,8 @@ package cmd import ( + "context" + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/async" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/team" @@ -22,13 +24,13 @@ import ( ) type teamClient interface { - GetInfo() (*team.TeamGetInfoResult, error) - GroupsList(*team.GroupsListArg) (*team.GroupsListResult, error) - GroupsListContinue(*team.GroupsListContinueArg) (*team.GroupsListResult, error) - MembersAdd(*team.MembersAddArg) (*team.MembersAddLaunch, error) - MembersList(*team.MembersListArg) (*team.MembersListResult, error) - MembersListContinue(*team.MembersListContinueArg) (*team.MembersListResult, error) - MembersRemove(*team.MembersRemoveArg) (*async.LaunchEmptyResult, error) + GetInfoContext(context.Context) (*team.TeamGetInfoResult, error) + GroupsListContext(context.Context, *team.GroupsListArg) (*team.GroupsListResult, error) + GroupsListContinueContext(context.Context, *team.GroupsListContinueArg) (*team.GroupsListResult, error) + MembersAddContext(context.Context, *team.MembersAddArg) (*team.MembersAddLaunch, error) + MembersListContext(context.Context, *team.MembersListArg) (*team.MembersListResult, error) + MembersListContinueContext(context.Context, *team.MembersListContinueArg) (*team.MembersListResult, error) + MembersRemoveContext(context.Context, *team.MembersRemoveArg) (*async.LaunchEmptyResult, error) } type teamInfoInput struct{} @@ -109,7 +111,7 @@ const ( ) var teamNewFunc = func(cfg dropbox.Config) teamClient { - return team.New(cfg) + return team.NewContext(cfg) } func teamInfoOperationOutput(info *team.TeamGetInfoResult) jsonOperationOutput { diff --git a/cmd/team_json_test.go b/cmd/team_json_test.go index d797fcb2..28b74389 100644 --- a/cmd/team_json_test.go +++ b/cmd/team_json_test.go @@ -2,6 +2,7 @@ package cmd import ( "bytes" + "context" "encoding/json" "errors" "strings" @@ -33,6 +34,10 @@ func (m *mockTeamClient) GetInfo() (*team.TeamGetInfoResult, error) { return nil, nil } +func (m *mockTeamClient) GetInfoContext(ctx context.Context) (*team.TeamGetInfoResult, error) { + return m.GetInfo() +} + func (m *mockTeamClient) GroupsList(arg *team.GroupsListArg) (*team.GroupsListResult, error) { if m.groupsListFn != nil { return m.groupsListFn(arg) @@ -40,6 +45,10 @@ func (m *mockTeamClient) GroupsList(arg *team.GroupsListArg) (*team.GroupsListRe return team.NewGroupsListResult(nil, "", false), nil } +func (m *mockTeamClient) GroupsListContext(ctx context.Context, arg *team.GroupsListArg) (*team.GroupsListResult, error) { + return m.GroupsList(arg) +} + func (m *mockTeamClient) GroupsListContinue(arg *team.GroupsListContinueArg) (*team.GroupsListResult, error) { if m.groupsListContinueFn != nil { return m.groupsListContinueFn(arg) @@ -47,6 +56,10 @@ func (m *mockTeamClient) GroupsListContinue(arg *team.GroupsListContinueArg) (*t return team.NewGroupsListResult(nil, "", false), nil } +func (m *mockTeamClient) GroupsListContinueContext(ctx context.Context, arg *team.GroupsListContinueArg) (*team.GroupsListResult, error) { + return m.GroupsListContinue(arg) +} + func (m *mockTeamClient) MembersAdd(arg *team.MembersAddArg) (*team.MembersAddLaunch, error) { if m.membersAddFn != nil { return m.membersAddFn(arg) @@ -54,6 +67,10 @@ func (m *mockTeamClient) MembersAdd(arg *team.MembersAddArg) (*team.MembersAddLa return nil, nil } +func (m *mockTeamClient) MembersAddContext(ctx context.Context, arg *team.MembersAddArg) (*team.MembersAddLaunch, error) { + return m.MembersAdd(arg) +} + func (m *mockTeamClient) MembersList(arg *team.MembersListArg) (*team.MembersListResult, error) { if m.membersListFn != nil { return m.membersListFn(arg) @@ -61,6 +78,10 @@ func (m *mockTeamClient) MembersList(arg *team.MembersListArg) (*team.MembersLis return team.NewMembersListResult(nil, "", false), nil } +func (m *mockTeamClient) MembersListContext(ctx context.Context, arg *team.MembersListArg) (*team.MembersListResult, error) { + return m.MembersList(arg) +} + func (m *mockTeamClient) MembersListContinue(arg *team.MembersListContinueArg) (*team.MembersListResult, error) { if m.membersListContinueFn != nil { return m.membersListContinueFn(arg) @@ -68,6 +89,10 @@ func (m *mockTeamClient) MembersListContinue(arg *team.MembersListContinueArg) ( return team.NewMembersListResult(nil, "", false), nil } +func (m *mockTeamClient) MembersListContinueContext(ctx context.Context, arg *team.MembersListContinueArg) (*team.MembersListResult, error) { + return m.MembersListContinue(arg) +} + func (m *mockTeamClient) MembersRemove(arg *team.MembersRemoveArg) (*async.LaunchEmptyResult, error) { if m.membersRemoveFn != nil { return m.membersRemoveFn(arg) @@ -75,6 +100,10 @@ func (m *mockTeamClient) MembersRemove(arg *team.MembersRemoveArg) (*async.Launc return nil, nil } +func (m *mockTeamClient) MembersRemoveContext(ctx context.Context, arg *team.MembersRemoveArg) (*async.LaunchEmptyResult, error) { + return m.MembersRemove(arg) +} + type teamOperationOutputForTest[I, R any] struct { Input I `json:"input"` Results []teamOperationResultForTest[I, R] `json:"results"` diff --git a/cmd/testdata/json_contract/success_outputs.json b/cmd/testdata/json_contract/success_outputs.json index ef3ae5c0..1e41599f 100644 --- a/cmd/testdata/json_contract/success_outputs.json +++ b/cmd/testdata/json_contract/success_outputs.json @@ -219,6 +219,20 @@ "may_prompt": false, "value_kind": "enum" }, + { + "name": "timeout", + "type": "duration", + "default": "0s", + "usage": "Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h)", + "inherited": true, + "shorthand": "", + "enum_values": [], + "conflicts": [], + "required": false, + "sensitive": false, + "may_prompt": false, + "value_kind": "duration" + }, { "name": "verbose", "type": "bool", @@ -351,6 +365,15 @@ "x-cli-name": "time-format", "x-value-kind": "enum" }, + "timeout": { + "type": "string", + "description": "Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h)", + "default": "0s", + "x-cli-kind": "flag", + "x-cli-name": "timeout", + "x-value-kind": "duration", + "x-inherited": true + }, "verbose": { "type": "boolean", "description": "Enable verbose logging", diff --git a/cmd/users_client.go b/cmd/users_client.go index 0525c63c..24a6bcf4 100644 --- a/cmd/users_client.go +++ b/cmd/users_client.go @@ -1,16 +1,18 @@ package cmd import ( + "context" + "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox/users" ) type usersClient interface { - GetAccount(*users.GetAccountArg) (*users.BasicAccount, error) - GetCurrentAccount() (*users.FullAccount, error) - GetSpaceUsage() (*users.SpaceUsage, error) + GetAccountContext(context.Context, *users.GetAccountArg) (*users.BasicAccount, error) + GetCurrentAccountContext(context.Context) (*users.FullAccount, error) + GetSpaceUsageContext(context.Context) (*users.SpaceUsage, error) } var usersNewFunc = func(cfg dropbox.Config) usersClient { - return users.New(cfg) + return users.NewContext(cfg) } diff --git a/cmd/users_test.go b/cmd/users_test.go index 61befaa9..a1305900 100644 --- a/cmd/users_test.go +++ b/cmd/users_test.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "testing" "github.com/dropbox/dropbox-sdk-go-unofficial/v6/dropbox" @@ -20,6 +21,10 @@ func (m *mockUsersClient) GetAccount(arg *users.GetAccountArg) (*users.BasicAcco return nil, nil } +func (m *mockUsersClient) GetAccountContext(ctx context.Context, arg *users.GetAccountArg) (*users.BasicAccount, error) { + return m.GetAccount(arg) +} + func (m *mockUsersClient) GetCurrentAccount() (*users.FullAccount, error) { if m.getCurrentAccountFn != nil { return m.getCurrentAccountFn() @@ -27,6 +32,10 @@ func (m *mockUsersClient) GetCurrentAccount() (*users.FullAccount, error) { return nil, nil } +func (m *mockUsersClient) GetCurrentAccountContext(ctx context.Context) (*users.FullAccount, error) { + return m.GetCurrentAccount() +} + func (m *mockUsersClient) GetSpaceUsage() (*users.SpaceUsage, error) { if m.getSpaceUsageFn != nil { return m.getSpaceUsageFn() @@ -34,6 +43,10 @@ func (m *mockUsersClient) GetSpaceUsage() (*users.SpaceUsage, error) { return nil, nil } +func (m *mockUsersClient) GetSpaceUsageContext(ctx context.Context) (*users.SpaceUsage, error) { + return m.GetSpaceUsage() +} + func stubUsersClient(t *testing.T, client usersClient) { t.Helper() diff --git a/docs/commands/dbxcli.md b/docs/commands/dbxcli.md index 43e45d21..bce7f7cd 100644 --- a/docs/commands/dbxcli.md +++ b/docs/commands/dbxcli.md @@ -17,6 +17,7 @@ direct-token automation for scripts, CI jobs, and agent-style workflows. --as-member string Member ID to perform action as -h, --help help for dbxcli --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_account.md b/docs/commands/dbxcli_account.md index c362e1ae..b0e952c5 100644 --- a/docs/commands/dbxcli_account.md +++ b/docs/commands/dbxcli_account.md @@ -26,6 +26,7 @@ dbxcli account [flags] [] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_completion.md b/docs/commands/dbxcli_completion.md index dbadf889..a8111497 100644 --- a/docs/commands/dbxcli_completion.md +++ b/docs/commands/dbxcli_completion.md @@ -25,6 +25,7 @@ dbxcli completion [bash|zsh|fish|powershell] [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_completion_bash.md b/docs/commands/dbxcli_completion_bash.md index eca91119..ebbd1789 100644 --- a/docs/commands/dbxcli_completion_bash.md +++ b/docs/commands/dbxcli_completion_bash.md @@ -44,6 +44,7 @@ dbxcli completion bash ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_completion_fish.md b/docs/commands/dbxcli_completion_fish.md index a23874fe..8ff4a212 100644 --- a/docs/commands/dbxcli_completion_fish.md +++ b/docs/commands/dbxcli_completion_fish.md @@ -35,6 +35,7 @@ dbxcli completion fish [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_completion_powershell.md b/docs/commands/dbxcli_completion_powershell.md index 511b07cc..73464913 100644 --- a/docs/commands/dbxcli_completion_powershell.md +++ b/docs/commands/dbxcli_completion_powershell.md @@ -32,6 +32,7 @@ dbxcli completion powershell [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_completion_zsh.md b/docs/commands/dbxcli_completion_zsh.md index cf72c52e..86699009 100644 --- a/docs/commands/dbxcli_completion_zsh.md +++ b/docs/commands/dbxcli_completion_zsh.md @@ -46,6 +46,7 @@ dbxcli completion zsh [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_cp.md b/docs/commands/dbxcli_cp.md index 5365f443..0b0e665e 100644 --- a/docs/commands/dbxcli_cp.md +++ b/docs/commands/dbxcli_cp.md @@ -20,6 +20,7 @@ dbxcli cp [flags] [more sources] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_du.md b/docs/commands/dbxcli_du.md index 0e3cd7a4..8770ed34 100644 --- a/docs/commands/dbxcli_du.md +++ b/docs/commands/dbxcli_du.md @@ -19,6 +19,7 @@ dbxcli du [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_get.md b/docs/commands/dbxcli_get.md index d9b4cc84..6459fcd1 100644 --- a/docs/commands/dbxcli_get.md +++ b/docs/commands/dbxcli_get.md @@ -37,6 +37,7 @@ dbxcli get [flags] [] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_login.md b/docs/commands/dbxcli_login.md index 38632a95..8b1b3799 100644 --- a/docs/commands/dbxcli_login.md +++ b/docs/commands/dbxcli_login.md @@ -27,6 +27,7 @@ dbxcli login [personal|team-access|team-manage] [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_logout.md b/docs/commands/dbxcli_logout.md index cef199c9..4b951181 100644 --- a/docs/commands/dbxcli_logout.md +++ b/docs/commands/dbxcli_logout.md @@ -28,6 +28,7 @@ dbxcli logout [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_ls.md b/docs/commands/dbxcli_ls.md index e683f395..dc7dda4e 100644 --- a/docs/commands/dbxcli_ls.md +++ b/docs/commands/dbxcli_ls.md @@ -38,6 +38,7 @@ dbxcli ls [flags] [] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_mkdir.md b/docs/commands/dbxcli_mkdir.md index 4ac0177f..e18998ec 100644 --- a/docs/commands/dbxcli_mkdir.md +++ b/docs/commands/dbxcli_mkdir.md @@ -20,6 +20,7 @@ dbxcli mkdir [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_mv.md b/docs/commands/dbxcli_mv.md index a13210d8..e0f30ae2 100644 --- a/docs/commands/dbxcli_mv.md +++ b/docs/commands/dbxcli_mv.md @@ -20,6 +20,7 @@ dbxcli mv [flags] [more sources] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_put.md b/docs/commands/dbxcli_put.md index d51fa117..63b55408 100644 --- a/docs/commands/dbxcli_put.md +++ b/docs/commands/dbxcli_put.md @@ -47,6 +47,7 @@ dbxcli put [flags] [] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_restore.md b/docs/commands/dbxcli_restore.md index 930dc70d..2def1180 100644 --- a/docs/commands/dbxcli_restore.md +++ b/docs/commands/dbxcli_restore.md @@ -33,6 +33,7 @@ dbxcli restore [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_revs.md b/docs/commands/dbxcli_revs.md index 88db9de2..8559bdd9 100644 --- a/docs/commands/dbxcli_revs.md +++ b/docs/commands/dbxcli_revs.md @@ -23,6 +23,7 @@ dbxcli revs [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_rm.md b/docs/commands/dbxcli_rm.md index d6bce499..ef989d04 100644 --- a/docs/commands/dbxcli_rm.md +++ b/docs/commands/dbxcli_rm.md @@ -22,6 +22,7 @@ dbxcli rm [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_search.md b/docs/commands/dbxcli_search.md index e73e9835..56c7b0d8 100644 --- a/docs/commands/dbxcli_search.md +++ b/docs/commands/dbxcli_search.md @@ -27,6 +27,7 @@ dbxcli search [flags] [path-scope] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_share-link.md b/docs/commands/dbxcli_share-link.md index 3dbd84eb..bd72fd93 100644 --- a/docs/commands/dbxcli_share-link.md +++ b/docs/commands/dbxcli_share-link.md @@ -19,6 +19,7 @@ Create, list, inspect, download, update, and revoke Dropbox shared links. ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_share-link_create.md b/docs/commands/dbxcli_share-link_create.md index 7e26847e..d7bdfb37 100644 --- a/docs/commands/dbxcli_share-link_create.md +++ b/docs/commands/dbxcli_share-link_create.md @@ -44,6 +44,7 @@ dbxcli share-link create [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_share-link_download.md b/docs/commands/dbxcli_share-link_download.md index 361549f7..07396e31 100644 --- a/docs/commands/dbxcli_share-link_download.md +++ b/docs/commands/dbxcli_share-link_download.md @@ -43,6 +43,7 @@ dbxcli share-link download [target] [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_share-link_info.md b/docs/commands/dbxcli_share-link_info.md index 72f6923e..75581907 100644 --- a/docs/commands/dbxcli_share-link_info.md +++ b/docs/commands/dbxcli_share-link_info.md @@ -35,6 +35,7 @@ dbxcli share-link info [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_share-link_list.md b/docs/commands/dbxcli_share-link_list.md index 3fb8c36d..f3321521 100644 --- a/docs/commands/dbxcli_share-link_list.md +++ b/docs/commands/dbxcli_share-link_list.md @@ -31,6 +31,7 @@ dbxcli share-link list [path] [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_share-link_revoke.md b/docs/commands/dbxcli_share-link_revoke.md index b47f634d..ebd7abcb 100644 --- a/docs/commands/dbxcli_share-link_revoke.md +++ b/docs/commands/dbxcli_share-link_revoke.md @@ -31,6 +31,7 @@ dbxcli share-link revoke [url] [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_share-link_update.md b/docs/commands/dbxcli_share-link_update.md index b571935c..3c922dd0 100644 --- a/docs/commands/dbxcli_share-link_update.md +++ b/docs/commands/dbxcli_share-link_update.md @@ -43,6 +43,7 @@ dbxcli share-link update [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_share.md b/docs/commands/dbxcli_share.md index c300fc53..e38bec71 100644 --- a/docs/commands/dbxcli_share.md +++ b/docs/commands/dbxcli_share.md @@ -15,6 +15,7 @@ Sharing commands ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_share_list.md b/docs/commands/dbxcli_share_list.md index bf059df5..c4d1eb24 100644 --- a/docs/commands/dbxcli_share_list.md +++ b/docs/commands/dbxcli_share_list.md @@ -15,6 +15,7 @@ List shared things ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_share_list_folder.md b/docs/commands/dbxcli_share_list_folder.md index bd1777f7..76b1c20d 100644 --- a/docs/commands/dbxcli_share_list_folder.md +++ b/docs/commands/dbxcli_share_list_folder.md @@ -19,6 +19,7 @@ dbxcli share list folder [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_team.md b/docs/commands/dbxcli_team.md index 7d5fcf01..682902ce 100644 --- a/docs/commands/dbxcli_team.md +++ b/docs/commands/dbxcli_team.md @@ -15,6 +15,7 @@ Team management commands ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_team_add-member.md b/docs/commands/dbxcli_team_add-member.md index 60a37671..0224f536 100644 --- a/docs/commands/dbxcli_team_add-member.md +++ b/docs/commands/dbxcli_team_add-member.md @@ -19,6 +19,7 @@ dbxcli team add-member [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_team_info.md b/docs/commands/dbxcli_team_info.md index 82004890..bda4fe12 100644 --- a/docs/commands/dbxcli_team_info.md +++ b/docs/commands/dbxcli_team_info.md @@ -19,6 +19,7 @@ dbxcli team info [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_team_list-groups.md b/docs/commands/dbxcli_team_list-groups.md index 8418440b..bca08a3b 100644 --- a/docs/commands/dbxcli_team_list-groups.md +++ b/docs/commands/dbxcli_team_list-groups.md @@ -19,6 +19,7 @@ dbxcli team list-groups [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_team_list-members.md b/docs/commands/dbxcli_team_list-members.md index b1a91fb2..2097ba52 100644 --- a/docs/commands/dbxcli_team_list-members.md +++ b/docs/commands/dbxcli_team_list-members.md @@ -19,6 +19,7 @@ dbxcli team list-members [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_team_remove-member.md b/docs/commands/dbxcli_team_remove-member.md index 23e9c534..e20d484c 100644 --- a/docs/commands/dbxcli_team_remove-member.md +++ b/docs/commands/dbxcli_team_remove-member.md @@ -19,6 +19,7 @@ dbxcli team remove-member [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ``` diff --git a/docs/commands/dbxcli_version.md b/docs/commands/dbxcli_version.md index 36abe984..aaf76bc9 100644 --- a/docs/commands/dbxcli_version.md +++ b/docs/commands/dbxcli_version.md @@ -19,6 +19,7 @@ dbxcli version [flags] ``` --as-member string Member ID to perform action as --output string Output format: text, json (default "text") + --timeout duration Timeout for Dropbox network operations (0 disables; examples: 30s, 2m, 1h) -v, --verbose Enable verbose logging ```